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:
@@ -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,
|
||||
)
|
||||
@@ -18,6 +18,7 @@ DAG_IDS = [
|
||||
"historical_import",
|
||||
"mock_api_import",
|
||||
"derive",
|
||||
"retention",
|
||||
]
|
||||
TACHES = [
|
||||
("ml_train", "train"),
|
||||
@@ -27,6 +28,7 @@ TACHES = [
|
||||
("historical_import", "import_historical"),
|
||||
("mock_api_import", "import_mock_api"),
|
||||
("derive", "derive"),
|
||||
("retention", "archiver"),
|
||||
]
|
||||
|
||||
|
||||
@@ -228,6 +230,24 @@ def test_derive_never_retries_a_detected_drift(dagbag: DagBag) -> None:
|
||||
assert dagbag.dags["derive"].get_task("derive").retries == 0
|
||||
|
||||
|
||||
def test_retention_runs_nightly(dagbag: DagBag) -> None:
|
||||
# Entre `ml_score` (:00), `alertes` (:15) et `mock_api_import` (:45) : drop_chunks verrouille
|
||||
# reading, site et dataset jusqu'au COMMIT.
|
||||
assert dagbag.dags["retention"].timetable.expression == "20 3 * * *"
|
||||
|
||||
|
||||
def test_retention_calls_the_backend_retention_module(dagbag: DagBag) -> None:
|
||||
commande = dagbag.dags["retention"].get_task("archiver").bash_command
|
||||
|
||||
assert "app.etl.reading_retention" in commande
|
||||
|
||||
|
||||
def test_retention_runs_in_the_backend_environment(dagbag: DagBag) -> None:
|
||||
commande = dagbag.dags["retention"].get_task("archiver").bash_command
|
||||
|
||||
assert "/opt/backend" in commande
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("dag_id", "task_id"), TACHES)
|
||||
def test_tasks_never_resync_the_baked_environment(
|
||||
dagbag: DagBag, dag_id: str, task_id: str
|
||||
|
||||
Reference in New Issue
Block a user