/*
* Copyright © 2012-2014 Cask Data, Inc.
*
* 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.
*/
package co.cask.tephra;
import co.cask.tephra.persist.TransactionSnapshot;
import co.cask.tephra.persist.TransactionStateStorage;
import com.google.common.collect.ImmutableSet;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
/**
*
*/
public abstract class TransactionSystemTest {
public static final byte[] C1 = new byte[] { 'c', '1' };
public static final byte[] C2 = new byte[] { 'c', '2' };
public static final byte[] C3 = new byte[] { 'c', '3' };
public static final byte[] C4 = new byte[] { 'c', '4' };
protected abstract TransactionSystemClient getClient() throws Exception;
protected abstract TransactionStateStorage getStateStorage() throws Exception;
@Test
public void testCommitRaceHandling() throws Exception {
TransactionSystemClient client1 = getClient();
TransactionSystemClient client2 = getClient();
Transaction tx1 = client1.startShort();
Transaction tx2 = client2.startShort();
Assert.assertTrue(client1.canCommit(tx1, asList(C1, C2)));
// second one also can commit even thought there are conflicts with first since first one hasn't committed yet
Assert.assertTrue(client2.canCommit(tx2, asList(C2, C3)));
Assert.assertTrue(client1.commit(tx1));
// now second one should not commit, since there are conflicts with tx1 that has been committed
Assert.assertFalse(client2.commit(tx2));
}
@Test
public void testMultipleCommitsAtSameTime() throws Exception {
// We want to check that if two txs finish at same time (wrt tx manager) they do not overwrite changesets of each
// other in tx manager used for conflicts detection (we had this bug)
// NOTE: you don't have to use multiple clients for that
TransactionSystemClient client1 = getClient();
TransactionSystemClient client2 = getClient();
TransactionSystemClient client3 = getClient();
TransactionSystemClient client4 = getClient();
TransactionSystemClient client5 = getClient();
Transaction tx1 = client1.startShort();
Transaction tx2 = client2.startShort();
Transaction tx3 = client3.startShort();
Transaction tx4 = client4.startShort();
Transaction tx5 = client5.startShort();
Assert.assertTrue(client1.canCommit(tx1, asList(C1)));
Assert.assertTrue(client1.commit(tx1));
Assert.assertTrue(client2.canCommit(tx2, asList(C2)));
Assert.assertTrue(client2.commit(tx2));
// verifying conflicts detection
Assert.assertFalse(client3.canCommit(tx3, asList(C1)));
Assert.assertFalse(client4.canCommit(tx4, asList(C2)));
Assert.assertTrue(client5.canCommit(tx5, asList(C3)));
}
@Test
public void testCommitTwice() throws Exception {
TransactionSystemClient client = getClient();
Transaction tx = client.startShort();
Assert.assertTrue(client.canCommit(tx, asList(C1, C2)));
Assert.assertTrue(client.commit(tx));
// cannot commit twice same tx
try {
Assert.assertFalse(client.commit(tx));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
}
@Test
public void testAbortTwice() throws Exception {
TransactionSystemClient client = getClient();
Transaction tx = client.startShort();
Assert.assertTrue(client.canCommit(tx, asList(C1, C2)));
client.abort(tx);
// abort of not active tx has no affect
client.abort(tx);
}
@Test
public void testReuseTx() throws Exception {
TransactionSystemClient client = getClient();
Transaction tx = client.startShort();
Assert.assertTrue(client.canCommit(tx, asList(C1, C2)));
Assert.assertTrue(client.commit(tx));
// can't re-use same tx again
try {
client.canCommit(tx, asList(C3, C4));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
try {
Assert.assertFalse(client.commit(tx));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
// abort of not active tx has no affect
client.abort(tx);
}
@Test
public void testUseNotStarted() throws Exception {
TransactionSystemClient client = getClient();
Transaction tx1 = client.startShort();
Assert.assertTrue(client.commit(tx1));
// we know this is one is older than current writePointer and was not used
Transaction txOld = new Transaction(tx1.getReadPointer(), tx1.getTransactionId() - 1,
new long[] {}, new long[] {}, Transaction.NO_TX_IN_PROGRESS,
TransactionType.SHORT);
try {
Assert.assertFalse(client.canCommit(txOld, asList(C3, C4)));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
try {
Assert.assertFalse(client.commit(txOld));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
// abort of not active tx has no affect
client.abort(txOld);
// we know this is one is newer than current readPointer and was not used
Transaction txNew = new Transaction(tx1.getReadPointer(), tx1.getTransactionId() + 1,
new long[] {}, new long[] {}, Transaction.NO_TX_IN_PROGRESS,
TransactionType.SHORT);
try {
Assert.assertFalse(client.canCommit(txNew, asList(C3, C4)));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
try {
Assert.assertFalse(client.commit(txNew));
Assert.fail();
} catch (TransactionNotInProgressException e) {
// expected
}
// abort of not active tx has no affect
client.abort(txNew);
}
@Test
public void testAbortAfterCommit() throws Exception {
TransactionSystemClient client = getClient();
Transaction tx = client.startShort();
Assert.assertTrue(client.canCommit(tx, asList(C1, C2)));
Assert.assertTrue(client.commit(tx));
// abort of not active tx has no affect
client.abort(tx);
}
// todo add test invalidate method
@Test
public void testInvalidateTx() throws Exception {
TransactionSystemClient client = getClient();
// Invalidate an in-progress tx
Transaction tx1 = client.startShort();
client.canCommit(tx1, asList(C1, C2));
Assert.assertTrue(client.invalidate(tx1.getTransactionId()));
// Cannot invalidate a committed tx
Transaction tx2 = client.startShort();
client.canCommit(tx2, asList(C3, C4));
client.commit(tx2);
Assert.assertFalse(client.invalidate(tx2.getTransactionId()));
}
@Test
public void testResetState() throws Exception {
// have tx in progress, committing and committed then reset,
// get the last snapshot and see that it is empty
TransactionSystemClient client = getClient();
TransactionStateStorage stateStorage = getStateStorage();
Transaction tx1 = client.startShort();
Transaction tx2 = client.startShort();
client.canCommit(tx1, asList(C1, C2));
client.commit(tx1);
client.canCommit(tx2, asList(C3, C4));
Transaction txPreReset = client.startShort();
long currentTs = System.currentTimeMillis();
client.resetState();
TransactionSnapshot snapshot = stateStorage.getLatestSnapshot();
Assert.assertTrue(snapshot.getTimestamp() >= currentTs);
Assert.assertEquals(0, snapshot.getInvalid().size());
Assert.assertEquals(0, snapshot.getInProgress().size());
Assert.assertEquals(0, snapshot.getCommittingChangeSets().size());
Assert.assertEquals(0, snapshot.getCommittedChangeSets().size());
// confirm that transaction IDs are not reset
Transaction txPostReset = client.startShort();
Assert.assertTrue("New tx ID should be greater than last ID before reset",
txPostReset.getTransactionId() > txPreReset.getTransactionId());
}
@Test
public void testTruncateInvalidTx() throws Exception {
// Start few transactions and invalidate all of them
TransactionSystemClient client = getClient();
Transaction tx1 = client.startLong();
Transaction tx2 = client.startShort();
Transaction tx3 = client.startLong();
client.invalidate(tx1.getTransactionId());
client.invalidate(tx2.getTransactionId());
client.invalidate(tx3.getTransactionId());
// Remove tx2 and tx3 from invalid list
Assert.assertTrue(client.truncateInvalidTx(ImmutableSet.of(tx2.getTransactionId(), tx3.getTransactionId())));
Transaction tx = client.startShort();
// Only tx1 should be in invalid list now
Assert.assertArrayEquals(new long[] {tx1.getTransactionId()}, tx.getInvalids());
client.abort(tx);
}
@Test
public void testTruncateInvalidTxBefore() throws Exception {
// Start few transactions
TransactionSystemClient client = getClient();
Transaction tx1 = client.startLong();
Transaction tx2 = client.startShort();
// Sleep so that transaction ids get generated a millisecond apart for assertion
// TEPHRA-63 should eliminate the need to sleep
TimeUnit.MILLISECONDS.sleep(1);
long beforeTx3 = System.currentTimeMillis();
Transaction tx3 = client.startLong();
try {
// throws exception since tx1 and tx2 are still in-progress
client.truncateInvalidTxBefore(beforeTx3);
Assert.fail("Expected InvalidTruncateTimeException exception");
} catch (InvalidTruncateTimeException e) {
// Expected exception
}
// Invalidate all of them
client.invalidate(tx1.getTransactionId());
client.invalidate(tx2.getTransactionId());
client.invalidate(tx3.getTransactionId());
// Remove transactions before time beforeTx3
Assert.assertTrue(client.truncateInvalidTxBefore(beforeTx3));
Transaction tx = client.startShort();
// Only tx3 should be in invalid list now
Assert.assertArrayEquals(new long[] {tx3.getTransactionId()}, tx.getInvalids());
client.abort(tx);
}
@Test
public void testGetInvalidSize() throws Exception {
// Start few transactions and invalidate all of them
TransactionSystemClient client = getClient();
Transaction tx1 = client.startLong();
Transaction tx2 = client.startShort();
Transaction tx3 = client.startLong();
Assert.assertEquals(0, client.getInvalidSize());
client.invalidate(tx1.getTransactionId());
client.invalidate(tx2.getTransactionId());
client.invalidate(tx3.getTransactionId());
Assert.assertEquals(3, client.getInvalidSize());
}
private Collection<byte[]> asList(byte[]... val) {
return Arrays.asList(val);
}
}