[
https://issues.apache.org/jira/browse/CAMEL-26445?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Work on CAMEL-26445 started by shashank.
----------------------------------------
> camel-core - RedeliveryErrorHandler: an AsyncProcessor that throws hangs the
> exchange when the error handler has no redelivery (SimpleTask.run has no
> try/catch)
> ----------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-26445
> URL: https://issues.apache.org/jira/browse/CAMEL-26445
> Project: Camel
> Issue Type: Bug
> Components: camel-core
> Reporter: shashank
> Assignee: shashank
> Priority: Major
>
> Follow-up of CAMEL-26430, found while working on it.
> {{RedeliveryErrorHandler}} runs a step through its {{SimpleTask}} when it has
> nothing to do on a failure but give it back: no redelivery, no exception
> policies ({{onException}}), no dead letter channel and no
> {{onPrepareFailure}} processor ({{doStart}}: {{simpleTask = deadLetter ==
> null && !redeliveryEnabled && exceptionPolicies.isEmpty() &&
> onPrepareProcessor == null}}). That is the default error handler of a route
> without {{onException}}, so it is the path of most steps.
> {{SimpleTask.run()}} runs as reactive work
> ({{reactiveExecutor.scheduleMain(task)}} in
> {{RedeliveryErrorHandler.process}}) and calls the processor in
> {{handleFirst()}} ({{processOutput(exchange, this)}}) without a try/catch.
> When the processor is an {{AsyncProcessor}} that throws from
> {{process(exchange, callback)}} instead of setting the exception on the
> exchange, the exception reaches {{DefaultReactiveExecutor}}, which logs it
> ({noformat}Error executing reactive work due to ... This exception is
> ignored.{noformat}) and drops it. The callback is never called, so the caller
> waits forever: a synchronous {{template.send}} or a consumer thread blocks,
> an asynchronous caller never completes.
> {code:java}
> // default error handler, no onException: the error handler uses its
> SimpleTask
> from("direct:start")
> .process(new AsyncProcessorSupport() {
> @Override
> public boolean process(Exchange exchange, AsyncCallback callback) {
> throw new IllegalStateException("thrown"); // instead of
> exchange.setException(...) + callback.done(true)
> }
> })
> .to("mock:result");
> template.send("direct:start", e -> e.getMessage().setBody("Hello")); //
> never returns
> {code}
> On main (0fd7d9abf9c2, 2026-10-31) the send above times out; the same
> processor in a route with a handled {{onException}} or a dead letter channel
> (no redelivery) completes, because those use {{RedeliveryTask}}, whose
> {{run()}} has a try/catch. A plain {{Processor}} that throws does not hang
> either, as its converter to {{AsyncProcessor}} catches the exception and sets
> it on the exchange. CAMEL-26301 (the tokenizer, fixed in
> {{TokenizerProcessor}}) was this path; any other {{AsyncProcessor}} with the
> same bug hangs the same way. CAMEL-26430 fixes the same hang for a redelivery
> ({{RedeliveryTask.redeliver()}}).
> *Proposal*
> * Guard {{SimpleTask.handleFirst()}}: when {{processOutput}} throws before
> the callback was called, set the exception on the exchange and continue as
> when the processor sets the exception and calls the callback ({{run()}} then
> sees the failure and prepares the exchange after the failure, as for any
> failure there). Use the same exactly-once rule as CAMEL-26430: the callback
> and the takeover compete for one state (the number of the call that is
> awaited, in a field of the task), the takeover goes through the reactive
> executor after the work already queued there (so a callback that is already
> queued still wins, as now), a callback that comes after the takeover is
> ignored with a WARN, and an exception thrown after the callback was called is
> left to the reactive executor as now.
> * {{SimpleTask}} is the success path of nearly every step (see CAMEL-25132),
> so the guard must not add an allocation per step: for example a field updated
> with an {{AtomicIntegerFieldUpdater}} in the task (one CAS per step), and
> allocations only when the processor throws.
> * Tests: an {{AsyncProcessor}} that throws from {{process}} with the default
> error handler (the send must complete with the exception), the same in a
> route called from a route with {{onException}} (the called route fails as
> with a plain {{Processor}} that throws: its error handler gives the failure
> back as exhausted, so the caller does not handle it again), asynchronous
> (after an async step), transacted and with pooled exchanges, plus the
> controls of CAMEL-26430 (the callback called before the throw, a callback
> queued through a {{direct}} route, a callback called later from another
> thread).
> * A processor that calls its callback twice: the second call must be ignored
> too (on main it continues the exchange a second time, or with pooled tasks
> fails with a {{NullPointerException}} on the released task), and a stress
> test of a callback called from another thread while the processor throws
> (each exchange continued once).
> * Related, not part of this: a route with {{noErrorHandler()}} does not use
> {{RedeliveryErrorHandler}}, and the same {{AsyncProcessor}} hangs there too.
> * Related, not part of this (a separate follow-up): {{RedeliveryTask.run()}}
> (the first delivery when the error handler has redelivery or an
> {{onException}}) calls the callback directly when the processor throws, so
> that failure skips redelivery, the dead letter channel and {{onException}},
> while a throw on a redelivery goes through them after CAMEL-26430.
> CAMEL-24980 proposes to share the failure handling of the two tasks. Its
> catch has no exactly-once guard either (a callback handed to another thread
> before the throw continues the exchange a second time).
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)