
Configuration des pools de threads pour les tâches planifiées
Apprenez à personnaliser le pool de threads utilisé par les tâches planifiées (@Scheduled) dans Spring Boot pour contrôler la concurrence, nommer les threads et gérer les ressources.
Pourquoi configurer le pool de threads pour @Scheduled ?
L'annotation @Scheduled de Spring Boot, activée via @EnableScheduling, permet de définir très facilement des méthodes qui doivent s'exécuter périodiquement, à des heures fixes, ou après un délai. C'est idéal pour des tâches de maintenance, des rapports, des synchronisations, etc.
Par défaut, lorsque vous utilisez @EnableScheduling sans configuration supplémentaire, Spring Boot crée et utilise un pool de threads avec un seul thread pour exécuter toutes les méthodes @Scheduled de votre application. Cela signifie que si vous avez plusieurs tâches planifiées, elles s'exécuteront séquentiellement. Si une tâche prend du temps, elle retardera l'exécution des autres tâches prévues pendant ce temps.
Cette configuration par défaut peut être problématique dans plusieurs scénarios :
- Tâches concurrentes : Si vous avez plusieurs tâches qui doivent s'exécuter en parallèle (par exemple, plusieurs tâches planifiées pour s'exécuter toutes les minutes), un seul thread les bloquera les unes après les autres.
- Tâches longues : Une tâche longue peut monopoliser le thread unique et empêcher d'autres tâches, même courtes, de s'exécuter à l'heure prévue.
- Débogage et identification : Tous les logs des tâches planifiées proviendront du même thread (par exemple, `scheduling-1`), rendant plus difficile l'identification de quelle tâche est en cours d'exécution.
Il est donc souvent nécessaire de configurer explicitement le pool de threads utilisé par le planificateur de tâches pour mieux contrôler la concurrence, les ressources et l'identification des tâches.
Configuration simple via les propriétés
La manière la plus simple d'augmenter le nombre de threads disponibles pour les tâches planifiées est d'utiliser les propriétés de configuration de Spring Boot dans application.properties ou application.yml.
La propriété clé est spring.task.scheduling.pool.size.
Exemple dans application.properties :
# Configure le pool de threads du planificateur avec 5 threads
spring.task.scheduling.pool.size=5
# Optionnel: Définir un préfixe pour les noms de threads de ce pool
spring.task.scheduling.thread-name-prefix=MonAppScheduled-
Exemple dans application.yml :
spring:
task:
scheduling:
pool:
size: 5
thread-name-prefix: MonAppScheduled-
En définissant spring.task.scheduling.pool.size à une valeur supérieure à 1 (par exemple, 5), Spring Boot configurera le planificateur par défaut pour utiliser un pool de threads avec cette taille. Cela permettra à jusqu'à 5 tâches @Scheduled de s'exécuter simultanément. Le préfixe de nom de thread (thread-name-prefix) aide à identifier ces threads spécifiques dans les logs (par exemple, `MonAppScheduled-1`, `MonAppScheduled-2`, etc.).
Cette approche est suffisante pour de nombreux cas où vous avez juste besoin d'augmenter la concurrence de base.
Configuration avancée avec `SchedulingConfigurer`
Pour un contrôle plus fin sur le planificateur (taille de la file d'attente, politique de rejet, gestion des erreurs, etc.) ou si vous voulez utiliser un exécuteur différent, vous pouvez implémenter l'interface org.springframework.scheduling.annotation.SchedulingConfigurer.
Cette interface vous demande d'implémenter la méthode configureTasks(ScheduledTaskRegistrar taskRegistrar). Dans cette méthode, vous pouvez créer et configurer votre propre instance de TaskScheduler (typiquement un ThreadPoolTaskScheduler) et l'enregistrer auprès du `taskRegistrar`.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.util.ErrorHandler;
@Configuration
@EnableScheduling
public class CustomSchedulerConfig implements SchedulingConfigurer {
private static final Logger log = LoggerFactory.getLogger(CustomSchedulerConfig.class);
private final int POOL_SIZE = 10;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
threadPoolTaskScheduler.setThreadNamePrefix("custom-scheduled-task-pool-");
// Attendre la fin des tâches lors de l'arrêt (true par défaut)
// threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
// Délai d'attente maximal lors de l'arrêt
// threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
// Gestionnaire d'erreurs personnalisé pour les tâches planifiées
threadPoolTaskScheduler.setErrorHandler(new CustomErrorHandler());
// Important: Initialiser le scheduler après configuration
threadPoolTaskScheduler.initialize();
// Enregistrer le scheduler personnalisé
taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
}
// Gestionnaire d'erreurs simple (exemple)
class CustomErrorHandler implements ErrorHandler {
private static final Logger log = LoggerFactory.getLogger(CustomErrorHandler.class);
@Override
public void handleError(Throwable t) {
log.error("Erreur inattendue dans une tâche planifiée", t);
// Ajouter une logique de notification si nécessaire
}
}
Dans cet exemple :
- On implémente
SchedulingConfigurer. - Dans
configureTasks, on crée unThreadPoolTaskScheduler. - On configure sa taille (
setPoolSize), le préfixe du nom des threads (setThreadNamePrefix), et un gestionnaire d'erreurs personnalisé (setErrorHandler). - On initialise le scheduler avec
initialize(). - On le passe au
taskRegistrarviasetTaskScheduler().
Cette approche donne un contrôle total sur l'instance du scheduler utilisé pour toutes les tâches @Scheduled.
Définir un bean TaskScheduler personnalisé
Une alternative souvent plus simple à l'implémentation de SchedulingConfigurer consiste à simplement déclarer un bean de type org.springframework.scheduling.TaskScheduler dans votre configuration Spring.
Si un (et un seul) bean de ce type est présent dans le contexte d'application, et que @EnableScheduling est actif, Spring Boot l'utilisera automatiquement comme planificateur pour les tâches @Scheduled, sans qu'il soit nécessaire d'implémenter SchedulingConfigurer.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
@EnableScheduling
public class SchedulerBeanConfig {
@Bean
public TaskScheduler taskScheduler() { // Le nom du bean n'importe pas forcément ici
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(8);
scheduler.setThreadNamePrefix("bean-scheduled-task-");
scheduler.setErrorHandler(t -> System.err.println("Erreur dans le scheduler bean: " + t));
// scheduler.initialize(); // Pas besoin d'appeler initialize() ici, Spring s'en charge pour les beans
return scheduler;
}
}
Cette méthode est généralement préférée si vous n'avez besoin que d'un seul pool de threads personnalisé pour toutes vos tâches planifiées et que vous n'avez pas besoin de la flexibilité offerte par le ScheduledTaskRegistrar (par exemple, pour enregistrer des tâches programmatiquement).
Considérations et bonnes pratiques
- Taille du pool : Le choix de la taille du pool (
poolSize) est crucial. Une taille trop petite entraînera des retards si de nombreuses tâches s'exécutent simultanément. Une taille trop grande consommera inutilement des ressources système. Analysez le nombre et la durée de vos tâches pour déterminer une taille appropriée. Commencez petit et augmentez si nécessaire. - Nommage des threads : Utilisez toujours un préfixe de nom de thread (
threadNamePrefix) descriptif. Cela facilite grandement le suivi des logs et le débogage pour identifier quelles tâches s'exécutent. - Gestion des erreurs : Implémentez un
ErrorHandlerpour capturer et logger (ou notifier) les exceptions non gérées dans vos tâches planifiées. Sans cela, une exception peut arrêter silencieusement l'exécution future de cette tâche spécifique (selon le type d'erreur). - Arrêt gracieux : Par défaut,
ThreadPoolTaskSchedulerattend la fin des tâches en cours lors de l'arrêt de l'application (setWaitForTasksToCompleteOnShutdown(true)). Vous pouvez configurer un délai maximal d'attente avecsetAwaitTerminationSeconds()pour éviter de bloquer indéfiniment l'arrêt de l'application si une tâche est bloquée. - Tâches bloquantes : Même avec un pool de threads, si vos tâches planifiées effectuent des opérations longues et bloquantes (I/O réseau, accès base de données lents), elles peuvent toujours monopoliser les threads du pool. Envisagez d'utiliser des approches asynchrones (comme
@AsyncavecCompletableFuture) à l'intérieur de vos méthodes@Scheduledpour les opérations bloquantes si la concurrence au sein même d'une tâche est nécessaire.
En configurant judicieusement le pool de threads pour vos tâches planifiées, vous assurez une exécution plus fiable, concurrente et contrôlée de vos traitements périodiques dans Spring Boot.