-
Notifications
You must be signed in to change notification settings - Fork 38.9k
Description
📄 Context / Problem Statement
In distributed systems, it's common to run Spring Boot applications with multiple instances. For cron-based scheduled tasks (e.g., @Scheduled(cron = "0 0 2 * * ?")), users often need to ensure that only one instance executes the task for a given scheduled time—a pattern known as "cluster singleton per schedule".
To implement this safely, we must construct a globally unique lock key based on:
- The task identity (e.g., method name or explicit name)
- The exact scheduled fire time (e.g.,
2025-12-09T02:00:00Z)
However, Spring does not expose the scheduled fire time to the user method. Developers are forced to re-calculate it at execution time using libraries like cron-utils, like so:
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() {
// ❌ Dangerous: relies on current time to infer intended fire time
LocalDateTime scheduledTime = inferScheduledTimeFromCron("0 0 2 * * ?", ZoneId.of("UTC"));
distributedLock.execute("dailyReport", scheduledTime, () -> { ... });
}This approach is fundamentally flawed:
- If the system is under load or GC pause delays task execution (e.g., runs at
02:00:59instead of02:00:00), the inferred time may be wrong (e.g., resolved to next day). - This can cause task skipping or duplicate execution in edge cases.
- It violates the principle that the scheduler already knows the correct fire time—users shouldn’t have to guess it.
Why This Matters
The Spring TaskScheduler already computes the exact next execution time via Trigger.nextExecutionTime(TriggerContext) during scheduling. This time is precise, deterministic, and aligned across all instances (assuming clock sync). Yet, this critical piece of metadata is not exposed to application code.
Without it, building reliable, failover-capable, cluster-safe cron jobs requires fragile workarounds or abandoning @Scheduled entirely in favor of Quartz or custom schedulers.
Proposed Solution
Introduce a way to inject the scheduled fire time into @Scheduled methods, similar to how Spring MVC injects path variables or authentication principals.
Option 1: Parameter injection (recommended)
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport(@ScheduledTime LocalDateTime scheduledFireTime) {
// ✅ Safe: use the time the scheduler actually planned for this execution
distributedLock.execute("dailyReport", scheduledFireTime, Duration.ofHours(1), () -> {
// business logic
});
}@ScheduledTimewould be a new stereotype annotation (e.g.,org.springframework.scheduling.annotation.ScheduledTime)- Supported types:
LocalDateTime,ZonedDateTime,Instant,Date - Time zone respects the
zoneattribute of@Scheduled
Option 2: ThreadLocal context (fallback)
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() {
LocalDateTime fireTime = ScheduledTaskContext.getCurrentFireTime();
}But Option 1 is preferred for its clarity and testability.
Backward Compatibility
- This is an opt-in feature: existing
@Scheduledmethods are unaffected. - No breaking changes to APIs or behavior.
Prior Art
- Quartz Scheduler: Provides
JobExecutionContext.getFireTime()— widely used for exactly this purpose. - AWS EventBridge / Cloud Scheduler: Always includes the scheduled invocation time in payload.
- Kubernetes CronJob: Sets
metadata.annotations["cronjob.kubernetes.io/scheduled"].
Conclusion
Exposing the scheduled fire time would:
- Enable safe, standard implementation of distributed cron jobs
- Eliminate error-prone time inference logic
- Bring Spring’s scheduling model closer to production-grade reliability
This small addition would significantly improve Spring’s suitability for cloud-native, multi-instance deployments—without compromising its simplicity for single-node use cases.