Spaces:
Runtime error
Runtime error
wilenPxs Claude Fable 5 commited on
Commit ·
d1dc8e5
1
Parent(s): 77c855f
Suivi de job robuste pendant les phases CPU (fix Reconnexion au suivi)
Browse files- Cause : serveur mono-process — la porte harnais (100 graines sympy)
monopolise le GIL plusieurs minutes, le proxy HF renvoie 502 et l UI
abandonnait le suivi apres ~1 min alors que le job continuait.
- Serveur : yield 1 ms entre les graines du harnais + commutation de threads
a 1 ms (setswitchinterval) — /api/jobs/<id> repond pendant le calcul.
- UI : le suivi n abandonne PLUS JAMAIS avant 45 min — backoff leger puis
mode lent 10 s avec message explicite (le traitement continue) ; recalage
automatique a la premiere reponse.
- UI : reprise du suivi apres rechargement de page (job id persiste en
localStorage, raccrochage automatique au job en cours).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- app/__init__.py +6 -0
- app/validation/harness.py +5 -0
- app/web/templates/index.html +38 -10
app/__init__.py
CHANGED
|
@@ -35,6 +35,12 @@ def create_app():
|
|
| 35 |
_setup_logging()
|
| 36 |
_load_env()
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
from flask import Flask
|
| 39 |
from app.config import TEMPLATES_DIR
|
| 40 |
from app.server import register_routes
|
|
|
|
| 35 |
_setup_logging()
|
| 36 |
_load_env()
|
| 37 |
|
| 38 |
+
# Serveur mono-process : le job (thread CPU-bound — harnais sympy) et les
|
| 39 |
+
# requêtes de suivi partagent le GIL. Commutation à 1 ms (défaut 5 ms) pour
|
| 40 |
+
# que /api/jobs/<id> réponde pendant les phases de calcul (coût ~négligeable).
|
| 41 |
+
import sys
|
| 42 |
+
sys.setswitchinterval(0.001)
|
| 43 |
+
|
| 44 |
from flask import Flask
|
| 45 |
from app.config import TEMPLATES_DIR
|
| 46 |
from app.server import register_routes
|
app/validation/harness.py
CHANGED
|
@@ -23,6 +23,7 @@ import logging
|
|
| 23 |
import random
|
| 24 |
import re
|
| 25 |
import sys
|
|
|
|
| 26 |
import traceback
|
| 27 |
import types
|
| 28 |
import warnings as _warnings
|
|
@@ -384,6 +385,10 @@ def validate_text(text: str, seeds: int = 100) -> dict:
|
|
| 384 |
|
| 385 |
for s in range(seeds):
|
| 386 |
random.seed(s)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
env: dict = {"rd": random, "random": random}
|
| 388 |
out: dict = {}
|
| 389 |
|
|
|
|
| 23 |
import random
|
| 24 |
import re
|
| 25 |
import sys
|
| 26 |
+
import time as _time
|
| 27 |
import traceback
|
| 28 |
import types
|
| 29 |
import warnings as _warnings
|
|
|
|
| 385 |
|
| 386 |
for s in range(seeds):
|
| 387 |
random.seed(s)
|
| 388 |
+
# Céder la main entre les graines : en contexte serveur (mono-process),
|
| 389 |
+
# 100 graines de sympy monopolisent le GIL et rendent le suivi de job
|
| 390 |
+
# injoignable (proxy HF → 502) — 1 ms/graine rétablit la réactivité.
|
| 391 |
+
_time.sleep(0.001)
|
| 392 |
env: dict = {"rd": random, "random": random}
|
| 393 |
out: dict = {}
|
| 394 |
|
app/web/templates/index.html
CHANGED
|
@@ -477,7 +477,10 @@
|
|
| 477 |
'use strict';
|
| 478 |
|
| 479 |
const POLL_MS = 1000;
|
| 480 |
-
const
|
|
|
|
|
|
|
|
|
|
| 481 |
let currentJobId = null;
|
| 482 |
let pollTimer = null;
|
| 483 |
let pollFailures = 0;
|
|
@@ -486,7 +489,20 @@ let loadedFiles = []; // [{filename, content}]
|
|
| 486 |
let jobResults = []; // résultats par fichier
|
| 487 |
let activeResultIdx = 0;
|
| 488 |
|
| 489 |
-
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
async function fetchModels() {
|
| 492 |
try {
|
|
@@ -863,6 +879,7 @@ async function run() {
|
|
| 863 |
throw new Error(err.error || `HTTP ${res.status}`);
|
| 864 |
}
|
| 865 |
currentJobId = (await res.json()).job_id;
|
|
|
|
| 866 |
poll();
|
| 867 |
} catch (e) {
|
| 868 |
setProgress("error", "Échec", "—");
|
|
@@ -875,6 +892,7 @@ async function run() {
|
|
| 875 |
function jobFinished() {
|
| 876 |
document.getElementById("run-btn").disabled = false;
|
| 877 |
document.getElementById("stop-btn").hidden = true;
|
|
|
|
| 878 |
}
|
| 879 |
|
| 880 |
/* Arrêt coopératif : le serveur stoppe au prochain point d'étape (l'appel
|
|
@@ -920,6 +938,7 @@ async function poll() {
|
|
| 920 |
}
|
| 921 |
|
| 922 |
pollFailures = 0; // réponse valide → on repart à zéro
|
|
|
|
| 923 |
jobResults = data.results || [];
|
| 924 |
if (data.status === "running") {
|
| 925 |
setProgress("running", data.step_label || "…", `${data.files_done}/${data.files_total}`);
|
|
@@ -950,26 +969,35 @@ async function poll() {
|
|
| 950 |
jobFinished();
|
| 951 |
}
|
| 952 |
} catch (e) {
|
| 953 |
-
// Erreur TRANSITOIRE
|
| 954 |
-
//
|
|
|
|
|
|
|
| 955 |
pollFailures++;
|
| 956 |
-
if (
|
|
|
|
| 957 |
setProgress("error", "Connexion interrompue", "—");
|
| 958 |
setStatus("error",
|
| 959 |
-
"Suivi
|
| 960 |
-
+ "
|
| 961 |
-
+ "pour le retrouver, ou relance avec moins de fichiers à la fois.");
|
| 962 |
jobFinished();
|
| 963 |
return;
|
| 964 |
}
|
| 965 |
-
|
| 966 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 967 |
}
|
| 968 |
}
|
| 969 |
|
| 970 |
function clearAll() {
|
| 971 |
if (pollTimer) clearTimeout(pollTimer);
|
| 972 |
currentJobId = null;
|
|
|
|
| 973 |
document.getElementById("stop-btn").hidden = true;
|
| 974 |
document.getElementById("run-btn").disabled = false;
|
| 975 |
loadedFiles = []; jobResults = []; activeResultIdx = 0;
|
|
|
|
| 477 |
'use strict';
|
| 478 |
|
| 479 |
const POLL_MS = 1000;
|
| 480 |
+
const POLL_SLOW_AFTER = 12; // au-delà : mode lent (10 s) — on n'abandonne PAS
|
| 481 |
+
const POLL_SLOW_MS = 10000;
|
| 482 |
+
const POLL_GIVEUP_MS = 45 * 60 * 1000; // plafond absolu de reconnexion (45 min)
|
| 483 |
+
let pollFirstFailureAt = null;
|
| 484 |
let currentJobId = null;
|
| 485 |
let pollTimer = null;
|
| 486 |
let pollFailures = 0;
|
|
|
|
| 489 |
let jobResults = []; // résultats par fichier
|
| 490 |
let activeResultIdx = 0;
|
| 491 |
|
| 492 |
+
document.addEventListener("DOMContentLoaded", () => {
|
| 493 |
+
fetchModels(); bindTabs();
|
| 494 |
+
// Reprise après rechargement : si un job était en cours, on se raccroche
|
| 495 |
+
// (le serveur le garde JOB_TTL=30 min après sa fin ; 404 → suivi perdu).
|
| 496 |
+
const saved = localStorage.getItem("pyxi_job_id");
|
| 497 |
+
if (saved) {
|
| 498 |
+
currentJobId = saved;
|
| 499 |
+
document.getElementById("run-btn").disabled = true;
|
| 500 |
+
const sb = document.getElementById("stop-btn");
|
| 501 |
+
sb.hidden = false; sb.disabled = false; sb.textContent = "⏹ Arrêter";
|
| 502 |
+
setProgress("running", "Reprise du suivi du job en cours…", "…");
|
| 503 |
+
poll();
|
| 504 |
+
}
|
| 505 |
+
});
|
| 506 |
|
| 507 |
async function fetchModels() {
|
| 508 |
try {
|
|
|
|
| 879 |
throw new Error(err.error || `HTTP ${res.status}`);
|
| 880 |
}
|
| 881 |
currentJobId = (await res.json()).job_id;
|
| 882 |
+
localStorage.setItem("pyxi_job_id", currentJobId);
|
| 883 |
poll();
|
| 884 |
} catch (e) {
|
| 885 |
setProgress("error", "Échec", "—");
|
|
|
|
| 892 |
function jobFinished() {
|
| 893 |
document.getElementById("run-btn").disabled = false;
|
| 894 |
document.getElementById("stop-btn").hidden = true;
|
| 895 |
+
localStorage.removeItem("pyxi_job_id");
|
| 896 |
}
|
| 897 |
|
| 898 |
/* Arrêt coopératif : le serveur stoppe au prochain point d'étape (l'appel
|
|
|
|
| 938 |
}
|
| 939 |
|
| 940 |
pollFailures = 0; // réponse valide → on repart à zéro
|
| 941 |
+
pollFirstFailureAt = null;
|
| 942 |
jobResults = data.results || [];
|
| 943 |
if (data.status === "running") {
|
| 944 |
setProgress("running", data.step_label || "…", `${data.files_done}/${data.files_total}`);
|
|
|
|
| 969 |
jobFinished();
|
| 970 |
}
|
| 971 |
} catch (e) {
|
| 972 |
+
// Erreur TRANSITOIRE : pendant les phases de calcul (harnais 100 graines),
|
| 973 |
+
// le serveur mono-process peut être injoignable PLUSIEURS MINUTES — le job
|
| 974 |
+
// continue. On ne renonce donc JAMAIS avant le plafond absolu : backoff
|
| 975 |
+
// léger, puis mode lent (10 s) ; le suivi se recale à la 1re réponse.
|
| 976 |
pollFailures++;
|
| 977 |
+
if (!pollFirstFailureAt) pollFirstFailureAt = Date.now();
|
| 978 |
+
if (Date.now() - pollFirstFailureAt > POLL_GIVEUP_MS) {
|
| 979 |
setProgress("error", "Connexion interrompue", "—");
|
| 980 |
setStatus("error",
|
| 981 |
+
"Suivi injoignable depuis 45 min (" + e.message + "). "
|
| 982 |
+
+ "Recharge la page ; si le serveur a redémarré, relance le job.");
|
|
|
|
| 983 |
jobFinished();
|
| 984 |
return;
|
| 985 |
}
|
| 986 |
+
const slow = pollFailures >= POLL_SLOW_AFTER;
|
| 987 |
+
setStatus("warn",
|
| 988 |
+
slow
|
| 989 |
+
? "Serveur occupé (phase de calcul) — le traitement CONTINUE côté serveur, "
|
| 990 |
+
+ `reconnexion toutes les 10 s… (${Math.round((Date.now() - pollFirstFailureAt) / 1000)} s)`
|
| 991 |
+
: `Reconnexion au suivi… (${pollFailures})`);
|
| 992 |
+
pollTimer = setTimeout(
|
| 993 |
+
poll, slow ? POLL_SLOW_MS : Math.min(6000, POLL_MS * (1 + pollFailures)));
|
| 994 |
}
|
| 995 |
}
|
| 996 |
|
| 997 |
function clearAll() {
|
| 998 |
if (pollTimer) clearTimeout(pollTimer);
|
| 999 |
currentJobId = null;
|
| 1000 |
+
localStorage.removeItem("pyxi_job_id");
|
| 1001 |
document.getElementById("stop-btn").hidden = true;
|
| 1002 |
document.getElementById("run-btn").disabled = false;
|
| 1003 |
loadedFiles = []; jobResults = []; activeResultIdx = 0;
|