Files
nas-runbooks/hermes-tt/download-attachments-phase1-bug-timeout.md
T

8.1 KiB

nyora-notes-tt / download_attachments_phase1.py -- timeout traite comme "vide" (07/08/2026)

Instance auteur : hermes-tt (diagnostic + fix Claude, session chantier PJ mail O365 2026) Date : 2026-08-07 Tags : nyora-notes-tt, hermes-mail-browser, owa, timeout, attachments Statut : valide


Probleme

Le sweep des PJ 2026 (download_attachments_phase1.py) degradait progressivement la session OWA partagee au fil du run (timeouts croissants), jusqu'a marquer des notes attachments_scanned=1 (traitees, 0 PJ) alors qu'elles n'avaient jamais ete reellement verifiees. Symptome observe en test direct : sur un lot de 5 notes, 2 ont declenche Erreur listing PJ index N : timed out sans que le run ne s'en trouve affecte -- les 5 notes finissaient quand meme marquees scannees.


Contexte et contraintes

download_attachments_phase1.py boucle sur les notes attachments_scanned=0, triees par folder_path. Pour chaque dossier non encore vu, un seul appel search_folder() (cache en memoire pour tout le run) resout l'index courant des messages ; list_attachments() va ensuite chercher la liste des PJ pour le message matche. Les deux appels HTTP passent par hermes_mail_client.py, qui pilote hermes-mail-browser (Edge reel via CDP sur session OWA partagee). Code source a /volume1/docker/nyora-notes-tt/, cuit dans l'image (COPY . . dans le Dockerfile, pas de volume mount sur les .py) -- toute modif exige rebuild + recreate.


Ce qui NE fonctionnait PAS

Mecanisme Comportement observe Raison
search_folder() : except HermesMailClientError generique dans process_note() Un timeout (60s) sur le dossier etait catche exactement comme un 404 (dossier reellement introuvable) -- folder_items_cache[folder_path] = [] HermesFolderNotFoundError (404) et HermesFolderAmbiguousError (409) heritent toutes deux de HermesMailClientError ; le except ne distinguait pas un vrai "dossier absent" d'un simple timeout de transport
Resultat vide mis en cache pour tout le run Chaque note suivante du meme dossier tombait directement en NO-MATCH -> attachments_scanned=1, sans meme retenter l'appel Le cache par folder_path ne stocke qu'une seule fois par run, sans TTL ni distinction succes/echec
HermesMailClient.list_attachments() : except Exception generique Un timeout sur le listing PJ d'une note precise renvoyait aussi [] (silencieux), note marquee "0 PJ" a tort Meme pattern que ci-dessus mais a l'echelle d'une note plutot que d'un dossier -- le client officiel ne fait pas la distinction 404 vs timeout, choix delibere pour son autre consommateur (backfill texte) mais dangereux tel quel pour ce script

Solution validee

_fetch_folder_or_none() (nouvelle fonction, remplace l'appel direct a client.search_folder() dans process_note) : distingue 404 ([], dossier reellement absent), 409 ambigu (None, necessite resolution manuelle), et toute autre erreur (HermesMailClientError generique = timeout/transport) avec une retentative apres RETRY_DELAY_ON_TRANSIENT_ERROR = 5s avant d'abandonner (None).

_list_attachments_or_none() (nouvelle fonction, bypasse client.list_attachments() avec le meme appel HTTP brut) : meme principe a l'echelle d'une note -- seul un vrai 404 devient [], tout le reste (timeout inclus) devient None.

Contrat None propage jusqu'a process_note() : None != attachments_scanned=1. Une note dont le dossier ou le listing PJ n'a pas pu etre verifie n'est jamais marquee scannee -- elle reste attachments_scanned=0 et sera retentee au prochain passage.

Circuit-breaker dans main() : MAX_CONSECUTIVE_TRANSIENT_FAILURES = 3. Des que 3 notes d'affilee renvoient None (session probablement en train de se degrader), le run s'arrete proprement (break) plutot que de continuer a empoisonner des dossiers pour rien. Le run logue vu/telecharge/skippe separement au lieu d'un seul total ambigu.

Filtre year_bucket parametrable : YEAR_BUCKET = os.environ.get("ATTACH_YEAR_BUCKET", "2026"). Le SQL de main() n'avait aucun filtre annee avant ce fix (bug distinct, meme session) -- WHERE attachments_scanned = 0 AND year_bucket = ? desormais, avec ATTACH_YEAR_BUCKET=ALL pour lever le filtre une fois 2026 termine et enchainer sur 2024/2025 sans retoucher le code.

Orchestration en lots (script wrapper separe, hors image) : lots de 20 notes, redemarrage de hermes-mail-browser (cache Edge vide, cf ci-dessous) entre chaque lot, pause de quelques minutes. Le run precedent avait deja mis en evidence qu'un profil Edge de hermes-mail-browser peut accumuler plusieurs Go de cache disque (2,9 Go constate le 07/08 avant nettoyage) sans impact fonctionnel direct connu, mais un redemarrage periodique repart sur un process Edge/CDP propre plutot que de laisser une session tourner des heures.


Addendum (meme session, ~1h plus tard) : le circuit-breaker fonctionnait mais se faisait piegier par 3 notes precises du dossier DR Zone Sud|Gabes (index 7, 8, 14), qui timeoutaient de facon reproductible sur list_attachments (pas transitoire -- 2 lots de suite, memes 3 notes, meme resultat, y compris apres redemarrage confirme healthy du container). Diagnostic direct avec un timeout large (90s) : la reponse arrive en ~23,5s et ne fait que 28 octets -- donc pas un volume de PJ important, plutot une latence fixe de rendu OWA pour ces messages precis, juste au-dessus de l'ancien seuil de 20s. Fix : LIST_ATTACHMENTS_TIMEOUT = 35 (constante dediee, au lieu du 20 en dur). Sweep relance apres ce fix : les 3 notes passent du premier coup, aucun [LIST-FAILED] sur le lot suivant, telechargements de PJ reelles confirmes (PDF recus, tailles coherentes). Lecon : un circuit-breaker sur echecs consecutifs protege bien contre la corruption de donnees, mais ne distingue pas a lui seul "degradation de session" de "seuil de timeout trop serre pour un sous-ensemble reproductible de messages" -- verifier les deux angles avant de conclure sur la cause quand le meme point de blocage revient identique.

Verification

docker exec nyora-notes-tt python3 download_attachments_phase1.py 5

Log attendu : [START] N notes a scanner (year_bucket=2026, limit=5), puis en fin de run [DONE] N notes vues, X PJ telechargees, Y non verifiees. Verifier en base que les notes marquees attachments_scanned=1 ont bien un contenu attachments ([] ou liste reelle), pas juste le flag pose :

import sqlean as sqlite3
c = sqlite3.connect('/app/nyora-notes-tt.db')
c.execute("SELECT id, attachments, attachments_scanned FROM notes WHERE folder_path=?", (folder,)).fetchall()

Pieges specifiques

  • Le bug touchait deux endpoints differents avec le meme pattern (search_folder a l'echelle dossier, list_attachments a l'echelle note) -- en chercher d'autres occurrences si hermes_mail_client.py est reutilise ailleurs avec un except Exception qui renvoie []/None sans distinguer 404 (vrai negatif) d'un timeout (inconnu).
  • Le contrat None vs [] doit rester strict dans tout code qui consomme ces deux fonctions -- None ne doit jamais etre traite comme une liste vide par accident (ex. fresh_attachments or [] effacerait la distinction et referait apparaitre le bug).
  • Rebuild : docker compose build en SSH synchrone depuis mcp-nas fait tomber la connexion (NAT TT) -- lancer en nohup + tail du log (nohup docker compose build > build.log 2>&1 < /dev/null &, bien rediriger stdin sinon la commande backgroundee bloque quand meme la session SSH parente).
  • Sauvegarde systematique de l'ancien script avant remplacement (download_attachments_phase1.py.bak-20260807) avant tout remplacement de fichier cuit dans l'image.

References

  • Runbook associe : hermes-tt/nyora-notes-tt-roadmap-pieces-jointes.md (architecture 2 phases)
  • Runbook associe : hermes-tt/mail-o365-16-dossiers-error-etat-session-owa.md (degradation de session OWA, angle different -- degradation cote navigation plutot que cote script PJ)
  • Note NyoraNotes : "Chantier PJ 2026 -- fix timeout search_folder/list_attachments + lots de 20" (07/08/2026)