fix(knowledge): run the projection source/ACL backfill in the background instead of the deploy - #8058
Conversation
…und instead of the deploy Script migration 0021 filled the ranking projections' connector_id and acl columns synchronously, 500 rows per page under a 60s statement timeout. On embedding_search every filled row is re-inserted into each HNSW index, so a page's cost is index maintenance rather than the plan: one page ran past the timeout and the migration, and the deploy, failed. The migration now installs the triggers and builds the indexes only, both idempotent, and the backfill runs from a Trigger.dev task in keyset pages of unfilled rows, paced with a pause between pages and chained across bounded runs. Search does not depend on it: an unfilled row passes the on-row candidate predicate and is decided at hydration under the full document predicate, exactly as every candidate was before the columns existed. Claude-Session: https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX
|
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 10 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
… start the backfill from the outbox An unfilled row no longer passes the on-row candidate predicate outright: it is decided on its document under the resolved candidate predicate, the join per candidate every row paid before the columns existed, so the bounded candidate pools and the exact slice hold only rows that hydration will keep. A partial index on the unfilled rows keeps that branch, and the backfill's keyset pages, an index probe. The migration also leaves one outbox event whose handler starts the backfill task, so it runs after every deploy without an operator. Claude-Session: https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX
|
@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 12 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…out a worker fills the projections Without a Trigger.dev worker the outbox handler no longer detaches the backfill and completes the event; it runs one bounded slice per outbox run and yields with continueOutboxHandler until both projections are filled, so a restart loses at most one slice and the event is never marked done ahead of the work. The index builds run on one reserved connection, so the session-scoped lock timeout covers every build and its reset. Claude-Session: https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX
|
@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 13 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…ill is started The outbox event and its slice-and-defer handler are gone; nothing else in the repo starts background work that way. The fill is started from the app side as the table backfill is: tasks.trigger on the Trigger.dev worker when one is configured, runDetached in-process otherwise, with the manual script as the operator path after a deploy. The registered migration is now 0022_projection_source_acl_backfill, which supersedes 0021 so a database that already recorded the synchronous shape still gains the unfilled indexes. The page statement timeout is exported so a caller bounding a run can leave it as headroom. Claude-Session: https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX
|
@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 15 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…positive integer The page size is interpolated into the page statement and a page of nothing would report the projection filled, so a payload that asks for either is refused before the first page rather than quietly reshaped. Claude-Session: https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX
|
@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 16 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…between pages A pause that crossed the budget still let the next iteration open a page; the deadline is now checked after the pause, so a bounded run stops before its next page rather than after it. Claude-Session: https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
Summary
0021_embedding_search_connectorbackfilled the ranking projections'connector_idandaclcolumns synchronously, 500 rows per page under a 60s statement timeout. Onembedding_searchevery filled row is re-inserted into each HNSW index, so a page's cost is index maintenance rather than the plan: one 500-row page ran ~68s cold (~130 ms per row), the first page hit the timeout (57014), and the migration and the deploy failed with it. No page size or timeout makes a synchronous backfill of an HNSW-indexed projection fit inside a deploy job.projectionCandidateAccessConditiondecides a row whoseacl IS NULLon its document —acl IS NULL AND EXISTS (SELECT 1 FROM document WHERE document.id = <row>.document_id AND <knowledgeCandidateAccessConditionForConnectors>)— the per-candidate join every row paid before the columns existed, so bounded candidate pools (vector walk, exact slice, Tin window) hold only rows hydration will keep. A filled row is still decided on its mirrored columns.knowledgeCandidateAccessConditionForConnectorsat hydration is unchanged and remains the security gate. Covers both the vector walk and the Tin keyword window, which share the function.0022_projection_source_acl_backfill, whichsupersedes0021 (the shape 0016 uses over 0015): it installs the triggers and builds the indexes only (CREATE INDEX CONCURRENTLY IF NOT EXISTS, on one reserved connection so the session-scoped lock timeout covers every build; plus a partial index on(id) WHERE acl IS NULLper projection so the unfilled branch stays an index probe beside the ACL GIN and the backfill's keyset pages read from it). Both are idempotent, so the re-run on the next deploy completes quickly, and a database that already recorded 0021 still gains the unfilled indexes. The 0021 file keeps the shared helpers and its directdb:pushentry point, which still backfills inline.backfillProjectionSourceAclstays exported and is now resumable: keyset pages of 100 unfilled rows, each its own transaction with a per-page statement timeout (exported asPROJECTION_SOURCE_ACL_PAGE_TIMEOUT_MS), one statement per page with scalar binds only, a pause between pages (sleep), a time budget that returns the cursor, progress logging, and anANALYZEon completion.projection-source-acl-backfillTrigger.dev task (apps/sim/background/), modeled ontable-backfill: one run at a time, fills for up to an hour, then triggers its continuation from the cursor until both projections are filled. Safe to start repeatedly — a run only fills rows still unset.bun apps/sim/scripts/backfill-projection-source-acl.ts, which enqueues the task the way the table backfill is enqueued (tasks.trigger+ region when a Trigger worker is configured,runDetachedof the same runner in-process otherwise). Documented in the migration's TSDoc as the operator path.Type of Change
Testing
predicate.test.ts: renders the on-row predicate with the document branch for unfilled rows;predicate.postgres.test.ts(real database): an unfilled chunk of a document the caller cannot read is refused on the row, unfilled chunks of readable documents are admitted;queries.test.ts: the Tin window carries the branch. Each fails with the predicate change reverted.0021_embedding_search_connector.postgres.test.ts:upre-runs cleanly and builds the unfilled indexes; the backfill fills only unset rows in pages, skips a chunk whose document changed, stops at its budget with a cursor and resumes after it; a rerun writes nothing. The index and budget tests fail with those changes reverted.projection-source-acl-backfill.test.ts: projection order, cursor resume, budget return, connection close on failure, Trigger vs detached dispatch.packages/dbregistry and push tests,bun run type-checkinapps/simandpackages/db,bun run lint,bun run check:audits(47 audits),docs-manifest:check, block-registry check: all pass.Checklist
https://claude.ai/code/session_01XU6c7pKRpa5CMoMHKDdqxX