public class CronScheduler extends Object
CronScheduler is a facility to schedule periodic or one-shot tasks at specified times.
The main difference between CronScheduler and ScheduledThreadPoolExecutor is that CronScheduler uses currentTimeMillis as the source of time (or any other time provider
if configured), while ScheduledThreadPoolExecutor uses nanoTime. Using
currentTimeMillis as the source of time instead of nanoTime allows to protect
against unbounded clock drift and to accommodate for system time corrections, either manual, or
initiated by the NTP server. This makes CronScheduler similar to Timer.
However, Timer has a limited API and doesn't accommodate for significant
system time shifts, as well as machine sleep or hibernation events. CronScheduler allows
to mitigate the schedule disruptions which might be caused by such events via setting
sufficiently small sync period (see below for more details).
Additional features of CronScheduler:
scheduleAtRoundTimesInDay
(and its "skipping to latest"
counterpart) encapsulate the complexity of calculating the initial delay for a task so that
the first run time is a round wall clock time with respect to the specified period, as well
as maintaining to the wall clock time-driven schedule in the face of daylight saving time
changes.scheduleAtFixedRateSkippingToLatest and scheduleAtRoundTimesInDaySkippingToLatest prune
excessive runs in the face of observed abrupt forward time shifts. See below for more details
on this.CronScheduler is single-threaded. It is not meant to be used as a general
executor, but rather to provide the scheduling only. The tasks should be kept small and
non-blocking. Heavy work should be preferably delegated to other executors.
The interface is similar to ScheduledExecutorService, but because of some differences
this interface is not implemented by CronScheduler; an adapter is provided via asScheduledExecutorService().
CronScheduler has a sync period set at construction time. Sync period is the
maximum "asleep" period for the CronScheduler. With at most this period, CronScheduler checks if the current time changed and readjusts the waiting times for the
scheduled tasks if needed. Possible reasons for readjustment are backward time shifts, abrupt forward time shifts
(see below), or slow clock drift.
On machines that may go to sleep/hibernation (like phones, tablets, PCs, laptops), sync period
is the maximum time that will take CronScheduler to notice the sleep event, and therefore
sync period is effectively the upper bound for how much late the scheduled tasks can be run (not
considering factors that are outside of the control of CronScheduler, such as GC pauses).
For example, if the CronScheduler is used to notify the user about something every hour, and the
user sends the laptop to sleep from 15:01 to 16:01 (just one minute past the last notification),
a CronScheduler with sync period of 5 minutes will trigger the notification at 16:06 the
next time. If a ScheduledThreadPoolExecutor or a Timer was used for this task, the notification wouldn't trigger until 17:00, or 59
minutes after the laptop has waken up.
If CronScheduler is used in the server environment where machines don't go to
sleep or hibernation, sync period should be chosen considering the maximum tolerable clock drift
amount (which will then be the upper bound on how much late the scheduled tasks can be run) and
the clock drift rate that may conceivably happen in the system (10%, perhaps, or less than that
if there are reasons to think there is a stricter upper bound on the clock drift rate). For
example, if it's not tolerable that tasks run more than 30 seconds late, the sync period for the
CronScheduler could be chosen at 5 minutes. If at least one periodic task scheduled with
the CronScheduler will have a shorter period, or clock drift is generally not a concern,
the sync period could be chosen to be equal to the shortest scheduling period for the tasks.
scheduleAtFixedRateSkippingToLatest and scheduleAtRoundTimesInDaySkippingToLatest methods
skip scheduled times if there is at least one later scheduled time that has already come with
respect to currentTimeMillis, or a different time provider
if configured. In the case of observed abrupt
forward time shift, the task scheduled for periodic run using one of the *SkippingToLatest
methods won't be called with the scheduled times (see CronTask.run) that
already appear outdated. For example, if some task is scheduled with a 10 minute period and at
some point there is an apparent 1 hour gap between executions (the previous execution scheduled
at 17:00 has finished at 17:02, and the next observed clock time is 18:02), 5 non-latest run
times will be skipped: the next time CronTask.run(long) will be called with scheduledRunTimeMillis corresponding to 18:00 (rather than 17:10) as the argument.
Possible reasons for observed abrupt forward time shift:
Note that zone offset changes (e. g. DST changes) in the given time zone are not one of
the possible reasons, because zone offset governs the translation from a time instant (Instant) to local time (ZonedDateTime), whereas the abrupt time shifts scheduleAtFixedRateSkippingToLatest and scheduleAtRoundTimesInDaySkippingToLatest methods
additionally take into consideration (compared to scheduleAtFixedRate and scheduleAtRoundTimesInDay) are the
time instant shifts. However, scheduleAtRoundTimesInDaySkippingToLatest maintains the promise of scheduleAtRoundTimesInDay to call CronTask only with
scheduledRunTimeMillis arguments that represent round wall clock times in the given time
zone, whatever instant time shifts or zone offset changes occur in whatever order.
In any case, when CronTask is called, the scheduledRunTimeMillis provided into
it is still not guaranteed to be equal to the current millisecond instant, or being not more late
than the scheduling period of the task, or the sync period of the CronScheduler. It's
fundamentally impossible to provide such a guarantee because an arbitrarily long pause may always
happen between the check and the actual logic. The primary use case for *SkippingToLatest
methods is protecting an upstream object, system, or service from request flood (aka query
storm) after pauses, without requiring CronTask implementations to filter non-latest
scheduled runs manually. The timeliness of requests should be checked on the receiver side if
required.
Whenever some task is due for execution, or at least every sync period CronScheduler
checks if the current time is before the previously observed time. If this is the case, CronScheduler first logs the backward time shift occurrence (to System.err by default,
or to a custom logger if
configured). Then, CronScheduler ensures that all periodic tasks will run properly with
respect to the new current time.
Periodic tasks created with all scheduleAtFixedRate and scheduleAtRoundTimesInDay methods are assumed to extend indefinitely in the past if the time
is shifted before the initial trigger time of the task. The past scheduling times will be initialDelay - period, initialDelay - 2 * period, and so on for scheduleAtFixedRate methods (but not that a negative initialDelay is always replaced
with zero up front), and past round clock times within a day for scheduleAtRoundTimesInDay methods. If this is undesirable, the past run times could be filtered
manually in the definition of the CronTask:
long currentTime = System.currentTimeMillis();
CronTask task = scheduledRunTimeMillis -> {
if (scheduledRunTimeMillis >= currentTime) {
// perform the task
}
};
cronScheduler.scheduleAtFixedRate(0, 1, MINUTES, task);| Modifier and Type | Method and Description |
|---|---|
ScheduledExecutorService |
asScheduledExecutorService()
Returns an adapter of this
CronScheduler to ScheduledExecutorService
interface. |
boolean |
awaitTermination(long timeout,
TimeUnit unit)
Blocks until all tasks have completed execution after a shutdown
request, or the timeout occurs, or the current thread is
interrupted, whichever happens first.
|
static CronScheduler |
create(Duration syncPeriod)
Creates and returns a new
CronScheduler with the given sync period. |
long |
getCompletedTaskCount()
Returns the approximate total number of tasks that have completed execution.
|
long |
getTaskCount()
Returns the approximate total number of tasks that have ever been
scheduled for execution.
|
Collection<? extends Future<?>> |
getTasks()
Returns the task collection used by this CronScheduler.
|
boolean |
isShutdown()
Returns
true if this scheduler has been shut down. |
boolean |
isTerminated()
Returns
true if all tasks have completed following shut down. |
boolean |
isTerminating()
Returns true if this scheduler is in the process of terminating after
shutdown or shutdownNow but has not completely terminated. |
boolean |
isThreadActive()
Returns true if the scheduler thread is currently executing a task.
|
boolean |
isThreadRunning()
Returns true if the scheduler thread is currently running.
|
static CronSchedulerBuilder |
newBuilder(Duration syncPeriod)
Creates and returns a new
CronSchedulerBuilder which can be used to build CronSchedulers with the given sync period. |
boolean |
prestartThread()
Starts the scheduler thread, causing it to idly wait for work.
|
void |
purge()
Tries to remove from the work queue all
Future
tasks that have been cancelled. |
<V> Future<V> |
scheduleAt(Instant triggerTime,
Callable<V> callable)
Submits a one-shot task that becomes enabled at the given instant.
|
Future<?> |
scheduleAt(Instant triggerTime,
Runnable command)
Submits a one-shot task that becomes enabled at the given instant.
|
Future<?> |
scheduleAtFixedRate(long initialDelay,
long period,
TimeUnit unit,
CronTask task)
Submits a periodic action that becomes enabled first after the
given initial delay, and subsequently with the given period;
that is, executions will commence after
initialDelay, then initialDelay + period, then
initialDelay + 2 * period, and so on. |
Future<?> |
scheduleAtFixedRateSkippingToLatest(long initialDelay,
long period,
TimeUnit unit,
CronTask task)
Submits a periodic action that becomes enabled first after the
given initial delay, and subsequently with the given period;
that is, executions will commence after
initialDelay, then initialDelay + period, then
initialDelay + 2 * period, and so on; scheduled times will be skipped if there is at
least one other later scheduled time that has already come. |
Future<?> |
scheduleAtRoundTimesInDay(Duration period,
ZoneId zoneId,
CronTask task)
Submits a periodic task that becomes enabled at round clock times within a day, with the
given period, in the given time zone.
|
Future<?> |
scheduleAtRoundTimesInDaySkippingToLatest(Duration period,
ZoneId zoneId,
CronTask task)
Submits a periodic task that becomes enabled at round clock times within a day, with the
given period, in the given time zone, skipping scheduled times if there is at least one other
later scheduled time that has already come.
|
void |
shutdown(OneShotTasksShutdownPolicy oneShotTasksShutdownPolicy)
Initiates an orderly shutdown in which previously submitted
tasks are executed, but no new tasks will be accepted.
|
List<Future<?>> |
shutdownNow()
Attempts to stop all actively executing tasks, halts the
processing of waiting tasks, and returns a list of the tasks
that were awaiting execution.
|
String |
toString()
Returns a string identifying this scheduler, as well as its state,
including indications of run state and task counts.
|
public boolean isThreadRunning()
prestartThread(),
isTerminated()public static CronSchedulerBuilder newBuilder(Duration syncPeriod)
CronSchedulerBuilder which can be used to build CronSchedulers with the given sync period. See more details about sync period and
how it should be chosen in the class-level documentation for CronScheduler.syncPeriod - the sync period for CronSchedulers to be built with the returned
builderCronSchedulerBuilderNullPointerException - if the sync period is nullIllegalArgumentException - if sync period is less than 1 second or more than 1 daypublic static CronScheduler create(Duration syncPeriod)
CronScheduler with the given sync period. See more details
about sync period and how it should be chosen in the class-level documentation for CronScheduler. Other settings are equal to the defaults of CronSchedulerBuilder.
currentTimeMillis is used as the time source. The sole
thread of the CronScheduler will be a daemon thread of normal priority, not tied to any ThreadGroup, with a name of the
form "cron-scheduler-N" where N is a global sequence number. Backward time shift events will
be logged into System.err if observed.syncPeriod - the sync period for CronSchedulerCronSchedulerNullPointerException - if the sync period is nullIllegalArgumentException - if sync period is less than 1 second or more than 1 daypublic Future<?> scheduleAt(Instant triggerTime, Runnable command)
triggerTime - the moment of time when the task should be fired for execution
(Clock.millis() of the time provider used by this CronScheduler should become
greater than or equal to Instant.toEpochMilli() of the given trigger time)command - the task to be executedFuture representing pending completion of the task and whose get()
method will return null upon completionRejectedExecutionException - if this CronScheduler has already been shut downNullPointerException - if triggerTime or command is nullpublic <V> Future<V> scheduleAt(Instant triggerTime, Callable<V> callable)
V - the type of the callable's resulttriggerTime - the moment of time when the task should be fired for execution
(Clock.millis() of the time provider used by this CronScheduler should become
greater than or equal to Instant.toEpochMilli() of the given trigger time)callable - the task to be executedFuture representing pending completion of the taskRejectedExecutionException - if this CronScheduler has already been shut downNullPointerException - if triggerTime or callable is nullpublic Future<?> scheduleAtRoundTimesInDay(Duration period, ZoneId zoneId, CronTask task)
currentTimeMillis, or a custom time provider if configured) in the given time zone is 00:26 and the
period of 1 hour is given, the task will be first executed in 34 minutes, then in 1 hour 34
minutes, etc. In other words, it would be equivalent to a call to scheduleAtFixedRate(34, 60, MINUTES, task) (as long as the time zone doesn't change the
offset).
When daylight saving time or permanent offset changes occur in the given time zone, this
method preserves the affinity of the schedule to the round clock times in the day, which may
result in a shorter or longer than nominal period (in actual, physical time terms) between
two consecutive runs of the task when the clock is changed. When the clock in the given time
zone is changed backward at a time which is also a round time for the task call (for example,
the period is 2 hours, and the clock is changed at 02:00), CronTask will be run two
consecutive times at the same local wall clock time, with a period between these runs equal
to the zone offset change amount (if this is a DST change, it's usually 1 hour).
At the moment when a zone offset change occurs (such as a DST change), the task is
triggered only if the local wall clock time after the transition is divisible by the
given period, but not if the local wall clock time before the transition is
divisible by the given period. For example, if the period is 2 hours, and there is a DST
change from 02:00 to 03:00, the task will be called only at the local times 00:00 and then
04:00, but not 02:00. This is because the local wall clock time before the transition is not
actually considered valid by convention. See the documentation for ZoneOffsetTransition.getDateTimeBefore() and ZoneOffsetTransition.getDateTimeAfter().
If irregularity in physical time periods between the task runs in the face of zone offset
changes (e. g. DST changes) is not wanted, it can be avoided by passing a ZoneOffset into this method instead of a region-based time zone. The current zone offset
could be obtained from the region-based id as follows:
ZoneId regionZone = ZoneId.systemDefault();
ZoneOffset zoneOffset = regionZone.getRules().getOffset(Instant.now(clock));
This method is only as accurate with respect to zone offset transitions as the given
region-based ZoneId is. In practice, it means that if one of the standard time zones
with the default (static) ZoneRulesProvider is used, and then the
government of the territory decides to introduce or abolish daylight saving time, or to
change the zone offset permanently, the ongoing call to scheduleAtRoundTimesInDay
won't pick up these changes. In order to enable this method to take into account arbitrary
future ZoneRules changes, a ZoneId linked to a dynamic ZoneRulesProvider should be used.
Note that the time zone from the time provider configured for the CronScheduler (Clock.getZone()) is not taken into account.
If the time zone distinction is not important, or if 15 minutes are divisible by the
period of the task (like 5 minutes or 15 minutes; it makes the scheduling of the task
effectively indifferent to any real-world time zone changes) ZoneOffset.UTC
or ZoneId.systemDefault() could be passed into this method.
When a backward time shift occurs, the task might be executed at times preceding the first
run time of the task, as long as they represent round clock times within a day. See more
details about backward time shifts in the class-level documentation for CronScheduler.
period - the wall clock period between successive executionszoneId - time zone used to calculate the initial delay for the task and to adjust
the delay when zone offset changes (DST or permanent) happentask - the task to be executedFuture representing pending completion of
the series of repeated tasks. The future's get() method will never return normally,
and will throw an exception upon task cancellation or
abnormal termination of a task execution.RejectedExecutionException - if this CronScheduler has already been shut downNullPointerException - if period, zoneId, or task is nullIllegalArgumentException - if the period is less than 5 seconds, or is not an integral
part of a day, or is greater than a dayscheduleAtFixedRate(long, long, TimeUnit, CronTask),
scheduleAtRoundTimesInDaySkippingToLatest(Duration, ZoneId, CronTask)public Future<?> scheduleAtRoundTimesInDaySkippingToLatest(Duration period, ZoneId zoneId, CronTask task)
scheduleAtRoundTimesInDay, except
that in case of observed abrupt forward time shift, the task is not called provided with the
scheduling times (see CronTask.run) that already appear outdated. See
more details about "skipping to latest" concept in the class-level documentation for CronScheduler.
This method maintains the promise of scheduleAtRoundTimesInDay to call
CronTask only with scheduledRunTimeMillis arguments that represent round wall
clock times in the given time zone, whatever instant time shifts or zone offset changes occur
in whatever order.
period - the wall clock period between successive executionszoneId - time zone used to calculate the initial delay for the task and to adjust the
delay when zone offset changes (DST or permanent) happentask - the task to be executedFuture representing pending completion of
the series of repeated tasks. The future's get() method will never return normally,
and will throw an exception upon task cancellation or
abnormal termination of a task execution.RejectedExecutionException - if this CronScheduler has already been shut downNullPointerException - if period, zoneId, or task is nullIllegalArgumentException - if the period is less than 5 seconds, or is not an integral
number of seconds, or is not an integral part of a day, or is greater than a dayscheduleAtFixedRateSkippingToLatest(long, long, java.util.concurrent.TimeUnit, io.timeandspace.cronscheduler.CronTask),
scheduleAtRoundTimesInDay(Duration, ZoneId, CronTask)public Future<?> scheduleAtFixedRate(long initialDelay, long period, TimeUnit unit, CronTask task)
initialDelay, then initialDelay + period, then
initialDelay + 2 * period, and so on.
A negative initialDelay value is replaced with zero to determine all future (or
prior, which may be relevant when backward time shifts occur; see below) scheduling times.
When a backward time shift occurs, the task might be executed at the times preceding
the initial trigger time: initialDelay - period, initialDelay - 2 * period,
and so on. See more details about backward time shifts in the class-level documentation for
CronScheduler.
The sequence of task executions continues indefinitely until one of the following exceptional completions occur:
shutdown() or shutdownNow() is called; also resulting in task
cancellation.
get on the returned future will throw
ExecutionException, holding the exception as its cause.
isDone() on the returned future will
return true.task - the task to executeinitialDelay - the time to delay first executionperiod - the period between successive executionsunit - the time unit of the initialDelay and period parametersFuture representing pending completion of
the task, and whose get() method will throw an
exception upon cancellationRejectedExecutionException - if this CronScheduler has already been shut downNullPointerException - if the unit or the task is nullIllegalArgumentException - if period less than or equal to zero, or if the initial
delay converted to milliseconds is less than - (Long.MAX_VALUE / 2) or is
greater than Long.MAX_VALUE / 2public Future<?> scheduleAtFixedRateSkippingToLatest(long initialDelay, long period, TimeUnit unit, CronTask task)
initialDelay, then initialDelay + period, then
initialDelay + 2 * period, and so on; scheduled times will be skipped if there is at
least one other later scheduled time that has already come. This method behaves the same as
scheduleAtFixedRate, except
that in case of observed abrupt forward time shift, the task is not called provided with the
scheduling times (see CronTask.run) that already appear outdated. See
more details about "skipping to latest" concept in the class-level documentation for CronScheduler.task - the task to executeinitialDelay - the time to delay first executionperiod - the period between successive executionsunit - the time unit of the initialDelay and period parametersFuture representing pending completion of
the task, and whose get() method will throw an
exception upon cancellationRejectedExecutionException - if this CronScheduler has already been shut downNullPointerException - if the unit or the task is nullIllegalArgumentException - if period less than or equal to zero, or if the initial
delay converted to milliseconds is less than - (Long.MAX_VALUE / 2) or is
greater than Long.MAX_VALUE / 2public ScheduledExecutorService asScheduledExecutorService()
CronScheduler to ScheduledExecutorService
interface. The returned ScheduledExecutorService acts the same as a newly constructed
ScheduledThreadPoolExecutor with one thread, ThreadPoolExecutor.AbortPolicy as the RejectedExecutionHandler, and none setXxx() methods called,
i. e. using the default behaviour: stopping all periodic tasks after a shutdown, but still
executing all one-shot tasks that have already been scheduled.
The returned ScheduledExecutorService does not support scheduleWithFixedDelay operation: an UnsupportedOperationException is thrown when this method is called.
ScheduledExecutorServicepublic void shutdown(OneShotTasksShutdownPolicy oneShotTasksShutdownPolicy)
This method does not wait for previously submitted tasks to
complete execution. Use awaitTermination
to do that.
oneShotTasksShutdownPolicy - the policy determining whether delayed one-shot tasks
should be executedSecurityException - if a security manager exists and
shutting down this CronScheduler may manipulate threads that the caller is not
permitted to modify because it does not hold RuntimePermission("modifyThread"),
or the security manager's checkAccess method denies access.public List<Future<?>> shutdownNow()
This method does not wait for actively executing tasks to
terminate. Use awaitTermination to
do that.
There are no guarantees beyond best-effort attempts to stop
processing actively executing tasks. This implementation
interrupts tasks via Thread.interrupt(); any task that
fails to respond to interrupts may never terminate.
schedule methods.SecurityException - if a security manager exists and
shutting down this ExecutorService may manipulate
threads that the caller is not permitted to modify
because it does not hold RuntimePermission("modifyThread"),
or the security manager's checkAccess method
denies access.public Collection<? extends Future<?>> getTasks()
Each element of this collection is a Future returned from one of the schedule methods.
Iteration over this collection is not guaranteed to traverse tasks in the order in which they will execute.
Attempts to add elements to this collection will throw UnsupportedOperationException.
public boolean isShutdown()
true if this scheduler has been shut down.true if this scheduler has been shut downpublic boolean isTerminating()
shutdown or shutdownNow but has not completely terminated. This method
may be useful for debugging. A return of true reported a sufficient
period after shutdown may indicate that submitted tasks have
ignored or suppressed interruption, causing this CronScheduler not
to properly terminate.true if terminating but not yet terminatedpublic boolean isTerminated()
true if all tasks have completed following shut down.
Note that isTerminated is never true unless
either shutdown or shutdownNow was called first.true if all tasks have completed following shut downpublic boolean awaitTermination(long timeout,
TimeUnit unit)
throws InterruptedException
timeout - the maximum time to waitunit - the time unit of the timeout argumenttrue if this CronScheduler terminated and
false if the timeout elapsed before terminationInterruptedException - if interrupted while waitingpublic boolean prestartThread()
false
if the scheduler thread has already been started.true if a thread was startedpublic void purge()
Future
tasks that have been cancelled. This method can be useful as a
storage reclamation operation, that has no other impact on
functionality. Cancelled tasks are never executed, but may
accumulate in work queues until worker threads can actively
remove them. Invoking this method instead tries to remove them now.
However, this method may fail to remove tasks in
the presence of interference by other threads.public boolean isThreadActive()
public long getTaskCount()
public long getCompletedTaskCount()