Michael Rabinovich Claude Opus 4.8 commited on
Commit
06ba2de
Β·
1 Parent(s): 190633b

Fix sharded-eval poll loop: stop killing healthy QUEUED shards, reconcile before failing

Browse files

The 45-min poll deadline cancelled shards that were merely QUEUED waiting
for a10g capacity β€” throwing away their GPU queue position β€” then, after
the retry rounds, marked the row failed while leaving jobs running, so
their outputs completed late and stranded in staging. This failed
otherwise-successful submissions under GPU load.

- Poll loop never cancels healthy QUEUED/RUNNING shards; only ERROR shards
re-dispatch. Each job's EVAL_JOB_TIMEOUT is the real RUNNING bound.
- Replace the per-round reaper with one generous env-tunable backstop
(default 6h) that only stops an unbounded loop.
- Reconcile-before-fail: always attempt the merge from whatever landed;
a shard flagged ERROR/timeout may still have written all its fixtures
(outputs are the source of truth). Only fail on genuinely-missing ones.
- Cancel orphan jobs on the true-failure path so nothing keeps billing.
- Boot sweep now reconciles stranded pending rows (finalize if artifacts
are complete) instead of blindly failing them β€” self-heals across Space
restarts, which is what stranded the recovered submission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (1) hide show
  1. submit.py +196 -89
submit.py CHANGED
@@ -234,19 +234,20 @@ SHARD_BUCKET_PREFIX = os.getenv(
234
  # overwrites its upload prefix), so one cheap retry absorbs a transient
235
  # job/runtime blip without re-running the shards that already passed.
236
  SHARD_MAX_RETRIES = 1
237
- # Whole-fan-out poll deadline. Each shard job carries its own
238
- # ``EVAL_JOB_TIMEOUT``; this guards the Space-side poll loop. Generous
239
- # vs. the per-shard ceiling because queued shards (past the ~8
240
- # concurrent slots) wait their turn before their own timeout starts.
241
- SHARD_POLL_DEADLINE_SECONDS = 45 * 60
242
- # When the poll window elapses with shards still non-terminal β€” typically the
243
- # tail shard stuck QUEUED waiting for GPU (a10g-large) capacity rather than a
244
- # compute failure β€” re-dispatch those stragglers and reset the window, up to
245
- # this many rounds, before giving up. A fresh dispatch after the window can land
246
- # a freed slot; shard uploads are idempotent (each rewrites its own staging
247
- # prefix), so a re-dispatch is safe. Worst-case total wait is roughly
248
- # SHARD_POLL_DEADLINE_SECONDS * (1 + SHARD_DEADLINE_RETRY_ROUNDS).
249
- SHARD_DEADLINE_RETRY_ROUNDS = 2
 
250
 
251
  # One HfApi client per process. HF_TOKEN is picked up from the env at
252
  # construction time and reused for every call.
@@ -1164,7 +1165,42 @@ def _run_worker_sharded(
1164
  failures = _poll_shards_until_done(
1165
  submission_id, submission_blob_url, shards,
1166
  )
 
 
 
 
 
 
1167
  if failures:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1168
  reason = ("sharded eval failed: " + "; ".join(failures))[
1169
  :FAILURE_REASON_MAX_CHARS
1170
  ]
@@ -1172,17 +1208,18 @@ def _run_worker_sharded(
1172
  progress.publish(
1173
  submission_id, progress.FAILED, _failed_progress_message(reason),
1174
  )
1175
- logger.warning("Sharded eval for %s failed: %s", submission_id, reason)
 
 
 
1176
  return
1177
 
1178
- progress.publish(
1179
- submission_id,
1180
- progress.RUNNING,
1181
- "All chunks evaluated β€” merging results…",
1182
- )
1183
- summary = _merge_shards_and_publish(
1184
- submission_id, list(shards.keys()), fixture_names,
1185
- )
1186
  _flip_row_to_completed(submission_id, summary)
1187
  progress.publish(
1188
  submission_id, progress.COMPLETED, _completed_progress_message(summary),
@@ -1291,13 +1328,13 @@ def _dispatch_shard(
1291
 
1292
 
1293
  def _cancel_shard_job(state: dict[str, Any]) -> None:
1294
- """Best-effort cancel of a shard's in-flight job before re-dispatch.
1295
 
1296
- Used on the poll-deadline retry path so a straggler that is still
1297
- QUEUED/RUNNING releases its slot and does not keep writing its staging
1298
- prefix once a replacement is dispatched. Best-effort: a failure is
1299
- logged and ignored, since shard uploads are idempotent (a stale job
1300
- only ever overwrites its own prefix with an equivalent result).
1301
  """
1302
  job_id = state.get("job_id")
1303
  if not job_id:
@@ -1310,11 +1347,24 @@ def _cancel_shard_job(state: dict[str, Any]) -> None:
1310
  namespace=EVAL_JOB_NAMESPACE,
1311
  token=_jobs_token(),
1312
  )
1313
- logger.info("Cancelled straggler shard job %s before retry", job_id)
1314
  except Exception as e: # noqa: BLE001 - cancel is best-effort
1315
  logger.warning("Could not cancel shard job %s: %s", job_id, e)
1316
 
1317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1318
  def _poll_shards_until_done(
1319
  submission_id: str,
1320
  submission_blob_url: str,
@@ -1322,24 +1372,29 @@ def _poll_shards_until_done(
1322
  ) -> list[str]:
1323
  """Poll every shard to terminal, retrying only ERROR shards.
1324
 
1325
- Mirrors the orchestrator's eval poll loop: a single thread sweeps
1326
- all running shards each tick (``inspect_job`` calls are cheap), an
1327
- ERROR shard re-dispatches up to :data:`SHARD_MAX_RETRIES` times,
1328
- and a non-terminal stage just waits. Returns a list of
1329
- ``"<shard_id>: <reason>"`` strings for shards that stayed ERROR
1330
- after their retries (empty list means every shard COMPLETED).
 
 
 
 
 
 
 
 
 
1331
  Transient ``inspect_job`` failures retry up to
1332
  :data:`JOB_POLL_MAX_CONSECUTIVE_ERRORS` before raising.
1333
 
1334
- If the :data:`SHARD_POLL_DEADLINE_SECONDS` window elapses with shards
1335
- still non-terminal (the GPU-capacity-starvation case, where a tail
1336
- shard sits QUEUED), those stragglers are cancelled and re-dispatched
1337
- and the window resets, up to :data:`SHARD_DEADLINE_RETRY_ROUNDS`
1338
- rounds, before the submission is finally failed. All-or-nothing is
1339
- preserved: the list is non-empty unless every shard COMPLETED.
1340
  """
1341
  deadline = time.monotonic() + SHARD_POLL_DEADLINE_SECONDS
1342
- deadline_rounds_left = SHARD_DEADLINE_RETRY_ROUNDS
1343
  consecutive_errors = 0
1344
  last_done = -1
1345
  total = len(shards)
@@ -1402,46 +1457,22 @@ def _poll_shards_until_done(
1402
  "Shard %s FAILED after %d attempt(s): %s",
1403
  shard_id, st["attempts"], st["message"],
1404
  )
 
 
 
1405
 
1406
  if time.monotonic() >= deadline:
1407
- stragglers = [
1408
- sid for sid, st in shards.items()
1409
- if st["stage"] not in ("COMPLETED", "FAILED")
1410
- ]
1411
- if stragglers and deadline_rounds_left > 0:
1412
- deadline_rounds_left -= 1
1413
- logger.warning(
1414
- "Poll deadline (%ds) hit for %s with %d straggler shard(s) "
1415
- "%s; re-dispatching (%d round(s) left).",
1416
- SHARD_POLL_DEADLINE_SECONDS, submission_id,
1417
- len(stragglers), stragglers, deadline_rounds_left,
1418
- )
1419
- for sid in stragglers:
1420
- st = shards[sid]
1421
- _cancel_shard_job(st)
1422
- # Give the replacement a fresh ERROR-retry budget too.
1423
- st["attempts"] = 0
1424
- _dispatch_shard(
1425
- submission_id, submission_blob_url, sid, st,
1426
- )
1427
- progress.publish(
1428
- submission_id,
1429
- progress.RUNNING,
1430
- f"GPU capacity was tight β€” retrying {len(stragglers)} "
1431
- f"straggler chunk(s) (round "
1432
- f"{SHARD_DEADLINE_RETRY_ROUNDS - deadline_rounds_left} of "
1433
- f"{SHARD_DEADLINE_RETRY_ROUNDS})…",
1434
- )
1435
- deadline = time.monotonic() + SHARD_POLL_DEADLINE_SECONDS
1436
- last_done = -1 # force a progress republish on the next sweep
1437
- continue
1438
  for shard_id, st in shards.items():
1439
  if st["stage"] not in ("COMPLETED", "FAILED"):
1440
  st["stage"] = "FAILED"
1441
  st["message"] = (
1442
- f"Space-side poll deadline exceeded "
1443
- f"({SHARD_POLL_DEADLINE_SECONDS}s) after "
1444
- f"{SHARD_DEADLINE_RETRY_ROUNDS} retry round(s)"
1445
  )
1446
  break
1447
  time.sleep(JOB_POLL_INTERVAL_SECONDS)
@@ -1806,21 +1837,94 @@ def _flip_row_to_failed(submission_id: str, reason: str) -> None:
1806
  )
1807
 
1808
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1809
  # ---------------------------------------------------------------------------
1810
  # Boot-time stuck-pending sweep
1811
  # ---------------------------------------------------------------------------
1812
 
1813
 
1814
  def _sweep_stuck_pending() -> None:
1815
- """Flip pending rows older than the threshold to failed.
1816
-
1817
- A ``pending`` row whose worker died (Space restart, OOM, crash)
1818
- has no one to flip it; without this sweep it stays pending in
1819
- the leaderboard forever. The check is "submitted_at older than
1820
- 30 min" - well above the real eval ceiling (~5 min on
1821
- cpu-upgrade), so any genuinely-still-running submission is safe.
1822
- Runs once per process at module-import time inside a daemon
1823
- thread so app boot doesn't block on the Hub read.
 
 
 
 
1824
  """
1825
  try:
1826
  body = _download_results_jsonl()
@@ -1862,19 +1966,22 @@ def _sweep_stuck_pending() -> None:
1862
  stuck_ids.append(sid)
1863
 
1864
  if not stuck_ids:
1865
- logger.info("Stuck-pending sweep: nothing stale")
1866
  return
1867
 
1868
  logger.warning(
1869
- "Stuck-pending sweep: flipping %d row(s) to failed: %s",
1870
  len(stuck_ids), stuck_ids,
1871
  )
1872
  for sid in stuck_ids:
1873
  try:
1874
- _flip_row_to_failed(sid, STUCK_PENDING_REASON)
 
 
 
1875
  except Exception as e: # noqa: BLE001 - log + carry on per-row
1876
  logger.exception(
1877
- "Stuck-pending flip failed for %s (%s: %s)",
1878
  sid, type(e).__name__, e,
1879
  )
1880
 
 
234
  # overwrites its upload prefix), so one cheap retry absorbs a transient
235
  # job/runtime blip without re-running the shards that already passed.
236
  SHARD_MAX_RETRIES = 1
237
+ # Whole-fan-out poll backstop. This is NOT a per-round reaper: each shard
238
+ # job carries its own ``EVAL_JOB_TIMEOUT`` (which bounds RUNNING work and
239
+ # turns a genuinely-hung job into ERROR), and a shard sitting QUEUED is
240
+ # healthy β€” it is just waiting for a10g-large capacity past the account's
241
+ # ~8 concurrent slots. Cancelling a QUEUED shard only throws away its place
242
+ # in the GPU queue and makes a capacity crunch worse, so the poll loop never
243
+ # does that. This constant is a single generous absolute ceiling (hours),
244
+ # purely so the poll loop can't spin forever if a shard never reaches a
245
+ # terminal stage; on hitting it the caller still reconciles against durable
246
+ # staging artifacts before declaring failure. Env-tunable for very large
247
+ # fan-outs behind a deep GPU queue.
248
+ SHARD_POLL_DEADLINE_SECONDS = int(
249
+ os.getenv("CADGENBENCH_SHARD_POLL_DEADLINE_SECONDS", str(6 * 60 * 60))
250
+ )
251
 
252
  # One HfApi client per process. HF_TOKEN is picked up from the env at
253
  # construction time and reused for every call.
 
1165
  failures = _poll_shards_until_done(
1166
  submission_id, submission_blob_url, shards,
1167
  )
1168
+
1169
+ # Reconcile-before-fail: outputs are the source of truth. Even when a
1170
+ # shard was flagged ERROR or caught by the poll backstop, its fixtures
1171
+ # may all be in staging (it completed late, or wrote results before
1172
+ # erroring). Always attempt the merge from whatever landed; only fail
1173
+ # if the merged set genuinely can't cover every expected fixture.
1174
  if failures:
1175
+ logger.warning(
1176
+ "Sharded eval for %s: %d shard(s) did not COMPLETE (%s); "
1177
+ "reconciling from staging before deciding.",
1178
+ submission_id, len(failures), "; ".join(failures),
1179
+ )
1180
+ progress.publish(
1181
+ submission_id,
1182
+ progress.RUNNING,
1183
+ "Some chunks reported issues β€” checking for late results…",
1184
+ )
1185
+ else:
1186
+ progress.publish(
1187
+ submission_id,
1188
+ progress.RUNNING,
1189
+ "All chunks evaluated β€” merging results…",
1190
+ )
1191
+
1192
+ try:
1193
+ summary = _merge_shards_and_publish(
1194
+ submission_id, list(shards.keys()), fixture_names,
1195
+ )
1196
+ except Exception as merge_err: # noqa: BLE001 - map to a failed row
1197
+ if not failures:
1198
+ # Every shard COMPLETED but the merge itself broke: a real
1199
+ # error, surfaced to _run_worker's handler.
1200
+ raise
1201
+ # Genuinely incomplete. Tear down any shard job still running so
1202
+ # it stops billing now that the submission is failing, then fail.
1203
+ _cancel_orphan_shard_jobs(shards)
1204
  reason = ("sharded eval failed: " + "; ".join(failures))[
1205
  :FAILURE_REASON_MAX_CHARS
1206
  ]
 
1208
  progress.publish(
1209
  submission_id, progress.FAILED, _failed_progress_message(reason),
1210
  )
1211
+ logger.warning(
1212
+ "Sharded eval for %s failed; reconcile could not finalize "
1213
+ "(%s): %s", submission_id, merge_err, reason,
1214
+ )
1215
  return
1216
 
1217
+ if failures:
1218
+ logger.info(
1219
+ "Sharded eval for %s reconciled: all fixtures present in "
1220
+ "staging despite %d non-completed shard(s).",
1221
+ submission_id, len(failures),
1222
+ )
 
 
1223
  _flip_row_to_completed(submission_id, summary)
1224
  progress.publish(
1225
  submission_id, progress.COMPLETED, _completed_progress_message(summary),
 
1328
 
1329
 
1330
  def _cancel_shard_job(state: dict[str, Any]) -> None:
1331
+ """Best-effort cancel of a shard's in-flight job.
1332
 
1333
+ Used on the final failure path (via :func:`_cancel_orphan_shard_jobs`)
1334
+ so a shard still QUEUED/RUNNING when the submission is being failed is
1335
+ torn down instead of left to run and keep billing. Best-effort: a
1336
+ failure is logged and ignored, since a lingering job only ever
1337
+ overwrites its own idempotent staging prefix.
1338
  """
1339
  job_id = state.get("job_id")
1340
  if not job_id:
 
1347
  namespace=EVAL_JOB_NAMESPACE,
1348
  token=_jobs_token(),
1349
  )
1350
+ logger.info("Cancelled orphan shard job %s", job_id)
1351
  except Exception as e: # noqa: BLE001 - cancel is best-effort
1352
  logger.warning("Could not cancel shard job %s: %s", job_id, e)
1353
 
1354
 
1355
+ def _cancel_orphan_shard_jobs(shards: dict[str, dict[str, Any]]) -> None:
1356
+ """Cancel every shard job that never reached COMPLETED.
1357
+
1358
+ Called once, on the final failure path, after reconcile has confirmed
1359
+ the submission cannot be finalized. A shard still QUEUED/RUNNING (or
1360
+ one flagged ERROR/FAILED whose HF job lingers) is cancelled so it stops
1361
+ consuming GPU/billing after the submission has already been failed.
1362
+ """
1363
+ for st in shards.values():
1364
+ if st.get("stage") != "COMPLETED" and st.get("job_id"):
1365
+ _cancel_shard_job(st)
1366
+
1367
+
1368
  def _poll_shards_until_done(
1369
  submission_id: str,
1370
  submission_blob_url: str,
 
1372
  ) -> list[str]:
1373
  """Poll every shard to terminal, retrying only ERROR shards.
1374
 
1375
+ A single thread sweeps all shards each tick (``inspect_job`` calls
1376
+ are cheap). A shard that is QUEUED (waiting for a10g-large capacity
1377
+ past the account's concurrent-job cap) or RUNNING is *healthy* and
1378
+ is simply waited on β€” its own job ``EVAL_JOB_TIMEOUT`` bounds RUNNING
1379
+ work, and HF turns a genuinely-hung job into ERROR. Only an ERROR
1380
+ shard is re-dispatched, up to :data:`SHARD_MAX_RETRIES`. The loop
1381
+ never cancels a healthy QUEUED/RUNNING shard: doing so throws away
1382
+ its place in the GPU queue and makes a capacity crunch strictly
1383
+ worse (the bug that stranded otherwise-successful runs).
1384
+
1385
+ Returns a list of ``"<shard_id>: <reason>"`` for shards that did not
1386
+ COMPLETE (empty means every shard COMPLETED). This is a *signal, not
1387
+ a verdict*: the caller reconciles against durable staging artifacts
1388
+ before declaring failure, because a shard flagged ERROR or caught by
1389
+ the backstop may still have written all of its per-fixture outputs.
1390
  Transient ``inspect_job`` failures retry up to
1391
  :data:`JOB_POLL_MAX_CONSECUTIVE_ERRORS` before raising.
1392
 
1393
+ :data:`SHARD_POLL_DEADLINE_SECONDS` is a single generous absolute
1394
+ backstop (hours), not a per-round reaper: it only stops the loop
1395
+ spinning forever if a shard never reaches a terminal stage.
 
 
 
1396
  """
1397
  deadline = time.monotonic() + SHARD_POLL_DEADLINE_SECONDS
 
1398
  consecutive_errors = 0
1399
  last_done = -1
1400
  total = len(shards)
 
1457
  "Shard %s FAILED after %d attempt(s): %s",
1458
  shard_id, st["attempts"], st["message"],
1459
  )
1460
+ # QUEUED / RUNNING (or any other non-terminal stage): healthy,
1461
+ # just not done. Wait β€” the job's own EVAL_JOB_TIMEOUT bounds
1462
+ # it. No Space-side cancellation or re-dispatch.
1463
 
1464
  if time.monotonic() >= deadline:
1465
+ # Absolute backstop reached. Do NOT cancel here β€” a shard may
1466
+ # be finishing right now, and its outputs are the source of
1467
+ # truth. Mark the non-terminal ones so the caller reconciles
1468
+ # against staging; genuinely-orphaned jobs are torn down on
1469
+ # the failure path once reconcile confirms they're incomplete.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1470
  for shard_id, st in shards.items():
1471
  if st["stage"] not in ("COMPLETED", "FAILED"):
1472
  st["stage"] = "FAILED"
1473
  st["message"] = (
1474
+ f"shard did not reach a terminal stage within the "
1475
+ f"{SHARD_POLL_DEADLINE_SECONDS}s poll backstop"
 
1476
  )
1477
  break
1478
  time.sleep(JOB_POLL_INTERVAL_SECONDS)
 
1837
  )
1838
 
1839
 
1840
+ # ---------------------------------------------------------------------------
1841
+ # Reconcile: finalize a submission from durable artifacts alone
1842
+ # ---------------------------------------------------------------------------
1843
+
1844
+
1845
+ def _expected_fixtures(submission_id: str) -> list[str]:
1846
+ """The fixture (sample) names a submission was expected to cover.
1847
+
1848
+ Read from the submitted ``submissions/<id>.zip`` top-level sample
1849
+ dirs β€” the same set the eval fans out over. Used to verify staging
1850
+ shards are complete before finalizing a reconstructed run, without
1851
+ threading the in-memory fixture list through a dead worker.
1852
+ """
1853
+ zip_path = hf_hub_download(
1854
+ repo_id=HF_SUBMISSIONS_REPO,
1855
+ repo_type="dataset",
1856
+ filename=f"{SUBMISSIONS_DIR}/{submission_id}.zip",
1857
+ force_download=True,
1858
+ )
1859
+ with zipfile.ZipFile(zip_path) as zf:
1860
+ tops = {n.split("/")[0] for n in zf.namelist() if "/" in n}
1861
+ return sorted(
1862
+ t for t in tops
1863
+ if t and t != "meta.json" and not t.startswith("__")
1864
+ )
1865
+
1866
+
1867
+ def _reconcile_submission(submission_id: str) -> tuple[str, str]:
1868
+ """Try to finalize a stranded submission from artifacts alone.
1869
+
1870
+ Outputs are the source of truth: a submission whose eval actually
1871
+ produced results β€” a published report bundle, or staging shards that
1872
+ cover every expected fixture β€” is completed even though its worker
1873
+ died and left the row ``pending``. Only a submission with no usable
1874
+ artifacts is failed. Idempotent and safe to re-run.
1875
+
1876
+ Returns ``(outcome, detail)`` with ``outcome`` in
1877
+ ``{"completed", "failed"}``; the row flip is performed here.
1878
+ """
1879
+ # 1. Single-job (or already-merged) path: a complete report bundle
1880
+ # means the eval finished and only the row flip was lost.
1881
+ try:
1882
+ summary = _fetch_run_summary_from_report(submission_id)
1883
+ except Exception: # noqa: BLE001 - no usable report; try shard merge
1884
+ summary = None
1885
+ if summary is not None:
1886
+ _flip_row_to_completed(submission_id, summary)
1887
+ return "completed", "recovered from an existing report bundle"
1888
+
1889
+ # 2. Sharded path: merge the staging shards iff they cover every
1890
+ # expected fixture. _merge_shards_and_publish raises otherwise.
1891
+ try:
1892
+ expected = _expected_fixtures(submission_id)
1893
+ except Exception as e: # noqa: BLE001 - can't even read the submission
1894
+ reason = f"reconcile could not read submission fixtures: {e}"
1895
+ _flip_row_to_failed(submission_id, reason[:FAILURE_REASON_MAX_CHARS])
1896
+ return "failed", reason
1897
+ try:
1898
+ summary = _merge_shards_and_publish(submission_id, [], expected)
1899
+ except Exception as e: # noqa: BLE001 - no complete artifacts
1900
+ reason = f"{STUCK_PENDING_REASON}; no complete artifacts to reconcile"
1901
+ _flip_row_to_failed(submission_id, reason[:FAILURE_REASON_MAX_CHARS])
1902
+ return "failed", f"{reason} ({e})"
1903
+
1904
+ _flip_row_to_completed(submission_id, summary)
1905
+ _cleanup_shard_artifacts(submission_id)
1906
+ return "completed", f"merged {len(expected)} fixtures from staging"
1907
+
1908
+
1909
  # ---------------------------------------------------------------------------
1910
  # Boot-time stuck-pending sweep
1911
  # ---------------------------------------------------------------------------
1912
 
1913
 
1914
  def _sweep_stuck_pending() -> None:
1915
+ """Reconcile pending rows whose worker died, then decide their fate.
1916
+
1917
+ A ``pending`` row whose worker died (Space restart, OOM, crash) has
1918
+ no one to finalize it. At boot no worker thread from the previous
1919
+ process survives, so any such stale row is orphaned. Rather than
1920
+ blindly failing it β€” which is exactly how a fully-successful eval got
1921
+ marked failed with its outputs stranded β€” each stale row is run
1922
+ through :func:`_reconcile_submission`: if its report bundle or
1923
+ staging shards are complete it is finalized to ``completed``, else it
1924
+ is failed. The "submitted_at older than 30 min" gate avoids racing a
1925
+ submission whose worker is legitimately starting in *this* process.
1926
+ Runs once per process at module-import time inside a daemon thread so
1927
+ app boot doesn't block on the Hub read.
1928
  """
1929
  try:
1930
  body = _download_results_jsonl()
 
1966
  stuck_ids.append(sid)
1967
 
1968
  if not stuck_ids:
1969
+ logger.info("Boot reconcile sweep: nothing stale")
1970
  return
1971
 
1972
  logger.warning(
1973
+ "Boot reconcile sweep: reconciling %d stale pending row(s): %s",
1974
  len(stuck_ids), stuck_ids,
1975
  )
1976
  for sid in stuck_ids:
1977
  try:
1978
+ outcome, detail = _reconcile_submission(sid)
1979
+ logger.warning(
1980
+ "Boot reconcile: %s -> %s (%s)", sid, outcome, detail,
1981
+ )
1982
  except Exception as e: # noqa: BLE001 - log + carry on per-row
1983
  logger.exception(
1984
+ "Boot reconcile failed for %s (%s: %s)",
1985
  sid, type(e).__name__, e,
1986
  )
1987