/*
* Copyright © 2015 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.tephra.visibility;
import co.cask.tephra.TransactionAware;
import co.cask.tephra.TransactionContext;
import co.cask.tephra.TransactionFailureException;
import co.cask.tephra.TransactionSystemClient;
import java.util.concurrent.TimeoutException;
/**
* VisibilityFence is used to ensure that after a given point in time, all readers see an updated change
* that got committed.
* <p>
*
* 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.
*<p>
*
* 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.
* <p>
*
* 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.
*<p>
*
* 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.
* <p>
*
* 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.
*
* <p>
* Sample reader code:
* <pre>
* <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
* }
* </code>
* </pre>
*<p>
*
* Sample writer code:
* <pre>
* <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).
* }
* </code>
* </pre>
*
* 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.
*<p>
*
* 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.
*/
public final class VisibilityFence {
private VisibilityFence() {
// Cannot instantiate this class, all functionality is through static methods.
}
/**
* Used by a reader to get a fence that can be added to its transaction context.
*
* @param fenceId uniquely identifies the data that this fence is used to synchronize.
* If the data is a table cell then this id can be composed of the table name, row key
* and column key for the data.
* @return {@link TransactionAware} to be added to reader's transaction context.
*/
public static TransactionAware create(byte[] fenceId) {
return new ReadFence(fenceId);
}
/**
* Used by a writer to wait on a fence so that changes are visible to all readers with in-progress transactions.
*
* @param fenceId uniquely identifies the data that this fence is used to synchronize.
* If the data is a table cell then this id can be composed of the table name, row key
* and column key for the data.
* @return {@link FenceWait} object
*/
public static FenceWait prepareWait(byte[] fenceId, TransactionSystemClient txClient)
throws TransactionFailureException, InterruptedException, TimeoutException {
return new DefaultFenceWait(new TransactionContext(txClient, new WriteFence(fenceId)));
}
}