feat(apps): archive vers Garage puis supprime les chunks anciens de reading

Nouveau module app.etl.reading_retention : pour chaque chunk de `reading` entièrement plus
vieux que APP_READING_RETENTION_DAYS (1095 jours), export CSV gzip reproductible vers Garage
(SSE-C, sha256 en métadonnées), relecture et comparaison, puis drop_chunks ciblé sur ce seul
chunk dans une transaction dédiée. --dry-run. Réglages APP_S3_* optionnels, jamais exigés par
l'API. DAG Airflow `retention` quotidien à 03h20. 44 tests unitaires sans réseau ni base,
tests d'intégrité du DAG, vérification --help dans l'image Airflow en CI. Docs 40-data et
20-backend.

Closes #36
This commit is contained in:
Johan LEROY
2026-09-24 10:36:09 +02:00
parent 0462dd01ba
commit fa815f49b6
10 changed files with 1292 additions and 14 deletions
+45
View File
@@ -0,0 +1,45 @@
"""DAG de rétention de l'hypertable `reading` : export vers Garage puis suppression (issue #36).
La nuit, parce que `drop_chunks` pose un verrou exclusif sur `reading`, `site` et `dataset`
jusqu'au COMMIT : un chunk est supprimé dans une transaction courte, mais hors des heures où
l'API et les DAGs horaires écrivent. À :20 pour se glisser entre `ml_score` (à l'heure pile),
`alertes` (à :15) et `mock_api_import` (à :45), bien avant `derive` (05:30). Une reprise est sans
risque : le module est idempotent, un objet déjà exporté avec le même sha256 n'est pas réécrit et
un chunk déjà supprimé n'est plus éligible. La borne vient de `APP_READING_RETENTION_DAYS`
(1095 jours), posée par le compose sur `airflow-scheduler` avec les réglages `APP_S3_*`.
"""
from __future__ import annotations
from datetime import datetime, timedelta
from airflow.providers.standard.operators.bash import BashOperator
from airflow.sdk import DAG
# Le backend a son propre environnement uv dans l'image (ADR 0008). `--no-sync` et
# `env -u VIRTUAL_ENV` : cf. `ml_train.py`, même raisonnement.
COMMANDE_BACKEND = "cd /opt/backend && env -u VIRTUAL_ENV uv run --no-sync python -m"
TENTATIVES = 1
DELAI_ENTRE_TENTATIVES = timedelta(minutes=5)
PLAFOND = timedelta(minutes=20)
with DAG(
dag_id="retention",
description=(
"Exporte vers Garage puis supprime les chunks de reading plus vieux que la borne de "
"rétention (app.etl.reading_retention)."
),
schedule="20 3 * * *",
start_date=datetime(2026, 1, 1),
catchup=False,
max_active_runs=1,
tags=["etl", "retention"],
) as dag:
BashOperator(
task_id="archiver",
bash_command=f"{COMMANDE_BACKEND} app.etl.reading_retention",
retries=TENTATIVES,
retry_delay=DELAI_ENTRE_TENTATIVES,
execution_timeout=PLAFOND,
)