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 theEventConsumerpattern) - Two schedule types:
- Interval: every N seconds (e.g.,
interval_seconds=3600) - Cron: cron expressions (e.g.,
"0 1 * * *") viacroniter - Priority queue (
heapq) sorted bynext_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 withregister(),start(),stop()PeriodicTask— dataclass: name, handler,interval_secondsCronTask— 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
EventConsumerthreading 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¶
EventConsumerunchanged — 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¶
-
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
-
APScheduler
- Mature Python scheduling library with persistent job stores
- Why not chosen: Complex configuration, heavy dependency, version churn between major releases
-
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
- Database-backed scheduling with
-
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
- Each service writes its own
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