Files
Flash5/flash-extensions/flash-ext-scheduler/docs
Zakaria El OrcheandClaude Opus 5 5c163b7f8d feat(ext-scheduler): add interval and cron background jobs
jobs.every(Duration.ofMinutes(5), reports::refresh);
    jobs.cron("0 0 3 * * *", archive::sweep);

One daemon platform thread keeps time; every job body runs on a virtual thread,
so a slow job delays nothing but its own next run and cannot occupy the timer.

Cron expressions compile once into a bitmask per field — a long for seconds and
minutes, an int for the rest — so matching an instant is a shift and a mask
rather than a parse or a set lookup. Next-fire advances by the largest unit
that cannot match instead of ticking second by second, so a yearly expression
resolves in a few dozen iterations rather than thirty million. Five or six
fields, ranges, steps, lists, named months and weekdays, both Sunday encodings,
and the standard union semantics when both day fields are restricted. A
malformed expression throws when the job is registered, not when it would have
fired.

Overlapping runs are skipped, and deliberately not configurable: two copies of
one job at once is a bug in every case anyone has needed, and a flag would only
let it be set wrongly. A skipped run logs how long the previous one has been
going.

A throwing job is logged and keeps its schedule. scheduleAtFixedRate cancels
the task on first exception, silently — a job that dies at 3am and is never
heard from again is the failure this avoids.

Shutdown goes through FlashContext.onClose, so app.stop() drains HTTP first,
then gives running jobs a grace period before forcing them down. Nothing runs
after the app stops. The grace period is the only knob.

Known ceiling, marked in the source: schedules are per-instance, so two
replicas run every job twice. A distributed lock should not exist until there
is a second replica.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:23:34 +00:00
..

flash-ext-scheduler

Background jobs on an interval or a cron schedule. One platform thread keeps time, every job body runs on a virtual thread, and the whole thing stops with the app.

What it provides

Component Description
Scheduler every(interval, work) and cron(expression, work)
Cron A cron expression compiled to bitmasks; usable on its own

Dependency

<dependency>
    <groupId>dev.relism</groupId>
    <artifactId>flash-ext-scheduler</artifactId>
    <version>${flash.version}</version>
</dependency>

Quick start

FlashApp.create(8080)
    .install(new SchedulerExtension())
    .apply(new BlogApp())
    .start();
// inside BlogApp.configure(app)
app.ctx().onReady(() -> {
    Scheduler jobs = app.ctx().require(Scheduler.class);

    jobs.every(Duration.ofMinutes(5), reports::refresh);
    jobs.cron("0 0 3 * * *", archive::sweep);
});

onReady is the right place: the service graph is resolved by then, so a job can close over the services it needs.

Give a job an explicit name when the logs should be readable — a method reference names itself, a lambda cannot:

jobs.every("refresh-reports", Duration.ofMinutes(5), () -> reports.refresh());
jobs.cron("nightly-archive", "0 0 3 * * *", () -> archive.sweep());

jobNames() returns them in registration order, for an ops or health endpoint.

Cron expressions

Five fields (min hour dom mon dow) or six with a leading seconds field.

Expression Fires
0 0 3 * * * 03:00:00 daily
*/15 * * * * every 15 minutes
0 9 * * MON-FRI 09:00 on weekdays
0 0 1 MAR * midnight on 1 March
0,30 * * * * on the hour and the half hour

Supports *, ?, single values, a-b ranges, a/n steps, comma lists, and three-letter month and day names. Both 0 and 7 mean Sunday.

Standard cron semantics for the two day fields: when both are restricted, a match is their union0 0 1 * MON means "the 1st, or any Monday", not "a Monday that is the 1st".

A malformed expression throws when you register the job, not the first time it would have fired.

Overlapping runs are skipped

Not configurable. If a run is still going when the next is due, the next is skipped and a WARN records how long the previous one has been running.

Two copies of the same job running at once is a bug in every case anyone has needed so far, and a flag would only let it be configured wrongly. If you genuinely want concurrent runs, register the job twice under different names.

Failure

A throwing job is logged at ERROR and keeps its schedule. A raw scheduleAtFixedRate cancels the task on the first exception, silently — a job that dies at 3am and is never heard from again is the failure mode this avoids.

Shutdown

The scheduler is registered with FlashContext.onClose, so app.stop() drains in-flight HTTP requests first, then gives running jobs their grace period (10s by default) before forcing them down. Nothing runs after the app has stopped.

new SchedulerExtension(Duration.ofSeconds(30))   // longer grace for slow jobs

That is the only knob.

Threading

Scheduler holds one daemon platform thread for timing and dispatches every job body to a virtual thread. A slow job delays nothing but its own next run; it cannot occupy the timer or starve other jobs.

Performance

A cron expression is parsed once, into a bitmask per field — a long for seconds and minutes, an int for the rest. Matching a candidate instant is a shift and a mask, not a parse or a set lookup. Computing the next fire time advances by the largest unit that cannot match rather than ticking second by second, so a yearly expression resolves in a few dozen iterations rather than thirty million.

To be precise about where that matters: this runs once per fire, not once per request. The allocation discipline elsewhere in Flash is about the request hot path, and a scheduler that computes a ZonedDateTime a few times an hour is not on it. The bitmasks are here because parsing a string on every candidate instant would be genuinely wasteful, not to shave an allocation.

Known ceiling

Schedules are per-instance. Two replicas run every job twice. The fix is a distributed lock, and it should not exist until there is a second replica — it is marked with a ponytail: comment in SchedulerExtension.