L6.1 : dedupliquer les chargements paresseux en vol (single-flight)

Le serveur traite les tools/call en concurrence. Les trois services a cache
chargent paresseusement sans se coordonner : le premier appelant qui trouve le
cache invalide lance le fetch, et tous ceux qui arrivent pendant ce fetch le
trouvent *encore* invalide et lancent le leur. Une rafale de 6 appels
identiques declenchait donc 6 chargements complets pour une seule cle.

Ce n'est pas qu'un gaspillage : la duplication surcharge l'API AD au point de
la faire echouer. Rafale mixte de 14 appels, avant correction — les 4 appels
EasyWMS (~4000 workflows) reviennent en erreur, les memes passent en
sequentiel :

  [Workflow] Error fetching workflows for "EasyWMS": POST
  https://10.255.255.2/AD/api/Workflow/GetByApplication failed (HTTP 500)
  fetching from API: 8 | EntityResolver Cache expired or empty: 3

Motif commun extrait dans src/services/single-flight.js — une Map de
promesses, pas de dependance externe. Une cle par entree de cache
(workflows::<app>, applications, <app>::<type>, metadata) : deux cles
distinctes se chargent toujours en parallele, aucun prechargement (D26
intact). La promesse est retiree au reglement, succes *ou* echec, pour qu'un
fetch en erreur ne reste pas coince.

Le log de fetch reste l'observable (un par chargement reel) ; les appelants
joints emettent une ligne distincte "Fetch already in flight ... joining it".

--- Verifications (LIMAGRAIN), rafales rejouees 3 fois ---

Phase 0, reproduction avant correction :
  6 x search_workflows CustomApp  -> count 44 x6, 'fetching from API' : 6
  6 x query_wms_entities Container -> 6 succes, 'EntityResolver] Cache
                                      expired or empty' : 6

Rafale de 6 search_workflows {"query":"CST_","application":"CustomApp"} :

  ===== RUN 1 =====            ===== RUN 2 =====            ===== RUN 3 =====
  id 10 success=true application=CustomApp count=44   (idem RUN 2 et RUN 3,
  id 11 success=true application=CustomApp count=44    les 6 reponses a 44)
  id 12 success=true application=CustomApp count=44
  id 13 success=true application=CustomApp count=44
  id 14 success=true application=CustomApp count=44
  id 15 success=true application=CustomApp count=44
  -- 'fetching from API' : 1  | 'joining it' : 5   [RUN 1]
  -- 'fetching from API' : 1  | 'joining it' : 5   [RUN 2]
  -- 'fetching from API' : 1  | 'joining it' : 5   [RUN 3]

Rafale de 6 query_wms_entities {"entity_type":"Container","limit":1} :

  RUN 1/2/3 : id 10..15 success=true count=1 (6/6)
  -- 'EntityResolver] Cache expired or empty' : 1 | joins : 5 | GET
     Metadata/Entities : 5   [identique RUN 1, RUN 2, RUN 3]
  (avant : 6 chargements, soit 30 GET Metadata)

Rafale mixte EasyWMS + CustomApp (3 + 3) — un fetch par application :

  RUN 1/2/3 : CustomApp count=44 x3, EasyWMS count=50 x3
  [Workflow] Cache expired or empty for "CustomApp", fetching from API...
  [Workflow] Cache expired or empty for "EasyWMS", fetching from API...
     total fetch=2 joins=4   [identique RUN 1, RUN 2, RUN 3]
  Plus aucun HTTP 500 : un seul fetch EasyWMS concurrent au lieu de 4.

Chemin sequentiel nominal, strictement inchange (driver sequentiel) :

  [search_workflows] success=true application=EasyWMS count=50 len=14220
  [search_workflows] success=true application=EasyWMS count=50 len=14220
  --- fetch=1 cached=1 joins=0

Liberation de la Map sur echec (test direct, apiService.post substitue :
echoue au 1er appel, reussit ensuite) :

  [Workflow] Cache expired or empty for "TestApp", fetching from API...
  [Workflow] Fetch already in flight for "workflows::TestApp", joining it  (x2)
  --- rafale de 3 sur un fetch en echec :
    appelant 0/1/2: rejected - Failed to fetch workflows ... panne reseau simulee
    appels reseau reels: 1 (attendu 1 : les 3 partagent le meme fetch)
    cache pose ? {} (attendu {} : rien en cache sur echec)
  --- appel suivant (la Map doit avoir ete liberee) :
    resultat: 1 workflow(s), appels reseau cumules: 2

Baseline : tools/list 23, resources/list 6 ; npm test 4/4 exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Arthur Ria
2026-08-25 15:43:56 +02:00
parent 8a631f5ea4
commit b7151b3bc7
7 changed files with 173 additions and 10 deletions
-6
View File
@@ -95,12 +95,6 @@ Rejoués en séquentiel, les mêmes appels sont corrects.
Trois défauts structurels dans les services à cache (`workflow-service`,
`ad-service`, `entity-resolver`) :
### L6.1 — Dédupliquer les fetchs en vol
Aucun service ne mémorise la promesse de chargement en cours : N appelants
concurrents sur la même clé déclenchent N chaînes de fetch complètes.
Single-flight par clé de cache (promesse partagée, libérée au règlement).
### L6.2 — Protéger l'invalidation contre les fetchs en vol
Un fetch parti avant `clearCache()`/`invalidateCache()` (bascule de profil,