/*
* Copyright © 2015 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.visibility;
import co.cask.tephra.Transaction;
import co.cask.tephra.TransactionAware;
import co.cask.tephra.TransactionConflictException;
import co.cask.tephra.TransactionContext;
import co.cask.tephra.TransactionFailureException;
import co.cask.tephra.TransactionManager;
import co.cask.tephra.TransactionSystemClient;
import co.cask.tephra.inmemory.InMemoryTxSystemClient;
import co.cask.tephra.metrics.TxMetricsCollector;
import co.cask.tephra.persist.InMemoryTransactionStateStorage;
import com.google.common.base.Charsets;
import com.google.common.base.Throwables;
import org.apache.hadoop.conf.Configuration;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The following are all the possible cases when using {@link VisibilityFence}.
*
* In the below table,
* "Read Txn" refers to the transaction that contains the read fence
* "Before Write", "During Write" and "After Write" refer to the write transaction time
* "Before Write Fence", "During Write Fence", "After Write Fence" refer to the write fence transaction time
*
* Timeline is: Before Write < During Write < After Write < Before Write Fence < During Write Fence <
* After Write Fence
*
* +------+----------------------+----------------------+--------------------+--------------------+
* | Case | Read Txn Start | Read Txn Commit | Conflict on Commit | Conflict on Commit |
* | | | | of Read Txn | of Write Fence |
* +------+----------------------+----------------------+--------------------+--------------------+
* | 1 | Before Write | Before Write | No | No |
* | 2 | Before Write | During Write | No | No |
* | 3 | Before Write | After Write | No | No |
* | 4 | Before Write | Before Write Fence | No | No |
* | 5 | Before Write | During Write Fence | No | Yes |
* | 6 | Before Write | After Write Fence | Yes | No |
* | | | | | |
* | 7 | During Write | During Write | No | No |
* | 8 | During Write | After Write | No | No |
* | 9 | During Write | Before Write Fence | No | No |
* | 10 | During Write | During Write Fence | No | Yes |
* | 11 | During Write | After Write Fence | Yes | No |
* | | | | | |
* | 12 | After Write | After Write | No | No |
* | 13 | After Write | Before Write Fence | No | No |
* | 14 | After Write | During Write Fence | No | Yes # |
* | 15 | After Write | After Write Fence | Yes # | No |
* | | | | | |
* | 16 | Before Write Fence | Before Write Fence | No | No |
* | 17 | Before Write Fence | During Write Fence | No | Yes # |
* | 18 | Before Write Fence | After Write Fence | Yes # | No |
* | | | | | |
* | 19 | During Write Fence | During Write Fence | No | No |
* | 20 | During Write Fence | After Write Fence | No | No |
* | | | | | |
* | 21 | After Write Fence | After Write Fence | No | No |
* +------+----------------------+----------------------+--------------------+--------------------+
*
* Note: Cases marked with '#' in conflict column should not conflict, however current implementation causes
* them to conflict. The remaining conflicts are a result of the fence.
*
* In the current implementation of VisibilityFence, read txns that start "Before Write", "During Write",
* and "After Write" can be represented by read txns that start "Before Write Fence".
* Verifying cases 16, 17, 18, 20 and 21 will effectively cover all other cases.
*/
public class VisibilityFenceTest {
private static Configuration conf = new Configuration();
private static TransactionManager txManager = null;
@BeforeClass
public static void before() {
txManager = new TransactionManager(conf, new InMemoryTransactionStateStorage(), new TxMetricsCollector());
txManager.startAndWait();
}
@AfterClass
public static void after() {
txManager.stopAndWait();
}
@Test
public void testFence1() throws Exception {
byte[] fenceId = "test_table".getBytes(Charsets.UTF_8);
// Writer updates data here in a separate transaction (code not shown)
// start tx
// update
// commit tx
// Readers use fence to indicate that they are interested in changes to specific data
TransactionAware readFenceCase16 = VisibilityFence.create(fenceId);
TransactionContext readTxContextCase16 =
new TransactionContext(new InMemoryTxSystemClient(txManager), readFenceCase16);
readTxContextCase16.start();
readTxContextCase16.finish();
TransactionAware readFenceCase17 = VisibilityFence.create(fenceId);
TransactionContext readTxContextCase17 =
new TransactionContext(new InMemoryTxSystemClient(txManager), readFenceCase17);
readTxContextCase17.start();
TransactionAware readFenceCase18 = VisibilityFence.create(fenceId);
TransactionContext readTxContextCase18 =
new TransactionContext(new InMemoryTxSystemClient(txManager), readFenceCase18);
readTxContextCase18.start();
// Now writer needs to wait for in-progress readers to see the change, it uses write fence to do so
// Start write fence txn
TransactionAware writeFence = new WriteFence(fenceId);
TransactionContext writeTxContext = new TransactionContext(new InMemoryTxSystemClient(txManager), writeFence);
writeTxContext.start();
TransactionAware readFenceCase20 = VisibilityFence.create(fenceId);
TransactionContext readTxContextCase20 =
new TransactionContext(new InMemoryTxSystemClient(txManager), readFenceCase20);
readTxContextCase20.start();
readTxContextCase17.finish();
assertTxnConflict(writeTxContext);
writeTxContext.start();
// Commit write fence txn can commit without conflicts at this point
writeTxContext.finish();
TransactionAware readFenceCase21 = VisibilityFence.create(fenceId);
TransactionContext readTxContextCase21 =
new TransactionContext(new InMemoryTxSystemClient(txManager), readFenceCase21);
readTxContextCase21.start();
assertTxnConflict(readTxContextCase18);
readTxContextCase20.finish();
readTxContextCase21.finish();
}
private void assertTxnConflict(TransactionContext txContext) throws Exception {
try {
txContext.finish();
Assert.fail("Expected transaction to fail");
} catch (TransactionConflictException e) {
// Expected
txContext.abort();
}
}
@Test
public void testFence2() throws Exception {
byte[] fenceId = "test_table".getBytes(Charsets.UTF_8);
// Readers use fence to indicate that they are interested in changes to specific data
// Reader 1
TransactionAware readFence1 = VisibilityFence.create(fenceId);
TransactionContext readTxContext1 = new TransactionContext(new InMemoryTxSystemClient(txManager), readFence1);
readTxContext1.start();
// Reader 2
TransactionAware readFence2 = VisibilityFence.create(fenceId);
TransactionContext readTxContext2 = new TransactionContext(new InMemoryTxSystemClient(txManager), readFence2);
readTxContext2.start();
// Reader 3
TransactionAware readFence3 = VisibilityFence.create(fenceId);
TransactionContext readTxContext3 = new TransactionContext(new InMemoryTxSystemClient(txManager), readFence3);
readTxContext3.start();
// Writer updates data here in a separate transaction (code not shown)
// start tx
// update
// commit tx
// Now writer needs to wait for readers 1, 2, and 3 to see the change, it uses write fence to do so
TransactionAware writeFence = new WriteFence(fenceId);
TransactionContext writeTxContext = new TransactionContext(new InMemoryTxSystemClient(txManager), writeFence);
writeTxContext.start();
// Reader 1 commits before writeFence is committed
readTxContext1.finish();
try {
// writeFence will throw exception since Reader 1 committed without seeing changes
writeTxContext.finish();
Assert.fail("Expected transaction to fail");
} catch (TransactionConflictException e) {
// Expected
writeTxContext.abort();
}
// Start over writeFence again
writeTxContext.start();
// Now, Reader 3 commits before writeFence
// Note that Reader 3 does not conflict with Reader 1
readTxContext3.finish();
try {
// writeFence will throw exception again since Reader 3 committed without seeing changes
writeTxContext.finish();
Assert.fail("Expected transaction to fail");
} catch (TransactionConflictException e) {
// Expected
writeTxContext.abort();
}
// Start over writeFence again
writeTxContext.start();
// This time writeFence commits before the other readers
writeTxContext.finish();
// After this point all readers will see the change
try {
// Reader 2 commits after writeFence, hence this commit with throw exception
readTxContext2.finish();
Assert.fail("Expected transaction to fail");
} catch (TransactionConflictException e) {
// Expected
readTxContext2.abort();
}
// Reader 2 has to abort and start over again. It will see the changes now.
readTxContext2 = new TransactionContext(new InMemoryTxSystemClient(txManager), readFence2);
readTxContext2.start();
readTxContext2.finish();
}
@Test
public void testFenceAwait() throws Exception {
byte[] fenceId = "test_table".getBytes(Charsets.UTF_8);
final TransactionContext fence1 = new TransactionContext(new InMemoryTxSystemClient(txManager),
VisibilityFence.create(fenceId));
fence1.start();
final TransactionContext fence2 = new TransactionContext(new InMemoryTxSystemClient(txManager),
VisibilityFence.create(fenceId));
fence2.start();
TransactionContext fence3 = new TransactionContext(new InMemoryTxSystemClient(txManager),
VisibilityFence.create(fenceId));
fence3.start();
final AtomicInteger attempts = new AtomicInteger();
TransactionSystemClient customTxClient = new InMemoryTxSystemClient(txManager) {
@Override
public Transaction startShort() {
Transaction transaction = super.startShort();
try {
switch (attempts.getAndIncrement()) {
case 0:
fence1.finish();
break;
case 1:
fence2.finish();
break;
case 2:
break;
default:
throw new IllegalStateException("Unexpected state");
}
} catch (TransactionFailureException e) {
Throwables.propagate(e);
}
return transaction;
}
};
FenceWait fenceWait = VisibilityFence.prepareWait(fenceId, customTxClient);
fenceWait.await(1000, TimeUnit.MILLISECONDS);
Assert.assertEquals(3, attempts.get());
try {
fence3.finish();
Assert.fail("Expected transaction to fail");
} catch (TransactionConflictException e) {
// Expected exception
fence3.abort();
}
fence3.start();
fence3.finish();
}
@Test
public void testFenceTimeout() throws Exception {
byte[] fenceId = "test_table".getBytes(Charsets.UTF_8);
final TransactionContext fence1 = new TransactionContext(new InMemoryTxSystemClient(txManager),
VisibilityFence.create(fenceId));
fence1.start();
final long timeout = 100;
final TimeUnit timeUnit = TimeUnit.MILLISECONDS;
final AtomicInteger attempts = new AtomicInteger();
TransactionSystemClient customTxClient = new InMemoryTxSystemClient(txManager) {
@Override
public Transaction startShort() {
Transaction transaction = super.startShort();
try {
switch (attempts.getAndIncrement()) {
case 0:
fence1.finish();
break;
}
timeUnit.sleep(timeout + 1);
} catch (InterruptedException | TransactionFailureException e) {
Throwables.propagate(e);
}
return transaction;
}
};
try {
FenceWait fenceWait = VisibilityFence.prepareWait(fenceId, customTxClient);
fenceWait.await(timeout, timeUnit);
Assert.fail("Expected await to fail");
} catch (TimeoutException e) {
// Expected exception
}
Assert.assertEquals(1, attempts.get());
FenceWait fenceWait = VisibilityFence.prepareWait(fenceId, customTxClient);
fenceWait.await(timeout, timeUnit);
Assert.assertEquals(2, attempts.get());
}
}