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) {

Reply via email to