use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class DatabaseReplicatorTest method prepareTransfer.
private void prepareTransfer(boolean prepareCommit) throws Exception {
// Prepared transaction should be transferred to replica and finish.
var dbQueue = new LinkedBlockingQueue<Database>();
var txnQueue = new LinkedBlockingQueue<Transaction>();
Supplier<PrepareHandler> supplier = () -> new PrepareHandler() {
private Database mDb;
@Override
public void init(Database db) {
mDb = db;
}
@Override
public void prepare(Transaction txn, byte[] message) throws IOException {
dbQueue.add(mDb);
txnQueue.add(txn);
}
@Override
public void prepareCommit(Transaction txn, byte[] message) throws IOException {
prepare(txn, message);
}
};
Database[] dbs = startGroup(2, Role.NORMAL, supplier);
Database leaderDb = dbs[0];
Database replicaDb = dbs[1];
Index leaderIx = leaderDb.openIndex("test");
// Wait for replica to catch up.
fence(leaderDb, replicaDb);
Index replicaIx = replicaDb.openIndex("test");
Transaction txn1 = leaderDb.newTransaction();
PrepareHandler handler = leaderDb.prepareWriter("TestHandler");
byte[] k1 = "k1".getBytes();
byte[] v1 = "v1".getBytes();
leaderIx.store(txn1, k1, v1);
if (prepareCommit) {
handler.prepareCommit(txn1, null);
fastAssertArrayEquals(v1, leaderIx.load(null, k1));
} else {
handler.prepare(txn1, null);
try {
leaderIx.load(null, k1);
fail();
} catch (LockTimeoutException e) {
}
}
leaderDb.failover();
// Replica is now the leader and should have the transaction.
assertEquals(replicaDb, dbQueue.take());
Transaction txn2 = txnQueue.take();
assertNotEquals(txn1, txn2);
assertEquals(txn1.id(), txn2.id());
fastAssertArrayEquals(v1, replicaIx.load(txn2, k1));
byte[] k2 = "k2".getBytes();
byte[] v2 = "v2".getBytes();
replicaIx.store(txn2, k2, v2);
if (prepareCommit) {
fastAssertArrayEquals(v1, replicaIx.load(null, k1));
} else {
try {
replicaIx.load(null, k1);
fail();
} catch (LockTimeoutException e) {
}
}
txn2.commit();
// Wait for old leader to catch up. This will fail at first because the old leader
// transaction is stuck.
boolean pass = true;
try {
fence(replicaDb, leaderDb, true);
pass = false;
} catch (AssertionError e) {
}
assertTrue(pass);
try {
txn1.commit();
fail();
} catch (UnmodifiableReplicaException e) {
// This will unstick the transaction.
}
fence(replicaDb, leaderDb);
// Verify that the old leader observes the committed changes.
fastAssertArrayEquals(v1, leaderIx.load(null, k1));
fastAssertArrayEquals(v2, leaderIx.load(null, k2));
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class DatabaseReplicatorTest method prepareBlank.
private void prepareBlank(boolean prepareCommit) throws Exception {
// Test against a prepared transaction that has no changes. It should still ensure that
// the transaction is created properly on the replica.
var dbQueue = new LinkedBlockingQueue<Database>();
var txnQueue = new LinkedBlockingQueue<Transaction>();
var msgQueue = new LinkedBlockingQueue<byte[]>();
var pcQueue = new LinkedBlockingQueue<Boolean>();
Supplier<PrepareHandler> supplier = () -> new PrepareHandler() {
private Database mDb;
@Override
public void init(Database db) {
mDb = db;
}
@Override
public void prepare(Transaction txn, byte[] message) throws IOException {
doPrepare(txn, message, false);
}
@Override
public void prepareCommit(Transaction txn, byte[] message) throws IOException {
doPrepare(txn, message, true);
}
private void doPrepare(Transaction txn, byte[] message, boolean commit) throws IOException {
dbQueue.add(mDb);
txnQueue.add(txn);
msgQueue.add(message);
pcQueue.add(commit);
}
};
Database[] dbs = startGroup(2, Role.NORMAL, supplier);
Database leaderDb = dbs[0];
Database replicaDb = dbs[1];
Index leaderIx = leaderDb.openIndex("test");
// Wait for replica to catch up.
fence(leaderDb, replicaDb);
Index replicaIx = replicaDb.openIndex("test");
Transaction txn1 = leaderDb.newTransaction();
PrepareHandler handler = leaderDb.prepareWriter("TestHandler");
if (prepareCommit) {
handler.prepareCommit(txn1, "message".getBytes());
} else {
handler.prepare(txn1, "message".getBytes());
}
leaderDb.failover();
// Must capture the id before it gets replaced.
long txnId = txn1.id();
try {
txn1.commit();
fail();
} catch (UnmodifiableReplicaException e) {
// This will unstick the transaction.
}
// Replica is now the leader and should have the transaction.
assertEquals(replicaDb, dbQueue.take());
Transaction txn2 = txnQueue.take();
assertNotEquals(txn1, txn2);
assertEquals(txnId, txn2.id());
fastAssertArrayEquals("message".getBytes(), msgQueue.take());
assertEquals(prepareCommit, pcQueue.take());
byte[] k1 = "k1".getBytes();
byte[] v1 = "v1".getBytes();
replicaIx.store(txn2, k1, v1);
txn2.commit();
fence(replicaDb, leaderDb);
// Verify that the old leader observes the committed changes.
fastAssertArrayEquals(v1, leaderIx.load(null, k1));
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class TxnPrepareTest method noRedo.
@Test
public void noRedo() throws Exception {
var recovery = new NonHandler();
Database db = newTempDatabase(newConfig(recovery));
PrepareHandler handler = db.prepareWriter("TestHandler");
try {
prepare(handler, Transaction.BOGUS, null);
fail();
} catch (IllegalArgumentException e) {
}
Transaction txn = db.newTransaction();
txn.durabilityMode(DurabilityMode.NO_REDO);
try {
prepare(handler, txn, null);
fail();
} catch (IllegalStateException e) {
}
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class TxnPrepareTest method basicRecovery.
private void basicRecovery(String recoveryType, String recoveryAction) throws Exception {
byte[] key1 = "key-1".getBytes();
byte[] key2 = "key-2".getBytes();
class Recovered {
final long mTxnId;
final Transaction mTxn;
final byte[] mMessage;
Recovered(Transaction txn, byte[] message) {
// Capture the transaction id before the transaction is reset.
mTxnId = txn.id();
mTxn = txn;
mMessage = message;
}
}
var recoveredQueue = new LinkedBlockingQueue<Recovered>();
var recovery = new PrepareHandler() {
private Database db;
volatile boolean prepareCommit;
private boolean finished;
@Override
public void init(Database db) {
this.db = db;
}
@Override
public void prepare(Transaction txn, byte[] message) throws IOException {
recoveredQueue.add(new Recovered(txn, message));
switch(recoveryAction) {
default:
// Leak the transaction and keep the locks.
break;
case "modify-reset":
db.findIndex("test1").store(txn, key1, "modified-1".getBytes());
db.findIndex("test2").store(txn, key2, "modified-2".getBytes());
case "reset":
txn.reset();
break;
case "modify-commit":
db.findIndex("test1").store(txn, key1, "modified-1".getBytes());
db.findIndex("test2").store(txn, key2, "modified-2".getBytes());
case "commit":
txn.commit();
break;
}
synchronized (this) {
finished = true;
notifyAll();
}
}
@Override
public void prepareCommit(Transaction txn, byte[] message) throws IOException {
prepareCommit = true;
prepare(txn, message);
}
synchronized void reset() {
prepareCommit = false;
finished = false;
}
synchronized void waitUntilFinished() throws Exception {
while (!finished) {
wait();
}
}
};
DatabaseConfig config = newConfig(recovery);
Database db = newTempDatabase(config);
PrepareHandler handler = db.prepareWriter("TestHandler");
long txnId;
{
Index ix1 = db.openIndex("test1");
Index ix2 = db.openIndex("test2");
ix1.store(null, key1, "v1".getBytes());
ix2.store(null, key2, "v2".getBytes());
Transaction txn = db.newTransaction();
ix1.store(txn, key1, "value-1".getBytes());
if ("redo-undo".equals(recoveryType)) {
db.checkpoint();
// Suppress later assertion.
recoveryType = "redo";
}
ix2.store(txn, key2, "value-2".getBytes());
if ("undo".equals(recoveryType)) {
db.checkpoint();
} else if (!"redo".equals(recoveryType)) {
fail("Unknown recovery type: " + recoveryType);
}
txnId = txn.id();
prepare(handler, txn, null);
}
for (int i = 0; i < 3; i++) {
recovery.reset();
db = reopenTempDatabase(getClass(), db, config);
Recovered recovered = recoveredQueue.take();
assertEquals(txnId, recovered.mTxnId);
assertTrue(recoveredQueue.isEmpty());
assertEquals(isPrepareCommit(), recovery.prepareCommit);
recovery.waitUntilFinished();
Index ix1 = db.openIndex("test1");
Index ix2 = db.openIndex("test2");
switch(recoveryAction) {
default:
fail("Unknown recovery action: " + recoveryAction);
break;
case "none":
case "sticky":
// Locks are retained, unless using prepareCommit.
if (isPrepareCommit()) {
fastAssertArrayEquals("value-1".getBytes(), ix1.load(null, key1));
fastAssertArrayEquals("value-2".getBytes(), ix2.load(null, key2));
} else {
try {
ix1.load(null, key1);
fail();
} catch (LockTimeoutException e) {
// Expected.
}
try {
ix2.load(null, key2);
fail();
} catch (LockTimeoutException e) {
// Expected.
}
}
if ("sticky".equals(recoveryAction)) {
break;
}
recovered.mTxn.reset();
case "reset":
case "modify-reset":
// Everything was rolled back, unless using prepareCommit.
if (isPrepareCommit()) {
fastAssertArrayEquals("value-1".getBytes(), ix1.load(null, key1));
fastAssertArrayEquals("value-2".getBytes(), ix2.load(null, key2));
} else {
fastAssertArrayEquals("v1".getBytes(), ix1.load(null, key1));
fastAssertArrayEquals("v2".getBytes(), ix2.load(null, key2));
}
break;
case "modify-commit":
// Everything was modified and committed.
fastAssertArrayEquals("modified-1".getBytes(), ix1.load(null, key1));
fastAssertArrayEquals("modified-2".getBytes(), ix2.load(null, key2));
break;
case "commit":
// Everything was committed.
fastAssertArrayEquals("value-1".getBytes(), ix1.load(null, key1));
fastAssertArrayEquals("value-2".getBytes(), ix2.load(null, key2));
break;
}
if (!"sticky".equals(recoveryAction)) {
break;
}
// Transaction should stick around each time the database is reopened.
}
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class TxnPrepareTest method simpleCommit.
@Test
public void simpleCommit() throws Exception {
Database db = newTempDatabase(newConfig(new NonHandler()));
PrepareHandler handler = db.prepareWriter("TestHandler");
Index ix = db.openIndex("test");
byte[] key = "hello".getBytes();
byte[] value = "world".getBytes();
byte[] k2 = "k2".getBytes();
byte[] k3 = "k3".getBytes();
Transaction txn = db.newTransaction();
ix.store(txn, key, value);
assertEquals(LockResult.OWNED_EXCLUSIVE, txn.lockCheck(ix.id(), key));
assertEquals(LockResult.ACQUIRED, ix.lockShared(txn, k2));
assertEquals(LockResult.OWNED_SHARED, txn.lockCheck(ix.id(), k2));
ix.load(txn, k3);
assertEquals(LockResult.OWNED_UPGRADABLE, txn.lockCheck(ix.id(), k3));
prepare(handler, txn, null);
if (isPrepareCommit()) {
assertEquals(LockResult.UNOWNED, txn.lockCheck(ix.id(), key));
fastAssertArrayEquals(value, ix.load(null, key));
} else {
assertEquals(LockResult.OWNED_EXCLUSIVE, txn.lockCheck(ix.id(), key));
}
assertEquals(LockResult.UNOWNED, txn.lockCheck(ix.id(), k2));
assertEquals(LockResult.UNOWNED, txn.lockCheck(ix.id(), k3));
txn.commit();
fastAssertArrayEquals(value, ix.load(null, key));
}
Aggregations