Oracle sessionless transaction implementation - #3887
Conversation
…into sessionless-transaction
…into sessionless-transaction
…into sessionless-transaction
…into sessionless-transaction
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 39 out of 39 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
data-tx/src/main/java/io/micronaut/transaction/support/AbstractTransactionOperations.java:167
- If transaction begin fails after acquiring a new connection from the SynchronousConnectionManager, the connection is never completed because the completion synchronization is registered only after createAndBeginTransaction() returns. Wrap begin in try/catch and ensure synchronousConnectionManager.complete(newConnectionStatus) is called on failure to avoid leaking the connection and leaving it in a non-default state.
TransactionUtil.validateOracleSessionlessPropagation(definition, supportsOracleSessionlessTransactions());
boolean debugEnabled = logger.isDebugEnabled();
if (debugEnabled) {
logger.debug("Getting transaction for definition [{}]", definition);
}
| @NonNull | ||
| @Override | ||
| public T getTransaction(TransactionDefinition definition) throws TransactionException { | ||
| TransactionUtil.validateOracleSessionlessPropagation(definition, supportsOracleSessionlessTransactions()); | ||
| boolean debugEnabled = logger.isDebugEnabled(); | ||
| if (debugEnabled) { | ||
| logger.debug("Getting transaction for definition [{}]", definition); |
There was a problem hiding this comment.
@copilot this seems unrelated to this PR, can you provide another PR resolving this issue
|
@graemerocher I've opened a new pull request, #3890, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
please update the PR description with more detail |
…into sessionless-transaction
…into sessionless-transaction
| * @param definition The transaction definition | ||
| * @param supported Whether the transaction manager supports Oracle sessionless transaction propagation | ||
| */ | ||
| public static void validateOracleSessionlessPropagation(TransactionDefinition definition, boolean supported) { |
There was a problem hiding this comment.
Maybe this can be implemented using a TransactionSynchronization?
There was a problem hiding this comment.
I don’t think TransactionSynchronization is a good fit for this validation because it runs after a transaction status already exists. The purpose of this check is to reject Oracle sessionless modes before an unsupported transaction manager can treat them as a normal transaction.
If we waited until a synchronization could be registered, the unsupported manager may already have selected a connection definition, created a transaction status, or begun a normal transaction.
Even if TransactionSynchronization had an earlier callback, it would still need transaction-manager capability information to know whether the current manager supports Oracle sessionless transactions.
| NESTED | ||
| NESTED, | ||
| /** | ||
| * Start an Oracle sessionless transaction and suspend it instead of committing when the |
There was a problem hiding this comment.
Not sure I like the new Oracle only TX types; Maybe this should be a new annotation like for this logic or OracleTransactional?
There was a problem hiding this comment.
I moved those to OracleTransaction annotation. Now it looks like:
public @interface OracleTransactional {
enum Sessionless {
/**
* Do not apply Oracle sessionless transaction semantics.
*/
NONE,
/**
* Start an Oracle sessionless transaction and suspend it instead of committing when the
* transactional boundary completes.
* <p>The {@link OracleTransactional#timeout()} value is passed to Oracle when the
* sessionless transaction is started.
*/
SUSPEND,
/**
* Resume an Oracle sessionless transaction from the current propagation context and complete
* it when the transactional boundary completes.
*/
REQUIRES_SUSPENDED
}
/**
* The desired Oracle sessionless transaction mode.
*
* @return The sessionless transaction mode
* @since 5.1.0
*/
Sessionless sessionless() default Sessionless.NONE;
| * applications can replace the codec without changing propagation mechanics.</p> | ||
| */ | ||
| @Singleton | ||
| final class DefaultOracleSessionlessTransactionPropagationOperations implements OracleSessionlessTransactionPropagationOperations { |
There was a problem hiding this comment.
Maybe this should be in a special data-tx-jdbc-oracle module
There was a problem hiding this comment.
How this is going to work if we have multiple OracleSessionlessTransactionPropagationOperations?
There was a problem hiding this comment.
micronaut-data-tx-jdbc already contains code related to the Oracle Transaction Priority implementation. If we introduced a new module for Oracle-specific transaction support, we would ideally move all Oracle transaction functionality there, including both transaction priority and sessionless transactions. However, moving the existing priority implementation would be a breaking change. Moving only the sessionless transaction implementation would avoid that break, but it would make the module layout inconsistent because Oracle transaction features would be split across modules.
There was a problem hiding this comment.
The intended usage is to create one propagation scope for the suspend operation, export the transaction id from that scope, and then create a separate propagation scope later when resuming:
SuspendedSeat suspendedSeat = transactionPropagationOperations.withPropagation(() -> {
Long seatId = bookingService.holdSeat(new Seat("JU502", "3a", "msid"));
String transactionId = transactionPropagationOperations.currentTransactionId().orElseThrow();
return new SuspendedSeat(seatId, transactionId);
});
// do something else
transactionPropagationOperations.withPropagation(suspendedSeat.transactionId(), () -> {
bookingService.ticketSeat(suspendedSeat.seatId());
return null;
});
OracleSessionlessTransactionPropagationOperations itself does not own transaction state. It only installs or reads OracleSessionlessTransactionState from the current PropagatedContext. The transaction manager also reads that propagated state directly. OracleSessionlessTransactionState contains only transaction id retrieved from oracle jdbc when a transaction is suspended.
Multiple suspended transaction ids in the same active context are not supported by this model.
| this.expenseReportRepository = expenseReportRepository | ||
| } | ||
|
|
||
| @Transactional(propagation = TransactionDefinition.Propagation.SUSPEND, timeout = 3600) |
There was a problem hiding this comment.
Maybe suspend-resume would be implicit if Oracle session state is present?
There was a problem hiding this comment.
SUSPEND and REQUIRES_SUSPENDED have just been moved out of the core propagation enum and into @OracleTransactional so the Oracle-specific behavior is explicit. I don’t think suspend/resume should be implicit from the presence of Oracle sessionless state, because @OracleTransactional can also be used for other Oracle transaction options, such as transaction priority, where the transaction should still behave like a normal transaction.
The propagated sessionless state should only carry the GTRID. The annotation should decide whether the method starts/suspends or resumes/completes a sessionless transaction.
| * @since 5.1.0 | ||
| */ | ||
| @Experimental | ||
| public interface OracleSessionlessTransactionIdCodec { |
There was a problem hiding this comment.
Do we need this as a interface? Can we have multiple implementations?
There was a problem hiding this comment.
The reason for the interface is to let applications replace the default wire format/security policy for the GTRID. The GTRID becomes a capability token when it is propagated over HTTP or another transport, so some applications may want to:
- sign the encoded value to detect tampering
- encrypt it so the raw Oracle GTRID is not exposed
- bind it to a tenant/user/workflow
- add expiry metadata
The default implementation is simple: URL-safe Base64 without padding. A custom implementation would replace it as the single codec used by both HTTP propagation and programmatic propagation.
So no, multiple codecs are not intended to be active at the same time. Micronaut bean resolution should select one.
propagation values with OracleTransactional.Sessionless modes
…into sessionless-transaction
…into sessionless-transaction
…into sessionless-transaction # Conflicts: # data-tx-jdbc/build.gradle # data-tx/src/main/java/io/micronaut/transaction/annotation/OracleTransactional.java # data-tx/src/test/java/io/micronaut/transaction/support/DoRollbackOnCommitExceptionTest.java # src/main/docs/guide/shared/transactions.adoc # src/main/docs/guide/toc.yml
|



Adds Oracle JDBC sessionless transaction support to Micronaut Data. The implementation introduces SUSPEND and REQUIRES_SUSPENDED transaction propagation modes. SUSPEND starts an Oracle sessionless transaction, stores the returned GTRID in propagated state, and suspends instead of committing. REQUIRES_SUSPENDED reads the propagated GTRID, resumes the Oracle transaction, then commits or rolls back normally and clears the state.
The feature includes opt-in HTTP propagation through a server filter. The filter owns request-scoped sessionless transaction state, imports the GTRID from the configured header, and exports a suspended GTRID on the response. The default header is Oracle-Sessionless-Transaction-Id.
Non-HTTP propagation is supported through OracleSessionlessTransactionPropagationOperations, allowing callers to create lexical propagation scopes, export the current encoded transaction id, and later import it for resume workflows.
GTRID string conversion is handled by OracleSessionlessTransactionIdCodec. The default codec uses URL-safe Base64 without padding, and applications can replace it to add signing, encryption, expiration metadata, or transport-specific encoding.
Unsupported transaction managers now fail fast for SUSPEND / REQUIRES_SUSPENDED instead of silently treating them as normal transactions.