- Implement tasks/cloud_analysis_tasks.py: sync Celery bridge to async processing - Broker payload contains IDs only — credentials decrypted in worker (T-14-10) - _TransientError and _ClassificationRetry sentinels for bounded retry (max=3) - Auth errors are terminal (not retried); MaxRetriesExceeded writes failure status - celery_app.py: add cloud_analysis_tasks route and import registration - Add 6 contract tests: task registration, broker payload shape, queue routing, bounded retries, sentinel pattern, connection-not-found graceful drop
64 lines
2.4 KiB
Python
64 lines
2.4 KiB
Python
"""
|
|
Celery application factory for DocuVault.
|
|
|
|
Kept deliberately minimal to avoid circular imports (Pitfall 7 from RESEARCH.md):
|
|
- DO NOT import from config (triggers pydantic-settings env-loading side effects)
|
|
- DO NOT import from main or any FastAPI router module
|
|
- Only os + celery imported here
|
|
|
|
REDIS_URL is read directly from os.environ so that this module can be imported
|
|
safely by the Celery worker process without pulling in the FastAPI application
|
|
machinery.
|
|
"""
|
|
import os
|
|
from datetime import timedelta as _timedelta
|
|
|
|
from celery import Celery
|
|
from celery.schedules import crontab as _crontab
|
|
|
|
celery_app = Celery("docuvault")
|
|
|
|
# Broker + result backend — read REDIS_URL directly from env (not from config.settings)
|
|
_redis_url = os.environ.get("REDIS_URL", "redis://redis:6379/0")
|
|
celery_app.conf.broker_url = _redis_url
|
|
celery_app.conf.result_backend = _redis_url
|
|
|
|
# JSON-only serialization (safe default; avoids pickle deserialization risks)
|
|
celery_app.conf.task_serializer = "json"
|
|
celery_app.conf.result_serializer = "json"
|
|
celery_app.conf.accept_content = ["json"]
|
|
|
|
# Route document tasks to the dedicated `documents` queue;
|
|
# email tasks to the `email` queue (Phase 2 — D-03);
|
|
# audit tasks to the `documents` queue (D-17 — reuse documents worker)
|
|
celery_app.conf.task_routes = {
|
|
"tasks.document_tasks.*": {"queue": "documents"},
|
|
"tasks.email_tasks.*": {"queue": "email"},
|
|
"tasks.audit_tasks.*": {"queue": "documents"},
|
|
"tasks.cloud_tasks.*": {"queue": "documents"},
|
|
"tasks.cloud_analysis_tasks.*": {"queue": "documents"},
|
|
}
|
|
|
|
# Celery beat schedule:
|
|
# cleanup-abandoned-uploads — every 30 minutes (D-06)
|
|
# audit-log-daily-export — midnight UTC daily (D-17)
|
|
celery_app.conf.beat_schedule = {
|
|
"cleanup-abandoned-uploads": {
|
|
"task": "tasks.document_tasks.cleanup_abandoned_uploads",
|
|
"schedule": _timedelta(minutes=30),
|
|
},
|
|
"audit-log-daily-export": {
|
|
"task": "tasks.audit_tasks.audit_log_daily_export",
|
|
"schedule": _crontab(hour=0, minute=0),
|
|
},
|
|
}
|
|
celery_app.conf.timezone = "UTC"
|
|
|
|
# Explicitly import task modules — autodiscover_tasks(["tasks"]) looks for
|
|
# tasks.tasks (appends ".tasks") which doesn't exist in our structure.
|
|
import tasks.audit_tasks # noqa: F401, E402
|
|
import tasks.cloud_analysis_tasks # noqa: F401, E402
|
|
import tasks.cloud_tasks # noqa: F401, E402
|
|
import tasks.document_tasks # noqa: F401, E402
|
|
import tasks.email_tasks # noqa: F401, E402
|