Files
mcp-wms-api/ROADMAP.md
T
Arthur Ria b7151b3bc7 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>
2026-08-25 15:43:56 +02:00

152 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).