fix(search): bound Search retirement pages so the migration finishes under its statement timeout - #8460
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
|
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…der the statement timeout A retirement page read 25,000 IDs and updated or deleted every target row among them in one statement. Retiring a document is a non-HOT update that writes every index on `document`, and a deleted chunk cascades into its projections, so on a KB that dominates the table a page's write cost, not its scan, outran the two-minute statement timeout and failed the deploy migration. Each page now mutates at most a row limit of its target rows. A page that reaches the limit advances the cursor only to its last mutated row, and already-retired documents never spend the limit. The limit starts at 2,000, halves after a slow page or a statement timeout (the timed-out page rolls back with its cursor and is retried), and doubles after a fast full page. The completion rechecks, which walk every captured KB once, run with a 30-minute timeout. The retirement stays idempotent and resumes from its saved cursor.
…irement pages Only a statement timeout from a page's mutating statement now halves the row limit and retries the rolled-back page. Any other timeout, such as a completion recheck, fails the run at once instead of repeating the same statement at every smaller limit. Pages are timed around the whole call, commit included, so the synchronous-replication wait counts toward the slow-page threshold. Each page is followed by a pause as long as the page, up to five seconds, and the row limit is capped at 8,000. Phase changes no longer adjust the limit. The progress log now carries the phase, cursor and rows mutated, and the migration logs slow-page halvings, phase changes and the start of the completion recheck.
…limit A run of already-retired Search documents longer than the row limit must be crossed in one page. The test counts documents-phase statements and fails if the page filter on unretired rows is removed.
…time out resumed rechecks like completion A capped page re-reads its scan from its last mutated row, so a fixed 25,000-ID window re-read most of the same IDs on every page once the row limit shrank. Each page now reads at most four IDs per row of its limit, capped at 25,000, and any fast page doubles the limit so sparse stretches widen the window again. A retry that finds retirement already complete revalidates every captured KB, as completion does, so it now runs under the same 30-minute timeout instead of the two-minute page timeout.
cf63591 to
f202d9b
Compare
…timed out, and pause after a timeout A timed-out page halved the row limit, but one fast page doubled it straight back, so the run alternated between the size that timed out and half of it, rolling back a full statement-timeout page each time. The limit now grows only up to half of the smallest size that timed out, and a timed-out page is followed by the same pause as any other page.
f202d9b to
1e7d6fd
Compare
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
Summary
The Search retirement script migration (
0029_retire_all_search_embeddings, which runs the0027retirement code) failed during a deploy withcanceling statement due to statement timeout.Each retirement page read 25,000 IDs and updated or deleted every target row among them in a single statement. The slow part is the writes, not the scan:
user_excludedappears in partial-index predicates, so every index ondocumentgets a new entry.When one captured knowledge base holds most of a table, nearly every row in a page needs to be written. Those writes, plus checkpoint full-page writes and the synchronous-replication commit wait, pushed a page past the migration's own two-minute
statement_timeout. The query plan is fine: the scan portion of a page is a primary-key range scan that takes a few seconds.Changes
search-embedding-retirement.mdis updated to match.The retirement stays idempotent and resumes from its saved cursor.
0029is edited in place, following the earlier0027fixes: the runner records script migrations by name only, and deployments that already completed0029do not need to run it again.Tests
Real-PostgreSQL integration tests in
0027_retire_search_embeddings.integration.ts:Unblocking the release
The remaining cleanup, plus the index rebuild and vacuum that follow it, will likely take longer than the migrations job's five-hour limit. The retirement is resumable and safe to kill at any point:
REINDEX CONCURRENTLYleaves invalid_ccnew/_ccoldindexes, which the next run drops before retrying.script_migrationsuntil retirement and maintenance both finish.Option A: run it out of band first (recommended, so it does not hold up deploys)
Merge this PR to
staging.From a checkout that contains this change, run the standalone entry point. It uses the same resolution as the deploy runner (
MIGRATION_DATABASE_URL, falling back toDATABASE_URL):bun install --frozen-lockfile MIGRATION_DATABASE_URL='<direct, non-pooled writer DSN>' \ bun --no-env-file run packages/db/script-migrations/0027_retire_search_embeddings.ts--no-env-filestops Bun from loading a local.envinto the process.0029_retire_all_search_embeddings, and on success records0029and the names it supersedes, exactly as the deploy runner would.While it runs, do not start the migrations workflow. The standalone command does not take the deploy runner's advisory lock, so a concurrent deploy would retire pages in parallel. That is correct, because pages serialize on the progress row, but it doubles the load, and the second maintenance worker fails on the maintenance lock. Stop the standalone run before any deploy, then resume it afterwards.
Once
0029_retire_all_search_embeddingsis recorded, cut the release tomain. The migrations job skips the completed script migration.Option B: let the deploy run it
staging.mainthat includes it. Re-running the failed migrations workflow run will not work, because it checks out the old commit.0029from its saved cursor. Pages completed before the failure stay committed and are not redone.In both cases, keep writers to the captured Search knowledge bases stopped until
0029_retire_all_search_embeddingsis recorded. Progress can be inspected withSELECT * FROM search_embedding_cleanup_progress, and the migration logs its phase, cursor and rows mutated every ten pages.Test plan
0027_retire_search_embeddings.integration.tsagainst PostgreSQL 17 with pgvectorpackages/dbunit testsbun run type-check(packages/db)bun run lintbun run check:audits