137 lines
8.1 KiB
Markdown
137 lines
8.1 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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 :
|
|
```python
|
|
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)
|