Class DataSourceTransactionManager
- All Implemented Interfaces:
JdbcTransactionManager,TransactionManager
TransactionManager, and its JDBC-specific
sub-interface JdbcTransactionManager.
Uses ThreadLocal to associate transactions with threads, ensuring thread safety. Each thread can have at most one active transaction at a time.
No further commit is issued for a transaction whose commit or rollback failed (as of
6.3.0, UltiTools-Reborn#634): auto-commit is never switched on for it, and the commit is never
retried. JDBC commits an open transaction when auto-commit is switched on
(Connection.setAutoCommit(boolean)), so auto-commit is restored only after a commit or
rollback that succeeded. A connection whose commit or rollback failed is discarded instead: a
HikariCP pool is told to evict it (HikariDataSource.evictConnection(Connection), which
closes the physical connection, and the database rolls the open transaction back), and any other
connection is closed. Returning a connection after a failed rollback risks letting the next
borrower join an unresolved transaction; explicit eviction ensures that connection is never
handed out again.
What the caller can rely on afterwards (this class serves SQLite and MySQL):
- A rollback that succeeded is the confirmed outcome: none of the transaction's writes is stored.
- A commit that finds the transaction marked rollback-only rolls it back and throws
UnexpectedRollbackException: a confirmed rollback, none of its writes stored. If that rollback fails, the connection is discarded and nothing is committed either. - After a failed rollback, the connection is discarded and the framework issues no commit.
- After a commit that fails at
Connection.commit()(DataAccessException,TRANSACTION_FAILED), the outcome is unknown. The database may have applied the commit before the failure reached the framework (for example, a MySQL connection lost afterCOMMITexecuted and before its acknowledgement arrived), and discarding or evicting the connection cannot undo an applied commit. A caller that must not apply a write twice re-reads the data, or makes the write conditional, before trying again.
- Since:
- 6.2.0
- Author:
- wisdomme
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescription(package private) static classInternal class to hold transaction state. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate final ThreadLocal<DataSourceTransactionManager.TransactionContext> ThreadLocal holding the current transaction context for each thread.private final DataSourceprivate static final Loggerprivate final ThreadLocal<Deque<DataSourceTransactionManager.TransactionContext>> ThreadLocal holding the stack of contextssuspend()has detached and not yetresume(Object)d, most-recently-suspended on top (D-09). -
Constructor Summary
ConstructorsConstructorDescriptionDataSourceTransactionManager(DataSource dataSource) Creates a new DataSourceTransactionManager. -
Method Summary
Modifier and TypeMethodDescriptionvoidbegin()Begins a new transaction.private voidcleanup(DataSourceTransactionManager.TransactionContext ctx, boolean completed) Ends the transaction context: removes it from this thread and never throws.voidcommit()Commits the current transaction.static voiddiscardConnection(DataSource dataSource, Connection connection) Discards a connection whose commit or rollback failed, without ever switching auto-commit on (UltiTools-Reborn#634): JDBC commits an open transaction when auto-commit is switched on.Gets the current connection, creating a new one if necessary.(package private) DataSourceTransactionManager.TransactionContextGets the current transaction context (for testing).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.private static com.zaxxer.hikari.HikariDataSourcehikariPool(DataSource dataSource) 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.
-
Field Details
-
LOGGER
-
dataSource
-
contextHolder
ThreadLocal holding the current transaction context for each thread.Instance-scoped on purpose (FOUND-04) - one
DataSourceTransactionManagerper plugin container, built once inPluginManager.wireAop. Astaticfield here would let two plugin containers' transactions cross: manager A'srollback()would be able to reach aDataSourceTransactionManager.TransactionContextmanager B set up for a completely differentDataSource, on the same thread. Still aThreadLocal- the fix is "no longer static", not "no longer ThreadLocal". -
suspendedStack
ThreadLocal holding the stack of contextssuspend()has detached and not yetresume(Object)d, most-recently-suspended on top (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 (T-02-DOS-5).
-
-
Constructor Details
-
DataSourceTransactionManager
Creates a new DataSourceTransactionManager.- Parameters:
dataSource- the data source to use for connections
-
-
Method Details
-
getConnection
Description copied from interface:JdbcTransactionManagerGets the current connection, creating a new one if necessary.If a transaction is active, returns the connection associated with it. Otherwise, returns a new connection from the data source.
- Specified by:
getConnectionin interfaceJdbcTransactionManager- Specified by:
getConnectionin interfaceTransactionManager- Returns:
- the current connection
-
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.An
UnexpectedRollbackExceptionmeans the transaction had been marked rollback-only and was rolled back: none of its writes is stored. ADataAccessException(TRANSACTION_FAILED) raised becauseConnection.commit()failed means the outcome is unknown: the database may have applied the commit before the failure reached the framework, and discarding the connection cannot undo that. Either way the transaction has ended, its connection is never switched to auto-commit, and the commit is not retried.- Specified by:
commitin interfaceTransactionManager
- For the JDBC implementation used by SQLite and MySQL
(
-
rollback
public void rollback()Description copied from interface:TransactionManagerRolls back the current transaction.Discards all changes made during the transaction and releases the connection.
- 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
-
setIsolationLevel
public void setIsolationLevel(int level) Description copied from interface:JdbcTransactionManagerSets the isolation level for the current transaction.Must be called after begin() and before any statements are executed.
- Specified by:
setIsolationLevelin interfaceJdbcTransactionManager- Specified by:
setIsolationLevelin interfaceTransactionManager- Parameters:
level- the JDBC isolation level constant
-
setReadOnly
public void setReadOnly(boolean readOnly) Description copied from interface:JdbcTransactionManagerSets read-only mode for the current transaction.- Specified by:
setReadOnlyin interfaceJdbcTransactionManager- 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.
D-10: records a deadline on the active
DataSourceTransactionManager.TransactionContextrather than a wall-clock bound on the transaction as a whole --getTimeoutDeadlineNanos()exposes it so each statement issued afterward can be given the time remaining, not a fresh full allowance.seconds <= 0is inert (no deadline recorded), matching@Transactional(timeout=0)'s default --TransactionInterceptoritself never calls this method for a non-positive value in the first place, so this is a second, defensive guard for any other caller. Called with no active transaction, this logs a warning and records nothing, the same shapecommit()/rollback()already use for that case.- 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.Reads
DataSourceTransactionManager.TransactionContext.timeoutDeadlineNanosoff the active context --nullwhen no transaction is active, or whensetTimeout(int)was never called (or called only with a non-positive value) for it.suspend()/resume(Object)detach and restore the wholeDataSourceTransactionManager.TransactionContextobject, so a suspended-then-resumed transaction's deadline travels with it automatically -- no separate handling needed here.- Specified by:
getTimeoutDeadlineNanosin interfaceTransactionManager- Returns:
- the deadline, as a
System.nanoTime()value, ornullif none 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
DataSourceTransactionManager.TransactionContext(connection included) from this thread and pushes it ontosuspendedStack, leavinghasActiveTransaction()false. Abegin()that runs while the original stays suspended opens a fully independent transaction, on its own connection - the suspended one is never 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 (sameDataSourceTransactionManager.TransactionContextinstance, sameConnection, same depth) 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()
-
cleanup
Ends the transaction context: removes it from this thread and never throws.completed-- the commit or rollback succeeded: auto-commit is restored and the connection closed (back to the pool). Otherwise the connection is discarded throughdiscardConnection(DataSource, Connection)and auto-commit is never switched on (#634).- Parameters:
ctx- the context to endcompleted- whether its commit or rollback succeeded
-
discardConnection
Discards a connection whose commit or rollback failed, without ever switching auto-commit on (UltiTools-Reborn#634): JDBC commits an open transaction when auto-commit is switched on. WhendataSourceis, or wraps, aHikariDataSourceandconnectionis one of its connections, the pool evicts it -- the physical connection is closed, the database rolls the open transaction back, and the connection is never handed out again; the caller must not use it afterwards. Any other connection is closed. Never throws; a failure is logged.- Parameters:
dataSource- the data sourceconnectioncame from, possiblynullconnection- the connection to discard, possiblynull- Since:
- 6.3.0
-
hikariPool
-
getCurrentContext
DataSourceTransactionManager.TransactionContext getCurrentContext()Gets the current transaction context (for testing).Instance method, not static, per the same FOUND-04 fix as
contextHolderitself: twoDataSourceTransactionManagerinstances now have independent state.
-