Background Tasks¶
The portal runs recurring and deferred work through a background task runner built on Procrastinate. This replaces the Kubernetes CronJobs previously used for scheduled maintenance, keeping the scheduling logic inside the application and its database.
Why Procrastinate¶
Procrastinate is a PostgreSQL-based task queue: it uses the portal's existing Postgres database as the broker, so there is no separate message broker (such as Redis or RabbitMQ) to run and operate. Tasks, their state, retries, and the periodic schedule all live in Postgres.
This means the task runner requires Postgres. The SQLite development fallback cannot run the worker, because Procrastinate's schema is raw Postgres SQL. On SQLite the Procrastinate migrations are skipped and no worker runs.
Anatomy of a task¶
Tasks live in servala/core/tasks.py. A periodic task combines two decorators:
@app.periodic(cron=settings.TASK_CLEANUP_EXPIRED_ORGANIZATIONS_CRON)
@app.task(name="core.cleanup_expired_organizations", retry=RETRY)
def cleanup_expired_organizations(timestamp=None):
call_command("cleanup_expired_organizations")
@app.task(name=...)registers the task under a stable name.@app.periodic(cron=...)tells the worker to schedule it on a cron expression.- The
timestamp=Noneargument is required. The periodic scheduler passes atimestampwhen it fires the task, but a manual.defer()call does not, so the argument must stay optional. Omitting the default breaks manual deferral.
Most tasks are thin wrappers that call an existing Django management command, so
the same logic can be run manually (for example with --dry-run) and on a
schedule.
Retries¶
All periodic tasks share a retry strategy that mirrors the restartPolicy:
OnFailure behaviour of the CronJobs they replaced: up to 5 attempts with
exponential backoff (roughly 10s, 100s, 17min, 2.8h). Transient failures are
retried rather than skipped until the next scheduled run.
Deferring work¶
Application code can queue asynchronous work outside the periodic schedule by deferring a task:
The worker picks it up and runs it. This is the mechanism for offloading slow or retriable work from a web request.
Running the worker¶
The worker is a long-running process:
In production the worker runs as a sidecar container in the portal pod, so it shares the portal's image, configuration, and database connection and scales alongside it. A single worker both executes deferred tasks and schedules the periodic ones; no separate scheduler process is needed.
Logging is required
Procrastinate logs through Python's logging. Without an explicit logging
configuration, worker activity and task failures are invisible. The portal
configures a console handler for the procrastinate logger in settings so
that worker output and failures are surfaced.
The periodic tasks¶
The portal currently schedules three periodic tasks. Their cron schedules come from settings and can be overridden per environment with the environment variables below (all times are UTC).
| Task | Default cron | Env override | Purpose |
|---|---|---|---|
core.clear_sessions |
42 2 * * 1 |
SERVALA_TASK_CLEAR_SESSIONS_CRON |
Weekly cleanup of expired Django sessions. |
core.send_expiration_reminders |
42 6 * * * |
SERVALA_TASK_EXPIRATION_REMINDERS_CRON |
Daily trial expiration reminder emails. |
core.cleanup_expired_organizations |
0 * * * * |
SERVALA_TASK_CLEANUP_EXPIRED_ORGANIZATIONS_CRON |
Hourly pause and teardown of expired trial organizations. |
The cleanup task runs hourly rather than daily so that pause and teardown land
close to the humanised trial action moment
(SERVALA_TRIAL_ACTION_HOUR); a once-a-day schedule could fire up to a day late.
Testing¶
The test suite does not run a real worker or Postgres queue. An autouse fixture provides an in-memory connector, so deferred tasks can be asserted on without a running worker.