java.lang.Object
com.ultikits.ultitools.interfaces.impl.data.json.JsonLockTable

final class JsonLockTable extends Object
The table write locks of the JSON backend: one per JsonStore, shared by every operator the store hands out (an operator constructed directly keeps its own). A table is one SimpleJsonDataOperator, so one entity type of one plugin.

Ownership. A lock is owned by a thread, with a hold count. A thread that already owns a table takes it again at once (the count goes up), so a REQUIRES_NEW or NOT_SUPPORTED body re-enters the locks of the transaction it suspended instead of waiting for itself. Each caller releases exactly the holds it took; the table is free when the count is back at zero.

Waiting. A request for a table another thread owns registers the caller as waiting for that table and waits until the table is free or the caller's deadline has passed; past the deadline it fails with a DataAccessException naming the lock-wait timeout, also when it wakes after the deadline to find the table free: the deadline is checked before every attempt that follows the registration. The caller chooses the deadline: the default bound (5 seconds, question 7 option A of the Phase 20.4 decisions) from the time of the request, or the transaction's own timeout deadline if that comes sooner.

Deadlocks. Before it registers, a requester follows the wait-for chain from the table's owner (the owner, the table that owner waits for, that table's owner, and so on). A chain that leads back to the requester means its wait would close a cycle: the request fails at once with a DataAccessException naming the deadlock, without registering. Its transaction is marked rollback-only (JsonTransaction) and releases its locks when it ends, so the other side can go on. No global lock order is needed, which matters because module code touches tables in any order.

One monitor. Every owner, hold count and waiter registration is guarded by one lock and changed only while holding it, and every wait is on that lock's one condition. The cycle check and the registration are one critical section, so of two requests that close a cycle at the same moment the second always sees the first one's registration and fails, while the first waits and then acquires.

Lock order. A table lock is always requested before, never while holding, either side of the identity's visibility lock; the visibility write side is always the last lock a thread takes. The two mechanisms therefore cannot wait for each other in a cycle.

Since:
6.3.0
  • Field Details

    • DEFAULT_LOCK_WAIT_MILLIS

      static final long DEFAULT_LOCK_WAIT_MILLIS
      The default upper bound of one lock wait, in milliseconds (question 7, option A).
      See Also:
    • STEP_WAITER_REGISTERED

      static final String STEP_WAITER_REGISTERED
      Fired under the monitor right after a waiter has registered, before it starts waiting.
      See Also:
    • NO_STEP_HOOK

      private static final JsonTransaction.StepHook NO_STEP_HOOK
    • stepHook

      private static volatile JsonTransaction.StepHook stepHook
      Test seam: called with STEP_WAITER_REGISTERED. A no-op in production; a test installs a callback that only counts down a latch (it must never block, since it runs holding the monitor) and resets it afterwards.
    • monitor

      private final ReentrantLock monitor
    • released

      private final Condition released
      Signalled whenever a table becomes free.
    • holds

      The owner and hold count of every table currently held. Guarded by monitor.
    • waitsFor

      private final Map<Thread,SimpleJsonDataOperator<?>> waitsFor
      The table each waiting thread is waiting for. Guarded by monitor.
    • defaultWaitNanos

      private volatile long defaultWaitNanos
  • Constructor Details

    • JsonLockTable

      JsonLockTable()
  • Method Details

    • setDefaultWaitMillis

      void setDefaultWaitMillis(long millis)
      Test seam: sets the default upper bound of one lock wait.
    • defaultDeadlineNanos

      long defaultDeadlineNanos()
      The deadline of a wait that starts now and is bounded only by the default.
    • acquire

      void acquire(SimpleJsonDataOperator<?> table, long deadlineNanos)
      Takes table for the calling thread: at once if it is free or the caller already owns it, otherwise after waiting until it is free.
      Parameters:
      table - the table to lock
      deadlineNanos - the System.nanoTime() value past which the wait fails
      Throws:
      DataAccessException - if waiting would close a wait-for cycle (deadlock), if the deadline passes first (lock-wait timeout), or if the thread is interrupted while waiting
    • release

      void release(SimpleJsonDataOperator<?> table)
      Gives back one hold the calling thread took on table; the table becomes free, and every waiter is woken, when the thread's last hold is given back.
      Throws:
      IllegalStateException - if the calling thread does not own table
    • installStepHook

      static void installStepHook(JsonTransaction.StepHook hook)
    • resetStepHook

      static void resetStepHook()
    • tryTake

      private boolean tryTake(SimpleJsonDataOperator<?> table, Thread caller)
      Takes table if it is free or owned by caller. Called holding the monitor.
    • failIfWaitingClosesACycle

      private void failIfWaitingClosesACycle(SimpleJsonDataOperator<?> table, Thread caller)
      Follows the wait-for chain from table's owner; reaching caller means its wait would close a cycle. Called holding the monitor, right before caller registers.
    • lockWaitTimeout

      private DataAccessException lockWaitTimeout(SimpleJsonDataOperator<?> table, long start)
      Called holding the monitor.