package org.corfudb.runtime.object.transactions;
import com.google.common.reflect.TypeToken;
import org.corfudb.runtime.collections.SMRMap;
import org.corfudb.runtime.exceptions.TransactionAbortedException;
import org.corfudb.runtime.object.CorfuSharedCounter;
import org.junit.Test;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicIntegerArray;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Created by dalia on 12/29/16.
*/
public abstract class TXConflictScenariosTest extends AbstractTransactionContextTest {
/**
* test concurrent transactions for opacity. This works as follows:
*
* there are 'numTasks' shared counters numbered 0..numTasks-1,
* and 'numTasks' tasks executing by an interleaving engine by CONCURRENCY_SOME number of threads.
*
* all counters are initialized to INITIAL.
*
* Each task is specified as a state machine.
* The state machine starts a transaction.
* Then it repeats twice modify, read own counter, read other counter.
* Specifically,
* - task j modifies counter j to OVERWRITE_ONCE,
* - task j reads counter j (own), expecting to read OVERWRITE_ONCE,
* - task j reads counter (j+1) mod n, and records the value it reads,
*
* - then task j modifies counter j to OVERWRITE_TWICE,
* - task j reads counter j (own), expecting to read OVERWRITE_TWICE,
* - task j reads counter (j+1) mod n, expecting to read the same value as before,
*
* Then all tasks try to commit their transactions.
*
* @throws Exception
*/
void testOpacity(boolean testInterleaved)
throws Exception {
// populate numTasks and sharedCounters array
setupCounters();
// SM step 1: start an optimistic transaction
addTestStep((ignored_task_num) -> {
TXBegin();
});
// SM step 2: modify shared counter per task
addTestStep((task_num) -> {
sharedCounters.get(task_num).setValue(OVERWRITE_ONCE);
});
// SM step 3: each task reads a shared counter modified by another task and records it
addTestStep((task_num) -> {
snapStatus.set(task_num, sharedCounters.get((task_num + 1) % numTasks).getValue());
});
// SM step 4: each task verifies opacity, checking that it can read its own modified value
addTestStep((task_num) -> {
assertThat(sharedCounters.get(task_num).getValue())
.isEqualTo(OVERWRITE_ONCE);
});
// SM step 5: next, each task overwrites its own value again
addTestStep((task_num) -> {
sharedCounters.get(task_num).setValue(OVERWRITE_TWICE);
});
// SM step 6: each task again reads a counter modified by another task.
// it should read the same snapshot value as the beginning of the transaction
addTestStep((task_num) -> {
assertThat(sharedCounters.get((task_num + 1) % numTasks).getValue())
.isEqualTo(snapStatus.get(task_num));
});
// SM step 7: each task again verifies opacity, checking that it can read its own modified value
addTestStep((task_num) -> {
assertThat(sharedCounters.get(task_num).getValue())
.isEqualTo(OVERWRITE_TWICE);
});
// SM step 8: all tasks try to commit their transacstion.
// Task k aborts if, and only if, counter k+1 was modified after it read it and transaction k+1 already committed.
addTestStep((task_num) -> {
try {
TXEnd();
commitStatus.set(task_num, COMMITVALUE);
} catch (TransactionAbortedException tae) {
// do nothing
}
});
if (testInterleaved)
scheduleInterleaved(PARAMETERS.CONCURRENCY_SOME, numTasks);
else
scheduleThreaded(PARAMETERS.CONCURRENCY_SOME, numTasks);
}
/**
* test multiple threads optimistically manipulating objects concurrently. This works as follows:
*
* there are 'numTasks' shared counters numbered 0..numTasks-1,
* and 'numTasks' tasks executing by an interleaving engine by CONCURRENCY_SOME number of threads.
*
* all counters are initialized to INITIAL.
*
* Each task is specified as a state machine.
* The state machine starts a transaction.
* Within each transaction, each task repeats twice modify, read own, read other.
*
* Specifically,
* - task j modifies counter j to OVERWRITE_ONCE,
* - task j reads counter j (own), expecting to read OVERWRITE_ONCE,
* - task j reads counter (j+1) mod n, expecting to read either OVERWRITE_ONCE or INITIAL,
*
* - then task j modifies counter (j+1) mod n to OVERWRITE_TWICE,
* - task j reads counter j+1 mod n (own), expecting to read OVERWRITE_TWICE,
* - task j reads counter j (the one it changed before), expecting to read OVERWRITE_ONCE ,
*
* Then all tasks try to commit their transasctions.
*/
void testRWConflicts(boolean testInterleaved)
throws Exception {
// populate numTasks and sharedCounters array
setupCounters();
assertThat(numTasks).isGreaterThan(1); // don't change concurrency to less than 2, test will break
// SM step 1: start an optimistic transaction
addTestStep((ignored_task_num) -> {
TXBegin();
});
// SM step 2: task k modify counter k
addTestStep((task_num) -> {
sharedCounters.get(task_num).setValue(OVERWRITE_ONCE);
});
// SM step 3: task k reads counter k+1
addTestStep((task_num) -> {
assertThat(sharedCounters.get((task_num + 1) % numTasks).getValue())
.isBetween(INITIAL, OVERWRITE_ONCE);
});
// SM step 4: task k verifies opacity, checking that it can read its own modified value of counter k
addTestStep((task_num) -> {
assertThat(sharedCounters.get(task_num).getValue())
.isEqualTo(OVERWRITE_ONCE);
});
// SM step 5: task k overwrites counter k+1
addTestStep((task_num) -> {
sharedCounters.get((task_num + 1) % numTasks).setValue(OVERWRITE_TWICE);
});
// SM step 6: task k again check opacity, reading its own modified value, this time of counter k+1
addTestStep((task_num) -> {
assertThat(sharedCounters.get((task_num + 1) % numTasks).getValue())
.isEqualTo(OVERWRITE_TWICE);
});
// SM step 7: each thread again verifies opacity, checking that it can re-read counter k
addTestStep((task_num) -> {
assertThat(sharedCounters.get(task_num).getValue())
.isEqualTo(OVERWRITE_ONCE);
});
// SM step 8: try to commit all transactions;
// task k aborts only if one or both of (k-1), (k+1) committed before it
addTestStep((task_num) -> {
try {
TXEnd();
commitStatus.set(task_num, COMMITVALUE);
} catch (TransactionAbortedException tae) {
// do nothing
}
});
// invoke the execution engine
if (testInterleaved)
scheduleInterleaved(PARAMETERS.CONCURRENCY_SOME, numTasks);
else
scheduleThreaded(PARAMETERS.CONCURRENCY_SOME, numTasks);
}
void testNoWriteConflictSimple() throws Exception {
final CorfuSharedCounter sharedCounter1 =
instantiateCorfuObject(CorfuSharedCounter.class, "test"+1);
final CorfuSharedCounter sharedCounter2 =
instantiateCorfuObject(CorfuSharedCounter.class, "test"+2);
commitStatus = new AtomicIntegerArray(2);
t(1, this::TXBegin);
t(2, this::TXBegin);
t(1, () -> { sharedCounter1.setValue(OVERWRITE_ONCE);});
t(2, () -> { sharedCounter2.setValue(OVERWRITE_ONCE);});
t(1, () -> sharedCounter1.getValue());
t(2, () -> sharedCounter2.getValue());
t(1, () -> sharedCounter2.getValue());
t(2, () -> sharedCounter1.getValue());
t(1, () -> { sharedCounter1.setValue(OVERWRITE_TWICE);});
t(2, () -> { sharedCounter2.setValue(OVERWRITE_TWICE);});
t(1, () -> sharedCounter1.getValue());
t(1, () -> sharedCounter2.getValue());
t(2, () -> sharedCounter2.getValue());
t(2, () -> sharedCounter1.getValue());
t(1, () -> {
try {
TXEnd();
commitStatus.set(0, COMMITVALUE);
} catch (TransactionAbortedException tae) {
// do nothing
}
});
t(2, () -> {
try {
TXEnd();
commitStatus.set(1, COMMITVALUE);
} catch (TransactionAbortedException tae) {
// do nothing
}
});
}
@Test
@SuppressWarnings("unchecked")
public void finegrainedUpdatesDoNotConflict()
throws Exception {
AtomicBoolean commitStatus = new AtomicBoolean(true);
Map<String, String> testMap = getMap();
Map<String, String> testMap2 = (Map<String, String>)
instantiateCorfuObject(
new TypeToken<SMRMap<String, String>>() {}, "test stream");
t(1, () -> TXBegin() );
t(1, () -> testMap.put("a", "a") );
t(1, () -> assertThat(testMap.put("a", "b"))
.isEqualTo("a") );
t(2, () -> TXBegin() );
t(2, () -> testMap2.put("b", "f") );
t(2, () -> assertThat(testMap2.put("b", "g"))
.isEqualTo("f") );
t(2, () -> TXEnd() );
t(1, () -> {
try {
TXEnd();
} catch (TransactionAbortedException te) {
commitStatus.set(false);
}
} );
// verify that both transactions committed successfully
assertThat(commitStatus.get())
.isTrue();
}
void getAbortTestSM() {
Random rand = new Random(PARAMETERS.SEED);
Map<String, String> testMap = getMap();
testMap.clear();
// state 0: start a transaction
addTestStep((ignored_task_num) -> TXBegin() );
// state 1: do some puts/gets
addTestStep( (task_num) -> {
// put to a task-exclusive entry
testMap.put(Integer.toString(task_num),
Integer.toString(task_num));
// do some gets arbitrarily at random
for (int i = 0; i < PARAMETERS.NUM_ITERATIONS_VERY_LOW; i++)
testMap.get(Integer.toString(rand.nextInt(PARAMETERS.NUM_ITERATIONS_MODERATE)));
});
// state 2 (final): ask to commit the transaction
addTestStep( (task_num) -> {
try {
TXEnd();
commitStatus.set(task_num, COMMITVALUE);
} catch (TransactionAbortedException tae) {
}
});
}
public void concurrentAbortTest(boolean testInterleaved)
throws Exception
{
final int numThreads = PARAMETERS.CONCURRENCY_SOME;
final int numRecords = PARAMETERS.NUM_ITERATIONS_VERY_LOW;
numTasks = numThreads*numRecords;
long startTime = System.currentTimeMillis();
commitStatus = new AtomicIntegerArray(numTasks);
getAbortTestSM();
// invoke the execution engine
if (testInterleaved)
scheduleInterleaved(numThreads, numTasks);
else
scheduleThreaded(numThreads, numTasks);
int aborts = 0;
for (int i = 0; i < numTasks; i++)
if (commitStatus.get(i) != COMMITVALUE)
aborts++;
// print stats..
calculateRequestsPerSecond("TPS", numRecords * numThreads, startTime);
calculateAbortRate(aborts, numRecords * numThreads);
}
}