[ka6] fix connecteur groupe_bellemare: faux positif de supervision (micro-panne réseau vers api.scrapfly.io) — durcissement du retry de _fetch_listing aux erreurs réseau (njoyn.py) — le statut stale vient d'un unique échec à 04:28 (ConnectTimeout 180 s vers l'API Scrapfly elle-même, seul échec de tout le cycle app : 1105 connecteurs ok), alors que tous les syncs précédents étaient stables à found=34 = médiane exacte ; Scrapfly répond en 0,3 s au moment du diagnostic et le sync rejoué donne found=34 en 8,4 s — connecteur INTACT. Vraie faiblesse corrigée : le retry durci du 2026-09-09 (3 essais, ré-amorçage de session) ne couvrait que les réponses vides/anti-bot ; une requests.ConnectTimeout levée par base.scrapfly() traversait la boucle et avortait le sync au premier essai. _fetch_listing attrape désormais requests.RequestException — même traitement qu'une page vide (nouvelle session Scrapfly = nouveau proxy, backoff 10-20 s), ré-élévation après 3 échecs. Test unitaire ajouté (retry après 2 ConnectTimeout simulés + ré-élévation après 3). Sync rejoué post-patch : found=34, ok. 102 tests pytest verts. pm2 restart job-ka-sync OK, site 200.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 changed files +54 −3
modified
jobka/connectors/njoyn.py
+15 −2
@@ -5,7 +5,7 @@ | ||
| 5 | 5 | # Fichier : jobka/connectors/njoyn.py |
| 6 | 6 | # Rôle : Classe de plateforme Njoyn (CGI) — très répandu au secteur public |
| 7 | 7 | # québécois (santé, villes, sociétés d'État). HTML serveur + jeton. |
| 8 | −# Créé : 2026-08-18 Modifié : 2026-09-09 | |
| 8 | +# Créé : 2026-08-18 Modifié : 2026-09-13 | |
| 9 | 9 | # ============================================================================= |
| 10 | 10 | """Plateforme Njoyn (CGI). |
| 11 | 11 | |
@@ -30,6 +30,8 @@ import os | ||
| 30 | 30 | import re |
| 31 | 31 | import time |
| 32 | 32 | |
| 33 | +import requests | |
| 34 | + | |
| 33 | 35 | from ..schema import JobPosting, clean_html |
| 34 | 36 | from .base import BaseConnector |
| 35 | 37 | |
@@ -312,7 +314,18 @@ class NjoynConnector(BaseConnector): | ||
| 312 | 314 | if attempt: |
| 313 | 315 | time.sleep(10 * attempt) |
| 314 | 316 | self._sf_session_name = None # nouvelle session = nouveau proxy |
| 315 | − page = self._html(self._url("joblisting")) | |
| 317 | + try: | |
| 318 | + page = self._html(self._url("joblisting")) | |
| 319 | + except requests.RequestException: | |
| 320 | + # Panne réseau transitoire vers api.scrapfly.io ou le site | |
| 321 | + # (constaté 2026-09-13 04:28 : ConnectTimeout 180 s vers l'API | |
| 322 | + # Scrapfly sur groupe_bellemare, seul échec de tout le cycle) : | |
| 323 | + # même traitement qu'une réponse vide — nouvelle session, | |
| 324 | + # backoff, retry — au lieu de laisser l'exception avorter le | |
| 325 | + # sync au premier essai. | |
| 326 | + if attempt == 2: | |
| 327 | + raise | |
| 328 | + continue | |
| 316 | 329 | if _MAINT_RE.search(page): |
| 317 | 330 | # Maintenance plateforme : inutile de retenter dans 10 s (la |
| 318 | 331 | # fenêtre dure des heures) — échec immédiat et explicite, les |
modified
tests/test_connectors.py
+39 −1
@@ -5,7 +5,7 @@ | ||
| 5 | 5 | # Fichier : tests/test_connectors.py |
| 6 | 6 | # Rôle : Tests de régression des connecteurs (rejeu des fixtures hors ligne) |
| 7 | 7 | # Ré-enregistrer après un changement de site : run.py record <source> |
| 8 | −# Créé : 2026-08-17 Modifié : 2026-08-17 | |
| 8 | +# Créé : 2026-08-17 Modifié : 2026-09-13 | |
| 9 | 9 | # ============================================================================= |
| 10 | 10 | import json |
| 11 | 11 | import re |
@@ -77,3 +77,41 @@ def test_finalize_is_safe(source_id, parsed): | ||
| 77 | 77 | assert re.fullmatch(r"\d{4}-\d{2}-\d{2}", f.date_posted) |
| 78 | 78 | if f.lat is not None: |
| 79 | 79 | assert 44.5 <= f.lat <= 63.0 and -80.0 <= f.lng <= -56.0 |
| 80 | + | |
| 81 | + | |
| 82 | +def test_njoyn_listing_retries_network_errors(monkeypatch): | |
| 83 | + """Une panne réseau transitoire vers l'API Scrapfly (ConnectTimeout, vu le | |
| 84 | + 2026-09-13 04:28 sur groupe_bellemare) doit engager le retry de | |
| 85 | + _fetch_listing — nouvelle session Scrapfly, backoff — au lieu d'avorter le | |
| 86 | + sync au premier essai ; après 3 échecs réseau, l'exception remonte.""" | |
| 87 | + import requests | |
| 88 | + | |
| 89 | + from jobka.connectors.njoyn import NjoynConnector | |
| 90 | + | |
| 91 | + monkeypatch.setattr("jobka.connectors.njoyn.time.sleep", lambda s: None) | |
| 92 | + | |
| 93 | + conn = NjoynConnector() | |
| 94 | + conn.source_id = "test_njoyn" | |
| 95 | + conn.USE_SCRAPFLY = True | |
| 96 | + calls = {"n": 0} | |
| 97 | + | |
| 98 | + def flaky_html(url): | |
| 99 | + calls["n"] += 1 | |
| 100 | + if calls["n"] < 3: | |
| 101 | + raise requests.ConnectTimeout("api.scrapfly.io injoignable (simulé)") | |
| 102 | + return "<form action='xweb.asp'></form>" | |
| 103 | + | |
| 104 | + monkeypatch.setattr(conn, "_html", flaky_html) | |
| 105 | + page, items = conn._fetch_listing() | |
| 106 | + assert calls["n"] == 3 and items == [] | |
| 107 | + | |
| 108 | + conn2 = NjoynConnector() | |
| 109 | + conn2.source_id = "test_njoyn" | |
| 110 | + conn2.USE_SCRAPFLY = True | |
| 111 | + | |
| 112 | + def dead_html(url): | |
| 113 | + raise requests.ConnectTimeout("api.scrapfly.io injoignable (simulé)") | |
| 114 | + | |
| 115 | + monkeypatch.setattr(conn2, "_html", dead_html) | |
| 116 | + with pytest.raises(requests.ConnectTimeout): | |
| 117 | + conn2._fetch_listing() | |
| 80 | 118 | |