This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/main by this push:
     new 4f8c1e0af1c CAUSEWAY-3883: fixes AsyncProxyInternal#join not waiting
4f8c1e0af1c is described below

commit 4f8c1e0af1c59b16aa48ce20b92f43b979e48171
Author: Andi Huber <[email protected]>
AuthorDate: Fri Jun 27 18:20:23 2025 +0200

    CAUSEWAY-3883: fixes AsyncProxyInternal#join not waiting
---
 .../applib/services/wrapper/WrapperFactory.java    |  9 ++--
 .../core/metamodel/object/MmEntityUtils.java       | 17 +++++--
 .../wrapper/AsyncExecutionFinisher.java            |  8 +--
 .../runtimeservices/wrapper/AsyncExecutor.java     | 18 +++++--
 .../wrapper/AsyncProxyInternal.java                | 41 +++++++++------
 .../wrapper/WrapperFactoryDefault.java             | 53 ++++++++------------
 .../BackgroundService_IntegTestAbstract.java       | 58 +++++++++++++++-------
 .../integtests/WrapperFactory_async_IntegTest.java | 42 +++++++++++-----
 8 files changed, 151 insertions(+), 95 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
index d64fd649f3d..1237f360695 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/wrapper/WrapperFactory.java
@@ -21,8 +21,9 @@
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Function;
+
+import org.springframework.util.function.ThrowingConsumer;
+import org.springframework.util.function.ThrowingFunction;
 
 import org.apache.causeway.applib.exceptions.recoverable.InteractionException;
 import org.apache.causeway.applib.services.factory.FactoryService;
@@ -78,8 +79,8 @@ public interface WrapperFactory {
      * @see CompletableFuture
      */
     interface AsyncProxy<T> {
-        AsyncProxy<Void> thenAcceptAsync(Consumer<? super T> action);
-        <U> AsyncProxy<U> thenApplyAsync(Function<? super T, ? extends U> fn);
+        AsyncProxy<Void> thenAcceptAsync(ThrowingConsumer<? super T> action);
+        <U> AsyncProxy<U> thenApplyAsync(ThrowingFunction<? super T, ? extends 
U> fn);
         AsyncProxy<T> orTimeout(long timeout, TimeUnit unit);
         T join();
     }
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java
index 3a15d768b1e..2f59e567d83 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/object/MmEntityUtils.java
@@ -21,6 +21,7 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
+import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
 import org.apache.causeway.applib.services.repository.EntityState;
@@ -29,11 +30,11 @@
 import 
org.apache.causeway.core.config.beans.CausewayBeanMetaData.PersistenceStack;
 import org.apache.causeway.core.metamodel.facets.object.entity.EntityFacet;
 import 
org.apache.causeway.core.metamodel.facets.properties.property.entitychangepublishing.EntityPropertyChangePublishingPolicyFacet;
+import org.apache.causeway.core.metamodel.objectmanager.ObjectManager;
 import 
org.apache.causeway.core.metamodel.services.objectlifecycle.PropertyChangeRecordId;
 import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
-import org.jspecify.annotations.NonNull;
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
@@ -62,18 +63,24 @@ public EntityState getEntityState(final @Nullable 
ManagedObject adapter) {
 
     public void persistInCurrentTransaction(final ManagedObject managedObject) 
{
         requiresEntity(managedObject);
-        var spec = managedObject.objSpec();
-        var entityFacet = spec.entityFacetElseFail();
+        var entityFacet = managedObject.objSpec().entityFacetElseFail();
         entityFacet.persist(managedObject.getPojo());
     }
 
     public void deleteInCurrentTransaction(final ManagedObject managedObject) {
         requiresEntity(managedObject);
-        var spec = managedObject.objSpec();
-        var entityFacet = spec.entityFacetElseFail();
+        var entityFacet = managedObject.objSpec().entityFacetElseFail();
         entityFacet.delete(managedObject.getPojo());
     }
 
+    public <T> T detachedPojo(ObjectManager objectManager, @Nullable T pojo) {
+        if(pojo == null) return null;
+        var managedObject = objectManager.adapt(pojo);
+        return isAttachedEntity(managedObject)
+                ? managedObject.objSpec().entityFacetElseFail().detach(pojo)
+                : pojo;
+    }
+
     public void requiresEntity(final ManagedObject managedObject) {
         if(ManagedObjects.isNullOrUnspecifiedOrEmpty(managedObject)) {
             throw _Exceptions.illegalArgument("requires an entity object but 
got null, unspecified or empty");
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
index e9a40b325b2..c98e6d35ca0 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutionFinisher.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.core.runtimeservices.wrapper;
 
+import org.jspecify.annotations.Nullable;
+
 import org.apache.causeway.applib.services.repository.RepositoryService;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
 import org.apache.causeway.core.metamodel.object.MmEntityUtils;
@@ -29,13 +31,13 @@ record AsyncExecutionFinisher(
         ObjectManager objectManager
         ) {
 
-    public <T> T finish(T t) {
+    public <T> T finish(@Nullable T t) {
+        if(t==null) return t;
         var pojo = wrapperFactory.unwrap(t);
-
         var domainObject = objectManager.adapt(pojo);
         if(MmEntityUtils.isAttachedEntity(domainObject)) {
             repositoryService.persistAndFlush(pojo);
-            return repositoryService.detach(pojo);
+            pojo = repositoryService.detach(pojo);
         }
         return pojo;
     }
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
index f85921c92b7..734a697c4da 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncExecutor.java
@@ -48,6 +48,7 @@ record AsyncExecutor(
          * but does NOT throw any exceptions if a transaction exists.
          */
         Optional<Propagation> propagation,
+        AsyncExecutionFinisher finisher,
         ExecutorService delegate) implements ExecutorService {
 
     @Override
@@ -119,19 +120,28 @@ public boolean isTerminated() {
 
     private void run(ThrowingRunnable runnable) {
         if(propagation.isEmpty())
-            interactionService.run(interactionContext, runnable);
+            interactionService.run(interactionContext, ()->finish(runnable));
         else
             interactionService.run(interactionContext, ()->transactionService
-                .runTransactional(propagation().get(), runnable)
+                .runTransactional(propagation().get(), ()->finish(runnable))
                 .ifFailureFail());
     }
 
     private <T> T call(Callable<T> callable) {
         return propagation.isEmpty()
-            ? interactionService.call(interactionContext, callable)
+            ? interactionService.call(interactionContext, ()->finish(callable))
             : interactionService.call(interactionContext, 
()->transactionService
-                .callTransactional(propagation().get(), callable)
+                .callTransactional(propagation().get(), ()->finish(callable))
                 .valueAsNullableElseFail());
     }
 
+    private <T> T finish(Callable<T> callable) throws Exception {
+        return finisher.finish(callable.call());
+    }
+
+    private void finish(ThrowingRunnable runnable) throws Exception {
+        runnable.run();
+        finisher.finish(null);
+    }
+
 }
\ No newline at end of file
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
index 502486f42a0..e475eb4dccb 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/AsyncProxyInternal.java
@@ -18,39 +18,48 @@
  */
 package org.apache.causeway.core.runtimeservices.wrapper;
 
-import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-import java.util.function.Function;
+
+import org.springframework.util.function.ThrowingConsumer;
+import org.springframework.util.function.ThrowingFunction;
 
 import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
 
-//TODO this is just a proof of concept; chaining makes non sense once future 
is no longer a proxy
+import lombok.SneakyThrows;
+
+//TODO this is just a proof of concept; chaining makes non sense once future 
no longer holds a proxy
 record AsyncProxyInternal<T>(
-        CompletableFuture<T> future,
-        AsyncExecutor executor,
-        AsyncExecutionFinisher finisher) implements AsyncProxy<T> {
+        Future<T> future,
+        AsyncExecutor executor) implements AsyncProxy<T> {
 
-    @Override public AsyncProxy<Void> thenAcceptAsync(Consumer<? super T> 
action) {
-        return map(in->in.thenAcceptAsync(action, executor));
+    @Override public AsyncProxy<Void> thenAcceptAsync(ThrowingConsumer<? super 
T> action) {
+        return thenApplyAsync(adapt(action));
     }
 
-    @Override public <U> AsyncProxy<U> thenApplyAsync(Function<? super T, ? 
extends U> fn) {
-        return map(in->in.thenApplyAsync(fn, executor));
+    @Override public <U> AsyncProxy<U> thenApplyAsync(ThrowingFunction<? super 
T, ? extends U> fn) {
+        return map(()->fn.apply(future.get()));
     }
 
     @Override public AsyncProxy<T> orTimeout(long timeout, TimeUnit unit) {
-        return map(in->in.orTimeout(timeout, unit));
+        return map(()->future.get(timeout, unit));
     }
 
+    @SneakyThrows
     @Override public T join() {
-        var t = future.join();
-        return finisher.finish(t);
+        return future.get();
     }
 
     // -- HELPER
 
-    private <U> AsyncProxy<U> map(Function<CompletableFuture<T>, 
CompletableFuture<U>> fn) {
-        return new AsyncProxyInternal<>(fn.apply(future), executor, finisher);
+    /// converts ThrowingConsumer<T> to ThrowingFunction<T, Void>
+    private ThrowingFunction<? super T, Void> adapt(ThrowingConsumer<? super 
T> action) {
+        return t->{action.accept(t); return (Void)null; };
     }
+
+    private <U> AsyncProxy<U> map(Callable<U> callable) {
+        return new AsyncProxyInternal<>(executor.submit(callable), executor);
+    }
+
 }
\ No newline at end of file
diff --git 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
index dcb41fe738d..0fcb27685b0 100644
--- 
a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
+++ 
b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/wrapper/WrapperFactoryDefault.java
@@ -74,6 +74,7 @@
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.core.metamodel.object.MmEntityUtils;
 import org.apache.causeway.core.metamodel.services.command.CommandDtoFactory;
 import org.apache.causeway.core.runtime.wrap.WrappingObject;
 import 
org.apache.causeway.core.runtimeservices.CausewayModuleCoreRuntimeServices;
@@ -159,20 +160,11 @@ public <T> T wrap(
             final @NonNull T domainObject,
             final @NonNull SyncControl syncControl) {
 
-        var spec = 
getSpecificationLoader().specForTypeElseFail(domainObject.getClass());
-        if(spec.isMixin()) {
-            throw _Exceptions.illegalArgument("cannot wrap a mixin instance 
directly, "
-                    + "use WrapperFactory.wrapMixin(...) instead");
-        }
-
         if (isWrapper(domainObject)) {
             var wrapperObject = (WrappingObject) domainObject;
             var origin = wrapperObject.__causeway_origin();
-            if(origin.syncControl().isEquivalent(syncControl)) {
-                return domainObject;
-            }
-            var underlyingDomainObject = 
wrapperObject.__causeway_origin().pojo();
-            return _Casts.uncheckedCast(createProxy(underlyingDomainObject, 
syncControl));
+            if(origin.syncControl().isEquivalent(syncControl)) return 
domainObject;
+            return _Casts.uncheckedCast(createProxy(origin.pojo(), 
syncControl));
         }
         return createProxy(domainObject, syncControl);
     }
@@ -191,34 +183,29 @@ public <T> T wrapMixin(
             final @NonNull SyncControl syncControl) {
 
         T mixin = factoryService.mixin(mixinClass, mixee);
-        // no need to inject services into the mixin, factoryService does it 
for us.
 
         if (isWrapper(mixee)) {
             var wrappingObject = (WrappingObject) mixee;
             var origin = wrappingObject.__causeway_origin();
-            var underlyingMixee = origin.pojo();
-
-            getServiceInjector().injectServicesInto(underlyingMixee);
-
-            if(origin.syncControl().isEquivalent(syncControl)) {
-                return mixin;
-            }
-            return _Casts.uncheckedCast(createMixinProxy(underlyingMixee, 
mixin, syncControl));
+            if(origin.syncControl().isEquivalent(syncControl)) return mixin;
+            return _Casts.uncheckedCast(createMixinProxy(origin.pojo(), mixin, 
syncControl));
         }
 
-        getServiceInjector().injectServicesInto(mixee);
-
         return createMixinProxy(mixee, mixin, syncControl);
     }
 
     protected <T> T createProxy(final T domainObject, final SyncControl 
syncControl) {
         var objAdapter = 
adaptAndGuardAgainstWrappingNotSupported(domainObject);
+        guardAgainstMixin(objAdapter);
+        MmEntityUtils.requiresAttached(objAdapter);
         return proxyGenerator.objectProxy(domainObject, objAdapter.objSpec(), 
syncControl);
     }
 
     protected <T> T createMixinProxy(final Object mixee, final T mixin, final 
SyncControl syncControl) {
         var mixeeAdapter = adaptAndGuardAgainstWrappingNotSupported(mixee);
         var mixinAdapter = adaptAndGuardAgainstWrappingNotSupported(mixin);
+        guardAgainstMixin(mixeeAdapter);
+        MmEntityUtils.requiresAttached(mixeeAdapter);
         return proxyGenerator.mixinProxy(mixin, mixeeAdapter, 
mixinAdapter.objSpec(), syncControl);
     }
 
@@ -242,21 +229,18 @@ AsyncExecutor asyncExecutor(AsyncControl asyncControl) {
                 transactionServiceProvider.get(),
                 asyncControl.override(InteractionContext.builder().build()),
                 Optional.of(Propagation.REQUIRES_NEW),
+                executionFinisher(),
                 Optional.ofNullable(asyncControl.executorService())
                     .orElse(commonExecutorService));
     }
 
-    AsyncExecutionFinisher finisher() {
-        return null;
-    }
-
     @Override
     public <T> AsyncProxy<T> asyncWrap(T domainObject, AsyncControl 
asyncControl) {
-        var proxy = wrap(domainObject, asyncControl.syncControl());
+        var pojo = unwrap(domainObject);
+        var proxy = wrap(pojo, asyncControl.syncControl());
         return new AsyncProxyInternal<>(
                 CompletableFuture.completedFuture(proxy),
-                asyncExecutor(asyncControl),
-                executionFinisher());
+                asyncExecutor(asyncControl));
     }
 
     @Override
@@ -267,8 +251,7 @@ public <T> AsyncProxy<T> asyncWrapMixin(
         var proxy = wrapMixin(mixinClass, mixee, asyncControl.syncControl());
         return new AsyncProxyInternal<>(
                 CompletableFuture.completedFuture(proxy),
-                asyncExecutor(asyncControl),
-                executionFinisher());
+                asyncExecutor(asyncControl));
     }
 
     // -- LISTENERS
@@ -314,6 +297,14 @@ private ManagedObject 
adaptAndGuardAgainstWrappingNotSupported(
         return adapter;
     }
 
+    private void guardAgainstMixin(ManagedObject mo) {
+        if(mo.objSpec().isMixin()) {
+            throw _Exceptions.illegalArgument("cannot wrap a mixin instance 
directly, "
+                    + "use WrapperFactory.wrapMixin(...) instead");
+        }
+    }
+
+
     // -- HELPER - SETUP
 
     private <T extends InteractionEvent> void putDispatcher(
diff --git 
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
 
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
index fa1081002c6..7fbbf988dde 100644
--- 
a/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
+++ 
b/extensions/core/commandlog/applib/src/test/java/org/apache/causeway/extensions/commandlog/applib/integtest/BackgroundService_IntegTestAbstract.java
@@ -20,6 +20,7 @@
 
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import jakarta.inject.Inject;
 
@@ -38,6 +39,7 @@
 import org.apache.causeway.applib.services.bookmark.BookmarkService;
 import org.apache.causeway.applib.services.iactnlayer.InteractionService;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
 import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
 import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
@@ -106,39 +108,49 @@ void setup_counter() {
     @Test
     void async_using_default_executor_service() {
 
+        final AtomicReference<AsyncProxy<Counter>> asyncProxyUnderTest1 = new 
AtomicReference<>();
+        final AtomicReference<AsyncProxy<Counter_bumpUsingMixin>> 
asyncProxyUnderTest2 = new AtomicReference<>();
+
         // when
         transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
             var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
 
             var control = AsyncControl.defaults();
-            wrapperFactory.asyncWrap(counter, control)
+
+            asyncProxyUnderTest1.set(wrapperFactory.asyncWrap(counter, 
control));
+
+        }).ifFailureFail();
+
+        // execute async and wait till done
+        {
+            asyncProxyUnderTest1.get()
                 .thenApplyAsync(Counter::bumpUsingDeclaredAction)
                 .orTimeout(5, TimeUnit.SECONDS)
                 .join(); // wait till done
-
-        }).ifFailureFail();
+        }
 
         // then
-        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
-            var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
-            assertThat(counter.getNum()).isEqualTo(1L);
-        }).ifFailureFail();
-
-        // when
         transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
             var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
             assertThat(counter.getNum()).isEqualTo(1L);
             var control = AsyncControl.defaults();
 
-            // when ...
-            // returns the detached counter entity, so we can immediately 
check whether the action was executed
-            counter = 
wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class, counter, control)
-                .thenApplyAsync(Counter_bumpUsingMixin::act)
-                .join(); // wait till done
+            // store the async proxy for later use below
+            
asyncProxyUnderTest2.set(wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class,
 counter, control));
+
+        }).ifFailureFail();
 
+        // execute async and wait till done
+        {
+            // returns the detached counter entity, so we can immediately 
check whether the action was executed
+            var counter = asyncProxyUnderTest2.get()
+                    .thenApplyAsync(Counter_bumpUsingMixin::act)
+                    // let's wait max 5 sec to allow executor to complete 
before continuing
+                    .orTimeout(5, TimeUnit.SECONDS)
+                    .join(); // wait till done
             assertThat(counter.getNum()).isEqualTo(2L);
+        }
 
-        }).ifFailureFail();
         // then
         transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
             var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
@@ -151,6 +163,8 @@ void async_using_default_executor_service() {
     @Test
     void using_background_service() {
 
+        final AtomicReference<AsyncProxy<Counter>> asyncProxyUnderTest = new 
AtomicReference<>();
+
         // given
         removeAllCommandLogEntriesAndCounters();
 
@@ -160,13 +174,19 @@ void using_background_service() {
             assertThat(counter.getNum()).isNull();
 
             // when
-            backgroundService.execute(counter)
-                .thenAcceptAsync(Counter::bumpUsingDeclaredAction)
-                .orTimeout(1, TimeUnit.SECONDS)
-                .join(); // wait for completion
+            asyncProxyUnderTest.set(backgroundService.execute(counter));
 
         }).ifFailureFail();
 
+        // execute async and wait till done
+        {
+            asyncProxyUnderTest.get()
+                .thenAcceptAsync(Counter::bumpUsingDeclaredAction)
+                // let's wait max 5 sec to allow executor to complete before 
continuing
+                .orTimeout(5, TimeUnit.SECONDS)
+                .join(); // wait till done
+        }
+
         // then no change to the counter
         transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
             var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
diff --git 
a/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
 
b/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
index 192de03e3b2..76df8059a4a 100644
--- 
a/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
+++ 
b/regressiontests/core-wrapperfactory/src/test/java/org/apache/causeway/regressiontests/core/wrapperfactory/integtests/WrapperFactory_async_IntegTest.java
@@ -23,11 +23,11 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Stream;
 
 import jakarta.inject.Inject;
 
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -37,6 +37,7 @@
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.bookmark.BookmarkService;
 import org.apache.causeway.applib.services.wrapper.WrapperFactory;
+import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
 import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
 import org.apache.causeway.applib.services.xactn.TransactionService;
 import org.apache.causeway.testdomain.wrapperfactory.Counter;
@@ -55,7 +56,6 @@ class WrapperFactory_async_IntegTest extends 
CoreWrapperFactory_IntegTestAbstrac
 
     Bookmark bookmark;
 
-    @BeforeEach
     void setup_counter() {
 
         runWithNewTransaction(() -> {
@@ -73,11 +73,17 @@ void setup_counter() {
         assertThat(counter.getNum()).isNull();
     }
 
+
     @SneakyThrows
     @ParameterizedTest(name = "executorService[{index}]: {0}")
     @MethodSource("executorServices")
     void async_using_default_executor_service(final String displayName, final 
ExecutorService executorService) {
 
+        setup_counter();
+
+        final AtomicReference<AsyncProxy<Counter>> asyncProxyUnderTest1 = new 
AtomicReference<>();
+        final AtomicReference<AsyncProxy<Counter_bumpUsingMixin>> 
asyncProxyUnderTest2 = new AtomicReference<>();
+
         // when - executing regular action
         runWithNewTransaction(() -> {
             var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();
@@ -85,11 +91,17 @@ void async_using_default_executor_service(final String 
displayName, final Execut
             var asyncControl = AsyncControl.defaults()
                     .with(executorService);
 
-            wrapperFactory.asyncWrap(counter, asyncControl)
+            // store the async proxy for later use below
+            asyncProxyUnderTest1.set(wrapperFactory.asyncWrap(counter, 
asyncControl));
+        });
+
+        // execute async and wait till done
+        {
+            asyncProxyUnderTest1.get()
                 .thenApplyAsync(Counter::increment)
                 .orTimeout(5_000, TimeUnit.MILLISECONDS)
                 .join(); // let's wait max 5 sec to allow executor to complete 
before continuing
-        });
+        }
 
         // then
         runWithNewTransaction(() -> {
@@ -105,17 +117,21 @@ void async_using_default_executor_service(final String 
displayName, final Execut
             var asyncControl = AsyncControl.defaults()
                     .with(executorService);
 
-            // when ...
-            // returns the detached counter entity, so we can immediately 
check whether the action was executed
-            counter = 
wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class, counter, 
asyncControl)
-                .thenApplyAsync(Counter_bumpUsingMixin::act)
-                // let's wait max 5 sec to allow executor to complete before 
continuing
-                .orTimeout(5_000, TimeUnit.MILLISECONDS)
-                .join(); // wait till done
-
-            assertThat(counter.getNum()).isEqualTo(2L); // verify execution 
succeeded
+            // store the async proxy for later use below
+            
asyncProxyUnderTest2.set(wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class,
 counter, asyncControl));
         });
 
+        // execute async and wait till done
+        {
+            // returns the detached counter entity, so we can immediately 
check whether the action was executed
+            var counter = asyncProxyUnderTest2.get()
+                    .thenApplyAsync(Counter_bumpUsingMixin::act)
+                    // let's wait max 5 sec to allow executor to complete 
before continuing
+                    .orTimeout(5, TimeUnit.SECONDS)
+                    .join(); // wait till done
+            assertThat(counter.getNum()).isEqualTo(2L);
+        }
+
         // then
         runWithNewTransaction(() -> {
             var counter = bookmarkService.lookup(bookmark, 
Counter.class).orElseThrow();

Reply via email to