SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
1 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%

[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 8 days ago (Sep 16, 2026) parent f5cd86d

2 changed files +77 −16

modified jobka/connectors/base.py +32 −16
@@ -5,7 +5,7 @@
5 5 # Fichier : jobka/connectors/base.py
6 6 # Rôle : Classe de base des connecteurs + backends de fetch (requests
7 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 10 from __future__ import annotations
11 11
@@ -204,25 +204,41 @@ if not getattr(BaseConnector, "_KA_RESILIENT_WRAPPED", False):
204 204 return url
205 205
206 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 215 timeout = getattr(self, "timeout", 30)
208 216 headers = kw.get("headers")
209 − try:
210 − return _ka_orig_get(self, url, **kw)
211 − except _ka_requests.HTTPError as exc:
212 − r = getattr(exc, "response", None)
213 − if r is not None and _kar.is_blocked(r):
214 − target = getattr(r, "url", None) or _ka_full_url(url, kw)
215 − better = _kar.escalate_if_blocked(
216 − r, target, timeout=timeout, headers=headers)
217 + last_exc = None
218 + for attempt in range(3):
219 + try:
220 + return _ka_orig_get(self, url, **kw)
221 + except _ka_requests.HTTPError as exc:
222 + r = getattr(exc, "response", None)
223 + if r is not None and _kar.is_blocked(r):
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 236 if better is not None and getattr(better, "status_code", 0) == 200:
218 237 return better
219 − raise
220 − except (_ka_requests.ConnectionError, _ka_requests.Timeout):
221 − better = _kar.escalate(_ka_full_url(url, kw),
222 − timeout=timeout, headers=headers)
223 − if better is not None and getattr(better, "status_code", 0) == 200:
224 − return better
225 − raise
238 + raise
239 + if attempt < 2:
240 + time.sleep(5 * (attempt + 1))
241 + raise last_exc
226 242
227 243 def _ka_get_resilient(self, url, *, render_js=False, country="ca", **kw):
228 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 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 165 def test_successfactors_sitemap_guard(monkeypatch):
121 166 """Une réponse 200 sans racine <rss>/<urlset> (maintenance, anti-bot,
122 167 corps vide — vu le 2026-09-13 06:14 sur alstom : found=0 unique contre
123 168