Federico Mariani created CAMEL-24066:
----------------------------------------
Summary: camel-spring - SpringScheduledPollConsumerScheduler
ignores startScheduler=false (breaks ScheduledPollConsumerScheduler contract)
Key: CAMEL-24066
URL: https://issues.apache.org/jira/browse/CAMEL-24066
Project: Camel
Issue Type: Bug
Components: camel-spring
Affects Versions: 4.21.0
Reporter: Federico Mariani
{{SpringScheduledPollConsumerScheduler}} (used with
{{scheduler=spring&scheduler.cron=...}} on any scheduled-poll endpoint) breaks
the {{ScheduledPollConsumerScheduler}} SPI contract.
In {{DefaultScheduledPollConsumerScheduler}}, {{doStart()}} only prepares the
thread pool and the actual scheduling happens in {{startScheduler()}} - that is
what makes the consumer option {{startScheduler=false}} ("schedule later, on
demand") work: {{ScheduledPollConsumer.doStart()}} starts the scheduler
service, but only calls {{scheduler.startScheduler()}} when
{{isStartScheduler()}} is true.
The Spring implementation inverts this: it schedules the cron trigger inside
{{doStart()}} ({{taskScheduler.schedule(runnable, trigger)}}) and implements
{{startScheduler()}} as a no-op. Consequences:
# the documented consumer option {{startScheduler=false}} is silently ignored -
the cron starts firing as soon as the route starts
# {{GenericFilePollingConsumer}} (i.e. {{pollEnrich()}} on file/FTP endpoints)
and file browse operations call {{setStartScheduler(false)}} internally because
they poll manually - combined with {{scheduler=spring}} they get an unwanted
background cron poll competing with the manual poll
# after {{unscheduleTask()}} (e.g. {{repeatCount}} reached) a later
{{startScheduler()}} call can never re-arm the trigger, unlike the default
scheduler
Minor related issue in the same class: {{isSchedulerStarted()}} calls
{{taskScheduler.getScheduledExecutor()}}, which throws
{{IllegalStateException}} when the scheduler is not initialized yet, instead of
returning {{false}}.
Reproducer (verified failing on 4.22.0-SNAPSHOT, dependencies: camel-core,
camel-spring, camel-file, awaitility, junit-jupiter). The identical route with
the default scheduler ({{delay=500}} instead of
{{scheduler=spring&scheduler.cron=...}}) passes:
{code:java}
package org.apache.camel.spring.repro;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.awaitility.Awaitility.await;
public class SpringSchedulerStartSchedulerOptionTest {
@TempDir
Path dir;
@Test
void startSchedulerFalseMustNotPoll() throws Exception {
Files.writeString(dir.resolve("hello.txt"), "hi");
AtomicInteger polled = new AtomicInteger();
try (DefaultCamelContext camel = new DefaultCamelContext()) {
camel.addRoutes(new RouteBuilder() {
@Override
public void configure() {
from("file:" + dir + "?noop=true&startScheduler=false"
+ "&scheduler=spring&scheduler.cron=0/1+*+*+*+*+?")
.process(e -> polled.incrementAndGet());
}
});
camel.start();
// EXPECTED: no poll happens, because startScheduler=false means
the scheduler
// must not run until
ScheduledPollConsumer.startScheduler() is invoked.
// (with the default scheduler this test passes)
// ACTUAL: with scheduler=spring the cron fires after ~1 second
anyway
await().during(3, TimeUnit.SECONDS).atMost(4, TimeUnit.SECONDS)
.until(() -> polled.get() == 0);
}
}
}
{code}
Suggested fix: align with {{DefaultScheduledPollConsumerScheduler}} - keep
creating/initializing the {{ThreadPoolTaskScheduler}} in {{doStart()}}, but
move the {{taskScheduler.schedule(runnable, trigger)}} call into
{{startScheduler()}} (guarded so it only schedules once / when not already
scheduled).
Note: the schedule-in-doStart behavior is long-standing, so a review of git
history / existing users is warranted before changing the contract (e.g. anyone
relying on {{scheduler=spring}} firing without {{startScheduler()}} being
called by a custom consumer).
_This issue was found during an AI-assisted code review of the camel-spring
component. Reported by Claude Code on behalf of Federico Mariani. The
reproducers were executed and verified against 4.22.0-SNAPSHOT (main) before
filing._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)