Class JsonTransactionManager

java.lang.Object
com.ultikits.ultitools.manager.JsonTransactionManager
All Implemented Interfaces:
TransactionManager

public class JsonTransactionManager extends Object implements TransactionManager
The TransactionManager for the JSON backend, giving @Transactional and DataOperator#transaction(...) on datasource.type: json the same propagation behaviour DataSourceTransactionManager gives the two JDBC backends.

Scoped to one requesting identity (a plugin name on the internal path, a canonical data-folder path on the external one) and shared by every SimpleJsonDataOperator that JsonStore hands out for that identity. begin() opens (or, by depth, joins) a transaction on the current thread; the context it binds holds one JsonTransaction.

  • Staging. A write an operator receives inside the transaction is staged in that JsonTransaction, per operator, instead of reaching the operator's committed entries. Reads made inside the transaction see its own staged writes; other threads see committed data only.
  • Commit, durable before it returns. commit() at depth 1 first writes one record of the identity's journal holding every staged change of every table, forces it to disk and renames it into place; the rename is the commit point. Only then does it apply the changes, holding this manager's visibility write lock once across all of the identity's operators, so another thread's read sees all of a commit's changes, on every table, or none of them. Once commit() has returned, the transaction survives a crash. One exception: when forcing the journal directory to disk after the record's rename fails with a real I/O error, commit() still returns, because the transaction is committed and visible, and a WARNING is logged; the transaction then survives a server crash, but survives a power loss or operating-system crash only once the next checkpoint has completed. A transaction that changed nothing writes no record and costs no disk access. A record that cannot be written fails the commit with a DataAccessException (TRANSACTION_FAILED) and rolls the transaction back. On Windows, a journal file move that another program (an antivirus scanner, an indexer, a backup tool) briefly holds is retried for at most 150 ms before the commit fails.
  • Rollback. rollback() at depth 1 discards the staged writes and touches nothing that was committed, by this transaction's own inner REQUIRES_NEW scope or by any other caller. At depth greater than 1 it marks the transaction rollback-only instead, mirroring DataSourceTransactionManager's D-08 semantics, and the outer commit() then discards the staged writes and throws UnexpectedRollbackException.
  • Table write locks. A transaction's first write to a table (one operator) takes that table's write lock, and the transaction holds it until its depth-1 commit() (after the apply) or rollback(); a write outside a transaction holds it for the call. Another writer of the table waits for it, at most 5 seconds (less if its own transaction's timeout ends sooner), then fails with a DataAccessException; a wait that would close a cycle of transactions waiting for each other fails at once. Either failure marks the waiting transaction rollback-only, as a failed nested scope does, so catching the exception does not save it: its outer commit() discards the staged writes and throws UnexpectedRollbackException. A write outside a transaction just fails. Dirty writes and blind overwrites therefore cannot interleave with an open transaction. In a @Transactional transaction at the default isolation reads take no table lock, so a read-modify-write can still lose an update: compute a write from a read with updateIf, or use REPEATABLE_READ or SERIALIZABLE, which lock a table at its first read.
  • Programmatic transactions lock at first read. A transaction begun by DataOperator#transaction(...) locks each table at its first read or write, as REPEATABLE_READ does (question 11 A of the Phase 20.4 decisions), which keeps the per-table serialisation 6.2.5's synchronized transaction(...) gave: a read inside it can wait for another transaction's table lock, bounded as a write is. A transaction(...) that joins an active transaction keeps that transaction's isolation.
  • Settings. setIsolationLevel(int): REPEATABLE_READ and SERIALIZABLE also lock a table at its first read; DEFAULT, READ_UNCOMMITTED and READ_COMMITTED lock at the first write (read uncommitted is served at read committed). setReadOnly(boolean) rejects every write. setTimeout(int) sets a deadline every operation and lock wait observes.
  • Suspension. suspend() detaches the context, staged writes and table locks included, so a REQUIRES_NEW or NOT_SUPPORTED body runs independently of it; resume(Object) restores it. Locks belong to the thread, so a body that writes a table its suspended caller holds re-enters that lock instead of waiting for itself.
  • Recovery at start-up. Before any table of the identity is read, its committed journal records are replayed into the table files, so a transaction whose commit returned is present after a crash and one interrupted before its commit point is absent, on every table. A damaged committed record keeps the identity's data closed, with an error naming the file, until the server operator moves the file aside.
  • Writes outside a transaction are not journaled; they reach disk at the next flush, which writes every changed entry of the identity through one checkpoint record, so a crash during a flush never leaves a half-written table.
Since:
6.3.0
Author:
wisdomme
  • Field Details

    • LOGGER

      private static final Logger LOGGER
    • identity

      private final String identity
      The requesting identity this manager governs -- a plugin name or a canonical data-folder path, matching whatever JsonStore used to key the operators it hands out for this manager. Used only for diagnostic log/exception messages; never compared against anything.
    • journalIdentity

      private final JsonIdentity journalIdentity
      The identity's durable state: its journal, commit gate and operators. A commit writes its journal record through it before the commit returns.
    • contextHolder

      private final ThreadLocal<JsonTransactionManager.Context> contextHolder
      Instance-scoped ThreadLocal, matching the FOUND-04 reasoning already documented on DataSourceTransactionManager.contextHolder: one manager per identity, built once by JsonStore, so two identities can never cross transaction state.
    • suspendedStack

      private final ThreadLocal<Deque<JsonTransactionManager.Context>> suspendedStack
      ThreadLocal holding the stack of contexts suspend() has detached and not yet resume(Object)d, most-recently-suspended on top -- the JSON analogue of DataSourceTransactionManager's field of the same name (02-11, D-09).

      Deliberately a sibling of contextHolder rather than folding the active context into the same structure: getTransactionDepth(), hasActiveTransaction(), commit() and rollback() all read the single active context and stay completely unchanged by this field's existence. Only suspend()/ resume(Object) ever touch it. Never left holding an empty Deque -- the last pop calls ThreadLocal.remove() so a Bukkit worker thread returned to the pool does not carry a stale frame.

    • visibility

      private final ReentrantReadWriteLock visibility
      The identity's visibility lock: one per manager, so one per identity. A read of committed state holds the read side while it collects references; a commit holds the write side once while it applies every operator's staged writes, and a write outside a transaction holds it while it applies its change. The write side covers in-memory map updates only.
  • Constructor Details

    • JsonTransactionManager

      public JsonTransactionManager(JsonIdentity journalIdentity)
      Constructed by JsonStore (a different package, hence public) when it first resolves a manager for a given identity, and by a SimpleJsonDataOperator that was constructed directly, for its own standalone identity.
      Parameters:
      journalIdentity - the identity this manager governs; its name is used in messages
  • Method Details

    • journalIdentity

      @Internal public JsonIdentity journalIdentity()
      The identity whose journal, commit gate and operators this manager's transactions use. Public because the JSON operators live in another package; not part of the supported API.
      Returns:
      this manager's identity
    • begin

      public void begin()
      Description copied from interface: TransactionManager
      Begins a new transaction.

      Creates a new connection, disables auto-commit, and associates it with the current thread.

      Specified by:
      begin in interface TransactionManager
    • commit

      public void commit()
      Commits the current transaction.

      Commits all changes made during the transaction and releases the connection.

      What a failed commit means depends on the implementation:

      • For the JDBC implementation used by SQLite and MySQL (DataSourceTransactionManager), a DataAccessException (TRANSACTION_FAILED) raised because Connection.commit() failed leaves the outcome unknown: the database may have applied the commit before the failure reached the framework.
      • For the JSON implementation (JsonTransactionManager), a failed commit commits nothing, as its commit() javadoc states.
      • On every implementation, an UnexpectedRollbackException means the transaction had been marked rollback-only and was rolled back: none of its writes was stored.
      Implementations do not retry the commit. A caller that must not apply a write twice re-reads the data, or makes the write conditional (DataOperator#updateIf), before trying again after an unknown outcome.

      At depth 1, writes the transaction's journal record and forces it to disk (the commit point), then applies every operator's staged writes under the visibility write lock, held once across all operators, so other threads see the whole commit at one instant, then releases the transaction's table locks. When this returns the transaction is durable. The context is removed from this thread, and the locks released, whether or not the commit succeeds; a record that cannot be written throws DataAccessException (TRANSACTION_FAILED) and nothing is committed.

      Specified by:
      commit in interface TransactionManager
    • rollback

      public void rollback()
      Rolls back the current transaction.

      Discards all changes made during the transaction and releases the connection.

      At depth 1, discards the staged writes and releases the transaction's table locks; committed data is never touched. At a greater depth, marks the transaction rollback-only (D-08).

      Specified by:
      rollback in interface TransactionManager
    • hasActiveTransaction

      public boolean hasActiveTransaction()
      Description copied from interface: TransactionManager
      Checks if there is an active transaction on the current thread.
      Specified by:
      hasActiveTransaction in interface TransactionManager
      Returns:
      true if a transaction is active
    • getTransactionDepth

      public int getTransactionDepth()
      Description copied from interface: TransactionManager
      Gets the current transaction depth (for nested transactions).
      Specified by:
      getTransactionDepth in interface TransactionManager
      Returns:
      the transaction depth (0 if no transaction)
    • suspend

      public Object suspend()
      Suspends the current transaction, if any, detaching it from this thread so an independent transaction can begin in its place. Backs Propagation.REQUIRES_NEW and Propagation.NOT_SUPPORTED (D-09).

      Deliberately typed to return an opaque Object rather than a backend-specific context type: a JDBC-backed manager suspends a Connection-holding context, the JSON manager suspends the transaction's context, which carries its staged writes and its table lock holds - neither shape belongs on this base interface. Pass whatever this returns to TransactionManager.resume(Object) to restore it; the caller must not otherwise inspect or reuse the returned value.

      The default implementation does not support suspension: it always returns null, meaning nothing was suspended. A backend that maintains thread-bound transaction state (JDBC, JSON) must override this together with TransactionManager.resume(Object) - overriding only one of the pair breaks the contract silently.

      Detaches the current Context, its staged writes included, from this thread and pushes it onto suspendedStack, leaving hasActiveTransaction() false. A begin() that runs while the original stays suspended opens a fully independent transaction with its own staged writes; its commit applies only those. The suspended one is not touched until resume(Object) restores it.

      Specified by:
      suspend in interface TransactionManager
      Returns:
      an opaque handle to resume later, or null if no transaction was active
    • resume

      public void resume(Object suspended)
      Restores a transaction previously detached by TransactionManager.suspend().

      null is a no-op - it means TransactionManager.suspend() found nothing active in the first place, so there is nothing to restore. Passing a handle this TransactionManager did not itself produce is a programming error.

      The default implementation is a no-op, pairing with TransactionManager.suspend()'s default of always returning null.

      null is a no-op -- suspend() found nothing active in the first place, so there is nothing on suspendedStack for this call to pop. A non-null handle must be exactly what this manager's own suspend() most recently returned; restoring it sets it back as the active context (same Context instance, same depth, same staged writes) and pops it off suspendedStack, removing the ThreadLocal entirely once the stack is empty rather than leaving a dangling empty Deque behind.

      Specified by:
      resume in interface TransactionManager
      Parameters:
      suspended - the handle returned by a prior TransactionManager.suspend() call, or null
      Throws:
      IllegalArgumentException - if suspended is non-null but was not produced by this manager's own suspend()
    • setIsolationLevel

      public void setIsolationLevel(int level)
      Sets the isolation level for the current transaction.

      Must be called after begin() and before any statements are executed.

      Maps the Connection constants TransactionInterceptor passes: -1 (Isolation.DEFAULT), Connection.TRANSACTION_READ_UNCOMMITTED and Connection.TRANSACTION_READ_COMMITTED lock a table at the transaction's first write to it (read uncommitted is served at read committed, which the SQL standard allows); Connection.TRANSACTION_REPEATABLE_READ and Connection.TRANSACTION_SERIALIZABLE also lock it at the first read, which gives repeatable, phantom-free reads at table granularity. With no active transaction a valid value does nothing, as on DataSourceTransactionManager.

      Specified by:
      setIsolationLevel in interface TransactionManager
      Parameters:
      level - the JDBC isolation level constant
      Throws:
      IllegalArgumentException - for any other value
    • setReadOnly

      public void setReadOnly(boolean readOnly)
      Sets read-only mode for the current transaction.

      A read-only JSON transaction rejects every write with a DataAccessException before the write locks or stages anything; reads work as usual. With no active transaction this does nothing, as on DataSourceTransactionManager.

      Specified by:
      setReadOnly in interface TransactionManager
      Parameters:
      readOnly - true for read-only mode
    • setTimeout

      public void setTimeout(int seconds)
      Sets the timeout for the current transaction.

      Note: Not all databases/drivers support transaction timeout.

      Records a deadline seconds from now on the active transaction, exactly as DataSourceTransactionManager.setTimeout(int) does. Every later operation of the transaction checks it first and fails with a DataAccessException once it has passed, and every lock wait ends at the sooner of it and the 5-second default bound. Either failure marks the transaction rollback-only before it is thrown, as Spring's ResourceHolderSupport#checkTransactionTimeout does, so catching it does not save the transaction: the outer commit() rolls back and throws UnexpectedRollbackException. A non-positive value is inert. With no active transaction this logs a warning and records nothing.

      Specified by:
      setTimeout in interface TransactionManager
      Parameters:
      seconds - the timeout in seconds
    • getTimeoutDeadlineNanos

      public Long getTimeoutDeadlineNanos()
      Returns the System.nanoTime() value at which the active transaction's timeout budget expires, or null when no timeout is configured for the active transaction -- no active transaction at all, or @Transactional(timeout=0) (the default).

      D-10: consulted per statement (by TimeoutAwareQueryRunner and the two direct PreparedStatement call sites AbstractRelationalDataOperator.insertAll/ updateAll build themselves) to compute the time remaining in the budget, not a value captured once -- the caller re-reads this on every statement.

      The default implementation always returns null, meaning "never enforced" -- the same posture a backend whose setTimeout(int) refuses any positive value outright (rather than ever recording a deadline) naturally has, since it never has anything to report here either. A backend that wants per-statement enforcement overrides this together with TransactionManager.setTimeout(int) -- overriding only one half of the pair leaves the other silently inert, the same caution TransactionManager.suspend()'s javadoc gives for its pair.

      The active transaction's deadline, carried along when the transaction is suspended and resumed; null with no active transaction or no positive timeout.

      Specified by:
      getTimeoutDeadlineNanos in interface TransactionManager
      Returns:
      the deadline, as a System.nanoTime() value, or null if none is active
    • currentTransaction

      @Internal public JsonTransaction currentTransaction()
      The JsonTransaction active on the current thread, or null if none is. Public because the JSON operators live in another package; not part of the supported API.
      Returns:
      the current thread's transaction for this identity, or null
    • readUnderVisibilityLock

      @Internal public <R> R readUnderVisibilityLock(Supplier<R> read)
      Runs read under the visibility read side. A JSON operator collects references to committed (and, inside a transaction, staged) entries here, and evaluates conditions, detaches and fires entity hooks only after this returns. Waits at most for one in-memory publication. Not part of the supported API.
      Type Parameters:
      R - the collected result
      Parameters:
      read - the in-memory collection step
      Returns:
      what read returned
    • publishUnderVisibilityLock

      @Internal public <R> R publishUnderVisibilityLock(Supplier<R> apply)
      Runs apply under the visibility write side: a commit's apply of every operator's staged writes, or a write outside a transaction. apply updates in-memory maps only and runs no module code. Not part of the supported API.
      Type Parameters:
      R - the step's result
      Parameters:
      apply - the in-memory publication step
      Returns:
      what apply returned
      Throws:
      IllegalStateException - if the current thread holds the read side, which would otherwise wait for itself forever