SPB Git

spb/job-ka

Public

[ka6] fix connecteur talan: relance des 5xx passagers dans le GET résilient — le statut stale vient d'un 502 Bad Gateway unique de api.smartrecruiters.com (page 2, offset=100) au sync de 03:13 le 2026-09-16, qui avortait tout le sync : contrairement au POST (relances 5xx ajoutées après l'incident jci/Workday du 2026-08-27), le GET résilient de base.py ne retentait pas les 5xx transitoires hors statuts anti-bot (502 absent de BLOCK_STATUS → pas d'escalade, pas de relance, raise immédiat). L'API est saine (vérifié live : 200 sur offset=0 et 100, totalFound=466, filtre QC → ~20) — hoquet serveur ponctuel. Correctif générique dans _ka_resilient_get : 3 tentatives directes avec backoff sur 5xx non-bloquant (même logique que _ka_resilient_post), escalade proxy inchangée pour 403/429/503/52x, 4xx remontés sans relance. Test unitaire ajouté (502×2 puis 200 → survit en 3 appels ; 502 persistant → 3 tentatives puis HTTPError ; 404 → aucune relance). Sync réel rejoué : found=20 (médiane exacte), ok. 106 tests pytest verts. pm2 restart job-ka-sync OK, site 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 16, 2026) parent f5cd86d Browse files

2 changed files +77 −16

modified jobka/connectors/base.py +32 −16
@@ -5,7 +5,7 @@
5 # Fichier : jobka/connectors/base.py 5 # Fichier : jobka/connectors/base.py
6 # Rôle : Classe de base des connecteurs + backends de fetch (requests 6 # Rôle : Classe de base des connecteurs + backends de fetch (requests
7 # direct, Scrapfly pour les pages carrières JS / derrière anti-bot) 7 # direct, Scrapfly pour les pages carrières JS / derrière anti-bot)
8−# Créé : 2026-08-17 Modifié : 2026-09-09 8+# Créé : 2026-08-17 Modifié : 2026-09-16
9 # ============================================================================= 9 # =============================================================================
10 from __future__ import annotations 10 from __future__ import annotations
11 11
@@ -204,25 +204,41 @@ if not getattr(BaseConnector, "_KA_RESILIENT_WRAPPED", False):
204 return url 204 return url
205 205
206 def _ka_resilient_get(self, url, **kw): 206 def _ka_resilient_get(self, url, **kw):
207+ """GET avec escalade anti-bot + relances sur 5xx passager.
208+
209+ Un 5xx transitoire hors statuts anti-bot (ex. 502 unique de
210+ api.smartrecruiters.com sur la page 2 de talan le 2026-09-16, retour
211+ à la normale immédiat) ne doit pas avorter tout le sync de la source
212+ — même logique que _ka_resilient_post. Les statuts anti-bot
213+ (403/429/503/52x) partent en escalade proxy comme avant, les 4xx
214+ remontent tels quels (rejouer à l'identique ne les débloque pas)."""
207 timeout = getattr(self, "timeout", 30) 215 timeout = getattr(self, "timeout", 30)
208 headers = kw.get("headers") 216 headers = kw.get("headers")
209− try: 217+ last_exc = None
210− return _ka_orig_get(self, url, **kw) 218+ for attempt in range(3):
211− except _ka_requests.HTTPError as exc: 219+ try:
212− r = getattr(exc, "response", None) 220+ return _ka_orig_get(self, url, **kw)
213− if r is not None and _kar.is_blocked(r): 221+ except _ka_requests.HTTPError as exc:
214− target = getattr(r, "url", None) or _ka_full_url(url, kw) 222+ r = getattr(exc, "response", None)
215− better = _kar.escalate_if_blocked( 223+ if r is not None and _kar.is_blocked(r):
216− r, target, timeout=timeout, headers=headers) 224+ target = getattr(r, "url", None) or _ka_full_url(url, kw)
225+ better = _kar.escalate_if_blocked(
226+ r, target, timeout=timeout, headers=headers)
227+ if better is not None and getattr(better, "status_code", 0) == 200:
228+ return better
229+ raise
230+ if r is None or r.status_code < 500:
231+ raise
232+ last_exc = exc
233+ except (_ka_requests.ConnectionError, _ka_requests.Timeout):
234+ better = _kar.escalate(_ka_full_url(url, kw),
235+ timeout=timeout, headers=headers)
217 if better is not None and getattr(better, "status_code", 0) == 200: 236 if better is not None and getattr(better, "status_code", 0) == 200:
218 return better 237 return better
219− raise 238+ raise
220− except (_ka_requests.ConnectionError, _ka_requests.Timeout): 239+ if attempt < 2:
221− better = _kar.escalate(_ka_full_url(url, kw), 240+ time.sleep(5 * (attempt + 1))
222− timeout=timeout, headers=headers) 241+ raise last_exc
223− if better is not None and getattr(better, "status_code", 0) == 200:
224− return better
225− raise
226 242
227 def _ka_get_resilient(self, url, *, render_js=False, country="ca", **kw): 243 def _ka_get_resilient(self, url, *, render_js=False, country="ca", **kw):
228 """Fetch anti-bot explicite : force la chaîne de secours au besoin. 244 """Fetch anti-bot explicite : force la chaîne de secours au besoin.
modified tests/test_connectors.py +45 −0
@@ -117,6 +117,51 @@ def test_njoyn_listing_retries_network_errors(monkeypatch):
117 conn2._fetch_listing() 117 conn2._fetch_listing()
118 118
119 119
120+def test_get_retries_transient_5xx(monkeypatch):
121+ """Un 5xx passager hors statuts anti-bot (502 unique de
122+ api.smartrecruiters.com sur la page 2 de talan le 2026-09-16, retour à la
123+ normale immédiat) doit être relancé en direct par le GET résilient au lieu
124+ d'avorter tout le sync — même logique que le POST (jci 2026-08-27). Après
125+ 3 échecs l'exception remonte ; un 4xx remonte immédiatement sans relance."""
126+ import requests
127+
128+ from jobka.connectors.base import BaseConnector
129+
130+ monkeypatch.setattr("jobka.connectors.base.time.sleep", lambda s: None)
131+
132+ def make_conn(statuses):
133+ conn = BaseConnector()
134+ calls = {"n": 0}
135+
136+ def fake_get(url, timeout=None, **kw):
137+ resp = requests.Response()
138+ resp.status_code = statuses[min(calls["n"], len(statuses) - 1)]
139+ resp.url = url
140+ resp._content = b"ok"
141+ calls["n"] += 1
142+ return resp
143+
144+ monkeypatch.setattr(conn.session, "get", fake_get)
145+ return conn, calls
146+
147+ # 502 ×2 puis 200 : le sync survit au hoquet serveur
148+ conn, calls = make_conn([502, 502, 200])
149+ resp = conn.get("https://api.example.com/postings")
150+ assert resp.status_code == 200 and calls["n"] == 3
151+
152+ # 502 persistant : 3 tentatives puis l'erreur remonte
153+ conn2, calls2 = make_conn([502])
154+ with pytest.raises(requests.HTTPError):
155+ conn2.get("https://api.example.com/postings")
156+ assert calls2["n"] == 3
157+
158+ # 404 (erreur métier) : aucune relance
159+ conn3, calls3 = make_conn([404])
160+ with pytest.raises(requests.HTTPError):
161+ conn3.get("https://api.example.com/postings")
162+ assert calls3["n"] == 1
163+
164+
120 def test_successfactors_sitemap_guard(monkeypatch): 165 def test_successfactors_sitemap_guard(monkeypatch):
121 """Une réponse 200 sans racine <rss>/<urlset> (maintenance, anti-bot, 166 """Une réponse 200 sans racine <rss>/<urlset> (maintenance, anti-bot,
122 corps vide — vu le 2026-09-13 06:14 sur alstom : found=0 unique contre 167 corps vide — vu le 2026-09-13 06:14 sur alstom : found=0 unique contre
123 168
Menu

Theme