Class JsonTransactionManager
- All Implemented Interfaces:
TransactionManager
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. Oncecommit()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 aWARNINGis 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 aDataAccessException(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 innerREQUIRES_NEWscope or by any other caller. At depth greater than 1 it marks the transaction rollback-only instead, mirroringDataSourceTransactionManager's D-08 semantics, and the outercommit()then discards the staged writes and throwsUnexpectedRollbackException. - 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) orrollback(); 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 aDataAccessException; 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 outercommit()discards the staged writes and throwsUnexpectedRollbackException. A write outside a transaction just fails. Dirty writes and blind overwrites therefore cannot interleave with an open transaction. In a@Transactionaltransaction 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 withupdateIf, or useREPEATABLE_READorSERIALIZABLE, 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, asREPEATABLE_READdoes (question 11 A of the Phase 20.4 decisions), which keeps the per-table serialisation 6.2.5'ssynchronizedtransaction(...)gave: a read inside it can wait for another transaction's table lock, bounded as a write is. Atransaction(...)that joins an active transaction keeps that transaction's isolation. - Settings.
setIsolationLevel(int):REPEATABLE_READandSERIALIZABLEalso lock a table at its first read;DEFAULT,READ_UNCOMMITTEDandREAD_COMMITTEDlock 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 aREQUIRES_NEWorNOT_SUPPORTEDbody 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
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionprivate static final classInternal class to hold JSON transaction state -- the JSON analogue ofDataSourceTransactionManager.TransactionContext, minus anything JDBC-specific. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate final ThreadLocal<JsonTransactionManager.Context> Instance-scopedThreadLocal, matching the FOUND-04 reasoning already documented onDataSourceTransactionManager.contextHolder: one manager per identity, built once byJsonStore, so two identities can never cross transaction state.private final StringThe requesting identity this manager governs -- a plugin name or a canonical data-folder path, matching whateverJsonStoreused to key the operators it hands out for this manager.private final JsonIdentityThe identity's durable state: its journal, commit gate and operators.private static final Loggerprivate final ThreadLocal<Deque<JsonTransactionManager.Context>> ThreadLocal holding the stack of contextssuspend()has detached and not yetresume(Object)d, most-recently-suspended on top -- the JSON analogue ofDataSourceTransactionManager's field of the same name (02-11, D-09).private final ReentrantReadWriteLockThe identity's visibility lock: one per manager, so one per identity. -
Constructor Summary
ConstructorsConstructorDescriptionJsonTransactionManager(JsonIdentity journalIdentity) Constructed byJsonStore(a different package, hencepublic) when it first resolves a manager for a given identity, and by aSimpleJsonDataOperatorthat was constructed directly, for its own standalone identity. -
Method Summary
Modifier and TypeMethodDescriptionvoidbegin()Begins a new transaction.voidcommit()Commits the current transaction.TheJsonTransactionactive on the current thread, ornullif none is.Returns theSystem.nanoTime()value at which the active transaction's timeout budget expires, ornullwhen no timeout is configured for the active transaction -- no active transaction at all, or@Transactional(timeout=0)(the default).intGets the current transaction depth (for nested transactions).booleanChecks if there is an active transaction on the current thread.The identity whose journal, commit gate and operators this manager's transactions use.<R> RpublishUnderVisibilityLock(Supplier<R> apply) Runsapplyunder the visibility write side: a commit's apply of every operator's staged writes, or a write outside a transaction.<R> RreadUnderVisibilityLock(Supplier<R> read) Runsreadunder the visibility read side.voidRestores a transaction previously detached byTransactionManager.suspend().voidrollback()Rolls back the current transaction.voidsetIsolationLevel(int level) Sets the isolation level for the current transaction.voidsetReadOnly(boolean readOnly) Sets read-only mode for the current transaction.voidsetTimeout(int seconds) Sets the timeout for the current transaction.suspend()Suspends the current transaction, if any, detaching it from this thread so an independent transaction can begin in its place.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface com.ultikits.ultitools.interfaces.TransactionManager
getConnection
-
Field Details
-
LOGGER
-
identity
The requesting identity this manager governs -- a plugin name or a canonical data-folder path, matching whateverJsonStoreused to key the operators it hands out for this manager. Used only for diagnostic log/exception messages; never compared against anything. -
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
Instance-scopedThreadLocal, matching the FOUND-04 reasoning already documented onDataSourceTransactionManager.contextHolder: one manager per identity, built once byJsonStore, so two identities can never cross transaction state. -
suspendedStack
ThreadLocal holding the stack of contextssuspend()has detached and not yetresume(Object)d, most-recently-suspended on top -- the JSON analogue ofDataSourceTransactionManager's field of the same name (02-11, D-09).Deliberately a sibling of
contextHolderrather than folding the active context into the same structure:getTransactionDepth(),hasActiveTransaction(),commit()androllback()all read the single active context and stay completely unchanged by this field's existence. Onlysuspend()/resume(Object)ever touch it. Never left holding an emptyDeque-- the last pop callsThreadLocal.remove()so a Bukkit worker thread returned to the pool does not carry a stale frame. -
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
Constructed byJsonStore(a different package, hencepublic) when it first resolves a manager for a given identity, and by aSimpleJsonDataOperatorthat was constructed directly, for its own standalone identity.- Parameters:
journalIdentity- the identity this manager governs; its name is used in messages
-
-
Method Details
-
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:TransactionManagerBegins a new transaction.Creates a new connection, disables auto-commit, and associates it with the current thread.
- Specified by:
beginin interfaceTransactionManager
-
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), aDataAccessException(TRANSACTION_FAILED) raised becauseConnection.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 itscommit()javadoc states. - On every implementation, an
UnexpectedRollbackExceptionmeans the transaction had been marked rollback-only and was rolled back: none of its writes was stored.
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:
commitin interfaceTransactionManager
- For the JDBC implementation used by SQLite and MySQL
(
-
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:
rollbackin interfaceTransactionManager
-
hasActiveTransaction
public boolean hasActiveTransaction()Description copied from interface:TransactionManagerChecks if there is an active transaction on the current thread.- Specified by:
hasActiveTransactionin interfaceTransactionManager- Returns:
- true if a transaction is active
-
getTransactionDepth
public int getTransactionDepth()Description copied from interface:TransactionManagerGets the current transaction depth (for nested transactions).- Specified by:
getTransactionDepthin interfaceTransactionManager- Returns:
- the transaction depth (0 if no transaction)
-
suspend
Suspends the current transaction, if any, detaching it from this thread so an independent transaction can begin in its place. BacksPropagation.REQUIRES_NEWandPropagation.NOT_SUPPORTED(D-09).Deliberately typed to return an opaque
Objectrather than a backend-specific context type: a JDBC-backed manager suspends aConnection-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 toTransactionManager.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 withTransactionManager.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 ontosuspendedStack, leavinghasActiveTransaction()false. Abegin()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 untilresume(Object)restores it.- Specified by:
suspendin interfaceTransactionManager- Returns:
- an opaque handle to resume later, or
nullif no transaction was active
-
resume
Restores a transaction previously detached byTransactionManager.suspend().nullis a no-op - it meansTransactionManager.suspend()found nothing active in the first place, so there is nothing to restore. Passing a handle thisTransactionManagerdid not itself produce is a programming error.The default implementation is a no-op, pairing with
TransactionManager.suspend()'s default of always returningnull.nullis a no-op --suspend()found nothing active in the first place, so there is nothing onsuspendedStackfor this call to pop. A non-null handle must be exactly what this manager's ownsuspend()most recently returned; restoring it sets it back as the active context (sameContextinstance, same depth, same staged writes) and pops it offsuspendedStack, removing theThreadLocalentirely once the stack is empty rather than leaving a dangling emptyDequebehind.- Specified by:
resumein interfaceTransactionManager- Parameters:
suspended- the handle returned by a priorTransactionManager.suspend()call, ornull- Throws:
IllegalArgumentException- ifsuspendedis non-null but was not produced by this manager's ownsuspend()
-
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
ConnectionconstantsTransactionInterceptorpasses:-1(Isolation.DEFAULT),Connection.TRANSACTION_READ_UNCOMMITTEDandConnection.TRANSACTION_READ_COMMITTEDlock 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_READandConnection.TRANSACTION_SERIALIZABLEalso 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 onDataSourceTransactionManager.- Specified by:
setIsolationLevelin interfaceTransactionManager- 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
DataAccessExceptionbefore the write locks or stages anything; reads work as usual. With no active transaction this does nothing, as onDataSourceTransactionManager.- Specified by:
setReadOnlyin interfaceTransactionManager- 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
secondsfrom now on the active transaction, exactly asDataSourceTransactionManager.setTimeout(int)does. Every later operation of the transaction checks it first and fails with aDataAccessExceptiononce 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'sResourceHolderSupport#checkTransactionTimeoutdoes, so catching it does not save the transaction: the outercommit()rolls back and throwsUnexpectedRollbackException. A non-positive value is inert. With no active transaction this logs a warning and records nothing.- Specified by:
setTimeoutin interfaceTransactionManager- Parameters:
seconds- the timeout in seconds
-
getTimeoutDeadlineNanos
Returns theSystem.nanoTime()value at which the active transaction's timeout budget expires, ornullwhen 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
TimeoutAwareQueryRunnerand the two directPreparedStatementcall sitesAbstractRelationalDataOperator.insertAll/updateAllbuild 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 whosesetTimeout(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 withTransactionManager.setTimeout(int)-- overriding only one half of the pair leaves the other silently inert, the same cautionTransactionManager.suspend()'s javadoc gives for its pair.The active transaction's deadline, carried along when the transaction is suspended and resumed;
nullwith no active transaction or no positive timeout.- Specified by:
getTimeoutDeadlineNanosin interfaceTransactionManager- Returns:
- the deadline, as a
System.nanoTime()value, ornullif none is active
-
currentTransaction
TheJsonTransactionactive on the current thread, ornullif 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
Runsreadunder 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
readreturned
-
publishUnderVisibilityLock
Runsapplyunder the visibility write side: a commit's apply of every operator's staged writes, or a write outside a transaction.applyupdates 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
applyreturned - Throws:
IllegalStateException- if the current thread holds the read side, which would otherwise wait for itself forever
-