[ 
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)

Reply via email to