use of org.cojen.tupl.UnmodifiableReplicaException 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.UnmodifiableReplicaException in project Tupl by cojen.
the class DatabaseReplicatorTest method standbyLeader.
@Test
public void standbyLeader() throws Exception {
// Test that a standby member can become an interim leader and prevent data loss.
Database[] dbs = startGroup(2, Role.STANDBY, null);
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");
leaderDb.suspendCheckpoints();
replicaDb.suspendCheckpoints();
leaderDb.checkpoint();
replicaDb.checkpoint();
byte[] key = "key".getBytes();
byte[] value = "value".getBytes();
leaderIx.store(null, key, value);
// Wait for replica to catch up.
fence(leaderDb, replicaDb);
fastAssertArrayEquals(value, replicaIx.load(null, key));
// Close and re-open leader, which doesn't have the key in the log. Must explicitly
// close the replicator, to ensure that closing the database doesn't flush the log.
mReplicators[0].close();
leaderDb = closeAndReopen(0);
leaderIx = leaderDb.openIndex("test");
byte[] found = null;
for (int i = 0; i < 100; i++) {
found = leaderIx.load(null, key);
if (found != null) {
break;
}
TestUtils.sleep(1000);
}
fastAssertArrayEquals(value, found);
value = "value!".getBytes();
for (int i = 0; i < 10; i++) {
try {
leaderIx.store(null, key, value);
break;
} catch (UnmodifiableReplicaException e) {
TestUtils.sleep(1000);
}
}
// Wait for replica to catch up.
fence(leaderDb, replicaDb);
fastAssertArrayEquals(value, replicaIx.load(null, key));
leaderDb.close();
}
use of org.cojen.tupl.UnmodifiableReplicaException in project Tupl by cojen.
the class DatabaseReplicatorTest method startGroup.
/**
* @return first is the leader
*/
private Database[] startGroup(int members, Role replicaRole, Supplier<PrepareHandler> handlerSupplier) throws Exception {
if (members < 1) {
throw new IllegalArgumentException();
}
mSockets = new ServerSocket[members];
for (int i = 0; i < members; i++) {
mSockets[i] = TestUtils.newServerSocket();
}
mReplBaseFiles = new File[members];
mReplConfigs = new ReplicatorConfig[members];
mReplicators = new StreamReplicator[members];
mDbConfigs = new DatabaseConfig[members];
mDatabases = new Database[members];
for (int i = 0; i < members; i++) {
mReplBaseFiles[i] = TestUtils.newTempBaseFile(getClass());
EventListener listener = false ? EventListener.printTo(System.out) : null;
mReplConfigs[i] = new ReplicatorConfig().groupToken(1).localSocket(mSockets[i]).baseFile(mReplBaseFiles[i]).eventListener(listener);
if (i > 0) {
mReplConfigs[i].addSeed(mSockets[0].getLocalSocketAddress());
mReplConfigs[i].localRole(replicaRole);
}
mReplicators[i] = StreamReplicator.open(mReplConfigs[i]);
((Controller) mReplicators[i]).keepServerSocket();
mDbConfigs[i] = new DatabaseConfig().baseFile(mReplBaseFiles[i]).replicate(mReplicators[i]).eventListener(listener).lockTimeout(5, TimeUnit.SECONDS).directPageAccess(false);
if (handlerSupplier != null) {
mDbConfigs[i].prepareHandlers(Map.of("TestHandler", handlerSupplier.get()));
}
Database db = Database.open(mDbConfigs[i]);
mDatabases[i] = db;
readyCheck: {
for (int trial = 0; trial < 100; trial++) {
TestUtils.sleep(100);
if (i == 0) {
try {
db.openIndex("control");
// Ensure that replicas obtain the index in the snapshot.
db.checkpoint();
break readyCheck;
} catch (UnmodifiableReplicaException e) {
// Not leader yet.
}
} else {
assertNotNull(db.openIndex("control"));
break readyCheck;
}
}
throw new AssertionError(i == 0 ? "No leader" : "Not joined");
}
}
return mDatabases;
}
Aggregations