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
commit ab70e86ee1e23e680f7d5db0f6712d568de21aa3 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 | 105 +++++++------- .../NoopTransactionSynchronizationService.java | 2 +- .../transaction/scope/StackedTransactionScope.java | 155 ++++++++++----------- .../TransactionScopeBeanFactoryPostProcessor.java | 3 +- 6 files changed, 173 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..cd19888eb3a 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,15 +54,16 @@ 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; /** @@ -87,13 +89,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 +110,8 @@ public TransactionServiceSpring( log.info("PersistenceExceptionTranslators: {}", persistenceExceptionTranslators); this.interactionLayerTrackerProvider = interactionLayerTrackerProvider; + + this.observationProvider = observationIntegration.provider(getClass()); } // -- API @@ -118,7 +125,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 +152,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 +165,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 +190,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 +214,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 +239,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 +267,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 +280,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,9 +292,8 @@ private Throwable translateExceptionIfPossible(final Throwable ex, final Platfor .findFirst() .orElse(null); - if(translatedEx!=null) { + if(translatedEx!=null) return translatedEx; - } } @@ -309,36 +309,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); - interaction.putAttribute(OnCloseHandle.class, new OnCloseHandle(onCloseTasks)); + platformTransactionManagers.forEach(txManager -> { - platformTransactionManagers.forEach(txManager -> { + var txDefn = new TransactionTemplate(txManager); // specify the txManager in question + txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); - var txDefn = new TransactionTemplate(txManager); // specify the txManager in question - txDefn.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + var obs = onCloseHandle.observationClosure().startAndOpenScope(observationProvider.get("Transaction")) + .observation() + .highCardinalityKeyValue("txManager", txManager.getClass().getName()); - // either participate in existing or create new transaction - TransactionStatus txStatus = txManager.getTransaction(txDefn); + // 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; + } - 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 - () -> { + registerTransactionSynchronizations(); + + // 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 +356,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 +403,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 +414,6 @@ void requestRollback() { } void runOnCloseTasks() { onCloseTasks.forEach(onCloseTask->{ - try { onCloseTask.runnable().run(); } catch(final Throwable ex) { @@ -419,6 +425,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); }
