Repository navigation
DM-56864: Stage chunks with BigQuery load jobs - #69
JeremyMcCormick wants to merge 30 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #69 +/- ##
==========================================
+ Coverage 80.83% 80.90% +0.06%
==========================================
Files 54 55 +1
Lines 2812 3000 +188
Branches 258 284 +26
==========================================
+ Hits 2273 2427 +154
- Misses 448 479 +31
- Partials 91 94 +3 ☔ View full report in Codecov by Harness. |
295a7d4 to
38bb1ad
Compare
38bb1ad to
f956d6a
Compare
93b0d42 to
ebee6e2
Compare
d3fc94c to
4ea9bbb
Compare
The new DatasetBuilder._convert_dia_tables converts DIA tables and adds the geo_point column to each, and the staging, internal and public builders use it. Staging DIA tables now have geo_point, set when a chunk is staged, and the promoter copies it from staging instead of computing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7
Staging, promotion and cleanup all select staging rows by chunk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7
The chunk ingest bucket holds only chunk data, so each chunk's folder is named after its ID at the top of the bucket instead of under a prefix. Removes the object_prefix config field, its --object-prefix option and its init_database parameter. Config files that still set it are accepted, and the key is ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7
The dataset builder test gets its own landing dataset name, so its cleanup can't drop the real ppdb_landing dataset, and two tests register their cleanup before creating datasets, so a failed setup doesn't leave datasets behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7
error_handling.py imports it directly to classify Cloud SQL connector errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7
dfb0f85 to
bbb4f9a
Compare
ChunkStager stages a replica chunk by loading its Parquet files into per-chunk landing tables and then copying them into the staging tables in one transaction, setting geo_point and checking each table's row count against the manifest. It replaces the Dataflow staging job. - Chunk status: new retrying status; status_time, error_message and staging_job_id chunk table columns; update_chunks sets status_time. Status is checked and written with the existing PpdbBigQuery methods. - Error handling: no raw errors leave stage_chunk. Temporary errors, including BigQuery quota and rate limits, set the chunk to retrying and raise ChunkStagingRetryableError; any other error sets it to failed and raises ChunkStagingError. If waiting for the staging transaction fails, the job is cancelled and the chunk failed, so a retry can't stage it twice. - BigQuery API requests are retried for up to retry_timeout; failed jobs are never resubmitted (job_retry=None). - Database errors: DATABASE_ERRORS adds the Cloud SQL connector errors that SQLAlchemy doesn't wrap, and handle_database_error classifies them as retryable or not. - Landing dataset (DatasetType.LANDING, TableRefs.landing), created by Terraform. - The uploader drops the unused dataset field from the stage chunk message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7
9b7f8a4 to
3fa6f55
Compare
A credential refresh that gets a 5xx from the token endpoint raises RefreshError(retryable=True). handle_database_error treated it as permanent, so a short token endpoint outage while opening a Cloud SQL connection failed the request instead of letting Pub/Sub retry it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6bb31df to
24c4032
Compare
ReviewReplacing the Dataflow job with load jobs and a single transaction is a clear improvement. The design holds up well. The conditional claim stops concurrent deliveries, and the Findings
🤖 Generated with Claude Code |
After jobs.get reports the job done with no error, QueryJob.result() still calls jobs.getQueryResults. If only that call failed, the error was re-raised as the job's own, so the chunk was set to retrying and the next attempt failed it because its rows were already staged. Now the job ID is returned and the error is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
… chunk The claim cleared error_message but not staging_job_id. An attempt that failed before submitting its transaction left the earlier attempt's job ID on the chunk, although the field is the job of the last attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
The connector's own exceptions subclass Exception directly, and SQLAlchemy doesn't wrap errors raised while opening a connection, so they escaped the database error handling. They are now in DATABASE_ERRORS. DnsResolutionError and RefreshNotValidError are retryable, and the configuration and closed connector errors are not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…limited A RetryError from client.query was treated as an unknown outcome, so a run of 429 or rateLimitExceeded refusals that outlasted the retry timeout failed the chunk. If the RetryError's cause is such a refusal, the cause is raised, and the outer handler sets the chunk to retrying. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Three tests with BigQuery and the PPDB mocked: a staging job that may still be running fails the chunk, a job that committed but whose result couldn't be read stages it, and a temporary error before the transaction retries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…f faking its query Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…n the stager error tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…f patching the class Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
|
@andy-slac @fajpunk I resolved the issues raised in the Copilot review. I'm still working on improving the tests in |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
|
Reply to the review above:
Notes on the tests
Possible follow-up work
🤖 Generated with Claude Code |
|
@andy-slac @fajpunk I think the tests are good enough for now after I made a few improvements. I won't make any other changes until there is a review. There's still a lot of uncovered code, but so much mocking would be required to test those paths that I don't think it is really worth it. |
fajpunk
left a comment
There was a problem hiding this comment.
This is great, I love getting rid of the dataflow and associated Google Cloud resources.
I admit that I get lost in the error handling
I've made my best effort in the error handling to:
But I know that it is starting to look a lot like Java code now. :) |
andy-slac
left a comment
There was a problem hiding this comment.
Looks OK to me. Exception handling is very dense and hard to follow. General question - in this new reimplementation with retries is there a possibility that chunks are ingested out of order? I worry is that chunks with update records implicitly depend on all previous chunks to be already ingested.
Chunks can be staged out of order as the function is not blocking and multiple copies can be running, but the promotion process should ensure that all prior chunks have already been promoted, including application of their updates. Updates from the whole set of chunks being promoted are applied at once to the new prod table. So it should not be possible for an update to be applied out of order or when a record it depends on is missing. |
…or_handling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…one dict of file names and row counts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
…ult error Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Agreed, the error handling got pretty byzantine, though much of that complexity is necessary for trapping and categorizing various types of errors. (We don't want to permanently fail a chunk if there is a brief connectivity issue within Google Cloud, for instance.) Some of this should be cleaned up on DM-56938 where I plan to have the |
…e, as retryable Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
… of files Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJ51nCVBE3sWQhjpm83YEC
Summary
Replaces the Dataflow job that staged replica chunks with BigQuery load jobs run from a new
ChunkStager. Thestage_chunkCloud Run function uses the stager (lsst-dm/ppdb-cloud-functions#38).Changes
ChunkStager(bigquery/chunk_stager.py): stages one chunk at a time withstage_chunk(logger, chunk_id, folder)uploadedorretrying, and raisesChunkStatusErrorotherwise (orChunkStagingErrorif the chunk doesn't exist); it then sets the chunk tostaging_check_not_already_staged)_copy_to_landing)geo_pointand checking each table's row count against the manifest with anASSERT(_copy_to_staging)stagedstage_chunk; it raises onlyChunkStatusError,ChunkStagingErrororChunkStagingRetryableErrorstagingleaves its status unchanged; it raisesChunkStagingRetryableErrorifhandle_database_errorclassifies it as temporary, andChunkStagingErrorotherwisequotaExceeded,rateLimitExceeded), set the chunk toretryingand raiseChunkStagingRetryableError; any other error, including an unexpected one, sets it tofailedand raisesChunkStagingErrorfailed, so a retry can't start a second transaction and stage the chunk twiceChunkStagingError; the chunk is left instagingretry_timeout, with the client's default delays; failed jobs are never resubmitted (job_retry=None), so the recorded job ID is the one that rancheck_timeout,job_timeout)bigquery/ppdb_replica_chunk_extended.py)retryingstatus, and newChunkStatusErrorstatus_time,error_messageandstaging_job_idfieldswith_new_error_messageandwith_new_staging_job_id, like the existingwith_new_statusandwith_new_gcs_uriUpdatableFieldmoved here fromppdb_bigquery.pyPpdbBigQuery)status_time,error_messageandstaging_job_idcolumnsupdate_chunkssetsstatus_timewhen it changes the statusDatasetType.LANDING,TableRefs.landing). It has no tables of its own, so it is created by Terraform, not by the dataset buildergeo_pointcolumn and are clustered byapdb_replica_chunkChunkPromotercopiesgeo_pointfrom staging instead of computing itChunkUploader,PpdbBigQueryConfig)object_prefixsetting and--object-prefixoption are removeddatasetfield is dropped from the stage chunk messagegcp/error_handling.py)DATABASE_ERRORS:SQLAlchemyErrorplus the authentication,aiohttpand network errors that the Cloud SQL Python Connector can raise when opening a connection, which SQLAlchemy doesn't wraphandle_database_errorclassifies all of them: dropped connections, network errors, token endpoint failures and 429/5xx Admin API responses are retryable; other errors are notaiohttpis now a listed dependencytests/test_chunk_stager.py: integration test forChunkStager, and pickling of its exceptionsDeployment notes
status_time,error_messageandstaging_job_idcolumnsgeo_pointcolumnstagingby a staging attempt that died has to be reset by handRelated
🤖 Generated with Claude Code
https://claude.ai/code/session_01VJ51nAbGJ4BBgwpjau1VX7