File size: 6,768 Bytes
f149ba4 3912d45 f149ba4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | # /// script
# requires-python = ">=3.10"
# dependencies = ["huggingface_hub", "requests"]
# ///
"""Heartbeat watchdog for the self-refreshing dataset-lineage pipeline.
Runs on its OWN schedule, independent of refresh_job.py, and fails LOUD β opens an
HF Discussion on the Space repo (the Hub emails watchers) and exits non-zero β the
moment any liveness signal goes stale. That turns the 2026-07 silent-rot failure
class (a dump 3 months stale, a Space serving a shadow db, a gpu phase that died
without finalizing) into an active, dashboard-red + emailed alert.
Checks (each independent; all are evaluated, then a single Discussion lists them):
(a) scratch refresh/state.json cutoff older than MAX_STATE_AGE_DAYS
(b) Space /stats reachable AND its built_at older than MAX_BUILT_AGE_DAYS
(skipped when the live db predates the built_at stamp)
(c) either cards dump (dataset + model) lastModified older than MAX_DUMP_AGE_DAYS
(d) an inflight gpu marker older than MAX_INFLIGHT_AGE_HOURS
Schedule (self-updates from the Space repo, same pattern as refresh_job.py):
hf jobs scheduled uv run "0 7 * * *" --flavor cpu-basic --timeout 15m \
--secrets HF_TOKEN \
https://huggingface.co/spaces/davanstrien/dataset-lineage-explorer/raw/main/heartbeat.py
"""
import datetime as dt
import json
import os
import requests
from huggingface_hub import HfApi, hf_hub_download
SCRATCH = "davanstrien/dataset-lineage-scratch"
SPACE = "davanstrien/dataset-lineage-explorer"
DATASET_DUMP = "librarian-bots/dataset_cards_with_metadata"
MODEL_DUMP = "librarian-bots/model_cards_with_metadata"
STATE_PATH = "refresh/state.json"
STATS_URL = f"https://{SPACE.replace('/', '-')}.hf.space/stats"
MAX_STATE_AGE_DAYS = 40 # a monthly refresh that stopped advancing the cutoff
MAX_BUILT_AGE_DAYS = 40 # a deployed db that stopped being rebuilt
MAX_DUMP_AGE_DAYS = 3 # upstream card-pipeline compile job stuck
MAX_INFLIGHT_AGE_HOURS = 36 # a gpu phase that died without finalizing
TOKEN = os.environ.get("HF_TOKEN")
api = HfApi(token=TOKEN)
def _parse(when):
"""Best-effort -> aware UTC datetime, or None."""
if when is None:
return None
if isinstance(when, dt.datetime):
return when if when.tzinfo else when.replace(tzinfo=dt.timezone.utc)
try:
d = dt.datetime.fromisoformat(str(when)[:19].replace(" ", "T"))
return d.replace(tzinfo=dt.timezone.utc)
except Exception: # noqa: BLE001
return None
def _age_days(when):
d = _parse(when)
if d is None:
return None
return (dt.datetime.now(dt.timezone.utc) - d).total_seconds() / 86400
def check_state(problems):
try:
p = hf_hub_download(SCRATCH, STATE_PATH, repo_type="dataset", token=TOKEN)
state = json.load(open(p))
except Exception as e: # noqa: BLE001
problems.append(f"(a) state.json unreadable in scratch ({e})")
return
age = _age_days(state.get("cutoff"))
if age is None:
problems.append(f"(a) state.json cutoff unparseable: {state.get('cutoff')!r}")
elif age > MAX_STATE_AGE_DAYS:
problems.append(f"(a) refresh cutoff is {age:.0f}d old (> {MAX_STATE_AGE_DAYS}) β "
"the monthly refresh has stopped advancing.")
def check_built(problems):
try:
stats = requests.get(STATS_URL, timeout=30).json()
except (requests.RequestException, ValueError) as e:
problems.append(f"(b) Space /stats unreachable/invalid ({e}) β the explorer may be down.")
return
built = stats.get("built_at")
if not built:
return # older db without a built_at stamp β nothing to check
age = _age_days(built)
if age is None:
problems.append(f"(b) /stats built_at unparseable: {built!r}")
elif age > MAX_BUILT_AGE_DAYS:
problems.append(f"(b) deployed db was built {age:.0f}d ago (> {MAX_BUILT_AGE_DAYS}) β "
"the Space is serving a stale graph.")
def check_dumps(problems):
for repo in (DATASET_DUMP, MODEL_DUMP):
try:
age = _age_days(api.dataset_info(repo).last_modified)
except Exception as e: # noqa: BLE001
problems.append(f"(c) dump {repo} info fetch failed ({e})")
continue
if age is None:
problems.append(f"(c) dump {repo} lastModified unparseable")
elif age > MAX_DUMP_AGE_DAYS:
problems.append(f"(c) dump {repo} lastModified is {age:.1f}d old "
f"(> {MAX_DUMP_AGE_DAYS}) β upstream compile job likely stuck.")
def check_inflight(problems):
try:
files = [f for f in api.list_repo_files(SCRATCH, repo_type="dataset")
if f.startswith("refresh/inflight_") and f.endswith(".json")]
except Exception as e: # noqa: BLE001
problems.append(f"(d) could not list scratch to check inflight markers ({e})")
return
for f in files:
try:
payload = json.load(open(hf_hub_download(SCRATCH, f, repo_type="dataset", token=TOKEN)))
except Exception: # noqa: BLE001
continue
age_h = None
d = _parse(payload.get("submitted_at"))
if d is not None:
age_h = (dt.datetime.now(dt.timezone.utc) - d).total_seconds() / 3600
if age_h is not None and age_h > MAX_INFLIGHT_AGE_HOURS:
problems.append(f"(d) inflight marker {f} is {age_h:.0f}h old "
f"(> {MAX_INFLIGHT_AGE_HOURS}) β a gpu phase died without finalizing.")
def main():
problems = []
for check in (check_state, check_built, check_dumps, check_inflight):
try:
check(problems)
except Exception as e: # noqa: BLE001 β a check crash is itself a problem
problems.append(f"heartbeat check {check.__name__} crashed: {e}")
if not problems:
print("heartbeat OK: all liveness signals fresh", flush=True)
return
body = ("The dataset-lineage refresh heartbeat found stale/failed signals:\n\n"
+ "\n".join(f"- {p}" for p in problems)
+ f"\n\nChecked at {dt.datetime.now(dt.timezone.utc):%Y-%m-%dT%H:%M:%SZ}. "
"See jobs/refresh_job.py and the scratch repo refresh/ state.")
print("HEARTBEAT PROBLEMS:\n" + body, flush=True)
try:
api.create_discussion(repo_id=SCRATCH, repo_type="dataset",
title="[refresh-heartbeat] stale/failed liveness signal",
description=body)
print("posted heartbeat Discussion", flush=True)
except Exception as e: # noqa: BLE001
print(f"could not post heartbeat Discussion ({e})", flush=True)
raise SystemExit(1)
if __name__ == "__main__":
main()
|