David J. M. Karlsen created CAMEL-26746:
-------------------------------------------

             Summary: 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


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.




--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to