osm: miroirs Overpass fiabilisés + réessais multi-rondes
Incident du 2026-08-22 : les 4 miroirs sont tombés en même temps (kumi 500, overpass-api.de injoignable, private.coffee 502, osm.jp certificat TLS invalide) et le run hebdo a fini à 0 POI (retraits suspendus par le garde-fou). - osm.jp retiré (hostname mismatch permanent), maps.mail.ru ajouté (instance planète complète, seul miroir opérationnel pendant l incident) - 3 rondes de réessai sur l ensemble des miroirs (attente 0/90/300 s) avant d abandonner un type d établissement Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 changed file +43 −25
modified
restoka/connectors/osm.py
+43 −25
@@ -20,6 +20,7 @@ | ||
| 20 | 20 | from __future__ import annotations |
| 21 | 21 | |
| 22 | 22 | import sys |
| 23 | +import time | |
| 23 | 24 | |
| 24 | 25 | from ..hours import parse_opening_hours |
| 25 | 26 | from ..normalize import classify_cuisines |
@@ -32,9 +33,20 @@ QC_AREA = 3600061549 | ||
| 32 | 33 | MIRRORS = [ |
| 33 | 34 | "https://overpass.kumi.systems/api/interpreter", |
| 34 | 35 | "https://overpass-api.de/api/interpreter", |
| 36 | + "https://maps.mail.ru/osm/tools/overpass/api/interpreter", | |
| 35 | 37 | "https://overpass.private.coffee/api/interpreter", |
| 36 | − "https://overpass.osm.jp/api/interpreter", | |
| 37 | 38 | ] |
| 39 | +# NB : overpass.osm.jp RETIRÉ (2026-08-23) — certificat TLS invalide pour ce | |
| 40 | +# nom d'hôte (hostname mismatch permanent). maps.mail.ru AJOUTÉ : instance | |
| 41 | +# planète complète (vérifiée sur l'aire Québec), seul miroir à répondre le | |
| 42 | +# 2026-08-22 quand kumi/de/coffee sont tombés ensemble et que le run OSM a | |
| 43 | +# fini à 0 POI. | |
| 44 | + | |
| 45 | +# Réessais : les pannes Overpass (500/502, surcharge) sont le plus souvent | |
| 46 | +# transitoires — on refait des passes complètes sur les miroirs avec une | |
| 47 | +# attente croissante avant d'abandonner le type d'établissement. | |
| 48 | +RETRY_ROUNDS = 3 | |
| 49 | +RETRY_BACKOFF_SECONDS = (0, 90, 300) | |
| 38 | 50 | |
| 39 | 51 | # User-Agent DÉDIÉ aux miroirs Overpass. Vérifié le 2026-08-18 : |
| 40 | 52 | # overpass-api.de répond 406 Not Acceptable à tout UA « déguisé en |
@@ -141,30 +153,36 @@ class OsmConnector(BaseConnector): | ||
| 141 | 153 | f'out tags center;' |
| 142 | 154 | ) |
| 143 | 155 | last_exc: Exception | None = None |
| 144 | − for mirror in MIRRORS: | |
| 145 | − try: | |
| 146 | − resp = self.post(mirror, data={"data": query}, | |
| 147 | − headers={"User-Agent": OVERPASS_UA}) | |
| 148 | − data = resp.json() | |
| 149 | − # Overpass peut répondre 200 avec des résultats TRONQUÉS et un | |
| 150 | − # « remark » (timeout/mémoire) — traiter comme un échec de | |
| 151 | − # miroir plutôt que d'avaler une liste partielle (incident du | |
| 152 | − # run #2 : 6 213 POI au lieu de 13 531). | |
| 153 | − remark = (data.get("remark") or "").lower() | |
| 154 | − if "timed out" in remark or "error" in remark: | |
| 155 | − raise RuntimeError(f"réponse partielle : {remark[:200]}") | |
| 156 | − elements = data.get("elements") or [] | |
| 157 | − # 0 élément pour une requête à l'échelle de la PROVINCE est | |
| 158 | − # toujours implausible (chaque type compte des centaines de | |
| 159 | − # POI au QC) : miroir sans les données (extrait régional…) | |
| 160 | − if not elements: | |
| 161 | − raise RuntimeError("0 élément — miroir sans données " | |
| 162 | − "pour l'aire Québec") | |
| 163 | − return elements | |
| 164 | − except Exception as exc: # miroir surchargé -> suivant | |
| 165 | − last_exc = exc | |
| 166 | − print(f"[resto-ka] osm: {mirror} a échoué ({exc}), miroir suivant", | |
| 167 | − file=sys.stderr) | |
| 156 | + for round_no in range(RETRY_ROUNDS): | |
| 157 | + wait = RETRY_BACKOFF_SECONDS[min(round_no, len(RETRY_BACKOFF_SECONDS) - 1)] | |
| 158 | + if wait: | |
| 159 | + print(f"[resto-ka] osm: nouvelle passe sur les miroirs dans {wait}s " | |
| 160 | + f"(ronde {round_no + 1}/{RETRY_ROUNDS})", file=sys.stderr) | |
| 161 | + time.sleep(wait) | |
| 162 | + for mirror in MIRRORS: | |
| 163 | + try: | |
| 164 | + resp = self.post(mirror, data={"data": query}, | |
| 165 | + headers={"User-Agent": OVERPASS_UA}) | |
| 166 | + data = resp.json() | |
| 167 | + # Overpass peut répondre 200 avec des résultats TRONQUÉS et un | |
| 168 | + # « remark » (timeout/mémoire) — traiter comme un échec de | |
| 169 | + # miroir plutôt que d'avaler une liste partielle (incident du | |
| 170 | + # run #2 : 6 213 POI au lieu de 13 531). | |
| 171 | + remark = (data.get("remark") or "").lower() | |
| 172 | + if "timed out" in remark or "error" in remark: | |
| 173 | + raise RuntimeError(f"réponse partielle : {remark[:200]}") | |
| 174 | + elements = data.get("elements") or [] | |
| 175 | + # 0 élément pour une requête à l'échelle de la PROVINCE est | |
| 176 | + # toujours implausible (chaque type compte des centaines de | |
| 177 | + # POI au QC) : miroir sans les données (extrait régional…) | |
| 178 | + if not elements: | |
| 179 | + raise RuntimeError("0 élément — miroir sans données " | |
| 180 | + "pour l'aire Québec") | |
| 181 | + return elements | |
| 182 | + except Exception as exc: # miroir surchargé -> suivant | |
| 183 | + last_exc = exc | |
| 184 | + print(f"[resto-ka] osm: {mirror} a échoué ({exc}), miroir suivant", | |
| 185 | + file=sys.stderr) | |
| 168 | 186 | raise RuntimeError(f"tous les miroirs Overpass ont échoué : {last_exc}") |
| 169 | 187 | |
| 170 | 188 | # cuisine par défaut quand ni le tag cuisine ni le nom ne classent le POI |
| 171 | 189 | |