Skip to content

Celery Tasks

The rota system uses Celery with Redis for asynchronous task processing. Long-running operations -- email delivery, database backups, notifications -- are offloaded from the request/response cycle to background workers.

Configuration

All Celery settings live in rota/settings/base.py under the CELERY_ namespace. The Celery app is created in tasks/celery_app.py.

Setting Value Notes
CELERY_BROKER_URL Built from REDIS_HOST, REDIS_PORT, etc. or CELERY_BROKER_URL env var Redis with optional auth
CELERY_RESULT_BACKEND Same as broker URL by default Stores task results
CELERY_ACCEPT_CONTENT ['json'] JSON only
CELERY_TASK_SERIALIZER 'json' --
CELERY_RESULT_SERIALIZER 'json' --
CELERY_TIMEZONE 'UTC' --
CELERY_TASK_TRACK_STARTED True Records when tasks begin
CELERY_TASK_TIME_LIMIT 1800 (30 min) Hard kill after 30 minutes
CELERY_WORKER_PREFETCH_MULTIPLIER 1 One task per worker at a time
CELERY_WORKER_MAX_TASKS_PER_CHILD 1000 Restarts worker process after 1000 tasks

Environment overrides (no Celery-specific overrides in dev.py or prod.py, but logging levels differ):

  • dev.py: celery logger at DEBUG.
  • prod.py: celery logger at WARNING.

App Bootstrap

The Celery app is instantiated in tasks/celery_app.py:

app = Celery('rota')
app.config_from_object('django.conf:settings', namespace='CELERY')

rota/celery.py re-exports the app so the standard CLI works:

celery -A rota worker -l info
celery -A rota beat -l info

Django is bootstrapped in the main process so AppConfig.ready() fires and registers @shared_task decorators. A worker_process_init signal re-initializes Django in each forked worker so the app registry survives the fork.

Task Registration

Tasks are registered through TasksConfig.ready() in tasks/apps.py, which explicitly imports the task modules:

  • tasks.backup_tasks
  • tasks.notification_tasks
  • tasks.rota_tasks

After Django setup, app.autodiscover_tasks() catches any remaining @shared_task definitions in other installed apps (e.g. tasks.email_tasks, tasks.shift_swap_tasks).

Task Modules

tasks/rota_tasks.py

Purpose: Rota generation entry points. Both tasks are deprecated -- the phase-based generation system in rota_generation/tasks is used instead.

Task Status Notes
generate_rota_task Deprecated Raises NotImplementedError. Use phase tasks or manage.py generate_rota --phase.
rebalance_rota_task Deprecated Raises NotImplementedError. Depended on the old RotaGenerator class.

tasks/email_tasks.py

Purpose: General-purpose email sending with exponential backoff retry. All tasks use bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_backoff_max=600, retry_jitter=True, and max_retries=5 (unless noted otherwise).

Task Recipients Description
send_email_with_retry Arbitrary Generic email send. Takes subject, message, from_email, recipient_list, html_message.
send_html_email Arbitrary Renders a Django template to HTML and sends. Takes template_name and context dict.
send_admin_email Admin users (filtered) Sends to Dashboard Admins group. Optional email_type filter (system_failures, admin_actions, operational) respects per-admin email preferences.
send_password_reset_email Single user Password reset email using allauth template. Takes user_email, reset_url, user_name.
send_task_failure_email Admins with system_failures pref Alerts admins when a Celery task fails.
send_rota_generation_failure_email Admins with system_failures pref Alerts admins when rota generation fails. max_retries=3.
send_invitation_email Single invitee Sends signup invitation with secure token link. Takes invitation_id and clinician_name.

Retry behavior: first retry ~1 s, then 2 s, 4 s, 8 s, up to a maximum of 600 s (10 min). Jitter randomises delays to prevent thundering herds.

tasks/notification_tasks.py

Purpose: Leave and shift change notifications. Tasks in this module do not use bind=True or auto-retry -- they use fail_silently=True and return bool instead.

Task Trigger Description
send_leave_request_submitted_admin_notification Clinician submits leave request Notifies admins with admin_actions preference.
send_leave_request_confirmation_email Clinician submits leave request Acknowledges receipt to the clinician.
send_leave_approval_email Admin approves leave Notifies clinician of approval. Delegates to send_email_with_retry.
send_leave_denial_email Admin denies leave Notifies clinician with denial reason. Delegates to send_email_with_retry.
send_shift_change_notification Rota updated for clinician Plain-text shift change notification. Delegates to send_email_with_retry.
send_batch_notifications Bulk operation Iterates a list of notification dicts (type: leave_approval, leave_denial, or shift_change) and dispatches each. Returns {success, failure, total} counts.

tasks/shift_swap_tasks.py

Purpose: Shift swap workflow emails. All tasks use bind=True, autoretry_for=(Exception,), exponential backoff, and max_retries=5 (unless noted otherwise).

Task Trigger Description
send_shift_swap_request_email Clinician initiates swap Notifies the target clinician about the incoming request.
send_shift_swap_accepted_email Target accepts swap Notifies the requester that the target accepted; notes admin approval is still needed.
send_shift_swap_rejected_email Target declines swap Notifies the requester with the rejection reason.
send_shift_swap_admin_notification_email Target accepts swap Notifies admins with admin_actions preference that a swap needs approval. max_retries=3.
send_shift_swap_outcome_email Admin approves/rejects swap Notifies both users of the final admin decision. Takes approved: bool.

Each task looks up the ShiftSwapRequest by swap_id with select_related to avoid extra queries. If the target or requester has no user account or email, the task returns True (not an error condition).

tasks/backup_tasks.py

Purpose: Scheduled database backup and cleanup. Uses bind=True, autoretry_for=(Exception,), exponential backoff, max_retries=3.

Task Schedule Description
daily_pg_dump_backup 3 AM daily (beat) Calls manage.py pg_dump_backup. On failure, dispatches send_backup_failure_email then retries with manual countdown (60 s, 120 s, 240 s).
send_backup_failure_email On backup failure Delegates to send_admin_email with email_type='system_failures'.
cleanup_old_backups Not scheduled (utility) Simple age-based cleanup with configurable retention_months (default 6).
cleanup_old_backups_task 4 AM daily (beat) Tiered retention: pg_dump (daily 30 d, weekly 12 w, monthly 12 m) plus JSON backups (6 months).

Periodic Tasks (Beat Schedule)

Defined in CELERY_BEAT_SCHEDULE in rota/settings/base.py:

Name Task Schedule
daily-pg-dump-backup tasks.backup_tasks.daily_pg_dump_backup crontab(hour=3, minute=0)
cleanup-old-backups tasks.backup_tasks.cleanup_old_backups_task crontab(hour=4, minute=0)

Management Command

manage.py run_celery wraps the Celery worker CLI for convenience.

uv run python manage.py run_celery [options]
Option Default Description
--loglevel, -l info Logging level (debug, info, warning, error, critical)
--concurrency, -c CPU count Number of worker processes
--pool, -P prefork Pool implementation (prefork, gevent, eventlet, solo, threads)
--queue, -Q celery Queue(s) to consume from
--purge off Purge all pending messages before starting
--beat, -B off Run the beat scheduler in the same process