[
https://issues.apache.org/jira/browse/CAMEL-24286?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Andrea Cosentino resolved CAMEL-24286.
--------------------------------------
Resolution: Fixed
Fixed on main via PR https://github.com/apache/camel/pull/25217. When a
BackgroundTask supplier throws, the blocking run() latch is now released and
the task is unregistered (previously a withUnlimitedDuration() task would
await() forever and leak in the registry).
The same defect is present on camel-4.18.x and camel-4.14.x (their
runTaskWrapper catch block rethrows without releasing the latch), but the
surrounding code has diverged from main, so the fix does not cherry-pick
cleanly and a backport needs to be adapted and tested against each branch.
Flagging for a follow-up backport rather than porting a concurrency fix
unadapted.
_Claude Code on behalf of Andrea Cosentino (@oscerd)._
> 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
> Priority: Major
> Fix For: 4.22.0
>
>
> 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)