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

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


The following commit(s) were added to refs/heads/main by this push:
     new ee9df19a53b6 CAMEL-24286: Release the latch and unregister when a 
BackgroundTask supplier throws (#25217)
ee9df19a53b6 is described below

commit ee9df19a53b6472ac08d077e7e23aa13c991754e
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Jul 30 10:14:07 2026 +0200

    CAMEL-24286: Release the latch and unregister when a BackgroundTask 
supplier throws (#25217)
    
    BackgroundTask.runTaskWrapper rethrew a supplier exception on the scheduler
    thread without counting down the completion latch or unregistering the task.
    doRun only catches TaskRunFailureException, so any other exception reached 
this
    path. scheduleWithFixedDelay then suppresses all further executions, and 
for a
    task built with withUnlimitedDuration() waitForTaskCompletion calls
    latch.await() with no timeout — so the calling thread blocked forever and 
the
    task leaked in the TaskManagerRegistry.
    
    This is reachable on main via CAMEL-24272: camel-ftp SFTP (tryConnect 
catches
    only JSchException) and camel-mongodb-gridfs (its Mongo cursor/find calls 
are
    outside the inner catch) both run an unlimited-duration reconnection through
    BackgroundTask.run().
    
    Fix: in the catch, mark the task not-completed, unregister it, and count 
down
    the latch before rethrowing, so the blocking run() caller unblocks and 
observes
    the failure. The rethrow is kept so the schedule() path still surfaces the
    error to the executor.
    
    Also move removeTask + task.cancel into the finally of 
waitForTaskCompletion,
    so an InterruptedException from latch.await() no longer leaves the task
    registered.
    
    Add a regression test that runs an unlimited-duration task whose supplier
    throws; it fails (run() hangs on the join) without the fix and passes with 
it.
    The runner is a daemon thread so a regression cannot wedge the JVM.
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../task/task/BackgroundTaskRegistryTest.java      | 43 ++++++++++++++++++++++
 .../apache/camel/support/task/BackgroundTask.java  | 27 +++++++++-----
 2 files changed, 60 insertions(+), 10 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/task/task/BackgroundTaskRegistryTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/task/task/BackgroundTaskRegistryTest.java
index 3f0154d49802..011228af1275 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/support/task/task/BackgroundTaskRegistryTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/task/task/BackgroundTaskRegistryTest.java
@@ -128,6 +128,49 @@ class BackgroundTaskRegistryTest {
         assertThat(registry.getTasks()).isEmpty();
     }
 
+    @DisplayName("Test that run() returns and unregisters when the supplier 
throws (CAMEL-24286)")
+    @Test
+    @Timeout(15)
+    void testRunReturnsAndUnregistersWhenSupplierThrows() throws Exception {
+        AtomicBoolean runReturned = new AtomicBoolean(false);
+        AtomicBoolean completed = new AtomicBoolean(true);
+
+        // an unlimited-duration task: before CAMEL-24286 a thrown supplier 
exception left the latch
+        // un-counted, so run() blocked forever here and the task leaked in 
the registry
+        BackgroundTask task = Tasks.backgroundTask()
+                
.withScheduledExecutor(Executors.newSingleThreadScheduledExecutor())
+                .withBudget(Budgets.iterationTimeBudget()
+                        .withInterval(Duration.ofMillis(100))
+                        .withInitialDelay(Duration.ZERO)
+                        .withUnlimitedDuration()
+                        .build())
+                .build();
+
+        // daemon so a regression (run() hanging forever) cannot keep the JVM 
alive
+        Thread runner = new Thread(() -> {
+            boolean result = task.run(camelContext, () -> {
+                throw new IllegalStateException("boom");
+            });
+            completed.set(result);
+            runReturned.set(true);
+        });
+        runner.setDaemon(true);
+        runner.start();
+
+        runner.join(TimeUnit.SECONDS.toMillis(10));
+
+        assertThat(runReturned.get())
+                .as("run() must return when the supplier throws, not block 
forever")
+                .isTrue();
+        assertThat(completed.get())
+                .as("run() should report the task as not completed after a 
thrown exception")
+                .isFalse();
+        await().atMost(2, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertThat(registry.getTasks())
+                        .as("the failed task must not leak in the registry")
+                        .isEmpty());
+    }
+
     @DisplayName("Test that task is removed from registry after supplier 
succeeds")
     @Test
     @Timeout(10)
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
index 5065391ca38e..c8776c352898 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/task/BackgroundTask.java
@@ -130,7 +130,14 @@ public class BackgroundTask extends AbstractTask 
implements BlockingTask {
             }
         } catch (Exception e) {
             status = Status.Failed;
+            completed.set(false);
             cause = e;
+            // release the blocking run() caller and unregister; without this 
a task built with
+            // withUnlimitedDuration() would await() forever and stay in the 
registry (CAMEL-24286)
+            if (!registeredByRun && registry != null) {
+                registry.removeTask(this);
+            }
+            latch.countDown();
             throw e;
         }
         // scheduleWithFixedDelay waits interval after this run finishes, so 
compute from now
@@ -196,20 +203,20 @@ public class BackgroundTask extends AbstractTask 
implements BlockingTask {
                     LOG.debug("The task has finished the execution and it is 
ready to continue");
                 }
             }
-
-            TaskManagerRegistry registry = null;
-            if (camelContext != null) {
-                registry = 
PluginHelper.getTaskManagerRegistry(camelContext.getCamelContextExtension());
-            }
-            if (registry != null) {
-                registry.removeTask(this);
-            }
-
-            task.cancel(true);
         } catch (InterruptedException e) {
             LOG.warn("Interrupted while waiting for the repeatable task to 
execute: {}", e.getMessage(), e);
             Thread.currentThread().interrupt();
         } finally {
+            // unregister and cancel even if the await was interrupted, 
otherwise the task leaks in
+            // the registry and the scheduled future keeps running 
(CAMEL-24286)
+            if (camelContext != null) {
+                TaskManagerRegistry registry
+                        = 
PluginHelper.getTaskManagerRegistry(camelContext.getCamelContextExtension());
+                if (registry != null) {
+                    registry.removeTask(this);
+                }
+            }
+            task.cancel(true);
             elapsed = budget.elapsed();
             running.set(false);
         }

Reply via email to