[
https://issues.apache.org/jira/browse/CAMEL-25504?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Work on CAMEL-25504 started by shashank.
----------------------------------------
> camel-jta, camel-spring - a transacted exchange that completes during a
> graceful shutdown is rolled back instead of committed
> -----------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25504
> URL: https://issues.apache.org/jira/browse/CAMEL-25504
> Project: Camel
> Issue Type: Bug
> Components: camel-spring
> Reporter: shashank
> Assignee: shashank
> Priority: Major
>
> {{DefaultShutdownStrategy}} first stops or suspends the route consumers, then
> calls {{prepareShutdown(suspendOnly=false, forced=false)}} on the route
> services ("notify the services we intend to shutdown",
> {{DefaultShutdownStrategy}} around line 659 at main c578a42a776d), then waits
> up to the shutdown timeout for the in-flight exchanges. Only if the timeout
> expires does it call {{prepareShutdown(false, forced=true)}}.
> Both transaction error handlers set {{preparingShutdown = true}} on both
> calls: {{org.apache.camel.jta.TransactionErrorHandler.prepareShutdown}}
> directly, and {{org.apache.camel.spring.spi.TransactionErrorHandler}} through
> {{RedeliveryErrorHandler.prepareShutdown}}, which it calls first. Since
> CAMEL-23234 both transaction callbacks do, after the route work (camel-jta
> {{doInTransactionTemplate}} line 205, camel-spring
> {{doInTransactionTemplate}} line 215):
> {code:java}
> // if forced shutdown is in progress, mark the exchange for rollback
> if (preparingShutdown) {
> exchange.setRollbackOnly(true);
> }
> {code}
> So every transacted exchange that is in flight when a graceful shutdown
> (CamelContext stop, or a route stop through the route controller) starts, and
> that completes normally inside the grace period, is rolled back. Graceful
> shutdown no longer lets in-flight transactions finish. The
> {{TransactionRolledbackException}} that forces the rollback is swallowed and
> no exception is set on the exchange, so a caller that does not redeliver
> (direct, platform-http/REST, timer, a ProducerTemplate) gets a normal reply
> while the database work is rolled back; with a transacted JMS consumer the
> message is redelivered after the restart and any non-transactional side
> effects run twice.
> The CAMEL-23234 PR description and its tests describe the intended behaviour
> as a rollback after the grace period has expired (forced shutdown); the
> graceful case was not covered by its tests
> ({{TransactionErrorHandlerShutdownTest}} calls {{prepareShutdown(false,
> true)}}, and the IT only checks the timeout path).
> By reading {{DefaultShutdownStrategy}} (not tested): a route whose consumer
> is deferred ({{shutdownRoute(Defer)}}) keeps taking new exchanges during the
> grace period, after the graceful {{prepareShutdown}}, so on main every
> transacted exchange it processes until the in-flight count reaches zero is
> rolled back too.
> h3. Reproduction
> New {{TransactionalClientDataSourceGracefulShutdownTest}} (camel-spring-xml,
> next to {{TransactionalClientDataSourceForcedShutdownTest}}; the
> {{transactionalClientDataSource.xml}} context: a real
> {{DataSourceTransactionManager}} on an embedded H2 database, 1 book inserted
> at startup):
> * {{inFlightTransactionCompletingDuringContextStopIsCommitted}}:
> {{from("direct:graceful").transacted().bean("bookService")}} inserts a book,
> then the exchange is held in the route; {{context.stop()}} with a 30 s
> timeout. A service in the transacted block releases the exchange from its own
> graceful {{prepareShutdown}} call (the strategy prepares the transaction
> error handler before it), and the exchange completes long before the timeout.
> On main:
> {noformat}
> an exchange that completed during a graceful shutdown must not be marked
> rollback only ==> expected: <false> but was: <true>
> {noformat}
> * {{gracefulPrepareShutdownCommitsInFlightTransaction}}: the same with
> {{prepareShutdown(false, false)}} called on the handler directly, as the
> existing forced-shutdown test does with {{prepareShutdown(false, true)}}. On
> main: {{graceful shutdown must not mark the exchange rollback only ==>
> expected: <false> but was: <true>}} (and the insert is rolled back: 1 book
> instead of 2).
> New {{TransactionErrorHandlerGracefulShutdownTest}} (camel-jta, no container):
> * {{inflightExchangeCompletingDuringGracefulShutdownIsCommitted}}:
> {{from("direct:start").transacted().process(blocking)}}, a test
> {{JtaTransactionPolicy}} counting commits and rollbacks, shutdown timeout 20
> s. The exchange is held in the route, {{camelContext.stop()}} is started on
> another thread, the test waits (Awaitility) until the handler received the
> graceful {{prepareShutdown}}, then releases the exchange, which completes
> long before the timeout. On main:
> {noformat}
> an exchange that completed during a graceful shutdown must not be marked
> rollback only ==> expected: <false> but was: <true>
> {noformat}
> * {{gracefulPrepareShutdownDoesNotRollBack}}: the same on the handler
> directly with {{prepareShutdown(false, false)}}. On main: {{graceful shutdown
> must not mark the exchange rollback only ==> expected: <false> but was:
> <true>}}.
> The existing forced-shutdown tests (camel-jta
> {{TransactionErrorHandlerShutdownTest}}, camel-spring-xml
> {{TransactionalClientDataSourceForcedShutdownTest}}) and the suspend/resume
> tests pass with and without the fix.
> h3. Proposed fix
> In both handlers, a separate {{volatile boolean forcedShutdown}} that
> {{prepareShutdown}} sets only when {{forced}} is true (reset in
> {{doStart}}/{{doResume}} like {{preparingShutdown}}); the check after the
> route work reads it instead of {{preparingShutdown}}. {{preparingShutdown}}
> itself is unchanged (in the Spring handler it still stops redeliveries during
> shutdown). The forced path of CAMEL-23234 is unchanged: on timeout the
> in-flight exchanges are still marked rollback only and an exchange finishing
> afterwards still rolls back. One secondary change: with
> {{shutdownNowOnTimeout=false}} the strategy never calls {{prepareShutdown(..,
> forced=true)}}, so an exchange that completes after the timeout is now
> committed (as before 4.19). The upgrade guide (4.23) gets a short note. Tests
> with the fix: camel-jta 5 tests, 0 failures (the Postgres IT
> {{TransactionErrorHandlerGracePeriodShutdownIT}} was skipped: no Docker; it
> exercises only the forced path, which the change does not touch);
> camel-spring-xml 1184 tests, 0 failures, 26 skipped (camel-spring itself has
> no tests).
> CAMEL-23234 added the check to both handlers in one change, so this ticket
> fixes both.
> Found with a TLA+ model of the handler (begin, route work, the flag check,
> commit/rollback) and the shutdown strategy (stop consumer, graceful prepare,
> wait, timeout, forced marking): "an exchange that completed successfully
> before the shutdown timeout is committed" is violated in 7 steps (begin,
> consumer stopped, work done, graceful prepare, check sets rollback only,
> rollback). With the fix the property holds for 2 and 3 exchanges, and the
> forced rollback stays reachable. Then confirmed with the real classes as
> above (the modelled flag logic is the same in both handlers; the model does
> not include redelivery).
> Affected: main, camel-4.22.x (LTS) and camel-4.19.x, i.e. 4.19.0 onwards
> (CAMEL-23234); camel-4.18.x and camel-4.14.x do not have the check (GitHub
> contents API).
> Duplicate check (2026-10-09): JIRA text "TransactionErrorHandler" +
> "shutdown" (CAMEL-23234 only), "graceful shutdown" + "transacted"
> (CAMEL-23234, CAMEL-23260 is Service Bus), "rollbackOnly" + "shutdown"
> (none); GitHub pull requests "jta shutdown", "TransactionErrorHandler"
> (open): only #22204 (CAMEL-23234, merged).
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)