Skip to content

Pipeline robustness: stop silent data loss, cache poisoning, and hangs - #6

Open
LukasWallrich wants to merge 3 commits into
mainfrom
audit/pipeline-robustness
Open

Pipeline robustness: stop silent data loss, cache poisoning, and hangs#6
LukasWallrich wants to merge 3 commits into
mainfrom
audit/pipeline-robustness

Conversation

@LukasWallrich

Copy link
Copy Markdown
Contributor

Part of a repo audit. Python data-pipeline changes only (no workflow/R/frontend edits).

refresh_data.py

  • A failed OpenCitations fetch for a replication no longer substitutes an empty citation list (which silently deflated that original's co-citation counts in a "complete" run) — the affected original is skipped, retried next run, and surfaced in meta.json (fetch_errors, fetch_error_dois, originals_errored) with partial_run set.
  • Cache poisoning fixed: non-200 responses no longer cache an empty OMID identifier set for 30 days (which defeated the conflation check), and a 200 with an unparseable body is no longer cached as "zero citations".
  • Co-citation matching now extracts a stable key (omid: token, DOI fallback) from OpenCitations' composite citing string instead of comparing the whole space-joined identifier list; applied at read time so existing caches stay valid (verified 41/41 rows on a real cache file).
  • 429s get exponential backoff over 4 attempts honoring Retry-After (was: one 8s retry then skip).
  • The per-year co-citation timeline now dedupes citing works that co-cite replications of different outcomes, matching the study-level counts.
  • Cache TTL is now a fetched_at timestamp stored inside each cache file — file mtimes are reset by every CI checkout, so the 30-day TTL was effectively never expiring. Old-format files are treated as stale and upgrade on first refetch.
  • The upstream flora.csv download now goes through the session with a timeout and 3-attempt backoff (was: pd.read_csv(URL) with no timeout, which could hang the 350-minute job).

compute_omc.py

  • Transient errors (network, 429, 5xx, parse) are no longer cached as permanent "no venue" results; only a genuine 200-with-no-results caches a negative. Retries with backoff added.
  • Aborts nonzero when >50% of new lookups (min 10) fail transiently, so an OpenAlex outage can't produce an incompletely enriched CSV with a fresh timestamp (follow-up to the codex review on Fix workflow push races, stale-data commits, and duplicate refresh #2).
  • Atomic cache writes (temp + os.replace); a crash can no longer truncate the committed venue cache.
  • The fuzzy venue search's top hit must now pass a token-overlap sanity check before being accepted (was: results[0] unconditionally, risking confidently wrong impact factors).
  • The docstring's promised per-run time budget now actually exists (MAX_RUNTIME_SECONDS, mirrors refresh_data.py).

compute_author_overlap.py — outcome breakdown now uses the frontend's classifyOutcome mapping plus an "other" bucket; it previously dropped 90 known-overlap rows with variant labels (verified: by_outcome now sums to 2298 == n_total, was 2208).

refresh_flora.py — download gets a timeout + 3-attempt backoff.

All scripts compile; key helpers (citing-key extraction against a real cache file, TTL round-trip, name matching, outcome classification) unit-tested; the author-overlap script was run end-to-end against real data. The full OC/OpenAlex pipelines were not run (live APIs, hours).

🤖 Generated with Claude Code

LukasWallrich and others added 3 commits July 18, 2026 23:54
refresh_data.py:
- Skip an original (retry next run) when a replication's citations fail to
  fetch, instead of substituting [] and committing under-counted co-citations;
  surface via meta.json fetch_errors/originals_errored and partial_run.
- oc_entity_ids: only cache on a confirmed 200; never cache a non-200 or a
  JSON-parse failure as an empty id set (was defeating the OMID-conflation
  check for 30 days).
- fetch_oc_citations: a 200 whose body fails to parse now returns None
  (transient) instead of caching [] as zero citations.
- Extract a stable citing key (omid: token, else DOI token) from the
  OpenCitations composite string at read time, so co-citation matching is
  robust and existing cache files stay valid.
- 429 handling: exponential backoff over several attempts honoring Retry-After.
- Timeline: add a deduped with_any per-year bucket so aggregate co-citation
  yearly totals no longer double-count a work co-citing two outcomes.
- Cache TTL now measured from an embedded fetched_at timestamp, not file mtime
  (git checkout resets mtime in CI); old bare-list caches refetch once.
- load_flora fetches via the session with timeout + retry, then read_csv.

compute_omc.py:
- Only cache negative venue results on a genuine 200-with-no-results; transient
  errors (network/429/5xx/parse) retry next run. Added backoff retries.
- Atomic cache write (temp file + os.replace).
- Similarity sanity check on the fuzzy top hit; record a miss rather than
  attaching a wrong venue.
- Add the per-run MAX_RUNTIME_SECONDS budget the docstring promised.

compute_author_overlap.py:
- Port classifyOutcome from assets/app.js and add an "other" bucket so the
  by_outcome breakdown reconciles with n_total (was 2208 vs 2298).

refresh_flora.py:
- Fetch with timeout + retry/backoff so a one-off blip doesn't fail the job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address codex review of the workflow PR: a widespread OpenAlex outage
previously produced an incompletely enriched CSV with a fresh
timestamp. If >50% of new lookups (min 10) fail transiently, exit
nonzero before writing the CSV so the workflow fails instead of
committing stale-looking-fresh data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…count, retry coverage, Retry-After dates

refresh_data.py:
- Mid-study time-budget exhaustion in the replication loop now marks the
  current original incomplete (RUN_STATS.originals_errored) and stops the outer
  loop, so it is deferred to the next run instead of being published from only
  the replications fetched so far — closing the same silent under-counting the
  skip-and-flag change was meant to prevent.
- build_panel n_cit now uses only+with_any for deduped timeline rows (with the
  legacy summed-bucket fallback), so a work co-citing replications of two
  outcomes is no longer double-counted in the event-study citation series.
- fetch_oc_citations now retries network exceptions and 5xx responses under the
  same exponential-backoff budget as 429 (previously bailed on the first one).
- retry_after_seconds parses HTTP-date Retry-After headers (parsedate_to_datetime)
  in addition to delta-seconds, clamped to 120s.

compute_omc.py:
- lookup_venue retries 5xx responses (not just network/429) within the attempt
  budget before giving up as transient.
- Shared retry_after_seconds helper parses HTTP-date headers too, clamped to 120s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@LukasWallrich

Copy link
Copy Markdown
Contributor Author

Codex review (gpt, read-only): REQUEST_CHANGES — four findings, all addressed in 95072ff:

  1. High — mid-study time-budget exhaustion still published an under-counted original (the inner should_stop() break left rep_fetch_failed false). ✅ Fixed: budget exhaustion now routes through the same skip-and-flag path as a fetch failure and stops the outer loop, so the affected original is deferred to the next run.
  2. High — the event-study citation denominator (n_cit) still double-counted works co-citing replications of different outcomes. ✅ Fixed: only + with_any for deduped timeline rows, with the legacy summed-bucket fallback for old rows. Unit-checked (only=2, with_any=3 → 5, not 8).
  3. Medium — exponential retries only covered 429; network exceptions and 5xx bailed immediately in both scripts. ✅ Fixed: both now retry under the same attempt budget (other 4xx stay non-retryable).
  4. Medium — Retry-After parsing ignored the HTTP-date form. ✅ Fixed: both helpers parse it via parsedate_to_datetime, clamped to 0–120 s. Unit-checked (numeric, clamp, past/future dates, garbage, missing).

Codex verified the rest cleanly: the wrapped cache-format migration is internally consistent (old bare-list files go stale without crashing), citing keys are normalized at every compare/store site, the transient-failure abort correctly excludes cached genuine and weak-match misses, and the Python outcome classifier matches the JS branch ordering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant