SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
6.3 KB

# Connectors — runtime guarantees (restart, checkpoints, anomaly guard, alerts)

One page per connector lives next to this file (clinvar.md, clinicaltrials.md, …). This page documents what the SDK (packages/connectors/src/sdk/run.ts) guarantees for every connector, so a connector author only has to respect three rules.

# Rules for connector authors

  1. Mutate ctx.cursor only at a safe point — after a page / batch is fully persisted (upsertSourceRecord, canonical rows, provenance). The SDK may persist the cursor at any moment after that; a cursor must never be ahead of the data.
  2. Call ctx.shouldStop() between pages and return when it is true (time budget, record cap or abort request). Do not catch and swallow the return — the run status is decided by runConnector.
  3. Use the SDK writers (ctx.upsertSourceRecord, ctx.upsertSourceRecordsBatch) or, when you keep your own batch writer (ClinVar), call await ctx.checkpoint(n) after each persisted batch.

# What the SDK guarantees

# Cursor persistence (CLAUDE.md §90)

Moment What is written Where
Run start cursor_before (the cursor the run starts from), last_attempt_at ingest_runs, connector_cursors
Automatic checkpoint current ctx.cursor if it changed since the last save — every manifest.checkpointEvery upserted records (default 2 000) or every 60 s, evaluated in upsertSourceRecord / upsertSourceRecordsBatch / ctx.checkpoint() connector_cursors.cursor, ingest_runs.cursor_after
await ctx.saveCursor() immediately, serialised (concurrent calls share one write) same
SIGTERM / SIGINT cursor saved, run marked aborted (provisionally, then finalised when sync() returns), raw-lake gzip streams flushed same + system_alerts
End of run (succeeded, partial, failed, aborted) final cursor, counters, log, schema drift, anomaly ingest_runs, connector_cursors

dry_run and probe modes never write the cursor (ctx.persistsCursor === false), so a smoke run cannot move a production cursor. --mode full and --reset-cursor start from {}.

# Restart semantics

  • Killed run (SIGKILL, OOM, power loss): the next run resumes from the last checkpoint — at most checkpointEvery records (or 60 s of work) are re-fetched; records are hashed (source_records.payload_hash), so re-processing is idempotent (unchanged, no duplicates).
  • SIGTERM / SIGINT (PM2 stop/restart, Ctrl-C): the first signal makes shouldStop() return true, saves the cursor and marks the run aborted; the connector returns at its next page boundary and runConnector finalises the run. A second signal exits the process immediately (status and cursor are already persisted). PM2's default kill_timeout is 1.6 s; the provisional bookkeeping is done first precisely so that a hard kill after it still leaves a consistent state. Handlers are installed for the duration of the run only (handleSignals: false disables them).
  • Time budget (--max-minutes, CI_MAX_RUN_MINUTES): status partial, cursor saved; the worker's next scheduled run (or first-run.sh's retry loop) continues.
  • Failure: status failed, cursor saved as it was at the last safe point, health failing.
  • Anomaly (see below): status failed with ingest_runs.anomaly set; nothing destructive happened, the previous data stays published.

# Anomaly guard (CLAUDE.md §171)

await ctx.guardCount(entity, declaredTotal) compares a total the source declares (or the number of parsed records) with the previous successful run's records_fetched for the connector. If declaredTotal < previous × manifest.anomalyGuard.minRatioOfPrevious (default 0.5) it throws AnomalyError, sets ctx.anomaly, and runConnector records the run as failed with ingest_runs.anomaly, health failing, and a critical anomaly alert. The first run of a connector is accepted as the baseline. Wired in: OncoTree (tumour types), NCIt (concepts + qualified states + retired), ClinicalTrials.gov (totalCount of a full crawl); connectors with a fixed floor (HGNC ≥ 30 k rows, GDC ≥ 40 projects, USCS ≥ 20 k rows…) keep their own checks — any error message starting with anomaly: is also recorded in ingest_runs.anomaly.

# Schema drift (CLAUDE.md §25)

Observed field names/types per entity are stored in connector_field_stats; new fields or type changes are listed in ingest_runs.schema_drift, set health to degraded and raise a schema_drift alert (resolved automatically on the next drift-free run).

# Alerts (CLAUDE.md §170)

system_alerts rows (packages/database/src/alerts.ts: raiseAlert, resolveAlerts, listAlerts) are deduplicated on kind + connector + message while open/acknowledged (count increments). Raised by the SDK: connector_failure (warn), anomaly (critical), connector_aborted (info, or warn when credentials are missing), schema_drift (warn). Raised by the worker's hourly health probe: source_failing, source_stale (no success within 2× the schedule interval). A successful run resolves the connector's failure/aborted/anomaly/stale/failing alerts. Read them with pnpm cix alerts, pnpm cix doctor, or GET /v1/admin/alerts.

# Operator cheat-sheet

bash
pnpm cix run clinvar --max-minutes 30        # partial → cursor saved → run again to continue
pnpm cix run clinvar --mode backfill         # reprocess a dataset version already marked complete
pnpm cix run oncotree --reset-cursor         # start from scratch (idempotent thanks to payload hashes)
pnpm cix doctor                              # readiness: env, DB, migrations, connectors, stale, alerts
pnpm cix alerts [--status resolved] ; pnpm cix alerts ack 12 ; pnpm cix alerts resolve 12
psql -c "SELECT connector_id, cursor, last_success_at, health FROM connector_cursors"
psql -c "SELECT id, status, records_fetched, anomaly, cursor_after FROM ingest_runs WHERE connector_id='clinvar' ORDER BY started_at DESC LIMIT 5"

Tests: packages/connectors/src/sdk/run.test.ts (fake paged connector: checkpoint every 5 records, SIGTERM at page 3 → aborted + resume from page 3, anomaly guard, alert dedupe, doctor) and lake.test.ts (5 000 concurrent raw writes, single drain listener). The database-backed tests run only when CI_TEST_DATABASE_URL (or DATABASE_URL) is set.