Infrastructure outage handling (MCP/A4H down: wait, clean up, rerun), dashboard fix, budget limit 129

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Kral
2026-10-05 22:27:48 +02:00
parent 5c07026bbb
commit 9c753a37ff
5 changed files with 105 additions and 3 deletions

View File

@@ -361,7 +361,9 @@ def render(d):
next50 = (n_acc // 50 + 1) * 50 next50 = (n_acc // 50 + 1) * 50
est = d["panel_est"] est = d["panel_est"]
pcls = "good" if est < 50 else ("warn" if est < 57 else "bad") pcls = "good" if est < 50 else ("warn" if est < 57 else "bad")
eta = ("%.1f h (%s)" % (d["eta_h"], time.strftime("%a %H:%M", time.localtime(d["now"] + d["eta_h"] * 3600)))) if d["eta_h"] else "n/a" eta = ("%.1f h (%s)" % (d["eta_h"], time.strftime("%a %H:%M", time.localtime(d["now"] + d["eta_h"] * 3600)))) if d["eta_h"] and alive else "n/a"
if not alive: # a stopped pipeline has no burn rate and no speed
d["burn_ledger_h"] = d["rate_runs_h"] = d["rate_acc_h"] = None
a30 = d["acc30"] a30 = d["acc30"]
a30s = "%.0f %%" % (100 * a30) if a30 is not None else "n/a (<30 runs)" a30s = "%.0f %%" % (100 * a30) if a30 is not None else "n/a (<30 runs)"
cost_per = d["run_cost"] / n_acc if n_acc else 0 cost_per = d["run_cost"] / n_acc if n_acc else 0

62
harness/infra.py Normal file
View File

@@ -0,0 +1,62 @@
"""Infrastructure outages: the MCP server (ADT in Eclipse, 127.0.0.1:3000) or A4H (localhost:50000) is not reachable.
On 2026-10-05 21:43 the MCP server refused connections for a short time; three runs got `Connection refused` and the
pipeline stopped because it counted three equal harness errors. An outage is not a model result and not a harness bug:
the run is cleaned up, the work waits until the services answer, and the same run is started again.
"""
import errno
import os
import socket
import time
import urllib.error
import urllib.request
MCP_URL = os.environ.get("MCP_URL", "http://127.0.0.1:3000/mcp")
A4H_URL = os.environ.get("A4H_URL", "http://localhost:50000").rstrip("/") + "/sap/public/ping"
def is_outage(exc):
"""True for a refused or lost connection to a local service (not for an HTTP error status)."""
if isinstance(exc, urllib.error.HTTPError):
return False
if isinstance(exc, urllib.error.URLError):
return is_outage(exc.reason) if isinstance(exc.reason, BaseException) else True
return isinstance(exc, (ConnectionError, socket.timeout, TimeoutError)) or \
(isinstance(exc, OSError) and exc.errno in (errno.ECONNREFUSED, errno.ECONNRESET, errno.EHOSTUNREACH, errno.EPIPE))
def _answers(url):
try:
urllib.request.urlopen(url, timeout=6)
return True
except urllib.error.HTTPError:
return True # 401 / 403 / 404: the server answers
except Exception: # noqa: BLE001
return False
def up():
return _answers(MCP_URL) and _answers(A4H_URL)
def wait_until_up(max_seconds=3600, poll=30, log=print):
"""Wait until MCP and A4H answer twice in a row (a restart shows a short flicker). False when the time is over."""
end, ok_in_row = time.time() + max_seconds, 0
while time.time() < end:
ok_in_row = ok_in_row + 1 if up() else 0
if ok_in_row >= 2:
return True
time.sleep(poll)
return False
def cleanup_prefix(prefix):
"""Delete the A4H objects of an interrupted run (best effort; returns the number of objects found)."""
from .mcp_client import McpClient
from .runner import DELETE_ORDER, Runner, delete_uris
with McpClient() as m:
objs = Runner("", "")._objects_with_prefix(m, prefix.upper())
objs.sort(key=lambda o: DELETE_ORDER.index(o["objectType"]) if o["objectType"] in DELETE_ORDER else 99)
if objs:
delete_uris([o["uri"] for o in objs])
return len(objs)

View File

@@ -23,8 +23,9 @@ import urllib.request
from .adt_client import load_env from .adt_client import load_env
from .agents import LlmAgent from .agents import LlmAgent
from . import mix from . import infra, mix
from .runner import Runner from .runner import Runner
from .task import prefix_for
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "train")) sys.path.insert(0, os.path.join(ROOT, "train"))
@@ -169,6 +170,21 @@ class Series:
try: try:
rep, run_dir = runner.run(item["task"], agent, run_no, teardown=True, tool_budget=SETTINGS["tool_call_budget"]) rep, run_dir = runner.run(item["task"], agent, run_no, teardown=True, tool_budget=SETTINGS["tool_call_budget"])
except Exception as e: # noqa: BLE001 a harness exception is not a model result except Exception as e: # noqa: BLE001 a harness exception is not a model result
if infra.is_outage(e): # MCP or A4H is down: wait, clean up, run the same task again (not a failure)
self.event("infra_outage", item, repr(e)[:150])
self.state["status"] = "paused"
self.save()
infra.wait_until_up(max_seconds=max(60, self.window_end() - now()))
try:
infra.cleanup_prefix(prefix_for(run_no, item["task"]))
except Exception: # noqa: BLE001
pass
for d in __import__("glob").glob(os.path.join(OUT, "runs", f"{run_no}_{item['task']}_*")):
self.park(d, "_paused")
if now() >= self.window_end():
return {"window_over": True}
restarts = min(restarts + 1, 9)
continue
self.event("harness_exception", item, repr(e)[:200]) self.event("harness_exception", item, repr(e)[:200])
return {"task": item["task"], "kind": item["kind"], "category": item["category"], "harness_error": repr(e)[:200]} return {"task": item["task"], "kind": item["kind"], "category": item["category"], "harness_error": repr(e)[:200]}
er = rep.get("end_reason") er = rep.get("end_reason")

View File

@@ -17,7 +17,9 @@ from concurrent.futures import ThreadPoolExecutor
from .adt_client import load_env from .adt_client import load_env
from .agents import LlmAgent from .agents import LlmAgent
from . import infra
from .ledger import BudgetExceeded, check_budget, spent from .ledger import BudgetExceeded, check_budget, spent
from .task import prefix_for
from .record import LEDGER_TO_USAGE from .record import LEDGER_TO_USAGE
from .runner import Runner from .runner import Runner
@@ -77,7 +79,25 @@ def one(task_id, attempt, stop):
t0 = time.time() t0 = time.time()
row = {"task": task_id, "attempt": attempt, "run": run_no, "model": MODEL} row = {"task": task_id, "attempt": attempt, "run": run_no, "model": MODEL}
try: try:
rep, run_dir = runner.run(task_id, agent, run_no, teardown=True, cds_calls=CDS_CALLS) for _try in range(4): # an outage of MCP or A4H is waited out and the same run starts again
try:
rep, run_dir = runner.run(task_id, agent, run_no, teardown=True, cds_calls=CDS_CALLS)
break
except Exception as e: # noqa: BLE001
if not infra.is_outage(e) or _try == 3:
raise
print(time.strftime("%F %T"), "infra outage at", task_id, "->", repr(e)[:120], "; waiting", flush=True)
append("outages.jsonl", {"t": time.time(), "task": task_id, "attempt": attempt, "error": repr(e)[:200]})
if not infra.wait_until_up():
raise
try:
infra.cleanup_prefix(prefix_for(run_no, task_id))
except Exception: # noqa: BLE001
pass
for d in glob.glob(os.path.join(OUT, f"{run_no}_{task_id}_*")):
os.makedirs(os.path.join(OUT, "_aborted"), exist_ok=True)
os.rename(d, os.path.join(OUT, "_aborted", os.path.basename(d) + "_" + str(int(time.time()))))
agent = LlmAgent(MODEL, loop_guard=3)
score = (rep.get("score") or {}).get("total") score = (rep.get("score") or {}).get("total")
rec = os.path.exists(os.path.join(run_dir, "record.json")) rec = os.path.exists(os.path.join(run_dir, "record.json"))
row.update(score=score, setup_failed=bool(rep.get("setup_failed")), end_reason=rep.get("end_reason"), row.update(score=score, setup_failed=bool(rep.get("setup_failed")), end_reason=rep.get("end_reason"),

View File

@@ -177,3 +177,5 @@ Training runs on HF Jobs with Unsloth, not on the Mac. No `mlx_lm` training.
- **Incident 19:25:** restarting the controller I started a second one by mistake (wrong `pgrep` pattern) and deleted the objects of a running run. Both controllers were stopped, leftovers cleaned, one controller runs. One table `Z4AJ50UB_PO_HEAD` kept a lock from the interrupted write (SM12 needed). Interrupting a write leaks the lock: never kill a controller during a run without checking. - **Incident 19:25:** restarting the controller I started a second one by mistake (wrong `pgrep` pattern) and deleted the objects of a running run. Both controllers were stopped, leftovers cleaned, one controller runs. One table `Z4AJ50UB_PO_HEAD` kept a lock from the interrupted write (SM12 needed). Interrupting a write leaks the lock: never kill a controller during a run without checking.
- 2026-10-05 21:31 budget correction 4: panel 50.00 at ledger 111.22; since 45.0 (100.62): 10.6 ledger / 5.0 usage = 2.12 (generation of new types and first runs). Guard recomputed with the pessimistic ratio 1.2: panel left 60 - 50 - 3.0 reserve = 7.0 usage x 1.2 = 8.4 ledger, guard 119.6, `BUDGET_LIMIT_USD` 128 (reserve 8). Only kinds below target run (Kral + Opus 2026-10-05). - 2026-10-05 21:31 budget correction 4: panel 50.00 at ledger 111.22; since 45.0 (100.62): 10.6 ledger / 5.0 usage = 2.12 (generation of new types and first runs). Guard recomputed with the pessimistic ratio 1.2: panel left 60 - 50 - 3.0 reserve = 7.0 usage x 1.2 = 8.4 ledger, guard 119.6, `BUDGET_LIMIT_USD` 128 (reserve 8). Only kinds below target run (Kral + Opus 2026-10-05).
- 2026-10-05 22:35 **pipeline stopped at 21:43 by an infrastructure outage, found 22:26 (my miss).** The MCP server (127.0.0.1:3000) refused connections for a short time; three runs got `URLError(ConnectionRefusedError)` and the pipeline counted three equal harness errors and stopped (rule). Not a model or harness bug: A4H was up (11 h), the MCP server answers again. New `harness/infra.py`: an outage of MCP or A4H is waited out (check every 30 s, two answers in a row), the run's objects are deleted, the same run starts again; it is not counted as an event or a failure (`runs/traj/outages.jsonl`). Same for series A (applies after a restart of `harness.localqwen`; the running series keeps the old code and would skip the task). The 3 interrupted runs were cleaned (11 objects) and get their second attempt. Budget: panel 50.84 at ledger 113.36 (2.54 ledger per usage in the last stretch); guard recomputed: panel left 9.16 - 3.0 reserve = 6.2 usage x 1.2 = 7.4 ledger, `BUDGET_LIMIT_USD` 129 (guard 121). Dashboard: a stopped pipeline shows no burn rate or ETA.