VisibilityFence is used to ensure that after a given point in time, all readers see an updated change
that got committed.
Typically a reader will never conflict with a writer, since a reader only sees committed changes when its
transaction started. However to ensure that after a given point all readers are aware of a change,
we have to introduce a conflict between a reader and a writer that act on the same data concurrently.
This is done by the reader indicating that it is interested in changes to a piece of data by using a fence
in its transaction. If there are no changes to the data when reader tries to commit the transaction
containing the fence, the commit succeeds.
On the other hand, a writer updates the same data in a transaction. After the write transaction is committed,
the writer then waits on the fence to ensure that all in-progress readers are aware of this update.
When the wait on the fence returns successfully, it means that
any in-progress readers that have not seen the change will not be allowed to commit anymore. This will
force the readers to start a new transaction, and this ensures that the changes made by writer are visible
to the readers.
In case an in-progress reader commits when the writer is waiting on the fence, then the wait method will retry
until the given timeout.
Hence a successful await on a fence ensures that any reader (using the same fence) that successfully commits after
this point onwards would see the change.
Sample reader code:
TransactionAware fence = VisibilityFence.create(fenceId);
TransactionContext readTxContext = new TransactionContext(txClient, fence, table1, table2, ...);
readTxContext.start();
// do operations using table1, table2, etc.
// finally commit
try {
readTxContext.finish();
} catch (TransactionConflictException e) {
// handle conflict by aborting and starting over with a new transaction
}
Sample writer code:
// start transaction
// write change
// commit transaction
// Now wait on the fence (with the same fenceId as the readers) to ensure that all in-progress readers are
aware of this change
try {
FenceWait fenceWait = VisibilityFence.prepareWait(fenceId, txClient);
fenceWait.await(50000, 50, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
// await timed out, the change may not be visible to all in-progress readers.
// Application has two options at this point:
// 1. Revert the write. Re-try the write and fence wait again.
// 2. Retry only the wait with the same fenceWait object (retry logic is not shown here).
}
fenceId in the above samples refers to any id that both the readers and writer know for a given
piece of data. Both readers and writer will have to use the same fenceId to synchronize on a given data.
Typically fenceId uniquely identifies the data in question.
For example, if the data is a table row, the fenceId can be composed of table name and row key.
If the data is a table cell, the fenceId can be composed of table name, row key, and column key.
Note that in this implementation, any reader that starts a transaction after the write is committed, and
while this read transaction is in-progress, if a writer successfully starts and completes an await on the fence then
this reader will get a conflict while committing the fence even though this reader has seen the latest changes.
This is because today there is no way to determine the commit time of a transaction.