Class JsonTransactionManager

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

public class JsonTransactionManager extends Object implements TransactionManager
Snapshot-based TransactionManager for the JSON backend (D-03), giving @Transactional the same consistent behaviour on datasource.type: json that DataSourceTransactionManager already 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 first write reaching any one of that identity's JSON operators captures a deep-copy snapshot of that operator's cache, lazily -- an operator never written to during the transaction is neither snapshotted nor restored (see captureIfAbsent(java.lang.Object, java.util.function.Supplier<java.lang.Object>, java.util.function.Consumer<java.lang.Object>)). rollback() at depth 1 restores every captured snapshot; at depth greater than 1 it marks the transaction rollback-only instead of tearing it down, exactly mirroring DataSourceTransactionManager's D-08 semantics so TransactionInterceptor sees one behaviour regardless of which backend built this manager. commit() at depth 1 discards the captured snapshots, or -- when the marker is set -- restores them and throws UnexpectedRollbackException, exactly as the JDBC manager does.

This is not a durable transaction. It is an in-process rollback of cached state, not a crash-safe one: a server killed mid-transaction has whatever the flush scheduler last wrote to disk. Nothing here, and nothing this plan's javadoc, log output, or COMPATIBILITY.md text produces, should read as a claim that a JSON transaction survives a crash the way a JDBC transaction's commit/rollback does.

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.
    • 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.

  • Constructor Details

    • JsonTransactionManager

      public JsonTransactionManager(String identity)
      Constructed only by JsonStore (a different package, hence public) when it first resolves a manager for a given identity.
      Parameters:
      identity - the requesting identity this manager governs, used only for diagnostics
  • Method Details

    • 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()
      Description copied from interface: TransactionManager
      Commits the current transaction.

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

      Specified by:
      commit in interface TransactionManager
    • rollback

      public void rollback()
      Description copied from interface: TransactionManager
      Rolls back the current transaction.

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

      Specified by:
      rollback in interface TransactionManager
    • restore

      private void restore(JsonTransactionManager.Context ctx)
    • 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, a JSON snapshot manager (02-05) suspends a set of operator snapshots (D-03) - 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 (captured restorers 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 empty snapshot map -- the suspended one is never touched until resume(Object) restores it.

      Like everything else this class does, this is in-process suspension of cached state, not a durable one: nothing here changes what is on disk, and a server killed mid-suspension still has whatever the flush scheduler last wrote.

      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 captured restorers) 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()
    • setTimeout

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

      Note: Not all databases/drivers support transaction timeout.

      The JSON backend has no statement or connection layer for a timeout to apply to -- there is nothing here that could time out. Rather than silently accept the value and do nothing (the shape DataSourceTransactionManager.setTimeout(int) itself uses, since JDBC has no portable timeout mechanism either, but at least genuinely has a connection the value could theoretically apply to), this refuses outright: @Transactional(timeout=) is refused rather than silently ignored on the JSON backend. TransactionInterceptor only calls this when a positive timeout is actually configured on the annotation, so a @Transactional method that never specifies one is unaffected.

      Specified by:
      setTimeout in interface TransactionManager
      Parameters:
      seconds - the timeout in seconds
      Throws:
      UnsupportedOperationException - always
    • captureIfAbsent

      public void captureIfAbsent(Object operatorKey, Supplier<Object> snapshotSupplier, Consumer<Object> restorer)
      Called by a SimpleJsonDataOperator write path (through JsonStore, its own package) the first time that operator is touched inside the active transaction on this thread. A no-op when no transaction is active on this thread, or when operatorKey was already captured earlier in this same transaction.

      Public because the caller lives in a different package ( interfaces.impl.data.json) than this class. The actual deep-copy capture/restore logic stays package-private on SimpleJsonDataOperator itself, reached only from inside JsonStore: snapshotSupplier and restorer are lambdas built there, closing over that package-private access, so this manager never touches an operator's cache directly and never needs to.

      Parameters:
      operatorKey - identifies the operator -- the operator instance itself, in practice
      snapshotSupplier - produces the deep-copy snapshot; invoked at most once per operator per transaction
      restorer - restores operatorKey's cache from the captured snapshot; invoked by rollback(), and by commit() when the transaction was marked rollback-only