[
https://issues.apache.org/jira/browse/CAMEL-26449?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen updated CAMEL-26449:
--------------------------------
Fix Version/s: 4.24.0
> camel-core - DefaultShutdownStrategy: a route stop that times out while the
> CamelContext stops forces or cuts short the graceful shutdown of the whole
> CamelContext
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-26449
> URL: https://issues.apache.org/jira/browse/CAMEL-26449
> Project: Camel
> Issue Type: Bug
> Components: camel-core
> Reporter: shashank
> Assignee: shashank
> Priority: Major
> Fix For: 4.24.0
>
>
> Found with a TLA+ model of {{DefaultShutdownStrategy.doShutdown}} called by
> two threads at once, and reproduced on the real classes.
> {{DefaultShutdownStrategy}} keeps the state of a shutdown in fields of the
> strategy: {{currentShutdownTaskFuture}} (the task the caller waits for and
> cancels on timeout), {{timeoutOccurred}} (also handed to the
> {{ShutdownTask}}, whose wait loop ends when it is set) and {{forceShutdown}}.
> Two shutdowns can run at the same time: a route stop or suspend
> ({{stopRoute(id, timeout, unit)}}, {{suspendRoute}}, a route policy, the
> control bus, a reload) holds the route lock of the CamelContext, while
> {{CamelContext.stop()}} ({{shutdownForced}}) runs under the lifecycle lock,
> so nothing keeps them apart. Then the timeout of one changes the other:
> * *The route stop starts first, the CamelContext stop after it.* The
> CamelContext stop overwrites {{currentShutdownTaskFuture}} with its own task.
> When the route stop times out it cancels {{currentShutdownTaskFuture}}, that
> is the task of the CamelContext stop (its own task keeps running), and sets
> {{forceShutdown}} to false. {{Future.get}} of the CamelContext stop throws
> {{CancellationException}}, which is handled as a timeout: the CamelContext
> forces every route to shut down right away ({{shutdownRoutesNow}}, forced
> {{prepareShutdown}}) after the timeout of the route stop (seconds) instead of
> its own (45 seconds by default). Consumers are stopped under in-flight
> exchanges. If the route stop clears the field ({{finally}}) between the
> assignment and the read in the CamelContext stop,
> {{currentShutdownTaskFuture.get}} throws a {{NullPointerException}}, also
> handled as a timeout.
> * *The CamelContext stop starts first, the route stop after it.* The task of
> the route stop waits behind the task of the CamelContext stop (single-thread
> executor), so the route stop times out and sets {{timeoutOccurred}}. The wait
> loop of the CamelContext's {{ShutdownTask}} reads the same flag and ends: the
> CamelContext stops all routes while their exchanges are still in flight,
> without any warning ({{Graceful shutdown ... completed}}, then {{Shutting
> down while there are still 2 inflight exchanges}}).
> Either way messages in flight on the other routes are cut off during a
> graceful shutdown (redelivered or lost, depending on the consumer). A pod
> that is stopped while a route is being stopped (a route policy, a cluster
> leadership change, the control bus, management) is the usual way to get there.
> *TLA+ model* (two callers, the single-thread executor, the shared fields;
> each caller and task step is one step of {{doShutdown}} /
> {{ShutdownTask.run}}). With the CamelContext stop never timing out,
> {{NoForeignForce}} (a caller forces its routes only when its own timeout
> elapsed) is violated:
> {noformat}
> 1 x: timeoutOccurred.set(false) (DSS:211)
> 2 x: currentShutdownTaskFuture = submit(Tx) (DSS:213)
> 3 r: timeoutOccurred.set(false); 4 r: currentShutdownTaskFuture = submit(Tr)
> 5 r: get(field) -> waits for Tr; 6 x: get(field) -> waits for Tr (re-read of
> the field, DSS:216)
> 7 r: its timeout elapses; 8 r: cancel(field) ...
> 9 x: CancellationException -> catch -> shutdownRoutesNow (forced, its timeout
> did not elapse)
> {noformat}
> and {{NoEarlyExit}} (the task of a caller stops waiting with exchanges in
> flight only when that caller timed out) is violated (a 10-state trace): the
> route stop times out, sets the shared flag, and the task of the CamelContext
> stop leaves its wait loop. When the CamelContext stop can time out,
> {{ForceStays}} is violated too (a route stop resets {{forceShutdown}} after a
> forced CamelContext stop). With the future and the flag local to each call,
> and {{forceShutdown}} only set to true, all three hold (1,126 states; 415
> when the CamelContext stop never times out); witnesses show both graceful
> completion and both callers forcing after their own timeouts are reachable.
> *Reproduced* on main with tests that use routes whose consumer is suspended
> during a graceful shutdown and an exchange in flight on each route: route a
> is stopped with a 5 second timeout while the CamelContext stops with a 60
> second timeout, and the CamelContext stop starts first and route a is stopped
> with a 1 second timeout. On main the consumer of route b is stopped while its
> exchange is in flight ({{forceShutdown}} true after 5 seconds), or the
> CamelContext stops after 2 seconds with 2 exchanges in flight and no timeout
> warning. With the change the CamelContext waits until the exchange of route b
> completes.
> A third way, found by repeating the tests: a route stop that times out while
> the CamelContext is stopping shuts its route down, and
> {{DefaultRoute.doShutdown}} clears the services list of the route while the
> task of the CamelContext stop iterates it:
> {{ConcurrentModificationException}}, and the CamelContext stops all routes
> without waiting ("Error occurred while shutting down routes. This exception
> will be ignored.").
> *Proposal*
> Keep the future and the timeout flag of a shutdown in local variables (the
> fields are still set, for {{getCurrentShutdownTaskFuture()}} and
> {{isTimeoutOccurred()}}, and the field is only cleared when it still holds
> the task of that call), only ever set {{forceShutdown}} to true, make the
> services list of a route copy-on-write, and let the timeout path use the copy
> of the route list. Code outside the strategy that reads
> {{isTimeoutOccurred()}} (camel-cxf {{CxfSpringEndpoint}},
> {{LoggingExceptionHandler}}) still sees the last writer; that is a follow-up.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)