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

Reply via email to