[
https://issues.apache.org/jira/browse/CAMEL-26746?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
David J. M. Karlsen updated CAMEL-26746:
----------------------------------------
Description:
Start a timer route on a CamelContext that is already started, either with
`stopRoute`/`startRoute` or by adding the route later. `TimerConsumer.doStart`
then calls `configureTask` immediately, while `BaseService.start()` still has
status STARTING.
With `delay=0`, `java.util.Timer` can run the TimerTask before `start()` sets
STARTED. The task then calls `isTaskRunAllowed()`, which is `isStarted()` and
returns false. The run is skipped silently, with a debug log only, and the next
fire comes only after `period`.
This cannot happen at context start: `InternalRouteStartupManager` starts the
consumers first and then calls `onCamelContextStarted`, which configures the
task.
*Expected:* after `startRoute` returns, a `delay=0` timer fires once
immediately, as it does at context start.
*Actual:* in about 2.6% of restarts under CPU load (77 of 3,000 in the
reproducer), the immediate fire is lost, and the next fire comes one period
later.
h3. Reproducer (JUnit 5, camel-core 4.23.0)
{code:java}
try (var camel = new DefaultCamelContext()) {
var fires = new AtomicInteger();
camel.addRoutes(new RouteBuilder() {
public void configure() {
from("timer:t?delay=0&period=3600000").routeId("t").process(e ->
fires.incrementAndGet());
}
});
camel.start();
await().until(() -> fires.get() == 1);
int dropped = 0;
for (int i = 0; i < 3000; i++) {
int before = fires.get();
camel.getRouteController().stopRoute("t");
camel.getRouteController().startRoute("t");
try {
await().atMost(Duration.ofMillis(500)).until(() -> fires.get() > before);
} catch (ConditionTimeoutException e) {
dropped++;
}
}
assertEquals(0, dropped); // fails, e.g. dropped=77 with background
CPU-burner threads
}
{code}
h3. Deterministic variant
Bind a custom `java.util.Timer` (`timer=#eager`) whose `schedule(task, delay,
period)` runs `task.run()` synchronously before delegating. After
`stopRoute`/`startRoute`, the fire count does not change.
h3. Suggested fix
Either make `isTaskRunAllowed()` use `isRunAllowed()`, which accepts STARTING
as `ScheduledPollConsumer.isPollAllowed` does, or configure the task only once
the consumer has reached STARTED.
h3. Environment
Java 25.0.5, Linux (GitHub Actions runner) and macOS.
was:
Start a timer route on a CamelContext that is already started, either with
`stopRoute`/`startRoute` or by adding the route later. `TimerConsumer.doStart`
then calls `configureTask` immediately, while `BaseService.start()` still has
status STARTING.
With `delay=0`, `java.util.Timer` can run the TimerTask before `start()` sets
STARTED. The task then calls `isTaskRunAllowed()`, which is `isStarted()` and
returns false. The run is skipped silently, with a debug log only, and the next
fire comes only after `period`.
This cannot happen at context start: `InternalRouteStartupManager` starts the
consumers first and then calls `onCamelContextStarted`, which configures the
task.
*Expected:* after `startRoute` returns, a `delay=0` timer fires once
immediately, as it does at context start.
*Actual:* in about 2.6% of restarts under CPU load (77 of 3,000 in the
reproducer), the immediate fire is lost, and the next fire comes one period
later.
h3. Reproducer (JUnit 5, camel-core 4.23.0)
{code:java}
try (var camel = new DefaultCamelContext()) {
var fires = new AtomicInteger();
camel.addRoutes(new RouteBuilder() {
public void configure() {
from("timer:t?delay=0&period=3600000").routeId("t").process(e ->
fires.incrementAndGet());
}
});
camel.start();
await().until(() -> fires.get() == 1);
int dropped = 0;
for (int i = 0; i < 3000; i++) {
int before = fires.get();
camel.getRouteController().stopRoute("t");
camel.getRouteController().startRoute("t");
try {
await().atMost(Duration.ofMillis(500)).until(() -> fires.get() > before);
} catch (ConditionTimeoutException e) {
dropped++;
}
}
assertEquals(0, dropped); // fails, e.g. dropped=77 with background
CPU-burner threads
}
{code}
h3. Deterministic variant
Bind a custom `java.util.Timer` (`timer=#eager`) whose `schedule(task, delay,
period)` runs `task.run()` synchronously before delegating. After
`stopRoute`/`startRoute`, the fire count does not change.
h3. Suggested fix
Either make `isTaskRunAllowed()` use `isRunAllowed()`, which accepts STARTING
as `ScheduledPollConsumer.isPollAllowed` does, or configure the task only once
the consumer has reached STARTED.
h3. Environment
Java 25.0.5, Linux (GitHub Actions runner) and macOS.
h3. Provenance (not for the issue)
Found by ppt-graph P1 (agent p1-s95, 2026-11-07), in CI runs 55930306 and
56030126 of payment/ppt-graph. Measured: a route restart lost 77 of 3,000 fires
in plain Camel and 17 of 400 in the graph-ingest context; a context restart
lost 0 of 40. Nobody has filed it; there are no ASF JIRA credentials here.
> Timer consumer drops its first fire (delay=0) when its route is started at
> runtime via RouteController.startRoute
> -----------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-26746
> URL: https://issues.apache.org/jira/browse/CAMEL-26746
> Project: Camel
> Issue Type: Bug
> Components: camel-core
> Affects Versions: 4.23.0
> Reporter: David J. M. Karlsen
> Priority: Minor
>
> Start a timer route on a CamelContext that is already started, either with
> `stopRoute`/`startRoute` or by adding the route later.
> `TimerConsumer.doStart` then calls `configureTask` immediately, while
> `BaseService.start()` still has status STARTING.
> With `delay=0`, `java.util.Timer` can run the TimerTask before `start()` sets
> STARTED. The task then calls `isTaskRunAllowed()`, which is `isStarted()` and
> returns false. The run is skipped silently, with a debug log only, and the
> next fire comes only after `period`.
> This cannot happen at context start: `InternalRouteStartupManager` starts the
> consumers first and then calls `onCamelContextStarted`, which configures the
> task.
> *Expected:* after `startRoute` returns, a `delay=0` timer fires once
> immediately, as it does at context start.
> *Actual:* in about 2.6% of restarts under CPU load (77 of 3,000 in the
> reproducer), the immediate fire is lost, and the next fire comes one period
> later.
> h3. Reproducer (JUnit 5, camel-core 4.23.0)
> {code:java}
> try (var camel = new DefaultCamelContext()) {
> var fires = new AtomicInteger();
> camel.addRoutes(new RouteBuilder() {
> public void configure() {
> from("timer:t?delay=0&period=3600000").routeId("t").process(e ->
> fires.incrementAndGet());
> }
> });
> camel.start();
> await().until(() -> fires.get() == 1);
> int dropped = 0;
> for (int i = 0; i < 3000; i++) {
> int before = fires.get();
> camel.getRouteController().stopRoute("t");
> camel.getRouteController().startRoute("t");
> try {
> await().atMost(Duration.ofMillis(500)).until(() -> fires.get() >
> before);
> } catch (ConditionTimeoutException e) {
> dropped++;
> }
> }
> assertEquals(0, dropped); // fails, e.g. dropped=77 with background
> CPU-burner threads
> }
> {code}
> h3. Deterministic variant
> Bind a custom `java.util.Timer` (`timer=#eager`) whose `schedule(task, delay,
> period)` runs `task.run()` synchronously before delegating. After
> `stopRoute`/`startRoute`, the fire count does not change.
> h3. Suggested fix
> Either make `isTaskRunAllowed()` use `isRunAllowed()`, which accepts STARTING
> as `ScheduledPollConsumer.isPollAllowed` does, or configure the task only
> once the consumer has reached STARTED.
> h3. Environment
> Java 25.0.5, Linux (GitHub Actions runner) and macOS.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)