Consistency guarantees in case of reusing committed versions

Hi folks, I was wondering if I’m correct in understanding the FDB isolation guarantees.

Let’s say that given the next code, can I assume that the secondTx will always result in a conflict?

Transaction firstTx = db.createTransaction();
firstTx.set(dataKey, payload);
firstTx.addReadConflictKey(fencingTokenKey);
CompletableFuture<Void> firstTxCompleted = tx.commit();
firstTxCompleted.get(); // wait for the future to be completed

db.run { tx ->
  tx.set(fencingTokenKey, newTokenValue);
}

Transaction secondTx = db.createTransaction();
secondTx.setReadVersion(firstTx.getCommittedVersion()); // reuse the version
secondTx.set(dataKey, payload2);
secondTx.addReadConflictKey(fencingTokenKey);
secondTx.commit();

Will it also result in a conflict if we update the fencing token while secondTx is it progress?

If your first two transactions are successful, then secondTx would conflict. The middle transaction would have a commit version that comes after the commit version of firstTx (and consequently the read version of secondTx). Because secondTx depends on fencingTokenKey and that key changes between its read and potential commit versions, the transaction cannot succeed.

If the middle transaction runs simultaneously with secondTx, then it is possible for secondTx to succeed. This could happen if secondTx commits first (i.e. with a lower commit version) than the middle transaction. In that case, it’s as if the modification to fencingTokenKey doesn’t occur until after secondTx is done. However, if it is certain that the middle transaction commits first, then secondTx will fail.

1 Like

I think it is possible for all three transactions succeed if:

  1. The secondTx runs in concurrent with the middle transaction db.run { tx -> tx.set(fencingTokenKey, newTokenValue); }
  2. The middle transaction starts after the middle transaction has been committed.

The three transactions will be serialized as:
firstTx, secondTx, middle transaction.

It still follows the Serializable Snapshot Isolation model.

I’m not certain which constraint was intended here (I’m guessing middle starts after secondTx), but I don’t think it matters when these two transactions start, only when they commit. This is because secondTx is being given an explicit read version regardless of when it starts, and the middle transaction has no reads and can’t be failed due to conflict.

The thing that matters is which of the two transactions commits first. If the middle transaction commits first, then we can’t commit secondTx because of the change to fencingTokenKey that occurred between secondTx’s read version and when it’s trying to commit. If secondTx commits first, then it can succeed and the middle transaction can commit afterward because it had no reads.

1 Like

[Not directly related but an observation]

If the middle transaction commits before the secondTx, the secondTx will never succeed, right?

If the secondTx is in a try-catch loop, it will cause infinitely loop, right?:thinking:

That’s correct, but I think the reason will eventually change once the 5-second window has passed. At first it will repeatedly fail with a conflict (i.e. not_committed), but after the read version becomes more than 5 seconds old, it will start to fail with transaction_too_old.

1 Like

Thank you @ajbeamon for the great explanation!