/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.procedure2;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseCommonTestingUtility;
import org.apache.hadoop.hbase.procedure2.store.ProcedureStore;
import org.apache.hadoop.hbase.testclassification.SmallTests;
import org.apache.hadoop.hbase.testclassification.MasterTests;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@Category({MasterTests.class, SmallTests.class})
public class TestYieldProcedures {
private static final Log LOG = LogFactory.getLog(TestYieldProcedures.class);
private static final int PROCEDURE_EXECUTOR_SLOTS = 1;
private static final Procedure NULL_PROC = null;
private ProcedureExecutor<TestProcEnv> procExecutor;
private TestScheduler procRunnables;
private ProcedureStore procStore;
private HBaseCommonTestingUtility htu;
private FileSystem fs;
private Path testDir;
private Path logDir;
@Before
public void setUp() throws IOException {
htu = new HBaseCommonTestingUtility();
testDir = htu.getDataTestDir();
fs = testDir.getFileSystem(htu.getConfiguration());
assertTrue(testDir.depth() > 1);
logDir = new Path(testDir, "proc-logs");
procStore = ProcedureTestingUtility.createWalStore(htu.getConfiguration(), fs, logDir);
procRunnables = new TestScheduler();
procExecutor = new ProcedureExecutor(htu.getConfiguration(), new TestProcEnv(),
procStore, procRunnables);
procStore.start(PROCEDURE_EXECUTOR_SLOTS);
procExecutor.start(PROCEDURE_EXECUTOR_SLOTS, true);
}
@After
public void tearDown() throws IOException {
procExecutor.stop();
procStore.stop(false);
fs.delete(logDir, true);
}
@Test
public void testYieldEachExecutionStep() throws Exception {
final int NUM_STATES = 3;
TestStateMachineProcedure[] procs = new TestStateMachineProcedure[3];
for (int i = 0; i < procs.length; ++i) {
procs[i] = new TestStateMachineProcedure(true, false);
procExecutor.submitProcedure(procs[i]);
}
ProcedureTestingUtility.waitNoProcedureRunning(procExecutor);
for (int i = 0; i < procs.length; ++i) {
assertEquals(NUM_STATES * 2, procs[i].getExecutionInfo().size());
// verify execution
int index = 0;
for (int execStep = 0; execStep < NUM_STATES; ++execStep) {
TestStateMachineProcedure.ExecutionInfo info = procs[i].getExecutionInfo().get(index++);
assertEquals(false, info.isRollback());
assertEquals(execStep, info.getStep().ordinal());
}
// verify rollback
for (int execStep = NUM_STATES - 1; execStep >= 0; --execStep) {
TestStateMachineProcedure.ExecutionInfo info = procs[i].getExecutionInfo().get(index++);
assertEquals(true, info.isRollback());
assertEquals(execStep, info.getStep().ordinal());
}
}
// check runnable queue stats
assertEquals(0, procRunnables.size());
assertEquals(0, procRunnables.addFrontCalls);
assertEquals(18, procRunnables.addBackCalls);
assertEquals(15, procRunnables.yieldCalls);
assertEquals(19, procRunnables.pollCalls);
assertEquals(3, procRunnables.completionCalls);
}
@Test
public void testYieldOnInterrupt() throws Exception {
final int NUM_STATES = 3;
int count = 0;
TestStateMachineProcedure proc = new TestStateMachineProcedure(true, true);
ProcedureTestingUtility.submitAndWait(procExecutor, proc);
// test execute (we execute steps twice, one has the IE the other completes)
assertEquals(NUM_STATES * 4, proc.getExecutionInfo().size());
for (int i = 0; i < NUM_STATES; ++i) {
TestStateMachineProcedure.ExecutionInfo info = proc.getExecutionInfo().get(count++);
assertEquals(false, info.isRollback());
assertEquals(i, info.getStep().ordinal());
info = proc.getExecutionInfo().get(count++);
assertEquals(false, info.isRollback());
assertEquals(i, info.getStep().ordinal());
}
// test rollback (we execute steps twice, one has the IE the other completes)
for (int i = NUM_STATES - 1; i >= 0; --i) {
TestStateMachineProcedure.ExecutionInfo info = proc.getExecutionInfo().get(count++);
assertEquals(true, info.isRollback());
assertEquals(i, info.getStep().ordinal());
info = proc.getExecutionInfo().get(count++);
assertEquals(true, info.isRollback());
assertEquals(i, info.getStep().ordinal());
}
// check runnable queue stats
assertEquals(0, procRunnables.size());
assertEquals(0, procRunnables.addFrontCalls);
assertEquals(12, procRunnables.addBackCalls);
assertEquals(11, procRunnables.yieldCalls);
assertEquals(13, procRunnables.pollCalls);
assertEquals(1, procRunnables.completionCalls);
}
@Test
public void testYieldException() {
TestYieldProcedure proc = new TestYieldProcedure();
ProcedureTestingUtility.submitAndWait(procExecutor, proc);
assertEquals(6, proc.step);
// check runnable queue stats
assertEquals(0, procRunnables.size());
assertEquals(0, procRunnables.addFrontCalls);
assertEquals(6, procRunnables.addBackCalls);
assertEquals(5, procRunnables.yieldCalls);
assertEquals(7, procRunnables.pollCalls);
assertEquals(1, procRunnables.completionCalls);
}
private static class TestProcEnv {
public final AtomicLong timestamp = new AtomicLong(0);
public long nextTimestamp() {
return timestamp.incrementAndGet();
}
}
public static class TestStateMachineProcedure
extends StateMachineProcedure<TestProcEnv, TestStateMachineProcedure.State> {
enum State { STATE_1, STATE_2, STATE_3 }
public class ExecutionInfo {
private final boolean rollback;
private final long timestamp;
private final State step;
public ExecutionInfo(long timestamp, State step, boolean isRollback) {
this.timestamp = timestamp;
this.step = step;
this.rollback = isRollback;
}
public State getStep() { return step; }
public long getTimestamp() { return timestamp; }
public boolean isRollback() { return rollback; }
}
private final ArrayList<ExecutionInfo> executionInfo = new ArrayList<>();
private final AtomicBoolean aborted = new AtomicBoolean(false);
private final boolean throwInterruptOnceOnEachStep;
private final boolean abortOnFinalStep;
public TestStateMachineProcedure() {
this(false, false);
}
public TestStateMachineProcedure(boolean abortOnFinalStep,
boolean throwInterruptOnceOnEachStep) {
this.abortOnFinalStep = abortOnFinalStep;
this.throwInterruptOnceOnEachStep = throwInterruptOnceOnEachStep;
}
public ArrayList<ExecutionInfo> getExecutionInfo() {
return executionInfo;
}
@Override
protected StateMachineProcedure.Flow executeFromState(TestProcEnv env, State state)
throws InterruptedException {
final long ts = env.nextTimestamp();
LOG.info(getProcId() + " execute step " + state + " ts=" + ts);
executionInfo.add(new ExecutionInfo(ts, state, false));
Thread.sleep(150);
if (throwInterruptOnceOnEachStep && ((executionInfo.size() - 1) % 2) == 0) {
LOG.debug("THROW INTERRUPT");
throw new InterruptedException("test interrupt");
}
switch (state) {
case STATE_1:
setNextState(State.STATE_2);
break;
case STATE_2:
setNextState(State.STATE_3);
break;
case STATE_3:
if (abortOnFinalStep) {
setFailure("test", new IOException("Requested abort on final step"));
}
return Flow.NO_MORE_STATE;
default:
throw new UnsupportedOperationException();
}
return Flow.HAS_MORE_STATE;
}
@Override
protected void rollbackState(TestProcEnv env, final State state)
throws InterruptedException {
final long ts = env.nextTimestamp();
LOG.debug(getProcId() + " rollback state " + state + " ts=" + ts);
executionInfo.add(new ExecutionInfo(ts, state, true));
Thread.sleep(150);
if (throwInterruptOnceOnEachStep && ((executionInfo.size() - 1) % 2) == 0) {
LOG.debug("THROW INTERRUPT");
throw new InterruptedException("test interrupt");
}
switch (state) {
case STATE_1:
break;
case STATE_2:
break;
case STATE_3:
break;
default:
throw new UnsupportedOperationException();
}
}
@Override
protected State getState(final int stateId) {
return State.values()[stateId];
}
@Override
protected int getStateId(final State state) {
return state.ordinal();
}
@Override
protected State getInitialState() {
return State.STATE_1;
}
@Override
protected boolean isYieldBeforeExecuteFromState(TestProcEnv env, State state) {
return true;
}
@Override
protected boolean abort(TestProcEnv env) {
aborted.set(true);
return true;
}
}
public static class TestYieldProcedure extends Procedure<TestProcEnv> {
private int step = 0;
public TestYieldProcedure() {
}
@Override
protected Procedure[] execute(final TestProcEnv env) throws ProcedureYieldException {
LOG.info("execute step " + step);
if (step++ < 5) {
throw new ProcedureYieldException();
}
return null;
}
@Override
protected void rollback(TestProcEnv env) {
}
@Override
protected boolean abort(TestProcEnv env) {
return false;
}
@Override
protected boolean isYieldAfterExecutionStep(final TestProcEnv env) {
return true;
}
@Override
protected void serializeStateData(final OutputStream stream) throws IOException {
}
@Override
protected void deserializeStateData(final InputStream stream) throws IOException {
}
}
private static class TestScheduler extends SimpleProcedureScheduler {
private int completionCalls;
private int addFrontCalls;
private int addBackCalls;
private int yieldCalls;
private int pollCalls;
public TestScheduler() {}
public void addFront(final Procedure proc) {
addFrontCalls++;
super.addFront(proc);
}
@Override
public void addBack(final Procedure proc) {
addBackCalls++;
super.addBack(proc);
}
@Override
public void yield(final Procedure proc) {
yieldCalls++;
super.yield(proc);
}
@Override
public Procedure poll() {
pollCalls++;
return super.poll();
}
@Override
public Procedure poll(long timeout, TimeUnit unit) {
pollCalls++;
return super.poll(timeout, unit);
}
@Override
public void completionCleanup(Procedure proc) {
completionCalls++;
}
}
}