This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch 3975-telemetry
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/3975-telemetry by this push:
new 9ec997faac2 CAUSEWAY-3975: adds transaction observation
9ec997faac2 is described below
commit 9ec997faac2cc98f50cac622e8b73386e0a7c9c9
Author: andi-huber <[email protected]>
AuthorDate: Thu Mar 26 12:48:21 2026 +0100
CAUSEWAY-3975: adds transaction observation
---
.../CausewayObservationIntegration.java | 4 +-
.../transaction/TransactionServiceDevNotes.adoc | 37 +++++
.../transaction/TransactionServiceSpring.java | 111 ++++++++-------
.../NoopTransactionSynchronizationService.java | 2 +-
.../transaction/scope/StackedTransactionScope.java | 155 ++++++++++-----------
.../TransactionScopeBeanFactoryPostProcessor.java | 3 +-
6 files changed, 179 insertions(+), 133 deletions(-)
diff --git
a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java
b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java
index 210926d8a7d..110f9a7b672 100644
---
a/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java
+++
b/commons/src/main/java/org/apache/causeway/commons/internal/observation/CausewayObservationIntegration.java
@@ -25,7 +25,7 @@
import org.springframework.util.StringUtils;
-import lombok.Data;
+import lombok.Getter;
import lombok.experimental.Accessors;
import io.micrometer.common.KeyValue;
@@ -84,7 +84,7 @@ public ObservationProvider provider(final Class<?> bean) {
/**
* Helps if start and stop of an {@link Observation} happen in different
code locations.
*/
- @Data @Accessors(fluent = true)
+ @Getter @Accessors(fluent = true)
public static final class ObservationClosure implements AutoCloseable {
private Observation observation;
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc
new file mode 100644
index 00000000000..808b2d56bcd
--- /dev/null
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceDevNotes.adoc
@@ -0,0 +1,37 @@
+= Transaction Service
+
+:Notice: Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with this work
for additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at. http://www.apache.org/licenses/LICENSE-2.0 . Unless required by
applicable law or ag [...]
+
+[plantuml,fig-transaction-flow,svg]
+.Transactional Code Flow
+----
+@startuml
+
+boundary RequestCycle
+participant InteractionService
+participant TransactionService
+boundary JpaTransactionManager as "JpaTransactionManager\n(Spring)"
+
+RequestCycle -> InteractionService: open (root) Interaction Layer
+
+InteractionService -> TransactionService: onOpen - sets up the initial\n\
+transaction against (all available) \nPlatformTransactionManager(s);\n\
+also installs ObservationClosure
+
+TransactionService -> JpaTransactionManager: getTransaction(txDefn)
+TransactionService <-- JpaTransactionManager: new or existing Transaction
+InteractionService <-- TransactionService: Interaction opened
+
+RequestCycle -> InteractionService: closeInteractionLayers()
+
+InteractionService -> TransactionService: onClose
+TransactionService -> JpaTransactionManager: commit(txStatus) or
rollback(txStatus)
+TransactionService <-- JpaTransactionManager: Transaction completed
+
+InteractionService <-- TransactionService : Observations closed\n\
+Interaction closed
+
+RequestCycle <-- InteractionService
+
+@enduml
+----
diff --git
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
index 1c91bf6736b..7800ffeb7ea 100644
---
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
+++
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/transaction/TransactionServiceSpring.java
@@ -18,6 +18,7 @@
*/
package org.apache.causeway.core.runtimeservices.transaction;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
@@ -53,17 +54,20 @@
import org.apache.causeway.commons.functional.ThrowingRunnable;
import org.apache.causeway.commons.functional.Try;
import org.apache.causeway.commons.internal.base._NullSafe;
-import org.apache.causeway.commons.internal.collections._Lists;
import org.apache.causeway.commons.internal.debug._Probe;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import
org.apache.causeway.commons.internal.observation.CausewayObservationIntegration;
+import
org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationClosure;
+import
org.apache.causeway.commons.internal.observation.CausewayObservationIntegration.ObservationProvider;
import org.apache.causeway.core.interaction.session.CausewayInteraction;
import org.apache.causeway.core.runtime.flushmgmt.FlushMgmt;
import
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
import org.apache.causeway.core.transaction.events.TransactionCompletionStatus;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import io.micrometer.observation.Observation;
+
/**
* Default implementation of {@link TransactionService}, which delegates to
Spring's own transaction management
* framework, such as {@link PlatformTransactionManager}.
@@ -87,13 +91,16 @@ public class TransactionServiceSpring
private final Provider<InteractionLayerTracker>
interactionLayerTrackerProvider;
private final Can<PersistenceExceptionTranslator>
persistenceExceptionTranslators;
private final ConfigurableListableBeanFactory
configurableListableBeanFactory;
+ private final ObservationProvider observationProvider;
@Inject
public TransactionServiceSpring(
final List<PlatformTransactionManager> platformTransactionManagers,
final List<PersistenceExceptionTranslator>
persistenceExceptionTranslators,
final Provider<InteractionLayerTracker>
interactionLayerTrackerProvider,
- final ConfigurableListableBeanFactory
configurableListableBeanFactory
+ final ConfigurableListableBeanFactory
configurableListableBeanFactory,
+ @Qualifier("causeway-runtimeservices")
+ final CausewayObservationIntegration observationIntegration
) {
this.platformTransactionManagers =
Can.ofCollection(platformTransactionManagers);
@@ -105,6 +112,8 @@ public TransactionServiceSpring(
log.info("PersistenceExceptionTranslators: {}",
persistenceExceptionTranslators);
this.interactionLayerTrackerProvider = interactionLayerTrackerProvider;
+
+ this.observationProvider = observationIntegration.provider(getClass());
}
// -- API
@@ -118,7 +127,7 @@ public <T> Try<T> callTransactional(final
TransactionDefinition def, final Calla
try {
TransactionStatus txStatus =
platformTransactionManager.getTransaction(def);
- registerTransactionSynchronizations(txStatus);
+ registerTransactionSynchronizations();
result = Try.call(() -> {
@@ -145,9 +154,8 @@ public <T> Try<T> callTransactional(final
TransactionDefinition def, final Calla
// return the original failure cause (originating from calling the
callable)
// (so we don't shadow the original failure)
// return the failure we just caught
- if (result != null && result.isFailure()) {
+ if (result != null && result.isFailure())
return result;
- }
// otherwise, we thought we had a success, but now we have an
exception thrown by either ,
// the call to rollback or commit above. We don't need to do
anything though; if either of
@@ -159,7 +167,7 @@ public <T> Try<T> callTransactional(final
TransactionDefinition def, final Calla
return result;
}
- private void registerTransactionSynchronizations(final TransactionStatus
txStatus) {
+ private void registerTransactionSynchronizations() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
configurableListableBeanFactory.getBeansOfType(TransactionSynchronization.class)
.values()
@@ -184,9 +192,8 @@ public void flushTransaction() {
var translatedEx = translateExceptionIfPossible(ex, txManager);
- if(translatedEx instanceof RuntimeException) {
+ if(translatedEx instanceof RuntimeException)
throw ex;
- }
throw new RuntimeException(ex);
@@ -209,11 +216,10 @@ public TransactionState currentTransactionState() {
return currentTransactionStatus()
.map(txStatus->{
- if(txStatus.isCompleted()) {
+ if(txStatus.isCompleted())
return txStatus.isRollbackOnly()
? TransactionState.ABORTED
: TransactionState.COMMITTED;
- }
return txStatus.isRollbackOnly()
? TransactionState.MUST_ABORT
@@ -235,9 +241,8 @@ public TransactionState currentTransactionState() {
private PlatformTransactionManager transactionManagerForElseFail(final
TransactionDefinition def) {
if(def instanceof TransactionTemplate) {
var txManager = ((TransactionTemplate)def).getTransactionManager();
- if(txManager!=null) {
+ if(txManager!=null)
return txManager;
- }
}
return platformTransactionManagers.getSingleton()
.orElseThrow(()->
@@ -264,9 +269,8 @@ private Optional<TransactionStatus>
currentTransactionStatus() {
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_MANDATORY);
// not strictly required, but to prevent stack-trace creation later on
- if(!TransactionSynchronizationManager.isActualTransactionActive()) {
+ if(!TransactionSynchronizationManager.isActualTransactionActive())
return Optional.empty();
- }
// get current transaction else throw an exception
return Try.call(()->
@@ -278,9 +282,8 @@ private Optional<TransactionStatus>
currentTransactionStatus() {
private Throwable translateExceptionIfPossible(final Throwable ex, final
PlatformTransactionManager txManager) {
- if(ex instanceof DataAccessException) {
+ if(ex instanceof DataAccessException)
return ex; // nothing to do, already translated
- }
if(ex instanceof RuntimeException) {
@@ -291,15 +294,18 @@ private Throwable translateExceptionIfPossible(final
Throwable ex, final Platfor
.findFirst()
.orElse(null);
- if(translatedEx!=null) {
+ if(translatedEx!=null)
return translatedEx;
- }
}
return ex;
}
+ static class X extends Observation.Context {
+
+ }
+
/**
* For use only by {@link
org.apache.causeway.core.runtimeservices.session.InteractionServiceDefault},
sets up
* the initial transaction automatically against all available {@link
PlatformTransactionManager}s.
@@ -309,36 +315,43 @@ private Throwable translateExceptionIfPossible(final
Throwable ex, final Platfor
public void onOpen(final @NonNull CausewayInteraction interaction) {
txCounter.get().reset();
+ if (platformTransactionManagers.isEmpty()) return;
if (log.isDebugEnabled()) {
log.debug("opening on {}", _Probe.currentThreadId());
}
- if (!platformTransactionManagers.isEmpty()) {
- var onCloseTasks =
_Lists.<CloseTask>newArrayList(platformTransactionManagers.size());
+ var onCloseHandle = new OnCloseHandle(new
ArrayList<>(platformTransactionManagers.size()), new ObservationClosure());
+ interaction.putAttribute(OnCloseHandle.class, onCloseHandle);
+
+ platformTransactionManagers.forEach(txManager -> {
- interaction.putAttribute(OnCloseHandle.class, new
OnCloseHandle(onCloseTasks));
+ var txDefn = new TransactionTemplate(txManager); // specify the
txManager in question
+
txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
- platformTransactionManagers.forEach(txManager -> {
+ var obs =
onCloseHandle.observationClosure().startAndOpenScope(observationProvider.get("Transaction"))
+ .observation()
+ .highCardinalityKeyValue("txManager",
txManager.getClass().getName());
- var txDefn = new TransactionTemplate(txManager); // specify
the txManager in question
-
txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
+ // either participate in existing or create new transaction
+ TransactionStatus txStatus = observationProvider.get("Transaction
Creation")
+ .observe(()->txManager.getTransaction(txDefn));
+ if(!txStatus.isNewTransaction()) {
+ // discard telemetry data when participating in existing
transaction
+ obs.getContext().put("micrometer.discard", true);
+ // we are participating in an exiting transaction (or
testing), nothing to do
+ return;
+ }
- // either participate in existing or create new transaction
- TransactionStatus txStatus = txManager.getTransaction(txDefn);
+ registerTransactionSynchronizations();
- if(!txStatus.isNewTransaction()) {
- // we are participating in an exiting transaction (or
testing), nothing to do
- return;
- }
- registerTransactionSynchronizations(txStatus);
-
- // we have created a new transaction, so need to provide a
CloseTask
- onCloseTasks.add(
- new CloseTask(
- txStatus,
- txManager.getClass().getName(), // info to be used for
display in case of errors
- () -> {
+ // we have created a new transaction, so need to provide a
CloseTask
+ onCloseHandle.onCloseTasks().add(
+ new CloseTask(
+ txStatus,
+ txManager.getClass().getName(), // info to be used for
display in case of errors
+ ()->observationProvider.get("Transaction Completion")
+ .observe(() -> {
_Xray.txBeforeCompletion(interactionLayerTrackerProvider.get(), "tx:
beforeCompletion");
final TransactionCompletionStatus event;
if (txStatus.isRollbackOnly()) {
@@ -349,13 +362,12 @@ public void onOpen(final @NonNull CausewayInteraction
interaction) {
event = TransactionCompletionStatus.COMMITTED;
}
_Xray.txAfterCompletion(interactionLayerTrackerProvider.get(),
String.format("tx: afterCompletion (%s)", event.name()));
-
txCounter.get().increment();
- }
- )
- );
- });
- }
+ })
+ )
+ );
+ });
+
}
/**
@@ -397,9 +409,10 @@ private record CloseTask(
@NonNull ThrowingRunnable runnable) {
}
- @RequiredArgsConstructor
- private static class OnCloseHandle {
- private final @NonNull List<CloseTask> onCloseTasks;
+ private record OnCloseHandle(
+ List<CloseTask> onCloseTasks,
+ ObservationClosure observationClosure) {
+
void requestRollback() {
onCloseTasks.forEach(onCloseTask->{
onCloseTask.txStatus.setRollbackOnly();
@@ -407,7 +420,6 @@ void requestRollback() {
}
void runOnCloseTasks() {
onCloseTasks.forEach(onCloseTask->{
-
try {
onCloseTask.runnable().run();
} catch(final Throwable ex) {
@@ -419,6 +431,7 @@ void runOnCloseTasks() {
ex);
}
});
+ observationClosure.close();
}
}
}
diff --git
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java
index 01769b5f51a..14cd95cbee8 100644
---
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java
+++
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/NoopTransactionSynchronizationService.java
@@ -22,7 +22,7 @@
/**
* This service, which does nothing in and of itself, exists in order to
ensure that the {@link StackedTransactionScope}
- * is always initialized, findinag at least one {@link TransactionScope
transaction-scope}d service.
+ * is always initialized, finding at least one {@link TransactionScope
transaction-scope}d service.
*/
@Service
@TransactionScope
diff --git
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java
index f3dd41b30a5..61dd09a93bd 100644
---
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java
+++
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/StackedTransactionScope.java
@@ -21,42 +21,27 @@
import java.util.Stack;
import java.util.UUID;
+import org.jspecify.annotations.Nullable;
+
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
-import org.jspecify.annotations.Nullable;
import org.springframework.transaction.support.TransactionSynchronization;
import
org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.apache.causeway.commons.internal.base._Refs;
+
public class StackedTransactionScope implements Scope {
@Override
public Object get(final String name, final ObjectFactory<?> objectFactory)
{
- var transactionNestingLevelForThisThread =
currentTransactionNestingLevelForThisThread();
-
- ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder)
TransactionSynchronizationManager.getResource(currentTransactionNestingLevelForThisThread());
+ var scopedObjects = currentScopedObjectsHolder();
if (scopedObjects == null) {
- scopedObjects = new
ScopedObjectsHolder(transactionNestingLevelForThisThread);
- if (TransactionSynchronizationManager.isSynchronizationActive()) {
- // this happen when TransactionSynchronization#afterCompletion
is called.
- // it's a catch-22 : we use TransactionSynchronization as a
resource to hold the scoped objects,
- // but those scoped objects can only be interacted with during
the transaction, not after it.
- //
- // see the 'else' clause below for the handling if we
encounter the ScopedObjectsHolder after the
- // transaction was completed.
- registerWithTransitionSynchronizationManager(scopedObjects);
- } else {
- scopedObjects.registered = false;
- }
-
TransactionSynchronizationManager.bindResource(transactionNestingLevelForThisThread,
scopedObjects);
+ scopedObjects = createAndBindScopedObjectsHolder();
} else {
- if (TransactionSynchronizationManager.isSynchronizationActive()) {
- // it's possible that this already-existing scopedObject was
added when a synchronization wasn't active
- // (see the 'if' block above) and so wouldn't be registered to
TSM. If that's the case, we register it now.
- if (!scopedObjects.registered) {
-
registerWithTransitionSynchronizationManager(scopedObjects);
- }
- }
+ // it's possible that this already-existing scopedObject was added
when a synchronization wasn't active
+ // (see the 'if' block above) and so wouldn't be registered to
TSM. If that's the case, we register it now.
+
registerWithTransactionSynchronizationManagerIfNotAlready(scopedObjects);
}
// NOTE: Do NOT modify the following to use Map::computeIfAbsent. For
details,
// see
https://github.com/spring-projects/spring-framework/issues/25801.
@@ -68,32 +53,52 @@ public Object get(final String name, final ObjectFactory<?>
objectFactory) {
return scopedObject;
}
- private void registerWithTransitionSynchronizationManager(final
ScopedObjectsHolder scopedObjects) {
- TransactionSynchronizationManager.registerSynchronization(new
CleanupSynchronization(scopedObjects));
- scopedObjects.registered = true;
+ private void
registerWithTransactionSynchronizationManagerIfNotAlready(final
ScopedObjectsHolder scopedObjects) {
+ if (scopedObjects.registered.isTrue()
+ ||
!TransactionSynchronizationManager.isSynchronizationActive()) return;
+ TransactionSynchronizationManager.registerSynchronization(new
CleanupSynchronization(this, scopedObjects));
+ scopedObjects.registered.setValue(true);
}
@Override
@Nullable
public Object remove(final String name) {
- var currentTransactionNestingLevel =
currentTransactionNestingLevelForThisThread();
- ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder)
TransactionSynchronizationManager.getResource(currentTransactionNestingLevel);
+ var scopedObjects = currentScopedObjectsHolder();
if (scopedObjects != null) {
scopedObjects.destructionCallbacks.remove(name);
return scopedObjects.scopedInstances.remove(name);
- } else {
+ } else
return null;
- }
}
@Override
public void registerDestructionCallback(final String name, final Runnable
callback) {
- ScopedObjectsHolder scopedObjects = (ScopedObjectsHolder)
TransactionSynchronizationManager.getResource(currentTransactionNestingLevelForThisThread());
+ var scopedObjects = currentScopedObjectsHolder();
if (scopedObjects != null) {
scopedObjects.destructionCallbacks.put(name, callback);
}
}
+ @Nullable
+ private ScopedObjectsHolder currentScopedObjectsHolder() {
+ return (ScopedObjectsHolder) TransactionSynchronizationManager
+ .getResource(currentTransactionNestingLevelForThisThread());
+ }
+
+ private ScopedObjectsHolder createAndBindScopedObjectsHolder() {
+ final UUID transactionNestingLevelForThisThread =
currentTransactionNestingLevelForThisThread();
+ var scopedObjects = new
ScopedObjectsHolder(transactionNestingLevelForThisThread);
+ // this happen when TransactionSynchronization#afterCompletion is
called.
+ // it's a catch-22 : we use TransactionSynchronization as a resource
to hold the scoped objects,
+ // but those scoped objects can only be interacted with during the
transaction, not after it.
+ //
+ // see the 'else' clause below for the handling if we encounter the
ScopedObjectsHolder after the
+ // transaction was completed.
+
registerWithTransactionSynchronizationManagerIfNotAlready(scopedObjects);
+
TransactionSynchronizationManager.bindResource(transactionNestingLevelForThisThread,
scopedObjects);
+ return scopedObjects;
+ }
+
/**
* Holds a unique id for each nested transaction within the current thread.
*
@@ -103,8 +108,8 @@ public void registerDestructionCallback(final String name,
final Runnable callba
* using an anonymous <code>new Object()</code>.
* </p>
*/
- private static final ThreadLocal<Stack<UUID>>
transactionNestingLevelThreadLocal = ThreadLocal.withInitial(() -> {
- Stack<UUID> stack = new Stack<>();
+ private static final ThreadLocal<Stack<UUID>> UUID_STACK =
ThreadLocal.withInitial(() -> {
+ var stack = new Stack<UUID>();
stack.push(UUID.randomUUID());
return stack;
});
@@ -113,29 +118,26 @@ public void registerDestructionCallback(final String
name, final Runnable callba
* Maintains a stack of keys representing nested transactions, where the
top-most is the key managed by
* {@link TransactionSynchronizationManager} holding the {@link
ScopedObjectsHolder} for the current transaction.
*
- * <p>
- * The keys themselves are {@link UUID}s, having no meaning in themselves
other than their identity as the key
+ * <p>The keys are {@link UUID}s, having no meaning in themselves other
than their identity as the key
* into a hashmap.
*
- * <p>
- * If a transaction is suspended, then the {@link
CleanupSynchronization#suspend() suspend} callback is used
+ * <p>If a transaction is suspended, then the {@link
CleanupSynchronization#suspend() suspend} callback is used
* to pop a new key onto the stack, unbinding the previous key's resources
(in other words, the
* {@link org.apache.causeway.applib.annotation.TransactionScope
transaction-scope}d beans of the suspended
- * transaction) from {@link TransactionSynchronizationManager}. As
transaction-scoped beans are then resolved,
+ * transaction) from {@link TransactionSynchronizationManager}. As
transaction-scoped beans are then resolved,
* they will be associated with the new key.
*
- * <p>
- * Conversely, when a transaction is resumed, then the process is
reversed; the old key is popped, and the previous
+ * <p>Conversely, when a transaction is resumed, then the process is
reversed; the old key is popped, and the previous
* key is rebound to the {@link TransactionSynchronizationManager},
meaning that the previous transaction's
* {@link org.apache.causeway.applib.annotation.TransactionScope
transaction-scope}d beans are brought back.
*
* @see #currentTransactionNestingLevelForThisThread()
* @see #pushToNewTransactionNestingLevelForThisThread()
* @see #popToPreviousTransactionNestingLevelForThisThread()
- * @see #transactionNestingLevelThreadLocal
+ * @see #UUID_STACK
*/
private static Stack<UUID> transactionNestingLevelForThread() {
- return transactionNestingLevelThreadLocal.get();
+ return UUID_STACK.get();
}
/**
@@ -174,48 +176,41 @@ public String getConversationId() {
/**
* Holder for scoped objects.
*/
- static class ScopedObjectsHolder {
-
- private final UUID transactionUuid;
-
- ScopedObjectsHolder(UUID transactionUuid) {
- this.transactionUuid = transactionUuid;
+ record ScopedObjectsHolder(
+ UUID transactionUuid,
+ Map<String, Object> scopedInstances,
+ Map<String, Runnable> destructionCallbacks,
+ /**
+ * Keeps track of whether these objects have been registered with
{@link TransactionSynchronizationManager}.
+ *
+ * <p>This can only be done if
+ * {@link
TransactionSynchronizationManager#isSynchronizationActive() synchronization is
active}, which
+ * isn't the case for {@link ScopedObjectsHolder scoped objects}
that are obtained as a result of the
+ * {@link TransactionSynchronization#afterCompletion(int)}
callback.
+ * We use this flag to keep track in case they are reused in a
subsequent transaction.
+ */
+ _Refs.BooleanReference registered) {
+
+ ScopedObjectsHolder(
+ final UUID transactionUuid) {
+ this(transactionUuid, new HashMap<>(), new LinkedHashMap<>(), new
_Refs.BooleanReference(false));
}
- final Map<String, Object> scopedInstances = new HashMap<>();
- final Map<String, Runnable> destructionCallbacks = new
LinkedHashMap<>();
-
- /**
- * Keeps track of whether these objects have been registered with
{@link TransactionSynchronizationManager}.
- *
- * <p>
- * This can only be done if
- * {@link TransactionSynchronizationManager#isSynchronizationActive()
synchronization is active}, which
- * isn't the case for {@link ScopedObjectsHolder scoped objects} that
are obtained as a result of the
- * {@link TransactionSynchronization#afterCompletion(int)} callback.
We use this flag to keep track in
- * case they are reused in a subsequent transaction.
- * </p>
- */
- private boolean registered = false;
-
+ @Override
public String toString() {
return String.format(
- "uuid: %s, registered: %s, scopedInstances.size(): %d,
destructionCallbacks.size(): %d",
- transactionUuid, registered, scopedInstances.size(),
destructionCallbacks.size());
+ "uuid: %s, registered: %b, scopedInstances.size(): %d,
destructionCallbacks.size(): %d",
+ transactionUuid, registered.isTrue(),
scopedInstances.size(), destructionCallbacks.size());
}
}
- private class CleanupSynchronization implements TransactionSynchronization
{
-
- private final ScopedObjectsHolder scopedObjects;
-
- public CleanupSynchronization(final ScopedObjectsHolder scopedObjects)
{
- this.scopedObjects = scopedObjects;
- }
+ private record CleanupSynchronization(
+ StackedTransactionScope scope,
+ ScopedObjectsHolder scopedObjects) implements
TransactionSynchronization {
@Override
public void suspend() {
- var transactionNestingLevelForThisThread =
currentTransactionNestingLevelForThisThread();
+ var transactionNestingLevelForThisThread =
scope.currentTransactionNestingLevelForThisThread();
TransactionSynchronizationManager.unbindResource(transactionNestingLevelForThisThread);
pushToNewTransactionNestingLevelForThisThread(); // subsequent
calls to obtain a @TransactionScope'd bean will be against this key
}
@@ -223,17 +218,17 @@ public void suspend() {
@Override
public void resume() {
popToPreviousTransactionNestingLevelForThisThread(); // the
now-completed transaction's @TransactionScope'd beans are no longer required,
and will be GC'd.
-
TransactionSynchronizationManager.bindResource(currentTransactionNestingLevelForThisThread(),
this.scopedObjects);
+
TransactionSynchronizationManager.bindResource(scope.currentTransactionNestingLevelForThisThread(),
scopedObjects);
}
@Override
public void afterCompletion(final int status) {
-
TransactionSynchronizationManager.unbindResourceIfPossible(StackedTransactionScope.this.currentTransactionNestingLevelForThisThread());
- for (Runnable callback :
this.scopedObjects.destructionCallbacks.values()) {
+
TransactionSynchronizationManager.unbindResourceIfPossible(scope.currentTransactionNestingLevelForThisThread());
+ for (Runnable callback :
scopedObjects.destructionCallbacks.values()) {
callback.run();
}
- this.scopedObjects.destructionCallbacks.clear();
- this.scopedObjects.scopedInstances.clear();
+ scopedObjects.destructionCallbacks.clear();
+ scopedObjects.scopedInstances.clear();
}
}
diff --git
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java
index ebc022decde..42af05b351e 100644
---
a/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java
+++
b/core/transaction/src/main/java/org/apache/causeway/core/transaction/scope/TransactionScopeBeanFactoryPostProcessor.java
@@ -32,7 +32,8 @@ public class TransactionScopeBeanFactoryPostProcessor
implements BeanFactoryPost
public static final String SCOPE_NAME =
org.apache.causeway.applib.annotation.TransactionScope.SCOPE_NAME;
@Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory
beanFactory) throws BeansException {
+ public void postProcessBeanFactory(@SuppressWarnings("exports") final
ConfigurableListableBeanFactory beanFactory)
+ throws BeansException {
var transactionScope = new StackedTransactionScope();
beanFactory.registerScope(SCOPE_NAME, transactionScope);
}