package io.eguan.dtx;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* Licensed 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.
* #L%
*/
import static io.eguan.dtx.DtxNodeState.INITIALIZED;
import static io.eguan.dtx.DtxResourceManagerState.UP_TO_DATE;
import static io.eguan.dtx.DtxTestHelper.DEFAULT_TX_MESSAGE;
import static io.eguan.dtx.proto.TxProtobufUtils.toUuid;
import static javax.transaction.xa.XAException.XAER_INVAL;
import static javax.transaction.xa.XAException.XAER_NOTA;
import static javax.transaction.xa.XAException.XAER_PROTO;
import static javax.transaction.xa.XAException.XAER_RMERR;
import static javax.transaction.xa.XAException.XAER_RMFAIL;
import static javax.transaction.xa.XAException.XA_RBINTEGRITY;
import static javax.transaction.xa.XAException.XA_RBPROTO;
import static javax.transaction.xa.XAException.XA_RBROLLBACK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import io.eguan.dtx.DtxManager;
import io.eguan.dtx.DtxManagerConfig;
import io.eguan.dtx.DtxNodeState;
import io.eguan.dtx.DtxResourceManager;
import io.eguan.dtx.DtxTaskStatus;
import io.eguan.dtx.TransactionManager;
import io.eguan.proto.Common.ProtocolVersion;
import io.eguan.proto.dtx.DistTxWrapper.TxMessage;
import io.eguan.proto.dtx.DistTxWrapper.TxNode;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.transaction.xa.XAException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.junit.runners.model.InitializationError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tests for all error cases included in the transaction execution protocol implemented by the
* {@link TransactionManager}.
*
* @author oodrive
* @author pwehrle
* @author ebredzinski
*
*/
@RunWith(Parameterized.class)
public final class TestTransactionManagerErrorCases {
private static final Logger LOGGER = LoggerFactory.getLogger(TestTransactionManagerErrorCases.class);
private static final Set<TxNode> PARTICIPANTS = DtxTestHelper.newRandomParticipantsSet();
private DtxManager dtxManager;
private Path tmpJournalDir;
/**
* Operation identifiers.
*
*
*/
public enum TestOp {
/** execute the {@link TransactionManager#start(TxMessage, Iterable)} method. */
START,
/** execute the {@link TransactionManager#prepare(long)} method. */
PREPARE,
/** execute the {@link TransactionManager#commit(long)} method. */
COMMIT,
/** execute the {@link TransactionManager#rollback(long)} method. */
ROLLBACK;
};
private static final int[] TM_PROTO = new int[] { XAER_PROTO };
private static final int[] TM_NOTA = new int[] { XAER_NOTA };
private static final int[] TM_INTERNAL = new int[] { XAER_NOTA, XAER_PROTO };
private static final int[] RM_INTERNAL = { XAER_RMERR, XAER_INVAL, XAER_RMFAIL };
private static final int[] RM_ROLLBACK = { XA_RBROLLBACK, XA_RBINTEGRITY, XA_RBPROTO };
/**
* Sets up common fixture.
*
* @throws InitializationError
* if setup fails
*/
@Before
public final void setUp() throws InitializationError {
try {
this.tmpJournalDir = Files.createTempDirectory(TestTransactionManagerErrorCases.class.getSimpleName());
}
catch (final IOException e) {
throw new InitializationError(e);
}
final DtxManagerConfig dtxConfig = DtxTestHelper.newDtxManagerConfig(tmpJournalDir);
this.dtxManager = new DtxManager(dtxConfig);
dtxManager.init();
assertEquals(INITIALIZED, dtxManager.getStatus());
target = new TransactionManager(dtxConfig, dtxManager);
target.startUp(null);
}
/**
* Tears down common fixture.
*
* @throws InitializationError
* if teardown fails
*/
@After
public final void tearDown() throws InitializationError {
try {
io.eguan.utils.Files.deleteRecursive(tmpJournalDir);
}
catch (final IOException e) {
throw new InitializationError(e);
}
}
/**
* Parameter generator method for the {@link Parameterized} test.
*
* @return a {@link List} of parameter combinations
*/
@Parameters
public static List<Object[]> data() {
final ArrayList<Object[]> result = new ArrayList<Object[]>();
for (final DtxTaskStatus currStatus : DtxTaskStatus.values()) {
final HashMap<TestOp, int[]> opErrMap = new HashMap<TestOp, int[]>();
switch (currStatus) {
case PENDING:
opErrMap.put(TestOp.START, concatAll(RM_INTERNAL, RM_ROLLBACK));
opErrMap.put(TestOp.PREPARE, TM_NOTA);
opErrMap.put(TestOp.COMMIT, TM_NOTA);
opErrMap.put(TestOp.ROLLBACK, TM_NOTA);
break;
case STARTED:
opErrMap.put(TestOp.START, TM_PROTO);
opErrMap.put(TestOp.PREPARE, concatAll(RM_INTERNAL, RM_ROLLBACK));
opErrMap.put(TestOp.COMMIT, TM_PROTO);
opErrMap.put(TestOp.ROLLBACK, concatAll(RM_INTERNAL, RM_INTERNAL));
break;
case PREPARED:
opErrMap.put(TestOp.START, TM_PROTO);
opErrMap.put(TestOp.PREPARE, TM_PROTO);
opErrMap.put(TestOp.COMMIT, concatAll(RM_INTERNAL, RM_ROLLBACK));
opErrMap.put(TestOp.ROLLBACK, RM_INTERNAL);
break;
case COMMITTED:
opErrMap.put(TestOp.START, TM_NOTA);
opErrMap.put(TestOp.PREPARE, TM_NOTA);
opErrMap.put(TestOp.COMMIT, TM_NOTA);
opErrMap.put(TestOp.ROLLBACK, TM_NOTA);
break;
case ROLLED_BACK:
opErrMap.put(TestOp.START, TM_NOTA);
opErrMap.put(TestOp.PREPARE, TM_NOTA);
opErrMap.put(TestOp.COMMIT, TM_NOTA);
opErrMap.put(TestOp.ROLLBACK, TM_NOTA);
break;
default:
break;
}
for (final TestOp currOp : opErrMap.keySet()) {
final int[] currErrList = opErrMap.get(currOp);
for (int i = 0; i < currErrList.length; i++) {
result.add(new Object[] { currOp, currStatus, new XAException(currErrList[i]) });
}
}
}
return result;
}
private final DtxTaskStatus initialStatus;
private final XAException checkException;
private final TestOp testOp;
private TransactionManager target;
/**
* Parameterized constructor.
*
* @param operation
* the {@link TestOp operation} to test
* @param status
* the status of the transaction upon calling the operation
* @param xe
* the exception to throw
*/
public TestTransactionManagerErrorCases(final TestOp operation, final DtxTaskStatus status, final XAException xe) {
this.testOp = operation;
this.initialStatus = status;
this.checkException = xe;
}
/**
* Test a precise failure case for one of the {@link TransactionManager} methods.
*
* @throws XAException
* expected for this test
*/
@Test(expected = XAException.class)
public final void testOperationFailure() throws XAException {
LOGGER.debug("Executing operation failure test; status=" + initialStatus + ", operation=" + testOp
+ ", expected=" + checkException.errorCode);
final DtxResourceManager initialResMgr = DtxDummyRmFactory.newResMgrThatDoesEverythingRight(null);
final UUID resourceId = initialResMgr.getId();
dtxManager.init();
assertEquals(DtxNodeState.INITIALIZED, dtxManager.getStatus());
target.registerResourceManager(initialResMgr, null);
assertEquals(initialResMgr, target.getRegisteredResourceManager(resourceId));
final TxMessage startTx = TxMessage.newBuilder(DEFAULT_TX_MESSAGE).setVersion(ProtocolVersion.VERSION_1)
.setTxId(DtxTestHelper.nextTxId()).setResId(toUuid(resourceId)).build();
final long txId = startTx.getTxId();
switch (this.initialStatus) {
case STARTED:
target.start(startTx, PARTICIPANTS);
break;
case PREPARED:
target.start(startTx, PARTICIPANTS);
target.prepare(txId);
break;
case COMMITTED:
target.start(startTx, PARTICIPANTS);
target.prepare(txId);
target.commit(txId, PARTICIPANTS);
break;
case ROLLED_BACK:
target.start(startTx, PARTICIPANTS);
target.rollback(txId, PARTICIPANTS);
break;
case PENDING:
default:
}
// exclude exceptions generated by the transaction manager from the mock initialization
if (Arrays.binarySearch(TM_INTERNAL, checkException.errorCode) < 0) {
// unregister the initial resource manager and replace with a faulty one
target.unregisterResourceManager(initialResMgr.getId());
assertNull(target.getRegisteredResourceManager(resourceId));
final DtxResourceManager resMgr;
switch (this.testOp) {
case START:
resMgr = DtxDummyRmFactory.newResMgrFailingOnStart(resourceId, checkException);
break;
case PREPARE:
resMgr = DtxDummyRmFactory.newResMgrFailingOnPrepare(resourceId, checkException);
break;
case COMMIT:
resMgr = DtxDummyRmFactory.newResMgrFailingOnCommit(resourceId, checkException);
break;
case ROLLBACK:
resMgr = DtxDummyRmFactory.newResMgrFailingOnRollback(resourceId, checkException);
break;
default:
resMgr = DtxDummyRmFactory.newResMgrThatDoesEverythingRight(resourceId);
}
target.registerResourceManager(resMgr, null);
assertEquals(resMgr, target.getRegisteredResourceManager(resMgr.getId()));
}
target.setResManagerSyncState(resourceId, UP_TO_DATE);
// execute the target operation and analyze the thrown exception
try {
switch (this.testOp) {
case START:
target.start(startTx, PARTICIPANTS);
break;
case PREPARE:
target.prepare(txId);
break;
case COMMIT:
target.commit(txId, PARTICIPANTS);
break;
case ROLLBACK:
target.rollback(txId, PARTICIPANTS);
break;
default:
break;
}
LOGGER.error("No exception thrown; expected=" + checkException.errorCode + ", operation=" + this.testOp
+ ", initialState=" + this.initialStatus);
}
catch (final XAException xe) {
if (checkException.errorCode != xe.errorCode) {
LOGGER.error("Unexpected error; code=" + xe.errorCode + ", expected=" + checkException.errorCode
+ ", operation=" + this.testOp + ", initialState=" + this.initialStatus);
}
assertEquals(checkException.errorCode, xe.errorCode);
throw xe;
}
}
private static int[] concatAll(final int[] first, final int[]... rest) {
int totalLength = first.length;
for (final int[] currArray : rest) {
totalLength += currArray.length;
}
final int[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (final int[] array : rest) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
}