shashank created CAMEL-25094:
--------------------------------
Summary: camel-seda, camel-disruptor - a message sent InOnly to
seda: or disruptor: from inside a Multicast or Split loses its spooled stream
cache file when the parent exchange completes
Key: CAMEL-25094
URL: https://issues.apache.org/jira/browse/CAMEL-25094
Project: Camel
Issue Type: Bug
Components: camel-seda, camel-disruptor
Reporter: shashank
With stream caching spooled to disk ({{spoolEnabled=true}} and a body over the
spool threshold), the body is a {{FileInputStreamCache}}. Its temporary file is
deleted when the last exchange that holds it is done:
{{FileInputStreamCache.TempFileManager}} counts one for every {{addExchange}}
(the creation, and every {{StreamCache.copy(exchange)}}) and registers the
matching countdown as an on completion ({{FileInputStreamCache.java:255}}). If
the exchange has the property {{CamelStreamCacheUnitOfWork}}, the countdown is
registered on that unit of work instead of on the exchange.
Multicast and Split set {{CamelStreamCacheUnitOfWork}} to the parent's unit of
work on every sub-exchange ({{MulticastProcessor.java:1127-1129}},
{{Splitter.java:415-416}}), so that the stream caches of the sub-routes live
until the parent has aggregated them.
A sub-exchange that sends to {{seda:}} without waiting for a reply (InOnly)
hands a copy to the queue. {{SedaProducer.addToQueue}} creates the copy and,
since CAMEL-20866 (4.7), gives it its own reference to the stream cache with
{{sc.copy(target)}} ({{SedaProducer.java:227}}). The copy keeps
{{CamelStreamCacheUnitOfWork}}, so that reference is registered on the parent's
unit of work as well. The InOnly send returns at once, the parent finishes, its
unit of work counts down both references and the file is deleted. The seda
consumer then routes the copy and fails:
{noformat}
WARN SedaConsumer - Error processing exchange. Exchange[]. Caused by:
[org.apache.camel.StreamCacheException - Error during type conversion from
type: null to the required type:
org.apache.camel.StreamCache due to org.apache.camel.RuntimeCamelException:
Cannot reset stream from file ...]
{noformat}
The seda route never gets past its start, so the message is lost after the
parent was told that it was sent. {{SedaConsumer}} logs the failure at WARN, as
above. {{DisruptorProducer.prepareCopy}} ({{DisruptorProducer.java:234}}) does
the same for {{disruptor:}}, and there the failure is not logged at all: the
disruptor consumer processes the exchange with a no-op callback, so the message
disappears without a log line.
The Wire Tap EIP had exactly this problem and removes the property from its
copy ({{WireTapProcessor.java:282}}, CAMEL-12108). In CAMEL-20866, SEDA
(InOnly), Disruptor (InOnly) and Wire Tap were named as the cases where the
copied exchange is executed independently, but only the Wire Tap drops the
property.
h3. Reproduction
Stream caching with {{spoolEnabled=true}} and a small {{spoolThreshold}}, the
body is a stream of 16 KB. The seda (or disruptor) route waits until the
caller's {{sendBody}} has returned, so the parent exchange is done, and then
reads the body.
|| route || main ||
| {{multicast().to("seda:b", "mock:other")}} | fails every time, as above |
| {{multicast().parallelProcessing().to("seda:b", "mock:other")}} | fails every
time |
| {{split(body()).to("direct:part")}} with {{from("direct:part").to("seda:b")}}
(the parts are streams) | fails every time |
| the same three with {{disruptor:b}} | fail every time, nothing is logged |
| {{to("seda:b")}} or {{to("disruptor:b")}} without a Multicast or Split
(control) | works |
| {{recipientList(constant("seda:b,mock:other"))}} (control) | works: the
Recipient List creates its own sub-exchanges and does not set the property |
The wait in the seda route is not needed to trigger the failure: without it the
multicast case still failed in 20 of 20 runs, because the parent always
finishes first. The multicast case fails in the same way with the 4.6.0
{{SedaProducer}} (before the deep copy of CAMEL-20866): there the copy shares
the parent's cache, whose file the parent's unit of work deletes. So this is
not a regression of CAMEL-20866.
A TLA+ model of the counter (parent, sub-exchange, seda copy, seda consumer)
finds the same trace (send to seda, parent done, consumer reads a deleted
file). The model without a Multicast and the model of the fix below hold,
including that the file is always deleted in the end.
h3. Proposed fix
In {{SedaProducer.addToQueue}} and {{DisruptorProducer.prepareCopy}}, in the
branch that copies the exchange, remove
{{ExchangePropertyKey.STREAM_CACHE_UNIT_OF_WORK}} from the copy before
{{sc.copy(target)}}, as the Wire Tap does. The copy then releases the file when
it is done itself. The path that waits for a reply (InOut,
{{waitForTaskToComplete}}) is not changed, because the producer waits for the
copy there. With this change all the failing cases above read the full body,
and no spool file is left behind afterwards. camel-stub extends
{{SedaProducer}} and gets the same fix.
Affected: long-standing, all 4.x versions (checked on main and with the 4.6.0
{{SedaProducer}}).
Duplicate check (2026-09-28): JIRA text "Cannot reset stream from file"
(CAMEL-21162, CAMEL-12108, CAMEL-12067, CAMEL-8688),
"CamelStreamCacheUnitOfWork" (CAMEL-12108, CAMEL-13168 for direct-vm), "spool"
with seda or disruptor: none about seda or disruptor. GitHub pull requests on
"StreamCache", "spool", "FileInputStreamCache", "STREAM_CACHE_UNIT_OF_WORK":
#14502 (CAMEL-20866) and CAMEL-25004 (stream caching strategy lifecycle), not
this.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)