spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.64k stars 38.14k forks source link

SimpleAsyncTaskScheduler timing gets wrong after a few runs #32508

Closed benayat closed 7 months ago

benayat commented 7 months ago

I'm using SimpleAsyncTaskScheduler(tried both scheduleAtFixedRate, and scheduleWithFixedDelay), and 15-20 runs, the delay rate degrades from 3 seconds to sub-1 seconds.

spring boot version: 3.2.3. jdk version: 21.0.2.

my code to reproduce the problem:

@Service
@RequiredArgsConstructor
@EnableAsync
@Slf4j
public class TimerService {
    private final ApplicationEventPublisher applicationEventPublisher;

    @Qualifier("innerScheduler")
    private final SimpleAsyncTaskScheduler innerScheduler;
    @Qualifier("outerScheduler")
    private final SimpleAsyncTaskScheduler outerScheduler;
    @Async
    public void startTimerStream() {
        AtomicInteger i = new AtomicInteger();
        AtomicInteger j = new AtomicInteger();
        log.info("scheduling events, now in: " + i);
        outerScheduler.scheduleAtFixedRate(() -> {
            applicationEventPublisher.publishEvent(new TimingEvent("TOCK", "tock data, i is: " + i.getAndIncrement()));
            innerScheduler.scheduleAtFixedRate(() -> applicationEventPublisher
                    .publishEvent(new TimingEvent("TICK", "tick data, j is: " + j.getAndIncrement())), java.time.Duration.ofSeconds(3));
        }, java.time.Duration.ofSeconds(31));
    }
}
@Configuration
public class SchedulersConfig {
    @Bean(name = "innerScheduler")
    public SimpleAsyncTaskScheduler simpleScheduler() {
        return new SimpleAsyncTaskSchedulerBuilder()
                .virtualThreads(true)
                .build();
    }
    @Bean(name = "outerScheduler")
    public SimpleAsyncTaskScheduler simpleScheduler2() {
        return new SimpleAsyncTaskSchedulerBuilder()
                .virtualThreads(true)
                .build();
    }
}
jhoeller commented 7 months ago

Why are you using two different scheduler instances with the same configuration there? Each of those is going to have an independent scheduler thread, is that the goal? Otherwise, I rather recommend a unified scheduler with a single scheduler thread.

Technically, for scheduling purposes, SimpleAsyncTaskScheduler in a virtual threads setup just uses a standard java.util.concurrent.ScheduledThreadPoolExecutor with a single thread created via a virtual thread factory. Any kind of timing deviation is outside of our control there, this rather has to be an effect within the JDK ScheduledThreadPoolExecutor itself.

benayat commented 7 months ago

My intention is running 30 synchroneous tasks triggered by a Rest request from the client. Each task should take 2 minutes(running one after the other). Inside each task, I want to run 20 concurrent jobs, repeatedly every 10 secods - until the 2 minutes are up.
Since @Scheduled annotation wasn't flexible enough for my use case, I used the Executors.newSingleThreadExecutor bean, running execute method for all synchroneous tasks, and similarly, a ThreadPoolTaskExecutor for the concurrentjobs. Can I achieve all that depending solely on spring schedullers, without executorService? or even better, a spring scheduler with virtual threads? Or is that too much of a stretch?

snicoll commented 7 months ago

I am afraid there isn't really something actionable in terms of a bug report in the Spring Framework. As Juergen pointed out, the deviation you observe can certainly be reproduced without involving Spring at all. As for the code you've shared, having two SimpleAsyncTaskScheduler is useless as they don't have any sort of pooling (the 30 vs. 20 you've described). For any follow-up questions on how to structure your configuration, please refer to StackOverflow.