Skip to content

Commit 32b53ee

Browse files
committed
Gate index-autometa-tables cron to prod to avoid prod/staging index race
1 parent 48f5db8 commit 32b53ee

3 files changed

Lines changed: 25 additions & 0 deletions

File tree

cron/index-autometa-tables/CRON.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ timeout: 3600
66

77
Recrée les index d'`autometa_tables_db` et rafraîchit les statistiques (`ANALYZE`). Les tables sont reconstruites périodiquement par le DAG `populate_matometa_db` de pilotage-airflow (`if_exists="replace"`), ce qui supprime les index ; ce cron les recrée après le chargement. La liste des index vit dans `cron.py` (`INDEXES`).
88

9+
Prod uniquement : prod et staging visent la **même** `autometa_tables_db`. Deux runs concurrents (un par environnement) font courir `CREATE INDEX IF NOT EXISTS` et provoquent des collisions `pg_class` (`UniqueViolation`). Le script ne s'exécute donc que si `AUTOMETA_ENV == "prod"` ; ailleurs il sort immédiatement.
10+
911
Tolérance aux pannes : chaque statement s'exécute en AUTOCOMMIT ; un échec isolé (table absente, colonne renommée) est logué sans bloquer les suivants. Le script raise en fin de run si au moins un statement a échoué — `web.cron` remonte `failure` à Sentry et Slack.
1012

1113
Si le DAG termine après l'heure du cron, les index de la journée ne réapparaissent qu'au run suivant (ou via un déclenchement manuel depuis `/cron`).

cron/index-autometa-tables/cron.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ def main() -> None:
106106
logger.info("AUTOMETA_TABLES_DATABASE_URL not configured; skipping")
107107
return
108108

109+
# Why: prod et staging partagent la même autometa_tables_db ; deux runs concurrents font
110+
# courir CREATE INDEX IF NOT EXISTS (collision pg_class). Seul prod entretient les index.
111+
if config.AUTOMETA_ENV != "prod":
112+
logger.info("AUTOMETA_ENV=%s (not prod); skipping to avoid racing prod on the shared DB", config.AUTOMETA_ENV)
113+
return
114+
109115
# Why: AUTOCOMMIT — chaque statement est indépendant, un échec n'avorte pas les suivants.
110116
engine = create_engine(
111117
config.AUTOMETA_TABLES_DATABASE_URL,

tests/test_index_autometa_tables.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,24 @@ def test_main_skips_without_database_url(mocker):
6767
create_engine.assert_not_called()
6868

6969

70+
@pytest.mark.parametrize(
71+
("env_value", "should_run"),
72+
[("prod", True), ("staging", False), ("live", False), ("dev", False)],
73+
)
74+
def test_main_runs_only_on_prod(mocker, env_value, should_run):
75+
mocker.patch.object(cron.config, "AUTOMETA_TABLES_DATABASE_URL", "postgresql://example")
76+
mocker.patch.object(cron.config, "AUTOMETA_ENV", env_value)
77+
engine, _ = make_engine(mocker)
78+
create_engine = mocker.patch.object(cron, "create_engine", return_value=engine)
79+
80+
cron.main()
81+
82+
assert create_engine.called is should_run
83+
84+
7085
def test_main_executes_all_statements(mocker):
7186
mocker.patch.object(cron.config, "AUTOMETA_TABLES_DATABASE_URL", "postgresql://example")
87+
mocker.patch.object(cron.config, "AUTOMETA_ENV", "prod")
7288
engine, conn = make_engine(mocker)
7389
mocker.patch.object(cron, "create_engine", return_value=engine)
7490

@@ -79,6 +95,7 @@ def test_main_executes_all_statements(mocker):
7995

8096
def test_main_continues_after_failures_then_raises(mocker):
8197
mocker.patch.object(cron.config, "AUTOMETA_TABLES_DATABASE_URL", "postgresql://example")
98+
mocker.patch.object(cron.config, "AUTOMETA_ENV", "prod")
8299

83100
def execute(clause):
84101
if '"candidats"' in str(clause) and "CREATE INDEX" in str(clause):

0 commit comments

Comments
 (0)