Search in sources :

Example 1 with PrepareHandler

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

Example 2 with PrepareHandler

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));
}
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 3 with PrepareHandler

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

Example 4 with PrepareHandler

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

Example 5 with PrepareHandler

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

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