Skip to content

fix: restore Arrow download path, decouple delete list, detect stale step-01 - #18

Merged
Burdantes merged 1 commit into
mainfrom
fix/tomography-oom-and-delete-tables
Aug 8, 2026
Merged

fix: restore Arrow download path, decouple delete list, detect stale step-01#18
Burdantes merged 1 commit into
mainfrom
fix/tomography-oom-and-delete-tables

Conversation

@Burdantes

Copy link
Copy Markdown
Collaborator

Three fixes surfaced by the 2025-08 backfill, which failed in Phase D last night.

1. Missing google-cloud-bigquery-storage → tomography OOM

google-cloud-bigquery-storage was neither declared in pyproject.toml nor installed in the image, so every tomography download logged

Storage Read API unavailable (cannot import name 'bigquery_storage' ...); using REST download

and built result frames from JSON rows instead of Arrow buffers.

On 2025-08-01 that meant 13.7M node-edge rows in 19.5 minutes, and the retained frame plus Phase 4's allocations OOM-killed the worker at 22.1 GB inside a 28 GB cgroup, failing the whole chunk:

Memory cgroup out of memory: Killed process 1585062 (python3.14)
  total-vm:28387040kB, anon-rss:22138616kB

2026-05-27 survived the identical path at 11.2M rows, so the pipeline has been running just under this ceiling for a while. This affects every nightly, not just backfills — ~20 min of each Phase D is REST download.

Also pins pyarrow explicitly (it was only in dev extras and arriving transitively).

2. --delete-first could not clear the Phase-D correlation tables

SQL_FILES and OUTPUT_TABLES were zipped positionally, forcing one list to serve two purposes:

job needs
SQL_FILE_TO_OUTPUT_TABLE resume map exactly one table per SQL file, in order
delete_dates() delete list every table the pipeline writes

The three correlation tables are written by Phase-D Python with no 1:1 SQL file, so adding them raised on strict=True. Consequence: every re-run needed a manual pre-delete or it appended a second copy of that date's rows (they use insert_rows_json, which has no self-delete).

Split into an explicit SQL_FILE_TO_OUTPUT_TABLE dict, DERIVED_OUTPUT_TABLES, and DELETE_TABLES, with an import-time check replacing strict=True.

giga_meter_measurements is deliberately excluded — it accumulates history under first-writer-wins and part of it was recovered by hand, so a delete not followed by a successful 04 loses traces permanently. A test pins that exclusion so adding it has to be a conscious decision.

3. Nothing detected a stunted merged_download_upload

Step 01 can write a partition that is present and non-empty — invisible to step_already_done() — while holding a fraction of the available measurements. Everything downstream scales down with it.

Three such dates were found by hand this week; nothing in the pipeline saw them:

date captured of source
2026-05-27 650,293 8.1%
2026-07-13 385,526 4.5%
2026-07-17 1,177,571 13.7%

find_stale_merged_partitions() compares each partition to its neighbour median, excluding itself so a stale date cannot drag down its own baseline. Reads INFORMATION_SCHEMA.PARTITIONS only — free — so it runs after Phase A on every invocation. Warns rather than fails, because from metadata alone a genuine traffic dip is indistinguishable from under-capture; the log line says how to confirm.

Validated against live metadata:

  • replaying the three pre-repair counts flags all three (ratios 0.144 / 0.084 / 0.241 against a 0.5 cutoff)
  • zero false positives across ~350 current partitions

Testing

55 passed, 2 skipped on Python 3.14, including 14 new tests across test_output_table_wiring.py and test_stale_merged_check.py. ruff check and ruff format --check clean.

🤖 Generated with Claude Code

…step-01

Three fixes surfaced by the 2025-08 backfill.

1. google-cloud-bigquery-storage was neither declared nor installed, so every
   tomography download fell back to REST and built result frames from JSON rows
   instead of Arrow buffers. On 2025-08-01 that meant 13.7M node-edge rows in
   19.5 min, and the retained frame plus Phase 4's allocations OOM-killed the
   worker at 22.1 GB inside a 28 GB cgroup — failing the whole chunk. 2026-05-27
   survived the same path at 11.2M rows, so the pipeline has been running just
   under this ceiling. Also pins pyarrow explicitly rather than relying on it
   arriving transitively via pandas/db-dtypes. This affects every nightly, not
   just backfills.

2. SQL_FILES and OUTPUT_TABLES were zipped positionally, which forced one list
   to serve both the per-step resume map and the --delete-first delete list.
   That capped the delete list at "one table per SQL file", so the three
   Phase-D correlation tables — written by Python, with no 1:1 SQL file — could
   never be added without raising. Every re-run this week needed a manual
   pre-delete or it appended a second copy of that date's rows. Split into an
   explicit SQL_FILE_TO_OUTPUT_TABLE dict, DERIVED_OUTPUT_TABLES, and
   DELETE_TABLES, with an import-time check replacing strict=True.

   giga_meter_measurements is deliberately excluded: it accumulates history
   under first-writer-wins and part of it was recovered by hand, so a delete
   not followed by a successful 04 loses traces permanently.

3. find_stale_merged_partitions() detects step-01 partitions that are present
   and non-empty — invisible to step_already_done() — but hold a fraction of
   the available measurements. Three such dates (2026-05-27, 2026-07-13,
   2026-07-17, at 8.1%/4.5%/13.7% of source) were found only by hand; nothing
   in the pipeline saw them. Compares each partition to its neighbour median,
   excluding itself. Reads INFORMATION_SCHEMA.PARTITIONS only, so it is free
   and runs after Phase A on every invocation. Warns rather than fails: a real
   traffic dip is indistinguishable from under-capture using metadata alone.

   Validated against live metadata — flags all three known-bad dates when their
   pre-repair counts are replayed (ratios 0.144/0.084/0.241 against a 0.5
   cutoff) and zero false positives across ~350 current partitions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Burdantes
Burdantes merged commit bf8ce49 into main Aug 8, 2026
4 checks passed
@Burdantes
Burdantes deleted the fix/tomography-oom-and-delete-tables branch August 8, 2026 20:52
Burdantes added a commit that referenced this pull request Aug 9, 2026
* fix: REST fallback must not re-create the BigQuery Storage client

_read_df catches a Storage Read API failure and retries with a bare
query_job.to_dataframe(). That argument defaults to create_bqstorage_client=True,
so whenever google-cloud-bigquery-storage is importable the fallback silently
builds its own Storage client — re-attempting the exact thing it is falling back
from and re-raising the identical error.

This was latent while the package was absent: the import failed first, so the
retry had nothing to auto-create and REST genuinely worked. Installing the
package in #18 exposed it, and every Phase D died with

  403 ... does not have 'bigquery.readsessions.create' permission

logged immediately after "using REST download". Verified against the live
identity: to_dataframe() fails, to_dataframe(create_bqstorage_client=False)
succeeds.

Note this is a per-identity IAM question as well as a packaging one. The
pipeline's ADC identity is loqman@measurementlab.net, which lacks
roles/bigquery.readSessionUser on mlab-collaboration. Until that is granted the
Arrow path is unavailable and this degrades to REST — which works, but is slower
and much heavier: a 13.7M-row download OOM-killed the worker at 22.1 GB inside a
28 GB cgroup and failed the 2025-08 backfill chunk.

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

* test: make _read_df fallback tests independent of ambient credentials

The first version sequenced job.to_dataframe side effects assuming
BigQueryReadClient() would construct successfully. That holds on a machine with
application-default credentials but not in CI, where construction raises first
and the Arrow call never happens — so the side_effect list lined up differently
and three tests failed. They passed locally for the wrong reason.

Drive both branches explicitly with a fake google.cloud.bigquery_storage module
whose client constructor can be made to raise, covering the two real-world
cases (missing ADC, and the 403 when the identity lacks
bigquery.readsessions.create). Verified passing both with ambient ADC and with
credentials stripped.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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