shashank created CAMEL-25504:
--------------------------------
Summary: 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
{{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)