Skip to content

ADR-0017: Background Job Strategy

Status

PROPOSED

Date: 2026-03-02

Changelog

Date Revision Description Author
2026-03-02 1.0 Initial version souperk

Context

The platform service needs recurring background work: storage garbage collection (cleaning up DELETED files from MinIO), incomplete upload cleanup, and plugin health checks. The existing EventConsumer handles event-driven tasks only (outbox polling); it has no mechanism for periodic scheduling.

Services currently wire ad-hoc thread loops in worker/cli.py (e.g., the notifications delivery loop). There is no external scheduler in the stack — no Celery, Redis, or APScheduler. Operational simplicity is paramount for a small self-hosted platform.

Decision

Introduce a lightweight TaskScheduler in loryth-domain — a pure in-process thread runner with no database, no persistence, and no task queue.

Design

  • In-memory, code-configured: register callables with a schedule at startup
  • Daemon thread with start()/stop() lifecycle (mirrors the EventConsumer pattern)
  • Two schedule types:
  • Interval: every N seconds (e.g., interval_seconds=3600)
  • Cron: cron expressions (e.g., "0 1 * * *") via croniter
  • Priority queue (heapq) sorted by next_run_at; sleeps until next task is due
  • On failure: logs error, task runs again at next scheduled time (no retry, no dead-letter)
  • Graceful shutdown via condition variable (wakes sleeper on stop)

API

  • TaskScheduler — thread runner with register(), start(), stop()
  • PeriodicTask — dataclass: name, handler, interval_seconds
  • CronTask — dataclass: name, handler, cron expression
  • All in one module: task_scheduler.py

Worker Integration

The scheduler runs alongside EventConsumer in worker/cli.py; both are stopped on SIGINT/SIGTERM.

Platform Use Cases

Task Schedule Description
storage.gc Interval, hourly Garbage-collect DELETED files from MinIO
upload.timeout_cleanup Cron, daily 1am Clean up incomplete chunked uploads older than 24h
plugin.health_check Interval, 5 min Check health endpoint of all ENABLED plugins

Design Note — Bounded Task Handlers

Task handlers should be designed to run in bounded time (e.g., process at most N items per invocation) to avoid blocking subsequent tasks in the single-threaded scheduler.

Consequences

Positive

  • Unified mechanism for all recurring background work across services
  • Zero infrastructure dependencies — pure Python, in-process
  • Follows existing EventConsumer threading patterns familiar to contributors

Negative

  • Not crash-safe: if the process dies, scheduled work stops (acceptable for idempotent periodic tasks that catch up on next run)
  • Single-threaded per scheduler: a long-running task blocks others (mitigated by bounded handler design)

Neutral

  • EventConsumer unchanged — it continues to handle domain events via outbox polling
  • One new dependency: croniter (for cron expression parsing)
  • One-shot delayed tasks (e.g., 30-day space deletion grace period) use domain-level patterns (status field + periodic check, or outbox events), not the scheduler

Alternatives Considered

  1. Celery + Redis

    • Full-featured distributed task queue
    • Why not chosen: Adds Redis as infrastructure dependency; overkill for a small self-hosted platform with a handful of periodic tasks
  2. APScheduler

    • Mature Python scheduling library with persistent job stores
    • Why not chosen: Complex configuration, heavy dependency, version churn between major releases
  3. PostgreSQL-backed task queue

    • Database-backed scheduling with SELECT FOR UPDATE SKIP LOCKED
    • Why not chosen: Adds schema complexity for what are inherently idempotent periodic tasks; no need for persistence or distributed coordination
  4. Keep ad-hoc thread loops

    • Each service writes its own while True: sleep(N) loops
    • Why not chosen: Duplicated boilerplate, no cron support, inconsistent error handling across services

References

  • EventConsumer: server/libs/loryth-domain/src/loryth/domain/core/event_consumer.py
  • Notifications worker: server/services/loryth-notifications/src/loryth/notifications/worker/cli.py
  • croniter
  • ADR-0003: Event-Driven Architecture