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.junit.Test;
import java.util.ArrayList;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Created by dalia on 3/6/17.
*/
public class UndoTest extends AbstractTransactionsTest {
@Override
public void TXBegin() { WWTXBegin(); }
@Test
public void ckCorrectUndo()
throws Exception {
Map<String, String> testMap = getRuntime()
.getObjectsView()
.build()
.setStreamName("test")
.setTypeToken(new TypeToken<SMRMap<String, String>>() {
})
.open();
// populate the map with an element
t(0, () -> testMap.put("z", "z"));
// start a transaction whose snapshot of the map
// should contain precisely one element, ("z", "z")
//
t(0, () -> getRuntime().getObjectsView().TXBegin());
t(0, () -> {
assertThat(testMap.get("z"))
.isEqualTo("z");
testMap.put("a", "a");
});
// in another thread, do something to be undone
t(1, () -> {
assertThat(testMap.get("z"))
.isEqualTo("z");
testMap.put("y", "y");
});
// now check the map inside the transaction
// it should contain two elements, ("z", "z") and ("a", "a")
// it should not contain ("y", "y")
t(0, () -> {
assertThat(testMap.get("z"))
.isEqualTo("z");
assertThat(testMap.get("y"))
.isEqualTo(null);
assertThat(testMap.size())
.isEqualTo(2);
});
}
@Test
public void canRollbackWithoutUndo()
throws Exception {
Map<String, String> testMap = getRuntime()
.getObjectsView()
.build()
.setStreamName("test")
.setTypeToken(new TypeToken<SMRMap<String, String>>() {
})
.open();
// populate the map with an element
t(0, () -> testMap.put("z", "z"));
// start a transaction whose snapshot of the map
// should contain precisely one element, ("z", "z")
//
t(0, () -> getRuntime().getObjectsView().TXBegin());
t(0, () -> {
assertThat(testMap.get("z"))
.isEqualTo("z");
testMap.put("a", "a");
});
// in another thread, do something to the map that cannot be undone
t(1, () -> {
assertThat(testMap.get("z"))
.isEqualTo("z");
testMap.put("y", "y");
testMap.clear();
assertThat(testMap.size())
.isEqualTo(0);
assertThat(testMap.get("z"))
.isEqualTo(null);
});
// now check the map inside the transaction
// it should contain two elements, ("z", "z") and ("a", "a")
// it should not contain ("y", "y")
// it should not be clear
t(0, () -> {
assertThat(testMap.get("z"))
.isEqualTo("z");
assertThat(testMap.get("y"))
.isEqualTo(null);
assertThat(testMap.size())
.isEqualTo(2);
});
}
/**
* Check that optimisitcUndoable is properly reset.
*
* An irreversible modification causes a total object-rebuild.
* <p>
* This test verifies that after one transaction with undoable operation is rolled- back,
* the performance of further transactions is not hurt.
*
* @throws Exception
*/
@Test
public void canUndoAfterNoUndo()
throws Exception {
Map<Integer, String> testMap = getRuntime()
.getObjectsView()
.build()
.setStreamName("test")
.setTypeToken(new TypeToken<SMRMap<Integer, String>>() {
})
.open();
final int specialKey = 10;
final String normalValue = "z", specialValue = "y";
final int mapSize = 10 * PARAMETERS.NUM_ITERATIONS_LARGE;
// populate the map with many elements
for (int i = 0; i < mapSize; i++)
testMap.put(i, normalValue);
// start a transaction after the map was built
t(0, () -> {
getRuntime().getObjectsView().TXBegin();
assertThat(testMap.get(specialKey))
.isEqualTo(normalValue);
});
// in another thread, optimistically do something to the map that cannot be undone
t(1, () -> {
getRuntime().getObjectsView().TXBegin();
testMap.clear();
assertThat(testMap.size())
.isEqualTo(0);
assertThat(testMap.get(specialKey))
.isEqualTo(null);
});
// check how long it takes to rebuild the map for the first thread
t(0, () -> {
long startTime, endTime;
startTime = System.currentTimeMillis();
testMap.get(specialKey);
endTime = System.currentTimeMillis();
if (!testStatus.equals("")) {
testStatus += ";";
}
testStatus += "reset rebuild time="
+ String.format("%.0f", (float)(endTime-startTime))
+ "ms";
});
// abort the bad transaction,
// and start a new one that is easily undone
t(1, () -> {
getRuntime().getObjectsView().TXAbort();
assertThat(testMap.size())
.isEqualTo(mapSize);
getRuntime().getObjectsView().TXBegin();
testMap.put(specialKey, specialValue);
assertThat(testMap.get(specialKey))
.isEqualTo(specialValue);
});
// now , re-take that measurement causing only the simple undo
t(0, () -> {
long startTime, endTime;
startTime = System.currentTimeMillis();
assertThat(testMap.get(specialKey))
.isEqualTo(normalValue);
endTime = System.currentTimeMillis();
if (!testStatus.equals("")) {
testStatus += ";";
}
testStatus += "undo rebuild time=" +
String.format("%.0f", (float)(endTime-startTime))
+ "ms";
});
}
@Test
public void ckRollbackToRightPlace()
throws Exception {
Map<Integer, String> testMap = getRuntime()
.getObjectsView()
.build()
.setStreamName("test")
.setTypeToken(new TypeToken<SMRMap<Integer, String>>() {
})
.open();
final int specialKey = 10;
final String normalValue = "z", specialValue = "y";
final int mapSize = 10 * PARAMETERS.NUM_ITERATIONS_LARGE;
// put something in map before t1 starts
WWTXBegin();
testMap.put(specialKey, normalValue);
TXEnd();
// t1 starts transaction. snapshot should include the key inserted above
t(1, () -> WWTXBegin());
// another update to the entry is committed while TXs are pending on
// both t1 and t2
WWTXBegin();
testMap.putIfAbsent(specialKey, specialValue);
TXEnd();
// t2 starts a transaction. snapshot should include the second
// update to the key.
//
// t2 attempts to remove it, and commits.
t(2, () -> {
WWTXBegin();
testMap.remove(specialKey);
TXEnd();
});
// now t1 resume t1 and try to commit
t(1, () -> {
assertThat(testMap.get(specialKey))
.isEqualTo(normalValue);
TXEnd();
});
}
final int specialKey = 10;
final String normalValue = "z", specialValue = "y", specialValue2 = "x";
final int t1 = 1, t2 = 2, t3 = 3;
/**
* In this test, transactions are started on two threads, 1, 2.
* Then two things happen:
*
* 1. some updates are committed
* then t1 resumes and should roll back these commits.
*
* 2. then t2 is resumed and makes optimistic updates, which should roll
* back
* @throws Exception
*/
@Test
public void ckMultiStreamRollback()
throws Exception {
ArrayList<Map> maps = new ArrayList<>();
final int nmaps = 3;
for (int i = 0; i < nmaps; i++)
maps.add( (SMRMap<Integer, String>) instantiateCorfuObject(
new TypeToken<SMRMap<Integer, String>>() {}, "test stream" + i)
);
// before t1 starts
crossStream(maps, normalValue);
// t1 starts transaction.
// snapshot should include all the keys inserted above
t(t1, () -> {
WWTXBegin();
maps.get(0).size(); // size() is called to make the TX obtains a snapshot at this point,
// and does not wait to lazily obtain it later, when it reads for
// the first time
});
// t2 starts transaction.
t(t2, () -> {
WWTXBegin();
maps.get(0).size(); // size() is called to make the TX obtains a snapshot at this point,
// and does not wait to lazily obtain it later, when it reads for
// the first time
});
// t3 modifies everything
t(t3, () -> crossStream(maps, specialValue));
// t1 should undo everything by t2 and by t3
t(t1, () -> {
for (Map m : maps) {
assertThat(m.get(specialKey))
.isEqualTo(normalValue);
assertThat(m.get(specialKey+1))
.isEqualTo(normalValue);
}
});
// now, t2 optimistically modifying everything, but
// not yet committing
t(t2, () -> {
for (Map m : maps)
m.put(specialKey, specialValue2);
} );
// main thread, t2's work should be committed
for (Map m : maps) {
assertThat(m.get(specialKey))
.isEqualTo(specialValue);
assertThat(m.get(specialKey + 1))
.isEqualTo(specialValue);
}
// now, try to commit t2
t(t2, () -> {
boolean aborted = false;
try {
TXEnd();
} catch (TransactionAbortedException te) {
aborted = true;
}
assertThat(aborted);
});
// back to main thread, t2's work should be committed
for (Map m : maps) {
assertThat(m.get(specialKey))
.isEqualTo(specialValue);
assertThat(m.get(specialKey + 1))
.isEqualTo(specialValue);
}
}
/**
* In this test, a variant of multi-stream interleaving is
* tested.
*
* transactions are started on two threads, 1, 2.
* Then two things happen:
*
* 1. some updates are committed
* 2. then t2 is resumed and makes optimistic updates
*
*
* then t1 resumes and should roll back both the optimistic updates
* adn these commits.
*
* @throws Exception
*/
@Test
public void ckMultiStreamRollback2()
throws Exception {
ArrayList<Map> maps = new ArrayList<>();
final int nmaps = 3;
for (int i = 0; i < nmaps; i++)
maps.add( (SMRMap<Integer, String>) instantiateCorfuObject(
new TypeToken<SMRMap<Integer, String>>() {}, "test stream" + i)
);
// before t1 starts
crossStream(maps, normalValue);
// t1 starts transaction.
// snapshot should include all the keys inserted above
t(t1, () -> {
WWTXBegin();
maps.get(0).size(); // size() is called to make the TX obtains a snapshot at this point,
// and does not wait to lazily obtain it later, when it reads for
// the first time
});
// t2 starts transaction.
t(t2, () -> {
WWTXBegin();
maps.get(0).size(); // size() is called to make the TX obtains a snapshot at this point,
// and does not wait to lazily obtain it later, when it reads for
// the first time
});
// t3 modifies everything
t(t3, () -> crossStream(maps, specialValue));
// now, t2 optimistically modifying everything, but
// not yet committing
t(t2, () -> {
for (Map m : maps)
m.put(specialKey, specialValue2);
} );
// t1 should undo everything by t2 and by t3
t(t1, () -> {
for (Map m : maps) {
assertThat(m.get(specialKey))
.isEqualTo(normalValue);
assertThat(m.get(specialKey+1))
.isEqualTo(normalValue);
}
});
// main thread, t2's work should be committed
for (Map m : maps) {
assertThat(m.get(specialKey))
.isEqualTo(specialValue);
assertThat(m.get(specialKey + 1))
.isEqualTo(specialValue);
}
}
protected void crossStream(ArrayList<Map> maps, String value) {
// put a transaction across all streams
WWTXBegin();
for (Map m : maps)
m.put(specialKey, value);
TXEnd();
// put separate updates on all streams before t1 starts
for (Map m : maps)
m.put(specialKey + 1, value);
}
}