shashank created CAMEL-26445:
--------------------------------

             Summary: 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


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)

Reply via email to