This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch concurency in repository https://gitbox.apache.org/repos/asf/tomee.git
commit bdc3fe93952651f6d1ce5be0203aed0c8372295a Author: Richard Zowalla <[email protected]> AuthorDate: Thu Apr 2 13:13:38 2026 +0200 Fix scheduled async interceptor to call setFuture before ctx.proceed The TCK beans call Asynchronous.Result.getFuture() inside scheduled methods. The interceptor must call setFuture() before ctx.proceed() for both void and non-void return types, otherwise getFuture() throws IllegalStateException. Use Callable path for all scheduled methods to ensure proper future lifecycle. --- .../cdi/concurrency/AsynchronousInterceptor.java | 48 ++++++++++++---------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/AsynchronousInterceptor.java b/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/AsynchronousInterceptor.java index d896e9eaf2..a62438d3d7 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/AsynchronousInterceptor.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/cdi/concurrency/AsynchronousInterceptor.java @@ -119,19 +119,10 @@ public class AsynchronousInterceptor { final ZonedTrigger trigger = ScheduleHelper.toTrigger(schedules); final boolean isVoid = ctx.getMethod().getReturnType() == Void.TYPE; - if (isVoid) { - // void method: schedule as Runnable, runs indefinitely until cancelled - mses.schedule((Runnable) () -> { - try { - ctx.proceed(); - } catch (final Exception e) { - LOGGER.warning("Scheduled async method threw exception", e); - } - }, trigger); - return null; - } - - // non-void: schedule as Callable, each invocation gets a fresh future via Asynchronous.Result + // A single CompletableFuture represents ALL executions in the schedule. + // Each execution gets Asynchronous.Result.setFuture() called before ctx.proceed() + // so the bean method can call Asynchronous.Result.getFuture() / complete(). + // The schedule stops when the future is completed, cancelled, or an exception is thrown. final CompletableFuture<Object> outerFuture = mses.newIncompleteFuture(); mses.schedule((Callable<Object>) () -> { @@ -139,16 +130,29 @@ public class AsynchronousInterceptor { Asynchronous.Result.setFuture(outerFuture); final Object result = ctx.proceed(); + if (isVoid) { + // For void methods, the bean may call Asynchronous.Result.complete("value") + // to signal completion. If it didn't complete the future, the schedule continues. + Asynchronous.Result.setFuture(null); + return null; + } + if (result instanceof CompletionStage<?> cs) { - cs.whenComplete((val, err) -> { - if (err != null) { - outerFuture.completeExceptionally(err); - } else if (val != null) { - outerFuture.complete(val); - } + if (result == outerFuture) { + // Bean returned the container-provided future (via Asynchronous.Result.getFuture()). + // It may have been completed by Asynchronous.Result.complete() inside the method. Asynchronous.Result.setFuture(null); - }); - } else if (result != null && result != outerFuture) { + } else { + cs.whenComplete((val, err) -> { + if (err != null) { + outerFuture.completeExceptionally(err); + } else if (val != null) { + outerFuture.complete(val); + } + Asynchronous.Result.setFuture(null); + }); + } + } else if (result != null) { outerFuture.complete(result); Asynchronous.Result.setFuture(null); } @@ -159,7 +163,7 @@ public class AsynchronousInterceptor { return null; }, trigger); - return outerFuture; + return isVoid ? null : outerFuture; } private Exception validate(final Method method) {
