[
https://issues.apache.org/jira/browse/CAMEL-24066?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-24066.
---------------------------------
Resolution: Fixed
> 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
> Assignee: Claus Ibsen
> Priority: Major
> Fix For: 4.22.0
>
>
> {{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)