Open kzander91 opened 4 weeks ago
This behavior is an immediate consequence of the single scheduler thread within the internal ScheduledThreadPoolExecutor
there. The same behavior will occur with a regular ThreadPoolTaskScheduler
and its default pool size 1. In the latter case, you may configure a higher pool size exactly to avoid such contention on the scheduler thread. That option is not available on SimpleAsyncTaskScheduler
since it is hard-coded to a single internal scheduler thread.
Arguably fixed-delay tasks are just not idiomatic on SimpleAsyncTaskScheduler
and specifically with virtual threads. However, if there is a need for its hand-off behavior for regular triggers in combination with a higher number of scheduler threads for executing fixed-delay tasks, we can consider making the internal scheduler pool size configurable.
Do you see yourself using SimpleAsyncTaskScheduler
in such a scenario, configuring a higher number of internal scheduler threads if the option was available?
Do you see yourself using SimpleAsyncTaskScheduler in such a scenario, configuring a higher number of internal scheduler threads if the option was available?
I guess so, but wouldn't it be possible for the scheduling infrastructure to auto-detect the number of scheduling threads required to run all the registered tasks without blocking each other? SimpleAsyncTaskScheduler
could call setCorePoolSize(n)
on the wrapped executor where n=numFixedDelayTasks + 1 (this could grow out-of-hand though if we have a large number of fixed-delay tasks...).
Or, implement fixed-delay tasks differently if virtual threads are enabled, something like this pseudo code:
scheduleWithFixedDelay(task, delay) {
executor.execute(() -> {
while(!cancelled) {
task.run();
sleep(delay);
}
});
}
This should allow an arbitrary number of fixed-delay and fixed-rate tasks to run concurrently without blocking each other.
As additional context on why I feel there's definitely room for improvement, here's how I got here (and I imagine I'm not the only one):
@Scheduled
methods, with cron
, fixedRate
and fixedDelay
triggers. We quickly realized that the single scheduler thread only allowed one of these methods to run at a time, so we increased the number of scheduler threads with spring.task.scheduling.pool.size
.spring.threads.virtual.enabled
. Everything seems to work fine at first, Tomcat handles requests on VTs, scheduled methods still run concurrently. At some point though, the scheduled methods weren't called anymore. Turns out they were all blocked by a single fixedDelay
task that sometimes can take significant time.spring.task.scheduling.pool.size
doesn't have an effect anymore with VTs enabled, so there isn't something we can do with just configuration to remedy this situation. So we had to disable VTs again for the entire app, just because of that one fixedDelay
task.Backing up a bit, I can totally accept that SimpleAsyncTaskScheduler
has these limitations, it's called "Simple", after all 😉, and Framework has alternative implementations to work around them.
It's just that that's what Spring Boot auto-configures for me when I enable virtual threads (maybe Boot should allow more granular control about where I want VTs to apply instead of the current all-or-nothing approach).
Framework: 6.1.11 Boot: 3.3.2 Reproducer: demo.zip
As discussed in #31900 and documented in the reference docs, with virtual threads enabled,
fixedDelay
tasks run on a single thread. However, a long-runningfixedDelay
task also blocks concurrentfixedRate
tasks. This behaviour isn't documented and not obvious (to me at least), so I would consider this a bug.Consider the following app:
This prints:
"fixedRate"
every second, as expected.fixedDelay
task kicks in and performs some work for 10 seconds. While that's going on, no furtherfixedRate
tasks are run.fixedDelay
task completes and we get a bunch of"fixedRate"
messages again. Then, we continue with the regular schedule of one message per second.While debugging this, I found that the
SimpleAsyncTaskScheduler
that is used under the covers uses its first thread,scheduling-1
, both to run thefixedDelay
task, and to schedule thefixedRate
tasks. This means ifscheduling-1
is busy running a task, it can't submit newfixedRate
tasks for execution.