use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class TxnPrepareTest method basicMix.
@Test
public void basicMix() throws Exception {
// Test that unprepared transactions don't get passed to the recover handler, testing
// also with multiple recovered transactions.
var recovered = new LinkedBlockingQueue<Transaction>();
var recovery = new PrepareHandler() {
volatile boolean prepareCommit;
@Override
public void prepare(Transaction txn, byte[] message) {
recovered.add(txn);
}
@Override
public void prepareCommit(Transaction txn, byte[] message) {
prepareCommit = true;
prepare(txn, message);
}
};
DatabaseConfig config = newConfig(recovery);
Database db = newTempDatabase(config);
PrepareHandler handler = db.prepareWriter("TestHandler");
Index ix = db.openIndex("test");
// Should rollback and not be passed to the handler.
Transaction txn1 = db.newTransaction();
ix.store(txn1, "key-1".getBytes(), "value-1".getBytes());
// Should be passed to the handler.
Transaction txn2 = db.newTransaction();
ix.store(txn2, "key-2".getBytes(), "value-2".getBytes());
prepare(handler, txn2, null);
// Should be passed to the handler.
Transaction txn3 = db.newTransaction();
ix.store(txn3, "key-3".getBytes(), "value-3".getBytes());
prepare(handler, txn3, null);
// Should rollback and not be passed to the handler.
Transaction txn4 = db.newTransaction();
ix.store(txn4, "key-4".getBytes(), "value-4".getBytes());
// Should commit and not be passed to the handler.
Transaction txn5 = db.newTransaction();
ix.store(txn5, "key-5".getBytes(), "value-5".getBytes());
prepare(handler, txn5, null);
txn5.commit();
// Should rollback and not be passed to the handler.
Transaction txn6 = db.newTransaction();
ix.store(txn6, "key-6".getBytes(), "value-6".getBytes());
prepare(handler, txn6, null);
txn6.exit();
db = reopenTempDatabase(getClass(), db, config);
handler = db.prepareWriter("TestHandler");
ix = db.openIndex("test");
Transaction t1 = recovered.take();
Transaction t2 = recovered.take();
assertTrue(recovered.isEmpty());
assertEquals(isPrepareCommit(), recovery.prepareCommit);
// Transactions can be recovered in any order.
if (t1.id() == txn2.id()) {
assertEquals(t2.id(), txn3.id());
} else {
assertEquals(t1.id(), txn3.id());
assertEquals(t2.id(), txn2.id());
}
// Rollback of txn1, txn4, and txn6 (unless prepareCommit)
assertNull(ix.load(null, "key-1".getBytes()));
assertNull(ix.load(null, "key-4".getBytes()));
if (isPrepareCommit()) {
fastAssertArrayEquals("value-6".getBytes(), ix.load(null, "key-6".getBytes()));
} else {
assertNull(ix.load(null, "key-6".getBytes()));
}
// Commit of txn5.
fastAssertArrayEquals("value-5".getBytes(), ix.load(null, "key-5".getBytes()));
// Recovered transactions are still locked (unless prepareCommit)
if (isPrepareCommit()) {
fastAssertArrayEquals("value-2".getBytes(), ix.load(null, "key-2".getBytes()));
fastAssertArrayEquals("value-3".getBytes(), ix.load(null, "key-3".getBytes()));
} else {
try {
ix.load(null, "key-2".getBytes());
fail();
} catch (LockTimeoutException e) {
}
try {
ix.load(null, "key-3".getBytes());
fail();
} catch (LockTimeoutException e) {
}
}
t1.reset();
t2.reset();
// Explicit locks should be recovered (exclusive only).
Transaction txn7 = db.newTransaction();
ix.lockUpgradable(txn7, "key-7".getBytes());
ix.lockUpgradable(txn7, "key-8".getBytes());
ix.lockExclusive(txn7, "key-9".getBytes());
ix.lockExclusive(txn7, "key-8".getBytes());
prepare(handler, txn7, null);
db.checkpoint();
db = reopenTempDatabase(getClass(), db, config);
ix = db.openIndex("test");
Transaction t7 = recovered.take();
assertEquals(t7.id(), txn7.id());
assertEquals(LockResult.UNOWNED, t7.lockCheck(ix.id(), "key-7".getBytes()));
if (isPrepareCommit()) {
assertEquals(LockResult.UNOWNED, t7.lockCheck(ix.id(), "key-8".getBytes()));
assertEquals(LockResult.UNOWNED, t7.lockCheck(ix.id(), "key-9".getBytes()));
} else {
assertEquals(LockResult.OWNED_EXCLUSIVE, t7.lockCheck(ix.id(), "key-8".getBytes()));
assertEquals(LockResult.OWNED_EXCLUSIVE, t7.lockCheck(ix.id(), "key-9".getBytes()));
}
Transaction txn8 = db.newTransaction();
assertEquals(LockResult.ACQUIRED, txn8.tryLockExclusive(ix.id(), "key-7".getBytes(), 0));
if (isPrepareCommit()) {
assertEquals(LockResult.ACQUIRED, txn8.tryLockShared(ix.id(), "key-8".getBytes(), 0));
assertEquals(LockResult.ACQUIRED, txn8.tryLockExclusive(ix.id(), "key-9".getBytes(), 0));
} else {
assertEquals(LockResult.TIMED_OUT_LOCK, txn8.tryLockShared(ix.id(), "key-8".getBytes(), 0));
assertEquals(LockResult.TIMED_OUT_LOCK, txn8.tryLockExclusive(ix.id(), "key-9".getBytes(), 0));
}
txn8.reset();
t7.reset();
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class DatabaseReplicatorTest method prepareTransferPingPong.
@Test
public void prepareTransferPingPong() throws Exception {
// Prepared transaction should be transferred to replica, back to old leader, and then
// finish.
var dbQueue = new LinkedBlockingQueue<Database>();
var txnQueue = new LinkedBlockingQueue<Transaction>();
var msgQueue = new LinkedBlockingQueue<byte[]>();
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);
msgQueue.add(message);
}
@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();
byte[] k1 = "k1".getBytes();
byte[] v1 = "v1".getBytes();
leaderIx.store(txn1, k1, v1);
PrepareHandler handler = leaderDb.prepareWriter("TestHandler");
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());
fastAssertArrayEquals(v1, replicaIx.load(txn2, k1));
byte[] k2 = "k2".getBytes();
byte[] v2 = "v2".getBytes();
replicaIx.store(txn2, k2, v2);
handler = replicaDb.prepareWriter("TestHandler");
try {
handler.prepare(txn2, null);
fail();
} catch (IllegalStateException e) {
// Already prepared.
}
replicaDb.failover();
try {
txn2.commit();
fail();
} catch (UnmodifiableReplicaException e) {
// This will unstick the transaction.
}
// Now the old leader is the leader again.
assertEquals(leaderDb, dbQueue.take());
Transaction txn3 = txnQueue.take();
assertNotEquals(txn1, txn3);
assertNotEquals(txn2, txn3);
assertEquals(txnId, txn3.id());
fastAssertArrayEquals("message".getBytes(), msgQueue.take());
fastAssertArrayEquals(v1, leaderIx.load(txn3, k1));
assertNull(leaderIx.load(txn3, k2));
byte[] k3 = "k3".getBytes();
byte[] v3 = "v3".getBytes();
leaderIx.store(txn3, k3, v3);
txn3.commit();
fence(leaderDb, replicaDb);
// Verify that leader and replica observe the committed changes. Note that v2 was
// rolled back, because when the replica was acting as leader, it was unable to commit
// v2. It tried to prepare the transaction again, but that's currently illegal.
fastAssertArrayEquals(v1, leaderIx.load(null, k1));
assertNull(leaderIx.load(null, k2));
fastAssertArrayEquals(v3, leaderIx.load(null, k3));
fastAssertArrayEquals(v1, replicaIx.load(null, k1));
assertNull(replicaIx.load(null, k2));
fastAssertArrayEquals(v3, replicaIx.load(null, k3));
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class DatabaseReplicatorTest method doTxnPrepare.
private void doTxnPrepare() throws Exception {
// Test that unfinished prepared transactions are passed to the new leader.
TransferQueue<Database> recovered = new LinkedTransferQueue<>();
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 {
try {
recovered.transfer(mDb);
// Wait for the signal...
recovered.take();
// Modify the value before committing.
Index ix = mDb.openIndex("test");
Cursor c = ix.newCursor(txn);
c.find("hello".getBytes());
byte[] newValue = Arrays.copyOfRange(c.value(), 0, c.value().length + 1);
newValue[newValue.length - 1] = '!';
c.store(newValue);
c.reset();
txn.commit();
} catch (Exception e) {
throw Utils.rethrow(e);
}
}
@Override
public void prepareCommit(Transaction txn, byte[] message) throws IOException {
prepare(txn, message);
}
};
final int memberCount = 3;
Database[] dbs = startGroup(memberCount, Role.NORMAL, supplier);
Index ix0 = dbs[0].openIndex("test");
// Wait for all members to be electable.
allElectable: {
int count = 0;
for (int i = 0; i < 100; i++) {
count = 0;
for (StreamReplicator repl : mReplicators) {
if (repl.localRole() == Role.NORMAL) {
count++;
}
}
if (count >= mReplicators.length) {
break allElectable;
}
TestUtils.sleep(100);
}
fail("Not all members are electable: " + count);
}
Transaction txn = dbs[0].newTransaction();
PrepareHandler handler = dbs[0].prepareWriter("TestHandler");
byte[] key = "hello".getBytes();
ix0.store(txn, key, "world".getBytes());
handler.prepare(txn, null);
// Close the leader and verify handoff.
dbs[0].close();
Database db = recovered.take();
assertNotEquals(dbs[0], db);
// Still locked.
Index ix = db.openIndex("test");
txn = db.newTransaction();
assertEquals(LockResult.TIMED_OUT_LOCK, ix.tryLockShared(txn, key, 0));
txn.reset();
// Signal that the handler can finish the transaction.
recovered.add(db);
assertArrayEquals("world!".getBytes(), ix.load(null, key));
// Verify replication.
Database remaining = dbs[1];
if (remaining == db) {
remaining = dbs[2];
}
ix = remaining.openIndex("test");
for (int i = 10; --i >= 0; ) {
try {
assertArrayEquals("world!".getBytes(), ix.load(null, key));
break;
} catch (Throwable e) {
if (i <= 0) {
throw e;
}
TestUtils.sleep(100);
}
}
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class TxnPrepareTest method topLevelOnly.
@Test
public void topLevelOnly() throws Exception {
Database db = newTempDatabase(newConfig(new NonHandler()));
PrepareHandler handler = db.prepareWriter("TestHandler");
Transaction txn = db.newTransaction();
txn.enter();
try {
prepare(handler, txn, null);
fail();
} catch (IllegalStateException e) {
assertTrue(e.getMessage().indexOf("nested") > 0);
}
}
use of org.cojen.tupl.ext.PrepareHandler in project Tupl by cojen.
the class TxnPrepareTest method reopenNoHandler.
@Test
public void reopenNoHandler() throws Exception {
// When database is reopened without a recovery handler, the recovered transactions
// aren't lost.
var recovery = new PrepareHandler() {
volatile byte[] message;
volatile boolean prepareCommit;
@Override
public void prepare(Transaction txn, byte[] message) throws IOException {
this.message = message;
txn.commit();
}
@Override
public void prepareCommit(Transaction txn, byte[] message) throws IOException {
prepareCommit = true;
prepare(txn, message);
}
};
DatabaseConfig config = newConfig(recovery);
Database db = newTempDatabase(config);
PrepareHandler handler = db.prepareWriter("TestHandler");
Index ix = db.openIndex("test");
Transaction txn = db.newTransaction();
byte[] key = "hello".getBytes();
ix.store(txn, key, "world".getBytes());
prepare(handler, txn, "message".getBytes());
// Reopen without the handler.
config.prepareHandlers(null);
// Install a listener which is notified that recovery fails.
var listener = new EventListener() {
private boolean notified;
@Override
public void notify(EventType type, String message, Object... args) {
if (type == EventType.RECOVERY_HANDLER_UNCAUGHT) {
synchronized (this) {
notified = true;
notifyAll();
}
}
}
synchronized void waitForNotify() throws InterruptedException {
while (!notified) {
wait();
}
}
};
config.eventListener(listener);
db = reopenTempDatabase(getClass(), db, config);
// Still locked (unless prepareCommit)
ix = db.openIndex("test");
txn = db.newTransaction();
if (isPrepareCommit()) {
assertEquals(LockResult.ACQUIRED, ix.tryLockShared(txn, key, 0));
} else {
assertEquals(LockResult.TIMED_OUT_LOCK, ix.tryLockShared(txn, key, 0));
}
txn.reset();
listener.waitForNotify();
// Reopen with the handler installed.
config.prepareHandlers(Map.of("TestHandler", recovery));
config.eventListener(null);
recovery.message = null;
recovery.prepareCommit = false;
db = reopenTempDatabase(getClass(), db, config);
// Verify that the handler has committed the recovered transaction.
ix = db.openIndex("test");
fastAssertArrayEquals("world".getBytes(), ix.load(null, key));
byte[] message = recovery.message;
if (message == null && isPrepareCommit()) {
// Wait for it since no lock was held.
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
message = recovery.message;
if (message != null) {
break;
}
}
}
fastAssertArrayEquals("message".getBytes(), recovery.message);
assertEquals(isPrepareCommit(), recovery.prepareCommit);
}
Aggregations