spring_boot_starter_scheduling 0.4.0
spring_boot_starter_scheduling: ^0.4.0 copied to clipboard
Scheduling starter for spring_boot_dart: @Scheduled methods with fixed rate, fixed delay or cron expressions, a TaskScheduler bean, stopped with the context.
spring_boot_starter_scheduling #
No backward compatibility before 1.0. Any release may rename or change APIs, properties and behaviour; the breaking changes are listed in each package's CHANGELOG.
Scheduled tasks for spring_boot_dart
applications, like Spring's @Scheduled: annotate a bean method and it runs
every few seconds, after each run with a pause, or at cron times. Tasks stop
when the context closes.
Installation #
dependencies:
spring_boot_starter_scheduling: ^0.4.0
dev_dependencies:
build_runner: ^2.17.0
spring_core_dart_generator: ^0.4.0 # required: makes @Scheduled methods callable
The starter is the only dependency an application needs: it brings
spring_boot_dart and spring_boot_starter_logging (and re-exports them),
like every Spring Boot starter.
dart run build_runner build
That is all: the starter is found automatically (no @EnableScheduling).
Quick example #
import 'package:spring_boot_starter_scheduling/spring_boot_starter_scheduling.dart';
@Component()
class Reports {
Reports(this.repository);
final ReportRepository repository;
@Scheduled(fixedRate: Duration(minutes: 5))
void refreshCache() => repository.refresh();
@Scheduled(fixedDelay: Duration(seconds: 30))
Future<void> syncWithServer() async => await repository.sync();
@Scheduled(cron: '0 0 9 * * MON-FRI') // 9:00 on working days
void morningDigest() => repository.sendDigest();
}
Start the application as usual (runApplication(args)); the tasks start
once the context is ready.
@Scheduled #
Give exactly one of cron, fixedRate or fixedDelay.
| Attribute | Meaning | Example |
|---|---|---|
fixedRate |
start every period (measured between starts) | fixedRate: Duration(minutes: 5) |
fixedDelay |
wait this long after each run completes | fixedDelay: Duration(seconds: 30) |
initialDelay |
wait before the first run (not with cron); alone: one run after it (Spring 6.1) |
initialDelay: Duration(seconds: 10) |
cron |
run at cron times; - switches the task off |
cron: '0 */15 * * * *' |
zone |
time zone of cron: leave out for local time, or 'UTC' |
zone: 'UTC' |
scheduler |
the TaskScheduler bean to run on, by qualifier or bean name (${...} allowed); default: the only one, the primary one or taskScheduler |
scheduler: 'reportsScheduler' |
fixedRateString, fixedDelayString, initialDelayString |
the same as text, with ${...} placeholders and Spring Boot durations; blank (also once resolved) is not set |
fixedRateString: r'${reports.rate:5m}' |
Rules:
- The method must be public and take no parameters. It may return a
Future: the run counts as finished when the future completes. - Several
@Scheduledon one method give several schedules. - Only singleton beans are scheduled (
@Scope('prototype')beans are ignored with a warning). - A wrong attribute (two kinds, a bad cron, an unknown zone or
scheduler, a missing property) stops the startup with a message namingClass.method. - The method is called on the bean as other beans see it, so AOP advice
(
@Around,@Cacheable, ...) applies.
Cron expressions #
Six fields, like Spring (not five like Unix cron):
second minute hour day-of-month month day-of-week
| Expression | Runs |
|---|---|
0 * * * * * |
every minute, at second 0 |
*/10 * * * * * |
every 10 seconds |
0 */15 * * * * |
every 15 minutes |
0 0 * * * * |
every hour |
0 0 9-17 * * MON-FRI |
every hour from 9 to 17 on working days |
0 30 8 * * * |
8:30 every day |
0 0 0 1 * * |
midnight on the 1st of each month |
0 0 18 L * * |
18:00 on the last day of the month |
0 0 12 LW * * |
noon on the last working day of the month |
0 0 10 15W * * |
10:00 on the working day nearest to the 15th |
0 0 20 ? * FRIL |
20:00 on the last Friday of the month |
0 0 9 ? * MON#1 |
9:00 on the first Monday of the month |
0 0 9 ? * MON#1,MON#3 |
9:00 on the first and the third Monday |
0 0 0 1,L * * |
midnight on the first and the last day of the month |
0 0 10 * * SAT-SUN |
10:00 at the weekend |
0 0 0 25 DEC * |
midnight on December 25th |
@hourly, @daily (@midnight), @weekly, @monthly, @yearly (@annually) |
the usual macros |
- Values:
*,5, lists1,15,30, ranges9-17, steps*/5,0-30/10,5/15(from 5, every 15). MonthsJAN-DEC, case-insensitive.?means "any" in the day fields. - Days of the week as in Spring:
MON-SUNare 1-7 and 0 is Sunday too; Sunday at the start of a range is 0, soSAT-SUN,MON-SUNandSUN-TUEall work.*/2is MON, WED, FRI, SUN. - The specials
L,L-n,nW,LW,nL,n#kmay be combined in lists with each other and with values (1,L,MON-FRI,SATL). - As in Spring, a day must match both day fields, and ranges cannot wrap
around (
FRI-MONis an error: writeFRI-SUN,MON). - Local time follows the wall clock: a time inside the hour skipped when clocks go forward runs at the first valid time after it; a time inside the hour repeated when clocks go back runs once.
Check an expression in code:
final cron = CronExpression.parse('0 0 9 * * MON-FRI');
print(cron.next(DateTime.now())); // the next run
CronExpression.isValid('0 0 25 * * *'); // false: hour 25
Configuration from properties #
Every text attribute resolves placeholders, so schedules can live in
application.properties and change per profile:
@Scheduled(fixedRateString: r'${reports.rate:5m}')
void refresh() { ... }
@Scheduled(cron: r'${reports.cleanup-cron:-}') // off unless configured
void cleanup() { ... }
# lib/resources/application.properties
reports.rate=10m
reports.cleanup-cron=0 0 3 * * *
Durations: 500 (milliseconds), 10s, 5m, 2h, 1d, PT1H30M.
| Property | Default | Meaning |
|---|---|---|
spring.task.scheduling.enabled |
true |
false switches scheduling off (no TaskScheduler, no tasks) |
spring.task.scheduling.shutdown.await-termination |
false |
true: the context's closeAsync() waits for the runs going on when it closes (a run, or its Future, is never interrupted) |
spring.task.scheduling.shutdown.await-termination-period |
— | How long to wait at most (30s); then a warning is logged and the shutdown goes on. Without it the wait is unbounded |
The synchronous close() cannot wait: a run going on finishes after the
close (no further runs start) and TimerTaskScheduler.runningTasks lists
it. Your own TaskScheduler bean gets the same through a Future-returning
destroy() (DisposableBean).
Scheduling in code: TaskScheduler #
Inject the TaskScheduler bean to schedule work yourself:
@Service()
class Reminders {
Reminders(this.scheduler);
final TaskScheduler scheduler;
ScheduledTask remindAt(DateTime at, String text) =>
scheduler.scheduleAt(() => notify(text), at);
ScheduledTask poll() => scheduler.scheduleWithFixedDelay(
checkInbox,
const Duration(seconds: 30),
);
}
final task = reminders.poll();
print(task.nextExecution);
task.cancel();
Triggers: PeriodicTrigger (fixed rate or delay), CronTrigger,
OneShotTrigger, or your own Trigger. Declare your own TaskScheduler
bean to replace the default TimerTaskScheduler. With several
TaskScheduler beans, @Scheduled methods run on the primary one or the
one named taskScheduler, unless they name another with scheduler:.
Asynchronous tasks and overlapping #
Everything runs on Dart's event loop, so a task never runs twice at the same time:
fixedDelay: the pause starts when the run (itsFuture) completes.fixedRate: when a run takes longer than the period, the next run starts right after it, once; missed runs are not caught up.cron: the next time is computed after the run, so times missed during a long run are skipped.
TaskExecutor and @Async listeners #
The starter also registers Spring Boot's applicationTaskExecutor (alias
taskExecutor): a SimpleAsyncTaskExecutor of the core, which runs each
task in a later event-loop turn and logs its errors. Declaring your own
TaskExecutor bean replaces it. Having it switches @Async on for
@EventListener methods (Spring's @EnableAsync): such a listener runs on
the executor instead of the publishing call.
@Component()
class Mailer {
@EventListener()
@Async() // or @Async('mailExecutor') for another TaskExecutor bean
Future<void> onPlaced(OrderPlaced order) => sendConfirmation(order);
}
Errors #
An exception (thrown or from the returned Future) is logged at error
level to the spring.scheduling logger, with the task name; the task keeps
its schedule. Handle errors in the method if you need retries or alerts,
or declare an ErrorHandler bean (Spring's ErrorHandler, which Spring
sets on its ThreadPoolTaskScheduler): it receives every error instead
of the log. With several, the primary one is used (none without one). A handler that throws stops the task, like Spring's
LOG_AND_PROPAGATE.
@Component()
class ReportErrors implements ErrorHandler {
@override
void handleError(Object error, StackTrace stackTrace) =>
crashReporter.record(error, stackTrace);
}
Testing #
Scheduled methods run in springBootTest too. To keep tests
deterministic, either switch scheduling off and call the methods directly:
final app = springBootTest(
springApplication,
properties: {'spring.task.scheduling.enabled': false},
);
test('refresh', () => app.getBean<Reports>().refreshCache());
or replace the scheduler with one on a controlled clock
(TimerTaskScheduler(now: ..., createTimer: ...)) through BeanOverride.
ScheduledAnnotationBeanPostProcessor.scheduledTasks lists what is
scheduled.
Hot reload #
Tasks belong to the context: a hot-reload restart closes it (cancelling every task) and the new context schedules the methods of the new code.
Differences from Spring #
- No
@EnableScheduling: the dependency switches scheduling on;spring.task.scheduling.enabled=falseswitches it off. - Time zones: local time or UTC only (Dart has no time zone database).
- Durations are
Durations instead of numbers plustimeUnit. TaskScheduler.schedule(task, Instant)isscheduleAt(task, DateTime)(Dart has no overloads).fixedRatetasks never overlap and do not catch up missed runs: when a run takes longer than the period, the next one starts right after it, once (Spring's single-threaded scheduler runs the missed executions back to back).- A method returning a
Futureis awaited before its run counts as finished, withfixedRateandcrontoo, so a slow run delays the next one instead of overlapping it (Spring does not wait for a returnedCompletableFuture, and waits for a reactive result only withfixedDelay). #kin the day of week takes 1-5 (Spring accepts larger ordinals that never match).spring.task.scheduling.shutdown.await-termination[-period]works with the context'scloseAsync()only (close()cannot wait); a run that is going on is never interrupted and, withoutawait-termination, finishes after the close (no further runs start).@Asyncworks on@EventListenermethods only and needs no@EnableAsync(theTaskExecutorbean switches it on); there is nospring.task.execution.*pool configuration (Dart has no threads).- Not yet:
SchedulingConfigurer, scheduling of prototype beans.