org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler Maven / Gradle / Ivy
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.scheduling.concurrent;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskRejectedException;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable;
import org.springframework.scheduling.support.TaskUtils;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
/**
* A simple implementation of Spring's {@link TaskScheduler} interface, using
* a single scheduler thread and executing every scheduled task in an individual
* separate thread. This is an attractive choice with virtual threads on JDK 21,
* expecting common usage with {@link #setVirtualThreads setVirtualThreads(true)}.
*
* NOTE: Scheduling with a fixed delay enforces execution on a single
* scheduler thread, in order to provide traditional fixed-delay semantics!
* Prefer the use of fixed rates or cron triggers instead which are a better fit
* with this thread-per-task scheduler variant.
*
*
Supports a graceful shutdown through {@link #setTaskTerminationTimeout},
* at the expense of task tracking overhead per execution thread at runtime.
* Supports limiting concurrent threads through {@link #setConcurrencyLimit}.
* By default, the number of concurrent task executions is unlimited.
* This allows for dynamic concurrency of scheduled task executions, in contrast
* to {@link ThreadPoolTaskScheduler} which requires a fixed pool size.
*
*
NOTE: This implementation does not reuse threads! Consider a
* thread-pooling TaskScheduler implementation instead, in particular for
* scheduling a large number of short-lived tasks. Alternatively, on JDK 21,
* consider setting {@link #setVirtualThreads} to {@code true}.
*
*
Extends {@link SimpleAsyncTaskExecutor} and can serve as a fully capable
* replacement for it, for example, as a single shared instance serving as a
* {@link org.springframework.core.task.TaskExecutor} as well as a {@link TaskScheduler}.
* This is generally not the case with other executor/scheduler implementations
* which tend to have specific constraints for the scheduler thread pool,
* requiring a separate thread pool for general executor purposes in practice.
*
*
NOTE: This scheduler variant does not track the actual completion of tasks
* but rather just the hand-off to an execution thread. As a consequence,
* a {@link ScheduledFuture} handle (for example, from {@link #schedule(Runnable, Instant)})
* represents that hand-off rather than the actual completion of the provided task
* (or series of repeated tasks). Also, this scheduler participates in lifecycle
* management to a limited degree only, stopping trigger firing and fixed-delay
* task execution but not stopping the execution of handed-off tasks.
*
*
As an alternative to the built-in thread-per-task capability, this scheduler
* can also be configured with a separate target executor for scheduled task
* execution through {@link #setTargetTaskExecutor}: for example, pointing to a shared
* {@link ThreadPoolTaskExecutor} bean. This is still rather different from a
* {@link ThreadPoolTaskScheduler} setup since it always uses a single scheduler
* thread while dynamically dispatching to the target thread pool which may have
* a dynamic core/max pool size range, participating in a shared concurrency limit.
*
* @author Juergen Hoeller
* @since 6.1
* @see #setVirtualThreads
* @see #setTaskTerminationTimeout
* @see #setConcurrencyLimit
* @see SimpleAsyncTaskExecutor
* @see ThreadPoolTaskScheduler
*/
@SuppressWarnings("serial")
public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements TaskScheduler,
ApplicationContextAware, SmartLifecycle, ApplicationListener {
/**
* The default phase for an executor {@link SmartLifecycle}: {@code Integer.MAX_VALUE / 2}.
* @since 6.2
* @see #getPhase()
* @see ExecutorConfigurationSupport#DEFAULT_PHASE
*/
public static final int DEFAULT_PHASE = ExecutorConfigurationSupport.DEFAULT_PHASE;
private static final TimeUnit NANO = TimeUnit.NANOSECONDS;
private final ScheduledExecutorService triggerExecutor = createScheduledExecutor();
private final ExecutorLifecycleDelegate triggerLifecycle = new ExecutorLifecycleDelegate(this.triggerExecutor);
private final ScheduledExecutorService fixedDelayExecutor = createFixedDelayExecutor();
private final ExecutorLifecycleDelegate fixedDelayLifecycle = new ExecutorLifecycleDelegate(this.fixedDelayExecutor);
@Nullable
private ErrorHandler errorHandler;
private Clock clock = Clock.systemDefaultZone();
private int phase = DEFAULT_PHASE;
@Nullable
private Executor targetTaskExecutor;
@Nullable
private ApplicationContext applicationContext;
/**
* Provide an {@link ErrorHandler} strategy.
* @since 6.2
*/
public void setErrorHandler(ErrorHandler errorHandler) {
Assert.notNull(errorHandler, "ErrorHandler must not be null");
this.errorHandler = errorHandler;
}
/**
* Set the clock to use for scheduling purposes.
* The default clock is the system clock for the default time zone.
* @see Clock#systemDefaultZone()
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "Clock must not be null");
this.clock = clock;
}
@Override
public Clock getClock() {
return this.clock;
}
/**
* Specify the lifecycle phase for pausing and resuming this executor.
* The default is {@link #DEFAULT_PHASE}.
* @see SmartLifecycle#getPhase()
*/
public void setPhase(int phase) {
this.phase = phase;
}
/**
* Return the lifecycle phase for pausing and resuming this executor.
* @see #setPhase
*/
@Override
public int getPhase() {
return this.phase;
}
/**
* Specify a custom target {@link Executor} to delegate to for
* the individual execution of scheduled tasks. This can for example
* be set to a separate thread pool for executing scheduled tasks,
* whereas this scheduler keeps using its single scheduler thread.
*
If not set, the regular {@link SimpleAsyncTaskExecutor}
* arrangements kicks in with a new thread per task.
*/
public void setTargetTaskExecutor(Executor targetTaskExecutor) {
this.targetTaskExecutor = (targetTaskExecutor == this ? null : targetTaskExecutor);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
private ScheduledExecutorService createScheduledExecutor() {
return new ScheduledThreadPoolExecutor(1, this::newThread) {
@Override
protected void beforeExecute(Thread thread, Runnable task) {
triggerLifecycle.beforeExecute(thread);
}
@Override
protected void afterExecute(Runnable task, Throwable ex) {
triggerLifecycle.afterExecute();
}
};
}
private ScheduledExecutorService createFixedDelayExecutor() {
return new ScheduledThreadPoolExecutor(1, this::newThread) {
@Override
protected void beforeExecute(Thread thread, Runnable task) {
fixedDelayLifecycle.beforeExecute(thread);
}
@Override
protected void afterExecute(Runnable task, Throwable ex) {
fixedDelayLifecycle.afterExecute();
}
};
}
@Override
protected void doExecute(Runnable task) {
if (this.targetTaskExecutor != null) {
this.targetTaskExecutor.execute(task);
}
else {
super.doExecute(task);
}
}
private Runnable taskOnSchedulerThread(Runnable task) {
return new DelegatingErrorHandlingRunnable(task,
(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true)));
}
private Runnable scheduledTask(Runnable task) {
return () -> execute(new DelegatingErrorHandlingRunnable(task, this::shutdownAwareErrorHandler));
}
private void shutdownAwareErrorHandler(Throwable ex) {
if (this.errorHandler != null) {
this.errorHandler.handleError(ex);
}
else if (this.triggerExecutor.isShutdown()) {
LogFactory.getLog(getClass()).debug("Ignoring scheduled task exception after shutdown", ex);
}
else {
TaskUtils.getDefaultErrorHandler(true).handleError(ex);
}
}
@Override
public void execute(Runnable task) {
super.execute(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false));
}
@Override
public Future> submit(Runnable task) {
return super.submit(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false));
}
@Override
public Future submit(Callable task) {
return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
}
@SuppressWarnings({"deprecation", "removal"})
@Override
public org.springframework.util.concurrent.ListenableFuture> submitListenable(Runnable task) {
return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false));
}
@SuppressWarnings({"deprecation", "removal"})
@Override
public org.springframework.util.concurrent.ListenableFuture submitListenable(Callable task) {
return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
}
@Override
@Nullable
public ScheduledFuture> schedule(Runnable task, Trigger trigger) {
try {
Runnable delegate = scheduledTask(task);
ErrorHandler errorHandler =
(this.errorHandler != null ? this.errorHandler : TaskUtils.getDefaultErrorHandler(true));
return new ReschedulingRunnable(
delegate, trigger, this.clock, this.triggerExecutor, errorHandler).schedule();
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(this.triggerExecutor, task, ex);
}
}
@Override
public ScheduledFuture> schedule(Runnable task, Instant startTime) {
Duration delay = Duration.between(this.clock.instant(), startTime);
try {
return this.triggerExecutor.schedule(scheduledTask(task), NANO.convert(delay), NANO);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(this.triggerExecutor, task, ex);
}
}
@Override
public ScheduledFuture> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
Duration initialDelay = Duration.between(this.clock.instant(), startTime);
try {
return this.triggerExecutor.scheduleAtFixedRate(scheduledTask(task),
NANO.convert(initialDelay), NANO.convert(period), NANO);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(this.triggerExecutor, task, ex);
}
}
@Override
public ScheduledFuture> scheduleAtFixedRate(Runnable task, Duration period) {
try {
return this.triggerExecutor.scheduleAtFixedRate(scheduledTask(task),
0, NANO.convert(period), NANO);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(this.triggerExecutor, task, ex);
}
}
@Override
public ScheduledFuture> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
Duration initialDelay = Duration.between(this.clock.instant(), startTime);
try {
// Blocking task on scheduler thread for fixed delay semantics
return this.fixedDelayExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task),
NANO.convert(initialDelay), NANO.convert(delay), NANO);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(this.fixedDelayExecutor, task, ex);
}
}
@Override
public ScheduledFuture> scheduleWithFixedDelay(Runnable task, Duration delay) {
try {
// Blocking task on scheduler thread for fixed delay semantics
return this.fixedDelayExecutor.scheduleWithFixedDelay(taskOnSchedulerThread(task),
0, NANO.convert(delay), NANO);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(this.fixedDelayExecutor, task, ex);
}
}
@Override
public void start() {
this.triggerLifecycle.start();
this.fixedDelayLifecycle.start();
}
@Override
public void stop() {
this.triggerLifecycle.stop();
this.fixedDelayLifecycle.stop();
}
@Override
public void stop(Runnable callback) {
this.triggerLifecycle.stop(); // no callback necessary since it's just triggers with hand-offs
this.fixedDelayLifecycle.stop(callback); // callback for currently executing fixed-delay tasks
}
@Override
public boolean isRunning() {
return this.triggerLifecycle.isRunning();
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
this.triggerExecutor.shutdown();
this.fixedDelayExecutor.shutdown();
}
}
@Override
public void close() {
for (Runnable remainingTask : this.triggerExecutor.shutdownNow()) {
if (remainingTask instanceof Future> future) {
future.cancel(true);
}
}
for (Runnable remainingTask : this.fixedDelayExecutor.shutdownNow()) {
if (remainingTask instanceof Future> future) {
future.cancel(true);
}
}
super.close();
}
}