Search in sources :

Example 6 with PrepareHandler

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();
}
Also used : PrepareHandler(org.cojen.tupl.ext.PrepareHandler) LinkedBlockingQueue(java.util.concurrent.LinkedBlockingQueue)

Example 7 with PrepareHandler

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));
}
Also used : UnmodifiableReplicaException(org.cojen.tupl.UnmodifiableReplicaException) Transaction(org.cojen.tupl.Transaction) PrepareHandler(org.cojen.tupl.ext.PrepareHandler) Database(org.cojen.tupl.Database) Index(org.cojen.tupl.Index) LinkedBlockingQueue(java.util.concurrent.LinkedBlockingQueue)

Example 8 with PrepareHandler

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);
        }
    }
}
Also used : Index(org.cojen.tupl.Index) Cursor(org.cojen.tupl.Cursor) LinkedTransferQueue(java.util.concurrent.LinkedTransferQueue) UnmodifiableReplicaException(org.cojen.tupl.UnmodifiableReplicaException) LockTimeoutException(org.cojen.tupl.LockTimeoutException) IOException(java.io.IOException) Transaction(org.cojen.tupl.Transaction) PrepareHandler(org.cojen.tupl.ext.PrepareHandler) Database(org.cojen.tupl.Database)

Example 9 with PrepareHandler

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);
    }
}
Also used : PrepareHandler(org.cojen.tupl.ext.PrepareHandler)

Example 10 with PrepareHandler

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);
}
Also used : EventType(org.cojen.tupl.diag.EventType) PrepareHandler(org.cojen.tupl.ext.PrepareHandler) EventListener(org.cojen.tupl.diag.EventListener)

Aggregations

PrepareHandler (org.cojen.tupl.ext.PrepareHandler)12 LinkedBlockingQueue (java.util.concurrent.LinkedBlockingQueue)6 Database (org.cojen.tupl.Database)4 Index (org.cojen.tupl.Index)4 Transaction (org.cojen.tupl.Transaction)4 UnmodifiableReplicaException (org.cojen.tupl.UnmodifiableReplicaException)4 LockTimeoutException (org.cojen.tupl.LockTimeoutException)2 IOException (java.io.IOException)1 LinkedTransferQueue (java.util.concurrent.LinkedTransferQueue)1 Cursor (org.cojen.tupl.Cursor)1 EventListener (org.cojen.tupl.diag.EventListener)1 EventType (org.cojen.tupl.diag.EventType)1