b7151b3bc7
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>
152 lines
7.9 KiB
Markdown
152 lines
7.9 KiB
Markdown
# Roadmap
|
||
|
||
Travaux planifiés, par lot. Chaque lot est livrable indépendamment.
|
||
|
||
Constats issus de la session de diagnostic du **24/08/2026** (profil `LIMAGRAIN`,
|
||
host `10.255.255.2`, tenant `LIMAGRAI2512`), déclenchée par un rapport d'usage
|
||
d'une session Cowork. Toutes les anomalies ci-dessous ont été **reproduites**
|
||
contre le WMS réel — ce ne sont pas des hypothèses.
|
||
|
||
Les décisions actées vivent dans [DECISIONS.md](DECISIONS.md) ; ce fichier ne
|
||
contient que ce qui reste à faire.
|
||
|
||
---
|
||
|
||
## Lot 4 — Modèle de données et applications
|
||
|
||
Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
|
||
chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`.
|
||
|
||
### L4.1 (reliquat) — Explorer le contexte Metrics
|
||
|
||
L'exposition de `query_type` est livrée (D25). Reste l'investigation : le
|
||
contexte `Metrics` (`QueryType: 3`, `ApplicationMetricDataContext`) mérite une
|
||
exploration à part — c'est probablement là que vivent les données agrégées
|
||
produites par les jobs `MetricGatherer`. Livrable : un rapport, pas du code
|
||
(même phase d'investigation que L4.4).
|
||
|
||
### L4.3 — Identifier le MCP dans les logs du WMS
|
||
|
||
Les requêtes du MCP apparaissent dans les logs du WMS sous
|
||
`Execute error. Client: GNA` — le client OAuth partagé — donc indistinguables de
|
||
celles du vrai client GNA.
|
||
|
||
**La piste `ClientModule` est invalidée** (mesuré le 24/08/2026, lot 1) : le
|
||
champ est bien accepté par `QueryExecute` (pas d'erreur), mais il est **sans
|
||
effet observable**. Une requête en échec envoyée avec
|
||
`ClientModule: "MCP-WMS"` est tracée `Execute error. Client: GNA`, et ni
|
||
`MCP-WMS` ni `ClientModule` n'apparaissent nulle part dans
|
||
`ApplicationService.log` ni `HttpResponseTime.log`. Le `Client:` des logs vient
|
||
du client OAuth, pas du payload — le champ n'a donc **pas** été renseigné.
|
||
|
||
Piste restante (non vérifiée) : un client OAuth dédié au MCP côté EasySTS
|
||
changerait le `Client:` des logs, mais suppose une configuration côté WMS.
|
||
|
||
### L4.4 — Historique d'exécution des workflows par API
|
||
|
||
`ApplicationService` expose une API `WorkflowLog` que le MCP n'utilise pas :
|
||
|
||
| Endpoint | Usage |
|
||
|---|---|
|
||
| `GET /WorkflowLog/GetInstances?processDefinitionId=&skip=&take=&startDateFrom=&startDateTo=` | instances d'un workflow sur une plage de dates, filtrables par attribut |
|
||
| `GET /WorkflowLog/GetInstance?processId=` | une instance |
|
||
| `GET /WorkflowLog/GetLogs?processId=&skip=&take=&logDateFrom=&logDateTo=` | journal d'exécution d'une instance |
|
||
| `GET /WorkflowLog/Validate?applicationName=&processDefinitionId=` | validation d'une définition |
|
||
|
||
Endpoints joignables et fonctionnels — `Validate` renvoie
|
||
`{"Success":true,"ErrorMessage":null,"Warnings":[]}`. `GetInstances` répond `[]`
|
||
sur le workflow testé : à confirmer sur un workflow ayant réellement tourné, la
|
||
journalisation n'étant pas forcément active partout.
|
||
|
||
**Peut remettre en cause D16** (historique des shipment templates jugé hors de
|
||
portée faute de logs fichier) : si l'historique d'exécution est disponible par
|
||
API, la conclusion change. À vérifier avant d'écrire quoi que ce soit.
|
||
|
||
### L4.5 — Champs de `QueryExecute` inexploités
|
||
|
||
La référence de l'API documente des champs que le MCP n'envoie jamais :
|
||
|
||
| Champ | Intérêt |
|
||
|---|---|
|
||
| `Parameters` | requêtes **paramétrées** (dictionnaire `nom -> {TypeName, Value}`) — supprimerait toute concaténation de chaîne dans les filtres, et pourrait débloquer D13 (`Select`) |
|
||
| `CommandTimeout` | timeout par requête, au lieu du timeout HTTP global de 30 s |
|
||
| `QueryId` + `POST /QueryCancel` | annulation d'une requête longue |
|
||
| `POST /QueryExecuteStream` | résultats en flux — piste long terme pour les sorties volumineuses, au-delà du bornage signalé de D24 |
|
||
|
||
Autres endpoints jamais utilisés, à évaluer : `QueryEvents`, `QueryCommands`,
|
||
`QueryCorrelationEvents`, `QuerySnapshots` (event sourcing — utile en debug),
|
||
`GET /Metadata/Commands|Events|Aggregates` et leurs variantes `…All`,
|
||
`GET /configuration/applications` (liste les applications **avec leur version**,
|
||
plus riche que `/AD/api/Application/GetAll`), `GET /healthcheck?tenantCode=` et
|
||
`GET /ready?tenantCode=` (sondes de disponibilité, répondent 200).
|
||
|
||
---
|
||
|
||
## Lot 6 — Chargements paresseux sous concurrence
|
||
|
||
Mesuré le 25/08/2026 (révisions des lots 2 et 5) : sans aucune bascule de
|
||
profil, une rafale d'appels concurrents pendant un chargement paresseux
|
||
produit des résultats faux en silence — `search_workflows("CST_",
|
||
application: "CustomApp")` a répondu `count: 0` (contre 44 en séquentiel), et
|
||
une rafale au lot 2 a déclenché **6 chargements Metadata complets en
|
||
parallèle** (6 × `[EntityResolver] Cache expired or empty, fetching…`).
|
||
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.2 — Protéger l'invalidation contre les fetchs en vol
|
||
|
||
Un fetch parti avant `clearCache()`/`invalidateCache()` (bascule de profil,
|
||
D8) écrit son résultat **après** l'invalidation : cache repeuplé avec les
|
||
données de l'ancien tenant. Garde de génération (epoch) : un résultat issu
|
||
d'une génération antérieure est jeté, pas écrit.
|
||
|
||
### L6.3 — Ne pas encaisser un vide anormal
|
||
|
||
`fetchAllWorkflows` fait `response?.entities || []` puis met en cache le
|
||
résultat même vide, avec un timestamp valide : toute réponse transitoirement
|
||
anormale (forme inattendue sous concurrence) devient un **cache vide
|
||
empoisonné pour tout le TTL** — cause probable du `count: 0` mesuré. Une
|
||
forme sans `entities` doit lever ; un `entities: []` réel reste cachable
|
||
(des applications légitimement vides existent, ex. `SmartUI`).
|
||
|
||
---
|
||
|
||
## Écarté
|
||
|
||
| Proposition | Raison |
|
||
|---|---|
|
||
| Uniformiser les noms de paramètres (`entity_type` / `query` partout) | Casse les usages existants ; les alias de transition doublent la surface à maintenir. La cause réelle est traitée par L2.2. |
|
||
| Exposer un `indexStatus` sur `generic_search` | `TotalDocuments: 0` est déjà le signal. Le MCP n'a aucun moyen d'interroger l'état de l'index de recherche. |
|
||
| Outil dédié `get_query_syntax_help` | L'information doit se trouver dans le message d'erreur, là où elle est lue (L2.1), pas dans un outil qu'il faut penser à appeler. |
|
||
|
||
---
|
||
|
||
## Points ouverts (hors lots)
|
||
|
||
- **Profil `AD` : tenant introuvable** (mesuré le 24/08/2026). `npm test -- --all`
|
||
échoue 0/4 sur ce profil ; le STS de `10.255.255.2` répond
|
||
`400 {"error":"invalid_request","error_description":"Tenant not found"}` pour
|
||
le tenant `AD`. L'hôte et le STS fonctionnent (LIMAGRAIN, même hôte, passe
|
||
4/4) : c'est la valeur `AD_TENANT` du `.env` qui ne correspond plus à un
|
||
tenant existant. Correction côté propriétaire du dépôt (mettre à jour ou
|
||
retirer le profil) — pas un bug du code. Depuis L3.2, le corps de la réponse
|
||
du STS (`Tenant not found`) remonte dans les erreurs d'outils et du smoke
|
||
test.
|
||
- **Bascule de profil concurrente aux appels en vol** (mesuré le 25/08/2026,
|
||
révision du lot 2). Le serveur traite les `tools/call` **en concurrence** :
|
||
un `switch_wms_profile` émis pendant que des requêtes sont en vol les fait
|
||
partir sur le nouveau profil (observé : une requête destinée à EUROTRAFIC
|
||
exécutée sur l'hôte `10.255.255.2` après la bascule suivante). Conséquence du
|
||
singleton d'état global (D8). À traiter si un cas réel de mélange de profils
|
||
est observé (piste : sérialiser les `tools/call` ou figer le profil résolu au
|
||
début de chaque appel). La manifestation « caches » de la même concurrence
|
||
est traitée par le **lot 6** ci-dessus.
|
||
- **`select_expression`** : les projections via le paramètre `Select` provoquent
|
||
des erreurs de compilation côté serveur (D13). Irritant principal restant.
|
||
- **Déploiement SSH sur la VM** : l'exécutable est validé, la configuration SSH
|
||
reste à faire.
|
||
- **Historique des shipment templates** : hors de portée, les logs concernés
|
||
n'existent pas sur l'hôte joignable (D16).
|