shashank created CAMEL-26578:
--------------------------------
Summary: camel-core - ProducerTemplate: the future of an async
send or request never completes when the template is stopped before the task ran
Key: CAMEL-26578
URL: https://issues.apache.org/jira/browse/CAMEL-26578
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
Assignee: shashank
Found with Fray (a controlled concurrency testing tool for the JVM) on a test
that stops a {{ProducerTemplate}} while an {{asyncRequestBody}} is pending; it
failed at the third schedule, and the latch-based JUnit tests below reproduce
it deterministically.
{{DefaultProducerTemplate.asyncSendExchange}} creates the {{CompletableFuture}}
it returns and {{submit}}s a lambda to the template executor that calls
{{getProducerCache().asyncSendExchange(..., future)}}. Nobody completes that
future when the lambda does not get to the producer cache:
# *The task is still queued when the template stops.* {{doStop}} calls
{{ExecutorServiceManager.shutdownNow(executor)}}, which drops the queued tasks;
their futures are never completed.
# *The task runs after the stop* (an executor that does not drop it, such as a
custom one set with {{setExecutorService}}): {{getProducerCache()}} throws
{{IllegalStateException("ProducerTemplate has not been started")}} inside the
lambda, {{submit()}} keeps it in its own {{FutureTask}} which nobody reads, and
the returned future is never completed.
# *Anything else that throws before the producer cache's own try/catch*, e.g.
{{Endpoint.createExchange}} failing in
{{DefaultProducerCache.asyncSendExchange}}: same as 2.
A caller in {{future.get()}} without a timeout waits for ever;
{{thenApply}}/{{whenComplete}} stages never run.
{{DefaultFluentProducerTemplate.asyncSend()}}/{{asyncRequest()}} go through the
same method.
Short trace (case 1): caller {{asyncRequestBody("direct:start", body)}} -> task
queued behind a busy executor thread -> another thread {{template.stop()}} ->
{{shutdownNow}} returns the task, which is dropped -> {{future.get()}} never
returns.
{code:java}
ProducerTemplate producer = context.createProducerTemplate();
ExecutorService single =
context.getExecutorServiceManager().newSingleThreadExecutor(this, "test");
producer.setExecutorService(single);
single.execute(() -> await(release)); // keep the only thread busy
Future<Object> f = producer.asyncRequestBody("direct:start", "Hello World");
producer.stop(); // shutdownNow drops the
queued task
release.countDown();
f.get(); // never returns
{code}
The same happens with the default executor when the template is stopped while
more async requests are queued than the pool has threads. (Not covered here: a
template that is not stopped itself, whose pool the ExecutorServiceManager
shuts down when the CamelContext stops; its queued tasks are dropped after the
shutdown await while the template stays started.)
Fix idea: send through a small {{Runnable}} that holds the future and completes
it exceptionally when sending throws; keep the tasks that have not started in a
set and complete their futures exceptionally (with a
{{RejectedExecutionException}}) in {{doStop}} after the executor is shut down
(also a task handed over while the template is stopping). Sent exchanges and
the futures of requests that run are unchanged.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)