From f01b59f3902089a894c8f43416684834a03ebe78 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 30 Jun 2026 22:00:10 -0400 Subject: [PATCH] fix(agent): py3.10 startup crash + submit-path retry; pin agent ruff to py310 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent container (CUDA base, Python 3.10) crashed on startup with `NameError: name 'Config' is not defined` — an earlier `ruff --fix` unquoted the `from_env(cls) -> Config` self-reference, which is safe on CI's Python 3.14 (PEP 649 lazy annotations) but is evaluated at class-definition time on 3.10. CI lint/compile run on 3.14, so it slipped through. - config.py: `from __future__ import annotations` so the self-referential annotation is a string, never evaluated — works on 3.10 and every version. - agent/ruff.toml: pin the agent to `target-version = "py310"` (its real runtime) and inherit the root rules. Ruff now flags exactly this class as F821, so CI's lint lane catches it instead of shipping a broken image. (CI otherwise lints on 3.14, masking 3.10 issues.) - client.py: submit path now retries in-place. A dedicated session with a urllib3 Retry (connect/read/status, 0.5s backoff, 500/502/503/504, POST) so a momentary blip after the GPU work is done doesn't discard it and force a full re-download + recompute elsewhere. A duplicate submit after a lost response is a harmless 409 no-op. Lease/fetch keep the plain session + loop-level backoff. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa --- agent/fc_agent/app.py | 2 +- agent/fc_agent/client.py | 41 ++++++++++++++++++++++++++++++++-------- agent/fc_agent/config.py | 5 +++++ agent/ruff.toml | 7 +++++++ 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 agent/ruff.toml diff --git a/agent/fc_agent/app.py b/agent/fc_agent/app.py index f52435a..3a225e1 100644 --- a/agent/fc_agent/app.py +++ b/agent/fc_agent/app.py @@ -15,7 +15,7 @@ from .worker import Worker # Bump on every agent change. The page embeds this and /status reports it; the UI # warns to reload when they differ — so a stale browser-cached page can't be # mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.) -VERSION = "2026-06-30.5 · stop+poll+ci" +VERSION = "2026-06-30.6 · submit-retry+py310" logbuf.install() cfg = Config.from_env() diff --git a/agent/fc_agent/client.py b/agent/fc_agent/client.py index fb648a5..f226a08 100644 --- a/agent/fc_agent/client.py +++ b/agent/fc_agent/client.py @@ -5,19 +5,44 @@ bytes, all over HTTP with the bearer token. No DB/Redis. """ import requests from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry class FcClient: def __init__(self, base_url: str, token: str, agent_id: str): self.base = base_url.rstrip("/") self.agent_id = agent_id - self.s = requests.Session() - self.s.headers["Authorization"] = f"Bearer {token}" - # Many worker threads share this Session; the default pool (10) would + # Main session: NO in-request retry — lease/fetch are cheap to redo and + # the worker loop already backs off + re-leases on failure. (Auto-retrying + # a lease could double-claim a batch if a response is lost.) + self.s = self._session(token) + # Submit session: retry in-place, because by submit time the GPU work is + # already DONE — a momentary blip (dropped connection, gateway 5xx during + # a curator redeploy) must not throw that work away and force a full + # re-download + recompute on another agent. A duplicate submit after a + # lost response is harmless: the job is already closed, so it just returns + # 409 lease_invalid (a no-op). Idempotent enough to retry POST safely. + retry = Retry( + total=3, connect=3, read=3, status=3, + backoff_factor=0.5, # ~0.5s, 1s, 2s between tries + status_forcelist=(500, 502, 503, 504), # transient server/gateway + allowed_methods=frozenset({"POST"}), + raise_on_status=False, # let raise_for_status decide + ) + self._submit_s = self._session(token, retry) + + @staticmethod + def _session(token: str, retry: Retry | None = None) -> requests.Session: + s = requests.Session() + s.headers["Authorization"] = f"Bearer {token}" + # Many worker threads share a Session; the default pool (10) would # throttle them + spam "connection pool is full". Size it for the cap. - adapter = HTTPAdapter(pool_connections=64, pool_maxsize=64) - self.s.mount("http://", adapter) - self.s.mount("https://", adapter) + adapter = HTTPAdapter( + pool_connections=64, pool_maxsize=64, max_retries=retry or 0 + ) + s.mount("http://", adapter) + s.mount("https://", adapter) + return s def lease(self, batch_size: int) -> list[dict]: r = self.s.post( @@ -29,7 +54,7 @@ class FcClient: return r.json().get("jobs", []) def submit(self, job_id: int, regions: list[dict], replace_kinds: list[str]) -> dict: - r = self.s.post( + r = self._submit_s.post( f"{self.base}/api/gpu/jobs/submit", json={ "agent_id": self.agent_id, "job_id": job_id, @@ -42,7 +67,7 @@ class FcClient: def submit_embedding(self, job_id: int, embedding: list, version: str) -> dict: """Post a whole-image SigLIP embedding (the 'embed' task) → image_record.""" - r = self.s.post( + r = self._submit_s.post( f"{self.base}/api/gpu/jobs/submit_embedding", json={ "agent_id": self.agent_id, "job_id": job_id, diff --git a/agent/fc_agent/config.py b/agent/fc_agent/config.py index 059896d..42a7c75 100644 --- a/agent/fc_agent/config.py +++ b/agent/fc_agent/config.py @@ -1,4 +1,9 @@ """Agent config, all from env (the control container is configured at run).""" +# Lazy annotations so the `from_env(cls) -> Config` self-reference is a string, +# not evaluated at class-definition time — otherwise it NameErrors on the agent's +# Python 3.10 (CI lints on 3.14, where PEP 649 hides this). +from __future__ import annotations + import os from dataclasses import dataclass diff --git a/agent/ruff.toml b/agent/ruff.toml new file mode 100644 index 0000000..c796be5 --- /dev/null +++ b/agent/ruff.toml @@ -0,0 +1,7 @@ +# The agent runs on the CUDA base image's Python 3.10 — NOT the 3.14 that CI's +# ci-python image and the repo-root ruff.toml target. Pin the agent to py310 so +# ruff enforces 3.10 compatibility and never auto-applies a 3.11+/3.14-only fix +# (e.g. unquoting a self-referential annotation, which PEP 649 makes safe on 3.14 +# but NameErrors on 3.10). Inherit the root lint rules. +extend = "../ruff.toml" +target-version = "py310"