Andrea Cosentino created CAMEL-24286:
----------------------------------------

             Summary: camel-support: BackgroundTask supplier exception hangs an 
unlimited-duration run() and leaks the task registration
                 Key: CAMEL-24286
                 URL: https://issues.apache.org/jira/browse/CAMEL-24286
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: Andrea Cosentino
            Assignee: Andrea Cosentino


Follow-up defect from the ForegroundTask -> BackgroundTask reconnection-loop 
migration (CAMEL-24272 / CAMEL-24278, both merged).

h3. Problem

{{BackgroundTask.runTaskWrapper}} rethrows a supplier exception on the 
scheduler thread without releasing the completion latch or unregistering the 
task:

{code:java}
try {
    if (doRun(supplier)) {
        ...
        latch.countDown();
    }
} catch (Exception e) {          // BackgroundTask.java ~131-134 on main
    status = Status.Failed;
    cause = e;
    throw e;                     // rethrown onto the ScheduledExecutorService 
thread
}
{code}

When the supplier throws a non-{{TaskRunFailureException}}:

* the exception is rethrown onto the {{scheduleWithFixedDelay}} worker, which 
silently suppresses all further executions of that task (standard 
{{ScheduledExecutorService}} contract);
* {{latch.countDown()}} is never reached, and {{registry.removeTask(this)}} is 
never called on this path;
* for a task built with {{withUnlimitedDuration()}}, {{waitForTaskCompletion}} 
calls {{latch.await()}} with no timeout, so *the calling thread blocks forever* 
and the task *leaks in the TaskManagerRegistry*.

{{ReentrantLock}} is not involved here, but note {{latch.await()}} in the 
unlimited case is likewise unrecoverable once the only countdown site is 
skipped.

The interrupt path has a related smaller gap: in {{waitForTaskCompletion}}, 
{{registry.removeTask(this)}} sits inside the {{try}} (~line 205), while the 
{{finally}} only resets {{elapsed}}/{{running}}. An {{InterruptedException}} 
from {{latch.await()}} therefore also leaves the task registered.

h3. Reachability on main (post CAMEL-24272)

Both of these now route an *unlimited-duration* reconnection through 
{{BackgroundTask.run()}}:

* *camel-ftp SFTP* — {{SftpOperations.java:142}} uses 
{{withUnlimitedDuration()}}; {{tryConnect}} catches only {{JSchException}} 
({{:209}}, {{:221}}). A non-JSch {{RuntimeException}} on the connect path (e.g. 
an invalid {{filenameEncoding}} reaching {{Charset.forName}}) escapes into the 
throw path above -> hang + registry leak.
* *camel-mongodb-gridfs* — {{GridFsConsumer.java:122}} 
{{withUnlimitedDuration()}}, {{:131}} {{task.run(...)}}. In 
{{processCollection}} the inner {{catch (Exception) { // ignore }}} ({{:184}}) 
wraps *only* {{getProcessor().process(exchange)}}; the cursor acquisition 
{{getGridFSFileMongoCursor(...)}} ({{:143}}) and the pre-process 
{{findOneAndUpdate(...)}} ({{:155}}) are *outside* any catch. A transient 
{{MongoException}} there escapes -> same hang + leak.

Bounded tasks ({{withMaxDuration}}, e.g. Infinispan) do not hang because 
{{latch.await(maxDuration)}} times out -- but they still fail slow and lose the 
real cause. Suppliers that catch-all and return {{false}} (e.g. FTP 
{{tryConnect}}) are unaffected.

h3. Suggested fix

Treat a thrown supplier exception as a terminal failure that still releases the 
caller:

* in the {{catch}} block, set {{completed.set(false)}}, unregister when 
{{!registeredByRun}}, and call {{latch.countDown()}} (do not rely on rethrowing 
onto the scheduler thread, which serves no purpose there);
* move {{registry.removeTask(this)}} in {{waitForTaskCompletion}} into the 
{{finally}} so the interrupt path also unregisters.

A regression test can build an unlimited-duration {{BackgroundTask}} whose 
supplier throws on the first run and assert (with a bounded await, daemon 
thread) that {{run()}} returns and the registry ends empty -- mirroring the 
shape used in CAMEL-24244's {{StreamCachingStrategySpoolStatisticsTest}}.

h3. Notes

Found while reviewing the task-manager PR cluster (#25164 merged, #25169 
closed). Not attributable to a single PR -- it is the residual 
foreground->background porting gap now reachable on main. Raising for the 
owners of CAMEL-2427x to assess, since some of the rethrow behaviour may be 
intentional for programming errors; the latch-release/unregister omission is 
the part that looks unintended.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to