diff --git a/docs/DEPLOYMENT_OPERATOR_GUIDE.md b/docs/DEPLOYMENT_OPERATOR_GUIDE.md index 9b1f3a2..6a6a1fd 100644 --- a/docs/DEPLOYMENT_OPERATOR_GUIDE.md +++ b/docs/DEPLOYMENT_OPERATOR_GUIDE.md @@ -153,16 +153,24 @@ on throwaway databases. | --- | --- | --- | | `REDIS_URL` | `redis://redis:6379/0` | Celery broker/result backend when async workers are enabled. | | `CELERY_ENABLED` | `false` | Local/dev can send synchronously. Production campaign delivery should run workers and set this to `true`. | -| `CELERY_QUEUES` | `send_email,append_sent,default` | Queue list expected by worker/process manager definitions. | +| `CELERY_QUEUES` | `send_email,append_sent,notifications,calendar,default` | Queue list expected by worker/process manager definitions. The Calendar queue drains durable external-calendar operations. | Worker command: ```bash python -m celery -A govoplan_core.celery_app:celery worker \ - --queues send_email,append_sent,default \ + --queues send_email,append_sent,notifications,calendar,default \ --loglevel INFO ``` +Run Celery beat as a separately supervised process. Its built-in one-minute +schedule recovers Calendar outbox rows left behind by broker failures, process +crashes, and expired worker leases: + +```bash +python -m celery -A govoplan_core.celery_app:celery beat --loglevel INFO +``` + ### Storage | Setting | Default | Notes | @@ -275,7 +283,7 @@ the checked in `.env.example`. It runs: - explicit `ENABLED_MODULES` - explicit migrations and `--with-dev-data` bootstrap - API via the module-aware devserver -- a Celery worker for `send_email,append_sent,default` +- a Celery worker for `send_email,append_sent,notifications,calendar,default` - WebUI through the Vite dev server - durable local files under `runtime/production-like/files` diff --git a/src/govoplan_core/core/install_config.py b/src/govoplan_core/core/install_config.py index 784cf40..b12d465 100644 --- a/src/govoplan_core/core/install_config.py +++ b/src/govoplan_core/core/install_config.py @@ -5,7 +5,7 @@ import json import os from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Literal +from typing import Literal from cryptography.fernet import Fernet from sqlalchemy.engine import make_url @@ -274,7 +274,8 @@ ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,aud CELERY_ENABLED=true REDIS_URL=redis://127.0.0.1:6379/0 -CELERY_QUEUES=send_email,append_sent,default +CELERY_QUEUES=send_email,append_sent,notifications,calendar,default +CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90 CORS_ORIGINS=https://govoplan.example.org AUTH_COOKIE_SECURE=true @@ -317,7 +318,8 @@ DATABASE_URL=postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan REDIS_URL=redis://127.0.0.1:56379/0 CELERY_ENABLED=true -CELERY_QUEUES=send_email,append_sent,default +CELERY_QUEUES=send_email,append_sent,notifications,calendar,default +CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90 ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,docs,ops CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173 diff --git a/tests/test_install_config.py b/tests/test_install_config.py index 5c9a479..0890cf4 100644 --- a/tests/test_install_config.py +++ b/tests/test_install_config.py @@ -3,11 +3,24 @@ from __future__ import annotations import unittest from cryptography.fernet import Fernet +from pydantic import ValidationError from govoplan_core.core.install_config import env_template, validate_runtime_configuration +from govoplan_core.settings import Settings class InstallConfigTests(unittest.TestCase): + def test_calendar_outbox_retention_setting_is_validated(self) -> None: + self.assertEqual(Settings().calendar_outbox_terminal_retention_days, 90) + self.assertEqual( + Settings( + CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS="0" + ).calendar_outbox_terminal_retention_days, + 0, + ) + with self.assertRaises(ValidationError): + Settings(CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS="-1") + def test_self_hosted_validation_reports_actionable_missing_settings(self) -> None: result = validate_runtime_configuration({}, profile="self-hosted") @@ -68,8 +81,10 @@ class InstallConfigTests(unittest.TestCase): self.assertIn("GOVOPLAN_INSTALL_PROFILE=self-hosted", self_hosted) self.assertIn("MASTER_KEY_B64=