A global {@link Locker} that resolves inter-thread lock contention via{@link AbstractLocker} and resolves inter-process contention by reading andwriting lock data using {@link KeyColumnValueStore}.
Protocol and internals
Locking is done in two stages: first between threads inside a shared process, and then between processes in a Titan cluster.
Inter-thread lock contention
Lock contention between transactions within a shared process is arbitrated by the {@code LocalLockMediator} class. This mediator uses standard{@code java.util.concurrent} classes to guarantee that at most one threadholds a lock on any given {@link KeyColumn} at any given time. The code thatuses a mediator to resolve inter-thread lock contention is common to multiple {@code Locker} implementations and lives in the abstract base class{@link AbstractLocker}.
However, the mediator has no way to perform inter-process communication. The mediator can't detect or prevent a thread in another process (potentially on different machine) acquiring the same lock. This is addressed in the next section.
Inter-process lock contention
After the mediator signals that the current transaction has obtained a lock at the inter-thread/intra-process level, this implementation does the following series of writes and reads to {@code KeyColumnValueStore} to checkwhether it is the only process that holds the lock. These Cassandra operations go to a dedicated store holding nothing but locking data (a "store" in this context means a Cassandra column family, an HBase table, etc.)
Locking I/O sequence
- Write a single column to the store with the following data
- key
- {@link KeyColumn#getKey()} followed by {@link KeyColumn#getColumn()}.
- column
- the approximate current timestamp in nanoseconds followed by this process's {@code rid} (an opaque identifier which uniquely identifiethis process either globally or at least within the Titan cluster)
- value
- the single byte 0; this is unused but reserved for future use
- If the write failed or took longer than {@code lockWait} to completesuccessfully, then retry the write with an updated timestamp and everything else the same until we either exceed the configured retry count (in which case we abort the lock attempt) or successfully complete the write in less than {@code lockWait}.
- Wait, if necessary, until the time interval {@code lockWait} has passedbetween the timestamp on our successful write and the current time.
- Read all columns for the key we wrote in the first step.
- Discard any columns with timestamps older than {@code lockExpire}.
- If our column is either the first column read or is preceeded only by columns containing our own {@code rid}, then we hold the lock. Otherwise, another process holds the lock and we have failed to acquire it.
- To release the lock, we delete from the store the column that we wrote earlier in this sequence
As mentioned earlier, this class relies on {@link AbstractLocker} to obtainand release an intra-process lock before and after the sequence of steps listed above. The mediator step is necessary for thread-safety, because {@code rid} is only unique at the process level. Without a mediator, distinctthreads could write lock columns with the same {@code rid} and be unable totell their lock claims apart.