[
https://issues.apache.org/jira/browse/CAMEL-25559?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-25559.
---------------------------------
Resolution: Fixed
Fixed by https://github.com/apache/camel/pull/27698 (merged to main as
28e150782cdc, 4.24.0).
_Claude Code on behalf of Claus Ibsen_
> 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
> Priority: Minor
> Fix For: 4.24.0
>
>
> 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)