shashank created CAMEL-25037:
--------------------------------
Summary: camel-support - an interrupted
EventDrivenPollingConsumer.receive() never returns: it loops at 100 % CPU and
logs a WARN on every iteration (regression of CAMEL-20297, 4.4)
Key: CAMEL-25037
URL: https://issues.apache.org/jira/browse/CAMEL-25037
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
{{EventDrivenPollingConsumer.receive()}}
({{core/camel-support/.../EventDrivenPollingConsumer.java:128-153}}) loops
{{while (isRunAllowed())}} around {{queue.take()}}. On {{InterruptedException}}
it calls {{handleInterruptedException}} ({{:236-239}}). Since CAMEL-20297
(commit ff48bbc4d2, 4.4.0) that method restores the interrupt flag
({{Thread.currentThread().interrupt()}}) and then logs through the
{{LoggingExceptionHandler}} at WARN. The loop then calls {{take()}} again,
which throws at once because the flag is set, and so on. The thread never
leaves {{receive()}}, even when a message is queued, because {{take()}} checks
the flag first. It uses a full core and writes one WARN line per iteration.
This is the same pattern as CAMEL-22390 (SedaConsumer infinite loop on
interrupt, also caused by CAMEL-20297). That issue was closed as "Information
Provided" with the advice that end users should not interrupt Camel's own
threads. The case here is different. {{ConsumerTemplate.receive()}} and
{{PollingConsumer.receive()}} run on the caller's own thread, which the
application may legitimately interrupt or cancel. Camel itself also interrupts
route threads during a forced shutdown. A blocking {{receive()}} that is
interrupted should return, not spin.
A thread in {{receive()}} gets interrupted when, for example:
* a task doing {{consumerTemplate.receive(uri)}} is cancelled with
{{Future.cancel(true)}}, or its executor is shut down with {{shutdownNow()}};
* the thread pool of a route that waits in {{pollEnrich}} (default timeout -1)
is shut down with {{shutdownNow()}}, which Camel does on a forced shutdown (for
example the Threads EIP pool in {{ThreadsProcessor.doShutdown}}, or
{{BaseExecutorServiceManager.doShutdown}} after its await timeout).
Affected: 4.4.0 and later (4.4.x, 4.8.x, 4.14.x, 4.18.x, main).
*Reproduction* (4.23.0-SNAPSHOT):
{noformat}
thread A: consumerTemplate.receive("direct:idle") -> waiting in
EventDrivenPollingConsumer.receive:141
A.interrupt()
2 s later: A alive=true state=RUNNABLE, CPU time used by A in those 2 s = 1979
ms
200 stack samples of A: EventDrivenPollingConsumer.receive:141 /
handleInterruptedException:237 / LoggingExceptionHandler
with log4j at WARN: 603,114 lines "WARN EventDrivenPollingConsumer - Caused by:
[java.lang.InterruptedException - null]"
in about 3 s (the log file reached 544 MB)
{noformat}
With the fix below {{receive()}} returns null 591 ms after the interrupt and
the thread ends.
TLA+: the same model with interrupts violates {{ReceiveEndsAfterInterrupt}}
({{pc_shared_interrupt_live}}). The lasso is Interrupt -> RIntr -> RLoop ->
RAcq -> RIntr ..., and it holds even with a message in the queue. The fix
variant ({{pc_fix_interrupt}}) satisfies it.
*Proposed fix:* on {{InterruptedException}} in {{receive()}} /
{{receive(timeout)}}, restore the interrupt flag and return null (leave the
loop). Do not call {{take()}} again. Optionally log once at DEBUG, like
{{process()}} does for an interrupted put.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)