Skip to content

Ray uploads build their tables in the production database and publish them by rename - #267

Merged
fcollman merged 4 commits into
masterfrom
ray-build-in-production
Oct 29, 2026
Merged

fcollman merged 4 commits into
masterfrom
ray-build-in-production

Conversation

@fcollman

@fcollman fcollman commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Supersedes #266. This branch includes its commit: the GCS transfer and the CLUSTER skip, which the Celery staging path still uses.

Why

A Ray bulk upload today is built in staging, then copied into production. On minnie's 337M-row synapses_300m_ray2 that meant:

  • the annotation indexes were built twice (~5.3h each);
  • the tables were copied again;
  • both tables (~166 GB) were on the instance twice until cleanup.

The copied annotation table also came out unclustered (id correlation −0.0006), and its id sequence was left at 1.

What changes (Ray path only)

  • Build in production under temporary names. The tables are upl<hash6>_<table> and its segmentation table, with no metadata rows until publish. The table's metadata travels in the job's params.

  • Annotation table:

    1. Cloud SQL import into the temporary table.
    2. Primary key.
    3. CLUSTER by id, only when the source's ids are unsorted (correlation below UPLOAD_CLUSTER_SKIP_CORRELATION, 0.99).
    4. The remaining indexes.
  • Spatial lookup: runs against the temporary table. It takes the schema from the job, since there is no metadata row.

  • Segmentation table: clustered by id, then indexed, before publish.

  • Publish (one transaction):

    • re-checks that the final names are free;
    • renames the tables, indexes, constraints and sequence to the names index_manager expects;
    • setvals the sequence to max(id);
    • inserts both metadata rows.

    Tables, indexes and metadata become visible together. Publish is idempotent on resume (a table comment marks the job).

  • Yields to frozen materialization. create_analysis_database records a clone request in upload_state.clone_requests. It waits until no upload holds the database: each driver pauses its new work, lets in-flight statements finish and drops its connections. The wait is bounded by UPLOAD_CLONE_WAIT_SECONDS (default 2h). Clones made mid-upload drop the upl… tables.

  • Failure or cancel drops only the job's recorded upl… tables.

  • Name-length check: a job is refused at start if any derived temporary or final index name would exceed 63 bytes.

Celery uploads keep the staging path.

Expected effect on a 337M-row upload

About 22h → about 15.5h, because nothing is transferred and no index is built twice. There is one copy on disk instead of two.

Testing

  • Full suite: 741 passed, 2 skipped, on a fresh PostGIS.
  • New production-build tests:
    • load, lookup, finish and publish, with sources both in id order and shuffled. They assert clustering, the spatial bounds, points returned from the bbox, and ctid order;
    • publish idempotency and a name collision;
    • the clone gate.
  • ltv7, 1M-row runs (vBuildInProd3), checked against the earlier staging-path run:
    • 1,020,000 rows, all ids distinct;
    • the same zero supervoxel and root counts;
    • 6 + 3 indexes with the expected names;
    • both tables at id correlation 1.0;
    • sequence = max(id);
    • metadata valid, no upl leftovers.
  • Frozen materialization triggered mid-lookup on ltv7:
    • the upload released the database 24s after the request;
    • the clone completed and contained no upl tables;
    • the upload resumed and published normally.
  • ltv7, 3-part file (350 MB, 4,795,306 rows): finished in 20.5 min, after the submitter moved to standard-pool (chart 5.33.2).
    • rows equal distinct ids in both tables; every segmentation id is in the annotation table;
    • no null or zero supervoxel ids; 18 pre and 12 post zero root ids;
    • 6 + 3 indexes with the expected names;
    • both tables at id correlation 1.0;
    • sequence = max(id);
    • metadata valid, no upl leftovers.

Before a large run

  • UPLOAD_CLONE_WAIT_SECONDS should exceed the longest single statement. A CLUSTER of a 337M-row annotation table can take hours.
  • CLUSTER needs about the table's size in free disk.

🤖 Generated with Claude Code

fcollman and others added 4 commits October 28, 2026 23:34
…lready in id order

The 337M-row minnie upload failed its transfer twice (2026-10-28): pg_dump | psql
streams 137 GB through the driver, and psql had a fixed 1200 s timeout.

The Ray driver now transfers with Cloud SQL imports and exports (method
"cloudsql", transfer_table_using_cloudsql), run by the database server:
- the annotation table is imported from the processed CSV it was imported into
  staging from, which nothing changes afterwards (exported from staging if the CSV
  is gone);
- the segmentation table is exported from staging in id order, then imported;
- each export and import is recorded on the job (upload_cloudsql_ops, one kind per
  operation), so a rerun skips what finished, waits on or adopts what was started,
  and reruns what failed; the truncate only runs before a fresh import;
- the row count is checked against staging before the indexes are built;
- cleanup deletes the exported CSVs with the processed one.

Celery keeps pg_dump | psql (UPLOAD_TRANSFER_METHOD), with the psql timeout now
UPLOAD_TRANSFER_PSQL_TIMEOUT_SECONDS, default 6 h.

cluster_staging_tables skips a table whose id correlation (after ANALYZE of id) is
at least UPLOAD_CLUSTER_SKIP_CORRELATION (0.99). The annotation table is imported
in id order, so its CLUSTER (6h13m on that upload, mostly rebuilding GIST indexes)
changed nothing.

Needs the Ray service account to have cloudsql.instances.export and the Cloud SQL
service account to be able to write to the upload bucket (terraform-google-cave).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… them by rename

The staging path built every table twice: the annotation indexes in staging for the
spatial lookup and again in production after copying, with both copies on the
instance's disk until the end (the 337M-row minnie upload, 2026-10-28: ~166 GB twice,
~6.5 h of ~22 h spent re-importing, re-indexing and re-exporting).

New Ray jobs (params.build_in_production) build in the aligned volume database under
temporary names (upl<hash>_<table>, production_build.py), with no metadata rows, so
nothing in production lists them:
- load_annotations: the table from its model, the Cloud SQL import, its indexes;
- spatial_init/spatial_chunks: get_materialization_info takes the table's metadata
  directly (annotation_metadata), the segmentation table is created there;
- finish_tables: the segmentation table clustered by id, its indexes, ANALYZE;
- publish, one transaction: tables, indexes, constraints and the id sequence renamed
  to the names index_manager expects, the sequence set to max(id) (it stayed at 1),
  then the annotation and segmentation metadata rows. A name taken meanwhile raises
  PublishConflict and keeps the built tables. Cancels are refused from publish on.
A failed or cancelled job (and the cleanup CronJob) drops only tables carrying its
marker comment and no metadata row. Jobs started before keep the staging stages.

A frozen version terminates every connection to the live database to copy it, so
uploads give way: create_analysis_database records a clone request
(upload_state.clone_requests) and waits, up to UPLOAD_CLONE_WAIT_SECONDS (2 h), until
no upload holds the database (production_holds). The driver starts nothing new there
while a request is open, drains the chunks in flight (run_bounded should_pause),
closes its connections and resumes after the copy; workers close theirs after every
chunk. The gate is on wherever the upload state database exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he job, not a metadata row

The chunker's bounds query and each chunk's point query looked the schema up from
annotation_table_metadata, which a temporary table does not have until publish. The
bounds came back empty, the chunker fell back to one zero-volume chunk, and that chunk
failed. Both now use the schema spatial_init already has, and spatial_init stops when
the bounds can't be found instead of going on with a chunk that finds nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ce's ids are unsorted

A source file with its own ids keeps the file's order through the processed CSV and
the import; the minnie synapse files' ids are unsorted, so the 1M ltv7 test table was
published with id correlation 0.0001. load_annotations now builds the primary key,
clusters on it if the id correlation is below UPLOAD_CLUSTER_SKIP_CORRELATION, and
only then builds the other indexes, so the CLUSTER rewrites one index instead of six.
CLUSTER also gets the upload's maintenance_work_mem for its sort.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@fcollman
fcollman force-pushed the ray-build-in-production branch from 6d0f604 to da40993 Compare October 29, 2026 06:44
@fcollman
fcollman merged commit fa391c2 into master Oct 29, 2026
1 check passed
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