Spaces:
Paused
Paused
Download scripts/walkthrough.py from piika919/forgesight: direct link, hf CLI and curl.
- Browser
- Download file 31.9 kB
-
https://huggingface.co/spaces/piika919/forgesight/resolve/main/scripts/walkthrough.py
- Command line
-
hf download hf://spaces/piika919/forgesight/scripts/walkthrough.py
-
curl -L -o walkthrough.py https://huggingface.co/spaces/piika919/forgesight/resolve/main/scripts/walkthrough.py
31.9 kB
| #!/usr/bin/env python | |
| """The recorded walkthrough (design §18), as a script that verifies itself. | |
| Design §18 is a timed demo with eight beats. Two are the presenter's words -- the | |
| problem in one sentence, and the limitations -- so they print as text and are not | |
| checked. The other six are claims about what the system does, and this script | |
| drives each one against the real pipeline: real weights, the real ledger, the | |
| real worker, the real reaper. Every beat asserts the state it claims to | |
| demonstrate, so "the demo script runs end to end" is something a gate can check | |
| rather than something a person has to watch for. | |
| Driven in-process rather than against a running compose stack, for two reasons. | |
| A demo needing five services up cannot be a gate. And the failure beat exists to | |
| kill a worker, which is awkward against a supervised container and trivial | |
| against a `Worker` object. | |
| uv run python scripts/walkthrough.py # every beat | |
| uv run python scripts/walkthrough.py --only 4 6 # a subset | |
| Exit code is non-zero if any beat fails to demonstrate what it claims. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import contextlib | |
| import io | |
| import json | |
| import os | |
| import sys | |
| import time | |
| from datetime import UTC, datetime, timedelta | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| from forgesight.db.migrate import migrate | |
| from forgesight.db.pool import get_pool, ph | |
| from forgesight.ledger.claims import Ledger, PredictionIn | |
| from forgesight.settings import configure, get_settings | |
| from forgesight.vision.types import ( | |
| Candidate, | |
| Detection, | |
| ModelArtifact, | |
| PreprocessProfile, | |
| RuntimeProfile, | |
| ) | |
| # Wide enough to reach any item under test, given the queue is global. | |
| WIDE_CLAIM = 256 | |
| NARRATIVE = { | |
| 0: ( | |
| "The problem, in one sentence:\n" | |
| ' "Is a cheaper CPU configuration of a layout model safe to ship,\n' | |
| ' and how would I know?"\n' | |
| "Laptop CPU only. Every page is generated and labelled SYNTHETIC." | |
| ), | |
| 8: ( | |
| "Limitations, stated plainly:\n" | |
| " - the synthetic ground truth uses this repo's box conventions, not\n" | |
| " DocLayNet's, so absolute mAP does not transfer to real documents\n" | |
| " - one host, CPU only\n" | |
| " - every number is valid for that host and date, and no other\n" | |
| " - the assumed memory model did not reproduce here; admission runs on\n" | |
| " measured peaks rather than a fitted line (ADR 0004)" | |
| ), | |
| } | |
| class Failed(Exception): | |
| """A beat could not even attempt its claim.""" | |
| class Walkthrough: | |
| def __init__(self, database_url: str, object_dir: Path | None = None) -> None: | |
| self.s = get_settings().model_copy(update={ | |
| "database_url": database_url, | |
| **({"objects_dir": object_dir} if object_dir else {}), | |
| }) | |
| # configure(), not just model_copy. seed_demo.main() calls get_pool() with | |
| # no argument, which reads the process-wide settings. With only | |
| # model_copy, the seed wrote to the default database while every check | |
| # read this script's own, so nothing matched and every beat failed on a | |
| # query timeout. | |
| configure(self.s) | |
| self.s.ensure_dirs() | |
| if database_url.startswith("sqlite:///"): | |
| path = Path(database_url.removeprefix("sqlite:///")) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| if path.exists(): | |
| path.unlink() | |
| self.pool = get_pool(self.s.database_url) | |
| migrate(self.pool, verbose=False) | |
| self.pool_ledger = Ledger(self.pool) | |
| self.p = ph(self.pool.dialect) | |
| self.failures: list[str] = [] | |
| self.t0 = time.perf_counter() | |
| self.ctx: dict = {} | |
| # -- reporting -------------------------------------------------------- | |
| def beat(self, n: int, title: str) -> None: | |
| print(f"\n{'=' * 74}\n[{n}] {title} (t+{time.perf_counter() - self.t0:5.1f}s)\n{'=' * 74}") | |
| def check(self, ok: bool, what: str, detail: str = "") -> bool: | |
| print(f" [{'ok ' if ok else 'FAIL'}] {what}" + (f" -- {detail}" if detail else "")) | |
| if not ok: | |
| self.failures.append(what) | |
| return ok | |
| def note(self, text: str) -> None: | |
| print(f" {text}") | |
| # -- plumbing --------------------------------------------------------- | |
| def client(self): | |
| import forgesight.api.deps as deps | |
| deps.set_pool(self.pool) | |
| from fastapi.testclient import TestClient | |
| from forgesight.api.app import create_app | |
| return TestClient(create_app()) | |
| def store(self): | |
| from forgesight.storage.object_store import FsObjectStore | |
| return FsObjectStore(self.s.objects_dir) | |
| def seed(self, pages: int, shadow: bool = False) -> dict: | |
| """Register candidates, seed a release and upload a batch. | |
| Delegates to seed_demo.py so the walkthrough and the manual demo start | |
| from exactly the same state; a walkthrough that built its own state would | |
| be demonstrating a setup nobody else runs. | |
| """ | |
| from seed_demo import main as seed_main | |
| argv = ["--pages", str(pages)] + (["--shadow"] if shadow else []) | |
| buf = io.StringIO() | |
| with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(io.StringIO()): | |
| rc = seed_main(argv) | |
| if rc != 0: | |
| raise Failed(f"seed_demo returned {rc}") | |
| return json.loads(buf.getvalue()) | |
| def drain(self, pool_name: str, max_rounds: int = 8) -> int: | |
| """Run the worker until its pool's queue is empty. Returns rounds used.""" | |
| from forgesight.worker.main import Worker | |
| rounds = 0 | |
| for _ in range(max_rounds): | |
| depth = self.scalar( | |
| f"SELECT COUNT(*) AS n FROM work_item WHERE state = {self.p} " | |
| f"AND pool = {self.p}", ("queued", pool_name)) | |
| if not depth: | |
| break | |
| asyncio.run(Worker(self.s, self.pool, self.store(), | |
| pool_name=pool_name).run(once=True)) | |
| rounds += 1 | |
| return rounds | |
| def scalar(self, sql: str, params: tuple = ()) -> int: | |
| with self.pool.connection() as conn: | |
| return int(conn.fetchone(sql, params)["n"]) | |
| # -- beat 1: upload and the timing split ------------------------------ | |
| def beat_1_upload(self) -> None: | |
| info = self.seed(pages=12, shadow=True) | |
| self.check(info["pages"] == 12, "a 12-page batch was accepted", | |
| f"batch {info['batch_id']}") | |
| # Drain, don't tick. `Worker.run(once=True)` claims at most claim_batch | |
| # (8) and returns, so two calls process 16 of the 24 items a 12-page | |
| # shadow batch creates and beat 1 could never pass. | |
| for pool_name in ("torch", "onnxruntime"): | |
| self.drain(pool_name) | |
| client = self.client() | |
| h = {"Authorization": f"Bearer {info['token']}"} | |
| batch = client.get(f"/v1/batches/{info['batch_id']}", headers=h).json() | |
| self.check(batch["status"] == "succeeded", "batch reached succeeded", | |
| f"counts={batch['counts']}") | |
| items = client.get(f"/v1/batches/{info['batch_id']}/items", headers=h).json() | |
| # Filter to succeeded items. `timings` is a required field on every row and | |
| # is present-but-null on an unfinished one, so indexing timed[0] picked a | |
| # uniformly random item and the beat failed roughly one run in three on | |
| # correct code. | |
| done = [i for i in items if i["state"] == "succeeded"] | |
| self.check(len(done) == len(items) and bool(items), | |
| "every item succeeded and recorded timings", | |
| f"{len(done)}/{len(items)} succeeded") | |
| if not done: | |
| return | |
| t = done[0]["timings"] | |
| qi, svc = t.get("queue_inclusive_ms"), t.get("service_ms") | |
| self.check(qi is not None and svc is not None, | |
| "both latency figures are reported", | |
| f"queue_inclusive_ms={qi}, service_ms={svc}") | |
| if qi is not None and svc is not None: | |
| # The point of reporting two numbers: if queue-inclusive omitted the | |
| # wait it would read smaller than service. | |
| self.check(qi >= svc - 1.0, | |
| "queue-inclusive is not smaller than service", | |
| f"{qi:.0f} ms >= {svc:.0f} ms") | |
| self.check((t.get("infer_ms") or 0) > 0, "inference time recorded", | |
| f"{t.get('infer_ms')} ms") | |
| self.note(f"sample split: {json.dumps(t)}") | |
| self.ctx = {**info, "headers": h, "items": items} | |
| # -- beat 2: boxes and provenance ------------------------------------- | |
| def beat_2_provenance(self) -> None: | |
| if not self.ctx: | |
| raise Failed("beat 1 must run first; --only needs its prerequisites") | |
| client = self.client() | |
| h = self.ctx["headers"] | |
| done = next((i for i in self.ctx["items"] if i["state"] == "succeeded"), None) | |
| if not self.check(done is not None, "a finished item to inspect"): | |
| return | |
| img = client.get(f"/v1/items/{done['id']}/image", headers=h, | |
| follow_redirects=False) | |
| self.check(img.status_code in (200, 302), "page image served to its owner", | |
| f"HTTP {img.status_code}") | |
| rel = client.get("/v1/releases", headers=h).json() | |
| self.check(bool(rel.get("active_release_id")), "an active release is pinned", | |
| str(rel.get("active_release_id"))) | |
| n = self.scalar( | |
| f"SELECT COUNT(*) AS n FROM prediction WHERE workspace_id = {self.p}", | |
| (self.ctx["workspace_id"],)) | |
| self.check(n > 0, "predictions persisted", f"{n} rows") | |
| # Provenance: every prediction names the artifact it came from, by sha. | |
| with self.pool.connection() as conn: | |
| row = conn.fetchone( | |
| f"SELECT COUNT(*) AS n FROM prediction p JOIN work_item w " | |
| f"ON p.work_item_id = w.id JOIN candidate c ON w.candidate_id = c.id " | |
| f"JOIN model_artifact a ON c.artifact_id = a.id " | |
| f"WHERE p.workspace_id = {self.p} AND a.sha256 <> ''", | |
| (self.ctx["workspace_id"],)) | |
| self.check(row["n"] > 0, "every prediction traces to a pinned artifact sha", | |
| f"{row['n']} traced") | |
| # -- beat 3: shadow run ----------------------------------------------- | |
| def beat_3_shadow(self) -> None: | |
| shadow = self.ctx.get("shadow_candidate") | |
| if not self.check(bool(shadow), "a shadow candidate was registered"): | |
| return | |
| n = self.scalar( | |
| f"SELECT COUNT(*) AS n FROM prediction WHERE workspace_id = {self.p} " | |
| f"AND candidate_id = {self.p}", (self.ctx["workspace_id"], shadow)) | |
| self.check(n > 0, "the shadow candidate produced its own predictions", | |
| f"{n} rows on the same pages") | |
| # A shadow run creates its own work items, one per candidate per page, so | |
| # asking "two candidates on one work_item" is the wrong question -- there | |
| # is only ever one candidate per item by construction. The diff view joins | |
| # on the *page*, so that is what has to be checked here. | |
| both = self.scalar( | |
| f"SELECT COUNT(*) AS n FROM (SELECT w.page_id FROM prediction p " | |
| f"JOIN work_item w ON p.work_item_id = w.id " | |
| f"WHERE p.workspace_id = {self.p} GROUP BY w.page_id " | |
| f"HAVING COUNT(DISTINCT p.candidate_id) > 1) t", | |
| (self.ctx["workspace_id"],)) | |
| self.check(both > 0, "some pages carry both the release and the shadow result", | |
| f"{both} pages have two candidates") | |
| # -- beat 4: gates, and the negative control -------------------------- | |
| def beat_4_gates(self) -> None: | |
| from forgesight.eval.gates import GATE_POLICY, judge, negative_control_blocked | |
| def cand(name: str) -> Candidate: | |
| return Candidate( | |
| name=name, | |
| artifact=ModelArtifact( | |
| name=name, path="/nonexistent", sha256=f"{name}-sha", | |
| format="safetensors", repo_id=f"example/{name}", | |
| revision="0" * 40, license="Apache-2.0"), | |
| preprocess=PreprocessProfile( | |
| resize="pil_bilinear", target=640, render_dpi=150, | |
| tile_long_side_px=2200, tile_aspect=1.6, tile_overlap=0.2, | |
| label_map_hash="lm", processor_class="example"), | |
| runtime=RuntimeProfile( | |
| runtime="torch", intra_op_threads=4, inter_op_threads=1, | |
| graph_opt_level="ORT_ENABLE_ALL", max_batch=8, batch_window_ms=25), | |
| score_threshold=0.3, label_map_hash="lm") | |
| metrics = {"map": 0.50, "map50": 0.50, | |
| "per_class_ap": {k: 0.5 for k in ("table", "picture", "text", "title")}} | |
| ref = cand("reference") | |
| good = judge(candidate=cand("candidate"), reference=ref, | |
| clean={"reference": metrics, "candidate": dict(metrics)}, | |
| shift={"reference": metrics, "candidate": dict(metrics)}, | |
| agreement=0.995, peak_bytes=None, | |
| budget_bytes=self.s.worker_mem_budget, provenance_ok=True) | |
| self.check(all(v["passed"] for v in good), "an identical candidate passes every gate", | |
| f"{len(good)} verdicts") | |
| self.check({v["gate"] for v in good} == set(GATE_POLICY), | |
| "all six gates were evaluated, not just the ones that pass") | |
| # The negative control: degraded, so it must be refused. A gate suite that | |
| # stopped blocking bad candidates is worse than one that never existed. | |
| bad = judge(candidate=cand("candidate"), reference=ref, | |
| clean={"reference": metrics, "candidate": {**metrics, "map": 0.40}}, | |
| shift={"reference": metrics, "candidate": {**metrics, "map": 0.40}}, | |
| agreement=None, peak_bytes=None, | |
| budget_bytes=self.s.worker_mem_budget, provenance_ok=False) | |
| by = {v["gate"]: v for v in bad} | |
| g1 = by["G1_quality_clean"] | |
| self.check(not g1["passed"], "the degraded 320-px control is blocked by G1", | |
| f"observed {g1['observed']} against threshold {g1['threshold']}") | |
| self.check(negative_control_blocked(bad), "the refusal is recognised as a negative control") | |
| # G4 (runtime parity) and G5 (memory fit) report "passed" when they are | |
| # not applicable -- there was no shadow comparison and no calibration to | |
| # judge. Asserting that *nothing* passes would be asserting a bug. What | |
| # must fail is everything that had evidence to judge on. | |
| must_fail = {"G1_quality_clean", "G2_quality_shift", "G6_provenance"} | |
| self.check(all(not by[g]["passed"] for g in must_fail), | |
| "every gate with evidence against it failed", | |
| ", ".join(sorted(must_fail))) | |
| # -- beat 5: promote and roll back ------------------------------------ | |
| def beat_5_promote_rollback(self) -> None: | |
| if not self.ctx: | |
| raise Failed("beat 1 must run first; --only needs its prerequisites") | |
| client = self.client() | |
| h = self.ctx["headers"] | |
| ws = self.ctx["workspace_id"] | |
| self._build_datasets(ws) | |
| cands = client.get("/v1/candidates", headers=h).json() | |
| if not self.check(len(cands) >= 2, "more than one candidate is available", | |
| f"{len(cands)} registered"): | |
| return | |
| # The release rides on a measurement, so start from the pinned reference, | |
| # which is the one candidate that has to pass against itself. | |
| ref = next((c for c in cands if c["name"].startswith("ref-")), None) | |
| if not self.check(ref is not None, "a pinned reference is registered"): | |
| return | |
| res = self._evaluate(ws, ref["id"]) | |
| self._assert_measured(res, ref["name"]) | |
| failed = [v["gate"] for v in res.verdicts if not v["passed"]] | |
| if not self.check(not failed, "the reference passes every gate on real data", | |
| ", ".join(failed) or "all six passed"): | |
| return | |
| before = client.get("/v1/releases", headers=h).json() | |
| r = client.post("/v1/releases/promote", headers=h, json={ | |
| "candidate_id": ref["id"], | |
| "expected_channel_version": before["version"], | |
| "reason": f"walkthrough: promote {ref['name']}", | |
| }) | |
| if not self.check(r.status_code == 200, "promote accepted at the current version", | |
| f"HTTP {r.status_code} {r.text[:120]}"): | |
| return | |
| after = client.get("/v1/releases", headers=h).json() | |
| self.check(after["version"] > before["version"], "channel version advanced", | |
| f"{before['version']} -> {after['version']}") | |
| stale = client.post("/v1/releases/promote", headers=h, json={ | |
| "candidate_id": ref["id"], | |
| "expected_channel_version": before["version"], | |
| "reason": "walkthrough: stale version must be refused", | |
| }) | |
| self.check(stale.status_code == 409, | |
| "a promote against a stale version is refused (409)", | |
| f"HTTP {stale.status_code}") | |
| rb = client.post("/v1/releases/rollback", headers=h, json={ | |
| "expected_channel_version": after["version"], | |
| "reason": "walkthrough: roll back", | |
| }) | |
| self.check(rb.status_code == 200, "rollback accepted", f"HTTP {rb.status_code}") | |
| hist = client.get("/v1/releases", headers=h).json().get("history", []) | |
| self.check(len(hist) >= 2, "history records both events", f"{len(hist)} releases") | |
| # The half of the story that matters: a candidate that fails a gate must be | |
| # refused, and the refusal must name the gate. An earlier version of this | |
| # beat picked "the first candidate that is not ref-" and promoted it after | |
| # writing its own `passed: true` verdict row -- which meant the demo | |
| # promoted whatever it was handed and called it a gate suite. | |
| other = next((c for c in cands | |
| if not c["name"].startswith("ref-") | |
| and "negctl" not in c["name"]), None) | |
| if other is None: | |
| self.note("no second candidate registered; skipped the refusal path") | |
| return | |
| res2 = self._evaluate(ws, other["id"]) | |
| self._assert_measured(res2, other["name"]) | |
| failed2 = [v["gate"] for v in res2.verdicts if not v["passed"]] | |
| if not failed2: | |
| self.note(f"{other['name']} passed all gates on this host; " | |
| "nothing to refuse") | |
| return | |
| self.check("negctl" not in other["name"], | |
| "the refused candidate is not being confused with the control", | |
| other["name"]) | |
| cur = client.get("/v1/releases", headers=h).json() | |
| r2 = client.post("/v1/releases/promote", headers=h, json={ | |
| "candidate_id": other["id"], | |
| "expected_channel_version": cur["version"], | |
| "reason": f"walkthrough: try to promote {other['name']} anyway", | |
| }) | |
| # FastAPI nests a dict detail one level down: {"detail": {"code": ...}}. | |
| body = r2.json() if "json" in r2.headers.get("content-type", "") else {} | |
| inner = body.get("detail") if isinstance(body, dict) else None | |
| inner = inner if isinstance(inner, dict) else {} | |
| code = inner.get("code") or "" | |
| named = [g for g in (inner.get("failed") or []) if g in failed2] | |
| self.check(r2.status_code == 422 and code == "gate_failed", | |
| "promote is refused for a candidate that failed a gate", | |
| f"HTTP {r2.status_code} code={code or 'none'}") | |
| self.check(bool(named), | |
| "the refusal names the gates that actually failed", | |
| ", ".join(named) or f"failed={failed2}") | |
| def _assert_measured(self, result, name: str) -> None: | |
| """Refuse to call it an evaluation unless metrics were really computed.""" | |
| clean = result.metrics.get("clean") or {} | |
| cand_map = (clean.get("candidate") or {}).get("map") | |
| ref_map = (clean.get("reference") or {}).get("map") | |
| self.check(cand_map is not None and ref_map is not None, | |
| f"{name}: real mAP was measured, not stubbed", | |
| f"candidate={cand_map}, reference={ref_map}") | |
| self.check(len(result.verdicts) == 6, | |
| f"{name}: the release carries six real verdicts", | |
| f"{len(result.verdicts)} verdicts") | |
| def _build_datasets(self, ws: str, limit: int = 6) -> None: | |
| """Build the clean and shift splits with ground truth, for real evaluation.""" | |
| from forgesight.eval import dataset as ds | |
| for name in ("synth-clean", "synth-scan"): | |
| if ds.load_truth(self.pool, ws, name, "test"): | |
| continue | |
| built = ds.build(name, self.s, self.store(), limit=limit) | |
| did = ds.persist(self.pool, ws, built) | |
| ds.persist_pages(self.pool, ws, did, built) | |
| self.note(f"built {name} v{built.semver} " | |
| f"({built.counts['pages']} pages, manifest={built.manifest_hash[:12]})") | |
| def _evaluate(self, ws: str, candidate_id: str): | |
| """Run the real evaluator and persist exactly what it returned. | |
| An earlier version of this beat inserted an evaluation_run row whose gates | |
| were all `passed: true` with `observed: 1.0`, then promoted off it. That is | |
| a fabricated result: the release history said a candidate had passed six | |
| gates, and nothing had been measured. It is the exact thing the gate suite | |
| exists to prevent, done by the demo. | |
| This runs forgesight.eval.runner.run_evaluation instead: it loads the | |
| calibration/clean/shift splits, picks the operating threshold on calib, | |
| scores both candidate and reference on clean and shift, and lets judge() | |
| decide. Whatever it says is what gets stored, including a failure. | |
| """ | |
| from forgesight.eval import dataset as ds | |
| from forgesight.eval.runner import run_evaluation | |
| from forgesight.ledger.claims import new_id | |
| with self.pool.write() as conn: | |
| rows = [dict(r) for r in conn.execute( | |
| f"SELECT * FROM candidate WHERE id = {self.p}", (candidate_id,))] | |
| if not rows: | |
| raise RuntimeError(f"candidate {candidate_id} is not registered") | |
| row = rows[0] | |
| # A missing dataset must fail the run, not skip a gate. | |
| for label, split in (("synth-clean", "test"), ("synth-scan", "test")): | |
| if not ds.load_truth(self.pool, ws, label, split): | |
| raise RuntimeError( | |
| f"dataset {label}/{split} is not built for {ws}; " | |
| "a missing dataset must fail the evaluation") | |
| started = time.monotonic() | |
| result = run_evaluation(row, self.s, self.pool, self.store()) | |
| cols = ("id", "workspace_id", "candidate_id", "status", "dataset_versions", | |
| "metrics", "gates", "code_sha", "env_manifest") | |
| metrics = dict(result.metrics) | |
| metrics["walkthrough_elapsed_s"] = round(time.monotonic() - started, 2) | |
| with self.pool.write() as conn: | |
| conn.execute( | |
| f"INSERT INTO evaluation_run({', '.join(cols)}) " | |
| f"VALUES ({','.join([self.p] * len(cols))})", | |
| (new_id("ev"), ws, candidate_id, "done", | |
| json.dumps(result.dataset_versions), json.dumps(metrics), | |
| json.dumps(result.verdicts), "walkthrough", | |
| json.dumps(result.env_manifest))) | |
| return result | |
| # -- beat 6: the worker dies ------------------------------------------ | |
| def beat_6_failure(self) -> None: | |
| from forgesight.ledger.reaper import Reaper | |
| info = self.seed(pages=4) | |
| ws = info["workspace_id"] | |
| ledger = self.pool_ledger | |
| # claim() is a global, oldest-first work queue: it is not scoped to a | |
| # workspace. Claiming n=4 here returned an earlier beat's leftovers, so the | |
| # "worker killed mid-batch" beat never touched the batch it had just | |
| # seeded -- it claimed beat 1's pages, while every count below was scoped | |
| # to `ws`. Take a wide slice, keep this beat's own items, and return the | |
| # rest: there is no release, so an item leaves a worker by completing, | |
| # failing, or having its lease reaped. | |
| everything = ledger.claim("torch", "doomed-worker", n=WIDE_CLAIM, lease_s=60) | |
| held = [it for it in everything if it.workspace_id == ws] | |
| for it in everything: | |
| if it.workspace_id != ws: | |
| ledger.fail(it, "walkthrough: not under test") | |
| if not self.check(len(held) == 4, "the seeded batch was claimed in full", | |
| f"{len(held)} of 4 items in {ws}"): | |
| return | |
| zombie = held[0] | |
| before = self.scalar(f"SELECT COUNT(*) AS n FROM prediction " | |
| f"WHERE workspace_id = {self.p}", (ws,)) | |
| # The lease lapses. Forced into the past rather than slept through: the | |
| # point is the fencing, and a sleep would be slower and no more | |
| # convincing. Scoped to the item ids this worker held -- expiring a whole | |
| # workspace once made the outcome depend on what a previous run had left | |
| # behind, and the reaper reported 0 against a database whose items were | |
| # already finished. | |
| past = datetime.now(UTC) - timedelta(seconds=120) | |
| ids = [it.id for it in held] | |
| with self.pool.write() as conn: | |
| conn.execute( | |
| f"UPDATE work_item SET lease_expires_at = {self.p} " | |
| f"WHERE id IN ({','.join([self.p] * len(ids))})", | |
| (past, *ids)) | |
| result = Reaper(self.pool, self.s).reap_expired_leases() | |
| self.check(result.requeued >= len(held), | |
| "the reaper requeued every abandoned item", | |
| f"{result.requeued} requeued of {len(held)} abandoned") | |
| # A replacement takes the work. This is what rotates the fencing token: | |
| # an expired lease on its own does not block a write, which is the whole | |
| # point of the token. An earlier version of this beat asserted fencing | |
| # without a re-claim and was asserting a fiction -- it completed | |
| # successfully and reported that as the zombie being fenced out. | |
| taken = ledger.claim("torch", "replacement-worker", n=WIDE_CLAIM, lease_s=60) | |
| live = [it for it in taken if it.workspace_id == ws] | |
| for it in taken: | |
| if it.workspace_id != ws: | |
| ledger.fail(it, "walkthrough: not under test") | |
| if not self.check(bool(live), "a replacement worker can claim the same work", | |
| f"{len(live)} items in {ws}"): | |
| return | |
| live_item = next((it for it in live if it.id == zombie.id), None) | |
| if not self.check(live_item is not None, | |
| "the replacement took over the zombie's own item", | |
| f"zombie={zombie.id}"): | |
| return | |
| for it in live: | |
| if it.id != zombie.id: | |
| ledger.fail(it, "walkthrough: not under test") | |
| self.check(live_item.fencing_token > zombie.fencing_token, | |
| "the replacement's fencing token is newer", | |
| f"{zombie.fencing_token} -> {live_item.fencing_token}") | |
| self.check(ledger.complete(live_item, self._one_detection()) is True, | |
| "the live worker's result is accepted") | |
| # Now the zombie wakes up. Every write it attempts must be refused. | |
| self.check(ledger.complete(zombie, self._one_detection()) is False, | |
| "the zombie's completion is refused") | |
| self.check(ledger.fail(zombie, "inference_failed") is False, | |
| "the zombie's failure report is refused") | |
| self.check(ledger.mark_cancelled(zombie) is False, | |
| "the zombie cannot cancel an item it no longer owns") | |
| after = self.scalar(f"SELECT COUNT(*) AS n FROM prediction " | |
| f"WHERE workspace_id = {self.p}", (ws,)) | |
| self.check(after == before + 1, "exactly one prediction exists: the live one", | |
| f"{before} -> {after}") | |
| def _one_detection(): | |
| return PredictionIn([Detection(class_id=0, label="text", score=0.9, | |
| box=(0.1, 0.1, 0.2, 0.2))]) | |
| # -- runner ----------------------------------------------------------- | |
| def run(self, only: list[int]) -> int: | |
| print("ForgeSight walkthrough -- design §18") | |
| print(f"database: {self.s.database_url}") | |
| beats = [ | |
| (0, "the problem, in one sentence", lambda: self._say(0)), | |
| (1, "upload 12 synthetic pages, timing split", self.beat_1_upload), | |
| (2, "boxes by class, and provenance", self.beat_2_provenance), | |
| (3, "shadow run on the same pages", self.beat_3_shadow), | |
| (4, "gate report, negative control blocked", self.beat_4_gates), | |
| (5, "promote, then roll back", self.beat_5_promote_rollback), | |
| (6, "worker killed mid-batch, reaper requeues, no duplicates", | |
| self.beat_6_failure), | |
| (8, "limitations", lambda: self._say(8)), | |
| ] | |
| titles = {0: "the problem, in one sentence", 1: "upload 12 synthetic pages, timing split", | |
| 2: "boxes by class, and provenance", 3: "shadow run on the same pages", | |
| 4: "gate report, negative control blocked", 5: "promote, then roll back", | |
| 6: "worker killed mid-batch, reaper requeues, no duplicates", | |
| 8: "limitations"} | |
| for n, _, fn in beats: | |
| if only and n not in only: | |
| continue | |
| self.beat(n, titles[n]) | |
| try: | |
| fn() | |
| except Exception as exc: # a beat that raises is a beat that failed | |
| self.check(False, f"beat {n} could not complete", | |
| f"{type(exc).__name__}: {exc}") | |
| print(f"\n{'=' * 74}") | |
| if self.failures: | |
| print(f"WALKTHROUGH FAILED -- {len(self.failures)} claim(s) did not hold:") | |
| for f in self.failures: | |
| print(f" - {f}") | |
| return 1 | |
| print("WALKTHROUGH OK -- every checkable claim in design §18 was demonstrated.") | |
| return 0 | |
| def _say(self, n: int) -> None: | |
| print(NARRATIVE[n]) | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description="Drive and verify the design §18 walkthrough.") | |
| ap.add_argument("--only", nargs="*", type=int, default=[], | |
| help="beat numbers to run (default: all)") | |
| ap.add_argument("--database", default=None, | |
| help="DSN to run against. Defaults to a private SQLite file " | |
| "which is deleted first, so the walkthrough always " | |
| "starts from an empty database and its results do not " | |
| "depend on whatever a previous run left behind.") | |
| args = ap.parse_args() | |
| url = args.database or os.environ.get("FORGESIGHT_DATABASE_URL") \ | |
| or f"sqlite:///{Path(get_settings().data_dir) / 'walkthrough.db'}" | |
| return Walkthrough(url).run(args.only) | |
| if __name__ == "__main__": | |
| sys.exit(main()) |