Recurring jobs
A Serverpod future call fires once. Making one recurring — surviving restarts
without stacking duplicates, reporting failures, and re-arming even after a
failed run — is the same fiddly boilerplate in every project, and it is easy to
get the last part wrong: forget to reschedule inside a finally and one bad run
silently kills the schedule forever.
DwRecurringFutureCall owns that whole lifecycle. A job declares only what
runs and how often:
class SessionClosureFutureCall extends DwRecurringFutureCall {
String get name => 'sessionClosure';
Duration get interval => const Duration(minutes: 1);
Future<void> run(Session session) => closeTimedOutSessions(session);
}
Then, once at server startup (after pod.start()), hand the jobs over. This
registers each handler and arms its first run:
await DwRecurringJobs.startAll(pod, [
SessionClosureFutureCall(),
DatabaseRetentionFutureCall(),
]);
That is the entire contract. You never touch Serverpod's future-call plumbing —
registerFutureCall, futureCallWithDelay, cancelFutureCall are all handled
behind the base class.
What the base class guarantees
- Re-arms after every run, including a failed one — the schedule survives a bad run instead of dying on it.
- No duplicate schedules on restart — the pending run is cancelled and
re-queued under the job's
name, so a redeploy doesn't stack a second copy. - Failures are reported, not swallowed — a throw from
rungoes todw.alerts(with the exception and stack trace) and is logged; the next run is still queued.
First run timing
By default the first run happens one interval after startup. Override
initialDelay to change that — for example, run once immediately on boot:
Duration get initialDelay => Duration.zero;
The one job the framework ships
DwOrphanedAuthKeyCleanup deletes auth keys whose user profile is gone. Register it with
your own:
await DwRecurringJobs.startAll(pod, [
DwOrphanedAuthKeyCleanup(), // hourly by default; pass `interval:` to change it
SessionClosureFutureCall(),
]);
It exists because an auth key has no expiry and DwAuthKey.userId cannot carry a foreign key —
the user profile table belongs to the app, so nothing cascades from it. An account deleted without
dw.auth!.revokeAuthKeys therefore leaves a token that works forever.
Which makes this the honest home for an aliveness check. Until 0.3.0 one ran by accident on the
hot path: reading the caller's id fetched the whole profile row, so a deleted profile read back as
"not signed in" — correct, and paid for with a database round trip on every request. Here the
same fact costs one statement per interval, and the price is a window: up to interval during
which a forgotten revocation still authenticates. Shorten it if that trade reads wrong for your
app.
The job only sees hard deletion. A ban or a soft delete leaves the row in place, and that session
ends only when you call revokeAuthKeys yourself.