From 6dcb7aebcab2fb52a182789d3d213f81185bc96c Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Thu, 14 May 2026 16:18:36 +0200 Subject: [PATCH 1/2] Fix async handler duplicate detection exception types --- .../weld/invokable/AsyncHandlerRegistry.java | 21 ++++++++---- .../org/jboss/weld/logging/InvokerLogger.java | 3 ++ .../async/broken/AnotherParamTypeHandler.java | 10 ++++++ .../broken/AnotherReturnTypeHandler.java | 10 ++++++ .../async/broken/BothInterfacesHandler.java | 7 ++-- .../broken/BothInterfacesHandlerTest.java | 2 +- .../broken/DuplicateParamTypeHandler.java | 10 ++++++ .../broken/DuplicateParamTypeHandlerTest.java | 32 +++++++++++++++++++ .../broken/DuplicateReturnTypeHandler.java | 10 ++++++ .../DuplicateReturnTypeHandlerTest.java | 32 +++++++++++++++++++ .../invokable/async/broken/MyAsyncType.java | 4 +++ .../broken/TypeVariableAsyncHandler.java | 10 ++++++ .../broken/TypeVariableAsyncHandlerTest.java | 31 ++++++++++++++++++ 13 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherParamTypeHandler.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherReturnTypeHandler.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandler.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandlerTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandler.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandlerTest.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/MyAsyncType.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandler.java create mode 100644 tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandlerTest.java diff --git a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java index 8f58f93085..202235e36c 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncHandlerRegistry.java @@ -68,7 +68,7 @@ private void validateAndRegisterReturnType(AsyncHandler.ReturnType handler) { Class handlerClass = handler.getClass(); validateDirectImplementation(handlerClass, AsyncHandler.ReturnType.class); Class asyncType = extractAsyncType(handlerClass, AsyncHandler.ReturnType.class); - checkDuplicate(asyncType, handlerClass); + checkDuplicate(asyncType, handlerClass, true); handlers.put(asyncType, HandlerInfo.returnType(handler, asyncType)); } @@ -76,7 +76,7 @@ private void validateAndRegisterParameterType(AsyncHandler.ParameterType hand Class handlerClass = handler.getClass(); validateDirectImplementation(handlerClass, AsyncHandler.ParameterType.class); Class asyncType = extractAsyncType(handlerClass, AsyncHandler.ParameterType.class); - checkDuplicate(asyncType, handlerClass); + checkDuplicate(asyncType, handlerClass, false); handlers.put(asyncType, HandlerInfo.parameterType(handler, asyncType)); } @@ -89,9 +89,12 @@ private void validateDirectImplementation(Class handlerClass, Class target throw InvokerLogger.LOG.asyncHandlerIndirectImplementation(handlerClass); } - private void checkDuplicate(Class asyncType, Class handlerClass) { + private void checkDuplicate(Class asyncType, Class handlerClass, boolean isReturnType) { HandlerInfo existing = handlers.get(asyncType); if (existing != null && !existing.isBuiltin()) { + if (existing.getHandlerClass() == handlerClass && existing.isReturnType() != isReturnType) { + throw InvokerLogger.LOG.asyncHandlerBothKinds(handlerClass, asyncType); + } throw InvokerLogger.LOG.asyncHandlerDuplicate(asyncType, handlerClass); } } @@ -186,23 +189,25 @@ public static class HandlerInfo { private final AsyncHandler.ReturnType returnTypeHandler; private final AsyncHandler.ParameterType parameterTypeHandler; private final Class asyncType; + private final Class handlerClass; private final boolean isReturnType; private boolean builtin; static HandlerInfo returnType(AsyncHandler.ReturnType handler, Class asyncType) { - return new HandlerInfo(handler, null, asyncType, true); + return new HandlerInfo(handler, null, asyncType, handler.getClass(), true); } static HandlerInfo parameterType(AsyncHandler.ParameterType handler, Class asyncType) { - return new HandlerInfo(null, handler, asyncType, false); + return new HandlerInfo(null, handler, asyncType, handler.getClass(), false); } private HandlerInfo(AsyncHandler.ReturnType returnTypeHandler, AsyncHandler.ParameterType parameterTypeHandler, - Class asyncType, boolean isReturnType) { + Class asyncType, Class handlerClass, boolean isReturnType) { this.returnTypeHandler = returnTypeHandler; this.parameterTypeHandler = parameterTypeHandler; this.asyncType = asyncType; + this.handlerClass = handlerClass; this.isReturnType = isReturnType; } @@ -220,6 +225,10 @@ public Class getAsyncType() { return asyncType; } + public Class getHandlerClass() { + return handlerClass; + } + public boolean isReturnType() { return isReturnType; } diff --git a/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java b/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java index c5a83bd4c4..4356b4e78d 100644 --- a/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java +++ b/impl/src/main/java/org/jboss/weld/logging/InvokerLogger.java @@ -114,4 +114,7 @@ public interface InvokerLogger extends WeldLogger { @Message(id = 2029, value = "Unhandled primitive type: {0}", format = Format.MESSAGE_FORMAT) RuntimeException unhandledPrimitiveType(Object primitive); + + @Message(id = 2030, value = "AsyncHandler {0} implements both ReturnType and ParameterType for the same async type {1}", format = Format.MESSAGE_FORMAT) + DefinitionException asyncHandlerBothKinds(Object handlerClass, Object asyncType); } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherParamTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherParamTypeHandler.java new file mode 100644 index 0000000000..a6a9c4583f --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherParamTypeHandler.java @@ -0,0 +1,10 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.invoke.AsyncHandler; + +public class AnotherParamTypeHandler implements AsyncHandler.ParameterType> { + @Override + public MyAsyncType transformArgument(MyAsyncType original, Runnable completion) { + return original; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherReturnTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherReturnTypeHandler.java new file mode 100644 index 0000000000..f00aa491c4 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/AnotherReturnTypeHandler.java @@ -0,0 +1,10 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.invoke.AsyncHandler; + +public class AnotherReturnTypeHandler implements AsyncHandler.ReturnType> { + @Override + public MyAsyncType transform(MyAsyncType original, Runnable completion) { + return original; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java index b893251e80..f70f702766 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandler.java @@ -2,14 +2,15 @@ import jakarta.enterprise.invoke.AsyncHandler; -public class BothInterfacesHandler implements AsyncHandler.ReturnType, AsyncHandler.ParameterType { +public class BothInterfacesHandler + implements AsyncHandler.ReturnType>, AsyncHandler.ParameterType> { @Override - public T transform(T original, Runnable completion) { + public MyAsyncType transform(MyAsyncType original, Runnable completion) { return original; } @Override - public T transformArgument(T original, Runnable completion) { + public MyAsyncType transformArgument(MyAsyncType original, Runnable completion) { return original; } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java index 9db5985e8c..9fb540dea2 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/BothInterfacesHandlerTest.java @@ -21,7 +21,7 @@ public class BothInterfacesHandlerTest { public static Archive deploy() { return ShrinkWrap.create(BeanArchive.class, Utils.getDeploymentNameAsHash(BothInterfacesHandlerTest.class)) - .addClass(BothInterfacesHandler.class) + .addClasses(BothInterfacesHandler.class, MyAsyncType.class) .addAsServiceProvider(AsyncHandler.ReturnType.class, BothInterfacesHandler.class) .addAsServiceProvider(AsyncHandler.ParameterType.class, BothInterfacesHandler.class); } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandler.java new file mode 100644 index 0000000000..59ff0d3903 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandler.java @@ -0,0 +1,10 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.invoke.AsyncHandler; + +public class DuplicateParamTypeHandler implements AsyncHandler.ParameterType> { + @Override + public MyAsyncType transformArgument(MyAsyncType original, Runnable completion) { + return original; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandlerTest.java new file mode 100644 index 0000000000..ccf001a11f --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateParamTypeHandlerTest.java @@ -0,0 +1,32 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.enterprise.invoke.AsyncHandler; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.ShouldThrowException; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class DuplicateParamTypeHandlerTest { + + @Deployment + @ShouldThrowException(DeploymentException.class) + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(DuplicateParamTypeHandlerTest.class)) + .addClasses(DuplicateParamTypeHandler.class, AnotherParamTypeHandler.class, MyAsyncType.class) + .addAsServiceProvider(AsyncHandler.ParameterType.class, + DuplicateParamTypeHandler.class, AnotherParamTypeHandler.class); + } + + @Test + public void trigger() { + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandler.java new file mode 100644 index 0000000000..87a4dfc2fe --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandler.java @@ -0,0 +1,10 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.invoke.AsyncHandler; + +public class DuplicateReturnTypeHandler implements AsyncHandler.ReturnType> { + @Override + public MyAsyncType transform(MyAsyncType original, Runnable completion) { + return original; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandlerTest.java new file mode 100644 index 0000000000..f8e14092fc --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/DuplicateReturnTypeHandlerTest.java @@ -0,0 +1,32 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.enterprise.invoke.AsyncHandler; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.ShouldThrowException; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class DuplicateReturnTypeHandlerTest { + + @Deployment + @ShouldThrowException(DeploymentException.class) + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(DuplicateReturnTypeHandlerTest.class)) + .addClasses(DuplicateReturnTypeHandler.class, AnotherReturnTypeHandler.class, MyAsyncType.class) + .addAsServiceProvider(AsyncHandler.ReturnType.class, + DuplicateReturnTypeHandler.class, AnotherReturnTypeHandler.class); + } + + @Test + public void trigger() { + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/MyAsyncType.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/MyAsyncType.java new file mode 100644 index 0000000000..a6711cbe64 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/MyAsyncType.java @@ -0,0 +1,4 @@ +package org.jboss.weld.tests.invokable.async.broken; + +public interface MyAsyncType { +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandler.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandler.java new file mode 100644 index 0000000000..20b49f75a6 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandler.java @@ -0,0 +1,10 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.invoke.AsyncHandler; + +public class TypeVariableAsyncHandler implements AsyncHandler.ReturnType { + @Override + public T transform(T original, Runnable completion) { + return original; + } +} diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandlerTest.java new file mode 100644 index 0000000000..1e0db4b7f0 --- /dev/null +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/broken/TypeVariableAsyncHandlerTest.java @@ -0,0 +1,31 @@ +package org.jboss.weld.tests.invokable.async.broken; + +import jakarta.enterprise.inject.spi.DefinitionException; +import jakarta.enterprise.invoke.AsyncHandler; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.ShouldThrowException; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.BeanArchive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.weld.test.util.Utils; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class TypeVariableAsyncHandlerTest { + + @Deployment + @ShouldThrowException(DefinitionException.class) + public static Archive deploy() { + return ShrinkWrap.create(BeanArchive.class, + Utils.getDeploymentNameAsHash(TypeVariableAsyncHandlerTest.class)) + .addClass(TypeVariableAsyncHandler.class) + .addAsServiceProvider(AsyncHandler.ReturnType.class, TypeVariableAsyncHandler.class); + } + + @Test + public void trigger() { + } +} From 5dca3c67d4f0773e524ca1cd0ef184394a30a52d Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Thu, 14 May 2026 17:39:32 +0200 Subject: [PATCH 2/2] Defer dependent bean cleanup for parameter type async handlers --- .../weld/invokable/AsyncInvokerImpl.java | 95 ++++++++++++++++++- .../async/paramtype/ParamTypeBean.java | 15 +++ .../async/paramtype/ParamTypeExtension.java | 10 ++ .../ParameterTypeAsyncHandlerTest.java | 21 ++++ 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java b/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java index 9515f003c2..b9a7097693 100644 --- a/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java +++ b/impl/src/main/java/org/jboss/weld/invokable/AsyncInvokerImpl.java @@ -1,6 +1,7 @@ package org.jboss.weld.invokable; import java.lang.invoke.MethodHandle; +import java.util.concurrent.atomic.AtomicInteger; import jakarta.enterprise.inject.build.compatible.spi.InvokerInfo; import jakarta.enterprise.invoke.AsyncHandler; @@ -18,9 +19,11 @@ * ({@code foldArguments} is skipped). Instead, this invoker creates * {@code CleanupActions} externally and passes it as the first parameter to * {@code mh.invoke()}. This allows {@code transformArgument} to receive - * {@code ca::cleanup} as the completion callback before the method runs, which is - * required for correct behavior when the async parameter completes synchronously - * during the method body. + * the completion callback before the method runs. + *

+ * Cleanup of dependent beans is deferred via {@link DeferredCleanup} so that + * it only happens once both the method has returned and the completion callback + * has been called. See {@link DeferredCleanup} for details. *

* When no cleanup is needed, the chain has no {@code CleanupActions} at all and * the handler receives a no-op completion callback. @@ -44,11 +47,14 @@ class AsyncInvokerImpl implements Invoker, InvokerInfo { public R invoke(T instance, Object[] arguments) throws Exception { Runnable completion; CleanupActions ca; + DeferredCleanup deferred; if (requiresCleanup) { ca = new CleanupActions(); - completion = ca::cleanup; + deferred = new DeferredCleanup(ca); + completion = deferred; } else { ca = null; + deferred = null; completion = () -> { }; } @@ -63,11 +69,92 @@ public R invoke(T instance, Object[] arguments) throws Exception { } else { result = (R) mh.invoke(instance, arguments); } + if (deferred != null) { + deferred.methodReturned(); + } + // completion is still valid here; the state machine handles repeated calls return (R) parameterTypeHandler.transformReturnValue(result, completion); } catch (ValueCarryingException e) { + if (deferred != null) { + deferred.forceCleanup(); + } return (R) e.getMethodReturnValue(); } catch (Throwable e) { + if (deferred != null) { + deferred.forceCleanup(); + } throw SneakyThrow.sneakyThrow(e); } } + + /** + * Coordinates cleanup of dependent beans between the completion callback + * (fired by the async handler) and the method return. + *

+ * Cleanup must only run when both the completion callback has been + * called and the target method has returned. This prevents premature destruction + * of dependent beans when the completion callback fires synchronously during the + * method body (e.g., the method calls {@code asyncParam.resume()} directly). + *

+ * Uses a four-state machine driven by {@link AtomicInteger} CAS operations: + * + *

+     *   PENDING (0)  ──run()──►  COMPLETION_SIGNALED (1)
+     *       │                             │
+     *   methodReturned()              methodReturned()
+     *       │                             │
+     *       ▼                             ▼
+     *   METHOD_RETURNED (2)  ──run()──►  DONE (3) → cleanup runs
+     * 
+ *
    + *
  • Sync case (completion fires during method): transitions + * 0→1, then 1→3 on method return; cleanup runs after the method.
  • + *
  • Async case (completion fires after method): transitions + * 0→2 on method return, then 2→3 on completion; cleanup runs on completion.
  • + *
  • Exception case: {@link #forceCleanup()} sets state to 3 directly. + * The MH chain's {@code tryFinally/runExceptionOnly} may also call + * {@code ca.cleanup()}, but that is safe because {@link CleanupActions#cleanup()} + * clears its internal lists on first call.
  • + *
+ * Thread safety is ensured by CAS; the async completion callback may fire + * on a different thread than the invoker. + */ + private static class DeferredCleanup implements Runnable { + private static final int PENDING = 0; + private static final int COMPLETION_SIGNALED = 1; + private static final int METHOD_RETURNED = 2; + private static final int DONE = 3; + + private final CleanupActions ca; + private final AtomicInteger state = new AtomicInteger(PENDING); + + DeferredCleanup(CleanupActions ca) { + this.ca = ca; + } + + @Override + public void run() { + if (state.compareAndSet(PENDING, COMPLETION_SIGNALED)) { + return; + } + if (state.compareAndSet(METHOD_RETURNED, DONE)) { + ca.cleanup(); + } + } + + void methodReturned() { + if (state.compareAndSet(PENDING, METHOD_RETURNED)) { + return; + } + if (state.compareAndSet(COMPLETION_SIGNALED, DONE)) { + ca.cleanup(); + } + } + + void forceCleanup() { + if (state.getAndSet(DONE) != DONE) { + ca.cleanup(); + } + } + } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java index 88ff2bfaef..fdb41829fa 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeBean.java @@ -1,5 +1,8 @@ package org.jboss.weld.tests.invokable.async.paramtype; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + import java.util.concurrent.CompletableFuture; import jakarta.enterprise.context.ApplicationScoped; @@ -18,6 +21,10 @@ public void helloNoLookup(CompletableFuture future, MyAsyncParam public void helloSync(DependentBean dep, MyAsyncParam async) { async.resume("sync-hello"); + + // completion was signaled, but the method didn't return yet, + // so the dependency must not be destroyed + assertEquals(0, DependentBean.destroyedCounter.get()); } public void hello(DependentBean dep, CompletableFuture future, MyAsyncParam async) { @@ -29,4 +36,12 @@ public void hello(DependentBean dep, CompletableFuture future, MyAsyncPa } }); } + + public void helloThrow(DependentBean dep, CompletableFuture future, MyAsyncParam async) { + future.whenComplete((value, error) -> { + assertNull(error); + async.resume(value); + }); + throw new IllegalArgumentException("synchronous throw"); + } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java index 6af559623a..267a0f8f6c 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParamTypeExtension.java @@ -14,6 +14,7 @@ public class ParamTypeExtension implements Extension { private Invoker invoker; private Invoker syncInvoker; private Invoker noLookupInvoker; + private Invoker throwInvoker; public void observeBean(@Observes WeldProcessManagedBean pmb) { Collection> methods = pmb.getAnnotatedBeanClass().getMethods(); @@ -31,6 +32,11 @@ public void observeBean(@Observes WeldProcessManagedBean pmb) { .build(); } else if ("helloNoLookup".equals(name)) { noLookupInvoker = pmb.createInvoker(m).build(); + } else if ("helloThrow".equals(name)) { + throwInvoker = pmb.createInvoker(m) + .withInstanceLookup() + .withArgumentLookup(0) + .build(); } } } @@ -46,4 +52,8 @@ public void observeBean(@Observes WeldProcessManagedBean pmb) { public Invoker getNoLookupInvoker() { return noLookupInvoker; } + + public Invoker getThrowInvoker() { + return throwInvoker; + } } diff --git a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java index f43a654a62..706fffea3b 100644 --- a/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java +++ b/tests-arquillian/src/test/java/org/jboss/weld/tests/invokable/async/paramtype/ParameterTypeAsyncHandlerTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.util.Arrays; @@ -109,4 +110,24 @@ public void testParameterTypeHandler() throws Exception { assertTrue(result.isComplete()); assertEquals("param-hello", result.getIfComplete()); } + + @Test + public void testSynchronousThrow() throws Exception { + DependentBean.reset(); + CompletableFuture future = new CompletableFuture<>(); + MyAsyncParam result = MyAsyncParam.createSuspended(); + + assertEquals(0, DependentBean.destroyedCounter.get()); + + assertThrows(IllegalArgumentException.class, () -> { + extension.getThrowInvoker().invoke(null, new Object[] { null, future, result }); + }); + + assertEquals(1, DependentBean.destroyedCounter.get()); + + // completion callback fired after exception must be a noop + future.complete("hello"); + + assertEquals(1, DependentBean.destroyedCounter.get()); + } }