package org.corfudb.runtime.object.transactions;
import org.corfudb.runtime.exceptions.TransactionAbortedException;
import org.corfudb.runtime.object.ConflictParameterClass;
import org.junit.Test;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Created by mwei on 11/16/16.
*/
public class OptimisticTransactionContextTest extends AbstractTransactionContextTest {
@Override
public void TXBegin() { OptimisticTXBegin(); }
/** Checks that the fine-grained conflict set is correctly produced
* by the annotation framework.
*/
@Test
public void checkConflictParameters() {
ConflictParameterClass testObject = getDefaultRuntime()
.getObjectsView().build()
.setStreamName("my stream")
.setType(ConflictParameterClass.class)
.open();
final String TEST_0 = "0";
final String TEST_1 = "1";
final int TEST_2 = 2;
final int TEST_3 = 3;
final String TEST_4 = "4";
final String TEST_5 = "5";
getRuntime().getObjectsView().TXBegin();
// RS=TEST_0
testObject.accessorTest(TEST_0, TEST_1);
// WS=TEST_3
testObject.mutatorTest(TEST_2, TEST_3);
// WS,RS=TEST_4
testObject.mutatorAccessorTest(TEST_4, TEST_5);
// Assert that the conflict set contains TEST_1, TEST_4
assertThat(TransactionalContext.getCurrentContext()
.getReadSetInfo()
.getReadSetConflicts().values().stream()
.flatMap(x -> x.stream())
.collect(Collectors.toList()))
.contains(Integer.valueOf(TEST_0.hashCode()));
// in optimistic mode, assert that the conflict set does NOT contain TEST_2, TEST_4
assertThat(TransactionalContext.getCurrentContext()
.getReadSetInfo()
.getReadSetConflicts().values().stream()
.flatMap(x -> x.stream())
.collect(Collectors.toList()))
.doesNotContain(Integer.valueOf(TEST_3), Integer.valueOf(TEST_4));
getRuntime().getObjectsView().TXAbort();
}
/** In an optimistic transaction, we should be able to
* read our own writes in the same thread.
*/
@Test
public void readOwnWrites()
{
t(1, this::OptimisticTXBegin);
t(1, () -> put("k" , "v"));
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v");
t(1, this::TXEnd);
}
/** We should not be able to read writes written optimistically
* by other threads.
*/
@Test
public void otherThreadCannotReadOptimisticWrites()
{
t(1, this::OptimisticTXBegin);
t(2, this::OptimisticTXBegin);
// T1 inserts k,v1 optimistically. Other threads
// should not see this optimistic put.
t(1, () -> put("k", "v1"));
// T2 now reads k. It should not see T1's write.
t(2, () -> get("k"))
.assertResult()
.isNull();
// T2 inserts k,v2 optimistically. T1 should not
// be able to see this write.
t(2, () -> put("k", "v2"));
// T1 now reads "k". It should not see T2's write.
t(1, () -> get("k"))
.assertResult()
.isNotEqualTo("v2");
}
/** Ensure that, upon two consecutive nested transactions, the latest transaction can
* see optimistic updates from previous ones.
*
*/
@Test
public void OptimisticStreamGetUpdatedCorrectlyWithNestedTransaction(){
t(1, this::OptimisticTXBegin);
t(1, () -> put("k", "v0"));
// Start first nested transaction
t(1, this::OptimisticTXBegin);
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v0");
t(1, () -> put("k", "v1"));
t(1, this::TXEnd);
// End first nested transaction
// Start second nested transaction
t(1, this::OptimisticTXBegin);
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v1");
t(1, () -> put("k", "v2"));
t(1, this::TXEnd);
// End second nested transaction
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v2");
t(1, this::TXEnd);
assertThat(getMap())
.containsEntry("k", "v2");
}
/** Threads that start a transaction at the same time
* (with the same timestamp) should cause one thread
* to abort while the other succeeds.
*/
@Test
public void threadShouldAbortAfterConflict()
{
// T1 starts non-transactionally.
t(1, () -> put("k", "v0"));
t(1, () -> put("k1", "v1"));
t(1, () -> put("k2", "v2"));
// Now T1 and T2 both start transactions and read v0.
t(1, this::OptimisticTXBegin);
t(2, this::OptimisticTXBegin);
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v0");
t(2, () -> get("k"))
.assertResult()
.isEqualTo("v0");
// Now T1 modifies k -> v1 and commits.
t(1, () -> put("k", "v1"));
t(1, this::TXEnd);
// And T2 modifies k -> v2 and tries to commit, but
// should abort.
t(2, () -> put("k", "v2"));
t(2, this::TXEnd)
.assertThrows()
.isInstanceOf(TransactionAbortedException.class);
// At the end of the transaction, the map should only
// contain T1's modification.
assertThat(getMap())
.containsEntry("k", "v1");
}
/** This test makes sure that a single thread can read
* its own nested transactions after they have committed,
* and that nested transactions are committed with the
* parent transaction.
*/
@Test
public void nestedTransactionsCanBeReadDuringCommit() {
// We start without a transaction and put k,v1
t(1, () -> put("k", "v1"));
// Now we start a transaction and put k,v2
t(1, this::OptimisticTXBegin);
t(1, () -> put("k", "v2"))
.assertResult() // put should return the previous value
.isEqualTo("v1"); // which is v1.
// Now we start a nested transaction. It should
// read v2.
t(1, this::OptimisticTXBegin);
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v2");
// Now we put k,v3
t(1, () -> put("k", "v3"))
.assertResult()
.isEqualTo("v2"); // previous value = v2
// And then we commit.
t(1, this::TXEnd);
// And we should be able to read the nested put
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v3");
// And we commit the parent transaction.
t(1, this::TXEnd);
// And now k,v3 should be in the map.
assertThat(getMap())
.containsEntry("k", "v3");
}
/** This test makes sure that the nested transactions
* of two threads are not visible to each other.
*/
@Test
public void nestedTransactionsAreIsolatedAcrossThreads() {
// Start a transaction on both threads.
t(1, this::OptimisticTXBegin);
t(2, this::OptimisticTXBegin);
// Put k, v1 on T1 and k, v2 on T2.
t(1, () -> put("k", "v1"));
t(2, () -> put("k", "v2"));
// Now, start a nested transaction on both threads.
t(1, this::OptimisticTXBegin);
t(2, this::OptimisticTXBegin);
// T1 should see v1 and T2 should see v2.
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v1");
t(2, () -> get("k"))
.assertResult()
.isEqualTo("v2");
// Now we put k,v3 on T1 and k,v4 on T2
t(1, () -> put("k", "v3"));
t(2, () -> put("k", "v4"));
// And each thread should only see its own modifications.
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v3");
t(2, () -> get("k"))
.assertResult()
.isEqualTo("v4");
// Now we exit the nested transaction. They should both
// commit, because they are in optimistic mode.
t(1, this::TXEnd);
t(2, this::TXEnd);
// Check that the parent transaction can only
// see the correct modifications.
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v3");
t(2, () -> get("k"))
.assertResult()
.isEqualTo("v4");
// Commit the parent transactions. T2 should abort
// due to concurrent modification with T1.
t(1, this::TXEnd);
t(2, this::TXEnd)
.assertThrows()
.isInstanceOf(TransactionAbortedException.class);
// And the map should contain k,v3 - T1's update.
assertThat(getMap())
.containsEntry("k", "v3")
.doesNotContainEntry("k", "v4");
}
/**
* Check that on abortion of a nested transaction
* the modifications that happened within it are not
* leaked into the parent transaction.
*/
@Test
public void nestedTransactionCanBeAborted() {
t(1, this::OptimisticTXBegin);
t(1, () -> put("k", "v1"));
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v1");
t(1, this::OptimisticTXBegin);
t(1, () -> put("k", "v2"));
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v2");
t(1, this::TXAbort);
t(1, () -> get("k"))
.assertResult()
.isEqualTo("v1");
t(1, this::TXEnd);
assertThat(getMap())
.containsEntry("k", "v1");
}
/** This test makes sure that a write-only transaction properly
* commits its updates, even if there are no accesses
* during the transaction.
*/
@Test
public void writeOnlyTransactionCommitsInMemory() {
// Write twice to the transaction without a read
OptimisticTXBegin();
write("k", "v1");
write("k", "v2");
TXEnd();
// Make sure the object correctly reflects the value
// of the most recent write.
assertThat(getMap())
.containsEntry("k", "v2");
}
}