feat(calendar): document outbox retention policy

This commit is contained in:
2026-07-20 17:06:19 +02:00
parent b5cfba666c
commit d1c033edc7
3 changed files with 31 additions and 6 deletions

View File

@@ -153,16 +153,24 @@ on throwaway databases.
| --- | --- | --- | | --- | --- | --- |
| `REDIS_URL` | `redis://redis:6379/0` | Celery broker/result backend when async workers are enabled. | | `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_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: Worker command:
```bash ```bash
python -m celery -A govoplan_core.celery_app:celery worker \ 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 --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 ### Storage
| Setting | Default | Notes | | Setting | Default | Notes |
@@ -275,7 +283,7 @@ the checked in `.env.example`. It runs:
- explicit `ENABLED_MODULES` - explicit `ENABLED_MODULES`
- explicit migrations and `--with-dev-data` bootstrap - explicit migrations and `--with-dev-data` bootstrap
- API via the module-aware devserver - 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 - WebUI through the Vite dev server
- durable local files under `runtime/production-like/files` - durable local files under `runtime/production-like/files`

View File

@@ -5,7 +5,7 @@ import json
import os import os
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Literal from typing import Literal
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from sqlalchemy.engine import make_url from sqlalchemy.engine import make_url
@@ -274,7 +274,8 @@ ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,aud
CELERY_ENABLED=true CELERY_ENABLED=true
REDIS_URL=redis://127.0.0.1:6379/0 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 CORS_ORIGINS=https://govoplan.example.org
AUTH_COOKIE_SECURE=true 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 GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan
REDIS_URL=redis://127.0.0.1:56379/0 REDIS_URL=redis://127.0.0.1:56379/0
CELERY_ENABLED=true 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 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 CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173

View File

@@ -3,11 +3,24 @@ from __future__ import annotations
import unittest import unittest
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from pydantic import ValidationError
from govoplan_core.core.install_config import env_template, validate_runtime_configuration from govoplan_core.core.install_config import env_template, validate_runtime_configuration
from govoplan_core.settings import Settings
class InstallConfigTests(unittest.TestCase): 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: def test_self_hosted_validation_reports_actionable_missing_settings(self) -> None:
result = validate_runtime_configuration({}, profile="self-hosted") 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("GOVOPLAN_INSTALL_PROFILE=self-hosted", self_hosted)
self.assertIn("MASTER_KEY_B64=<generate", self_hosted) self.assertIn("MASTER_KEY_B64=<generate", self_hosted)
self.assertIn("CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90", self_hosted)
self.assertIn("GOVOPLAN_INSTALL_PROFILE=production-like", production_like) self.assertIn("GOVOPLAN_INSTALL_PROFILE=production-like", production_like)
self.assertNotIn("MASTER_KEY_B64=<generate", production_like) self.assertNotIn("MASTER_KEY_B64=<generate", production_like)
self.assertIn("CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90", production_like)
if __name__ == "__main__": if __name__ == "__main__":