shashank created CAMEL-25559:
--------------------------------
Summary: camel-core - with the pooled exchange factory, an
exchange that a consumer gives back without routing it is put into the pool
twice, and the next two messages share one exchange
Key: CAMEL-25559
URL: https://issues.apache.org/jira/browse/CAMEL-25559
Project: Camel
Issue Type: Bug
Components: camel-core
Reporter: shashank
Assignee: shashank
This needs the pooled exchange factory ({{camel.main.exchangeFactory=pooled}}).
It is opt-in (not the default), and exchange pooling is deprecated since 4.23
(CAMEL-24979), hence the priority Minor (as CAMEL-24800, the previous pooled
exchange fix). A fix is still useful: the factory is still shipped and can be
selected on main, it is not deprecated on the 4.22.x, 4.18.x and 4.14.x
branches, the effect is silent (a message is routed with the content of another
one), and the change is limited to one method.
{{PooledExchangeFactory.createPooledExchange}} registers an on-done task on
every exchange created with auto release (line 136 at main 3a6cdba883a5); the
task calls {{release(exchange)}} (lines 159-165). {{release}} (lines 100-124)
calls {{done()}} on the exchange and then {{pool.offer(exchange)}}.
{{DefaultPooledExchange.done()}} (lines 81-122) resets the exchange and runs
the task, but only when the exchange is not done yet.
* Normal path: the unit of work calls {{done()}} ({{DefaultUnitOfWork}} line
299), the task calls {{release}}, the {{done()}} inside {{release}} does
nothing, one offer.
* A consumer gives back an exchange it did not route,
{{DefaultConsumer.releaseExchange(exchange, true)}} (lines 160-168):
{{release}} -> {{done()}} -> the task -> {{release}} again -> {{done()}} does
nothing -> offer; back in the outer {{release}} -> second offer.
* {{releaseExchange(exchange, false)}} for an exchange created with auto
release: {{DefaultConsumer}} calls {{done()}} (line 164) -> the task ->
{{release}} -> offer; then {{exchangeFactory.release}} (line 166) -> second
offer.
* An exchange that was routed (the unit of work already gave it back) and is
then released again: second offer.
The pool is an {{ArrayBlockingQueue}} ({{PooledObjectFactorySupport}} line 51)
and accepts the same object twice. The next two {{createExchange}} calls of
that consumer return the same exchange, so two messages share the body, headers
and properties, and the exchange is reset when the first of them completes.
Consumers on main that give back exchanges they did not route:
* with {{releaseExchange(exchange, true)}}: {{GenericFileConsumer}} line 295
(file, ftp, sftp and the other file based components: every file whose
{{processStrategy.begin}} fails or returns false, for example when the read
lock was not acquired ({{processExchange}} lines 430-458 returns false,
{{processBatch}} collects the exchange at line 264 and gives it back at line
269); the files beyond {{maxMessagesPerPoll}} with
{{eagerMaxMessagesPerPoll=false}}; and the rest of a batch when the consumer is
stopping). The first of these needs no special option: a file route with
default options and the pooled factory is enough, as soon as two files of one
poll cannot be locked; {{MailConsumer}} line 422, {{AWS2S3Consumer}} lines 313
and 398, {{IBMCOSConsumer}} line 166, {{BlobConsumer}} line 184,
{{OBSConsumer}} line 283, {{OSSConsumer}} line 217;
* created with auto release and given back with {{false}}: for example
{{GoogleCloudStorageConsumer}} line 221 and {{GoogleFirestoreConsumer}} line 89.
h3. Reproduction
New tests in camel-core (three classes).
{{PooledExchangeFactoryReleaseNotRoutedTest}}: the consumer of a {{direct:}}
route with {{PooledExchangeFactory}} and statistics; create an exchange, give
it back, then check the pool size (1), created 1 / released 1 / discarded 0,
and that the next two exchanges are different objects. On main three of the
four tests fail with {{exchanges in the pool ==> expected: <1> but was: <2>}}:
* {{autoReleaseExchangeThatWasNotRoutedIsPooledOnce}}
({{createExchange(true)}}, {{releaseExchange(exchange, true)}});
*
{{autoReleaseExchangeThatWasNotRoutedIsPooledOnceWhenReleasedAsNotAutoRelease}}
({{releaseExchange(exchange, false)}});
* {{autoReleaseExchangeThatWasRoutedIsPooledOnceWhenReleasedAgain}}.
{{exchangeWithoutAutoReleaseIsPooledOnce}} passes on main; it covers the other
branch.
{{FileConsumeNotEagerMaxMessagesPerPollPooledExchangeTest}}: files {{a.txt}} to
{{d.txt}} with bodies AAA to DDD,
{{from("file:...?maxMessagesPerPoll=2&eagerMaxMessagesPerPoll=false&sortBy=file:name")}}
to a mock that expects AAA, BBB, CCC, DDD. On main: {{mock://result Body of
message: 2. Expected: <CCC> but was: <DDD>}}. The first poll gives back the
exchanges of {{c.txt}} and {{d.txt}}, each twice; in the second poll both files
get the same exchange, and the route receives the content of {{d.txt}} for the
third message.
{{FileConsumeReadLockNotAcquiredPooledExchangeTest}}: files {{a.txt}} (AAA) and
{{b.txt}} (BBB), a file route with default options apart from
{{sortBy=file:name}} and an {{exclusiveReadLockStrategy}} that refuses the lock
for the two files of the first poll; the mock expects AAA, BBB. On main:
{{mock://result Received message count. Expected: <2> but was: <1>}}. The first
poll gives back both exchanges, each twice, and in the second poll both files
get the same exchange.
h3. Proposed fix
In {{PooledExchangeFactory.release}} only, so that all callers are covered: an
auto-release exchange is put into the pool by its on-done task and only by it
(the task now offers the exchange directly instead of calling {{release}});
{{release}} for such an exchange makes sure it is done and does not offer it
itself. An exchange without auto release is handled as before. No signature
changes, no new state; the normal path does one {{done()}} call less. Each
exchange is counted once in the statistics. For an auto-release exchange
{{release}} returns {{true}} unless the reset failed, also when the pool was
full and the exchange was discarded (now said in the javadoc of the method);
nothing in the repository reads that value.
{{PooledProcessorExchangeFactory}} has no on-done task and offers once; not
changed.
No upgrade-guide note: nothing changes except that the pool no longer holds an
exchange twice.
With the fix, the full camel-core suite: 8164 tests, 0 failures, 1 error, 45
skipped, 2 flakes. The error is {{ValidatorExternalResourceTest}}, which loads
an XSD from raw.githubusercontent.com and timed out on the connection; it fails
the same way on main without the change. The two flakes
({{FileConsumerInterceptEmptyFileTest}}, {{FileExclusiveReadNoneStrategyTest}})
failed once and passed on the automatic re-run; neither uses the pooled
factory. {{ManagedPooledExchangeTest}} (1) and the pooled exchange tests of
camel-netty, camel-jms, camel-sjms, camel-sql, camel-mybatis and camel-jooq (15
tests) pass.
Found by reading the release path after two objects of one poll shared an
exchange in a camel-aws2-s3 test (CAMEL-25558), then confirmed with the tests
above.
Affected: main. {{PooledExchangeFactory}} on camel-4.22.x (3daa566cad1c),
camel-4.18.x (4d0578ee450b) and camel-4.14.x (a01a1e1e913e) differs from main
only by the three lines of the deprecation (javadoc tag and annotation), and
the main-code part of the patch applies to each of the three branches as it is
({{git apply --check}}, all four hunks at an offset of -3 lines).
{{DefaultPooledExchange.done()}} runs the on-done task there too. The patch was
not built and the tests were not run on those branches. {{release}} has this
form since CAMEL-18210 (3.14.4, 3.18.0).
Duplicate check (2026-10-11): JIRA text "PooledExchangeFactory" (7:
CAMEL-25558, CAMEL-25527, CAMEL-24979, CAMEL-24933, CAMEL-24800, CAMEL-24133,
CAMEL-23686; none about a double insert on release), "releaseExchange" (5, none
about this), "exchange pool twice" (17; closest is CAMEL-18210 from 2022,
resolved, which fixed the unit of work calling done for exchanges without auto
release), "eagerMaxMessagesPerPoll pooled" (none); GitHub pull requests
"PooledExchangeFactory", "pooled exchange release" and open pull requests
"releaseExchange": none for this (the only open hit for "PooledExchangeFactory"
is #24844, a new component).
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)