|
| 1 | +## Context |
| 2 | + |
| 3 | +La route `execute_pipeline` (routes.py) crée le job, génère le code, tente la location Vast (ou lance le subprocess local), puis `session.commit()` et `return job`. SQLAlchemy/SQLModel a `expire_on_commit=True` par défaut : après `commit()`, le `__dict__` de l'instance est vidé et les attributs ne sont rechargés que lors d'un accès (lazy) ou d'un `refresh()`. |
| 4 | + |
| 5 | +Reproduction prouvée : mode gpu + rent refusé (400 insufficient_credit) → `execute` répond `200 {}` (0 champ) alors que `get_job` (session.get) répond `200` avec les 11 champs. `session.refresh(job)` avant sérialisation → 11 champs. La branche locale « survivait » par accident : l'accès `job.id` après le commit (ligne `job_id = job.id`) rechargeait l'instance avant le return. |
| 6 | + |
| 7 | +Côté frontend : `useBlockRunner.onRun` appelle `pollJob(job.id)` sans vérifier `job.id` ni `job.status` → avec un job vide, `pollJob("undefined")` boucle sur `GET /api/jobs/undefined → 422` (le catch continue indéfiniment, seule la limite `tries > 40` arrête après 120 s), et `finishRun(build)` est appelé juste après → faux succès. |
| 8 | + |
| 9 | +## Goals / Non-Goals |
| 10 | + |
| 11 | +**Goals:** |
| 12 | +- `execute` retourne toujours un job sérialisé complet (id, status, error). |
| 13 | +- Le frontend affiche l'erreur du job (ex. « Location GPU Vast.ai impossible… ») immédiatement, sans polling ni faux succès. |
| 14 | +- Le polling s'arrête sur les erreurs permanentes (4xx). |
| 15 | + |
| 16 | +**Non-Goals:** |
| 17 | +- Changer le comportement de `get_session` globalement (`expire_on_commit=False`) — impact large, non nécessaire. |
| 18 | +- Modifier le message d'erreur backend existant (déjà en français et pertinent). |
| 19 | +- Gérer les erreurs GPU côté Vast (location) — hors périmètre, le backend les traduit déjà en job error. |
| 20 | + |
| 21 | +## Decisions |
| 22 | + |
| 23 | +### D1 — `session.refresh(job)` avant chaque return de `execute_pipeline` |
| 24 | +Les 3 retours (branche local, dummy gpu, dispatched gpu) rechargent l'instance depuis la DB avant le `return job` → la sérialisation FastAPI est complète. |
| 25 | +*Alternatives* : `expire_on_commit=False` sur la session (impact global, risque de lectures stale) ; retourner un dict construit manuellement (duplique le schéma). Refresh : localisé, standard. |
| 26 | + |
| 27 | +### D2 — Frontend : pas de polling sur job invalide ou déjà en erreur |
| 28 | +Dans `onRun`, après `executePipeline` : |
| 29 | +- `!job?.id` → console « statut de l'exécution indisponible » + `failRun`, return (aucun polling). |
| 30 | +- `job.status === 'error'` → console « Exécution en erreur : {job.error} » + `failRun`, return (le cas budget — affiché sans attendre le polling). |
| 31 | +- Sinon → `setLastJob(job)` + `pollJob(job.id)`. |
| 32 | +`failRun` met l'état d'échec global (running=false) — cohérent avec les autres chemins d'erreur. |
| 33 | + |
| 34 | +### D3 — `pollJob` : arrêt net sur 4xx |
| 35 | +Dans le `catch` de `pollJob` : si `err.response?.status` est un 4xx hors 429 → `clearInterval` + console « job introuvable/statut indisponible » (erreur permanente, jamais résolue). Réseau/5xx → comportement actuel (continuer, limite `tries > 40`). |
| 36 | + |
| 37 | +### D4 — Test de régression backend |
| 38 | +Test unitaire : `execute_pipeline` (mode gpu mocké — `launch_instance` force dummy) retourne un JSON avec `id` non vide et `status` (error). Le mock remplace `VastAI.launch_instance` pour éviter tout appel réseau réel. |
| 39 | + |
| 40 | +## Risks / Trade-offs |
| 41 | + |
| 42 | +- **Refresh = requête DB supplémentaire** par exécution — négligeable (1 SELECT par run). |
| 43 | +- **Job déjà en erreur au retour** : le frontend n'appelle plus jamais `getJobOutputs` pour ce job (l'erreur de location n'a pas de sorties) — comportement souhaité. |
| 44 | +- **4xx dans pollJob** : un 401/403 (clé expirée) arrêterait aussi le polling — acceptable (erreur permanente, pas de résolution sans action utilisateur). |
0 commit comments