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)

Reply via email to