improvement(tables): bump rows version once at commit and serialize same-value unique writes - #8335
waleedlatif1 wants to merge 7 commits into
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 9 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
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 9 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
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.
No issues found across 9 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
1 similar comment
|
@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 8 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
This comment has been minimized.
This comment has been minimized.
dba8085 to
ecb119e
Compare
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
|
@greptile re the outside-diff note on service.ts:2325 (sparse filters may time out): this PR doesn't change that query. The capped update/delete limit branches are byte-for-byte the staging queries; the earlier fenced-CTE version was removed before this head, because it regressed dense filters. So there's no 'previous GIN-first query' to lose relative to staging. The only thing this PR changes for these plans is the dependency statistic, which corrects the per-table row estimate the planner uses to choose between the ordered walk and the tenant GIN. In the A/B (4,000 tables, skewed sizes, 2.1M rows), rare and late-matching filters had no plan trap after the statistic, and large-table |
|
You're right. I'm withdrawing the outside-diff note; no action is needed for |
|
@greptile re the concurrent-unique-insert risk in the summary: this PR doesn't change unique-constraint behavior. insertRow, batch insert, and upsert's insert branch are byte-identical to staging. An earlier revision moved the unique check under the per-table append lock, which closed the race but serialized every insert into a table with a unique column (2.5–10× slower under concurrency). That revision was reverted, so this PR doesn't touch the race either way. The race exists on staging today; fixing it properly needs per-value locks (table, column, normalized value), and that goes in a separate PR with its own benchmark. This PR only changes the rows_version trigger, the row FK's deferrability, and the planner statistic. |
|
You’re right: the concurrent unique-insert race is not introduced or changed by this PR. The changed application code contains no insert, batch-insert, or upsert-branch edits, and the migration only changes the rows-version triggers, FK deferrability, and planner statistics. I’m withdrawing that critical-risk finding from this review; it should be addressed separately with the per-value locking design and its own concurrency benchmarks. |
…h per-value advisory locks
…ck unique columns on every import batch Lower the per-transaction value-lock cap to 64 so concurrent writers cannot exhaust the shared lock table; larger writes keep the exclusive column-lock fallback. Import batches now take the unique-column locks and run their unique check inside their transaction, batchUpdateRows rejects two updates writing the same unique value and locks and checks only the unique columns each update changes, and the 0390 comment describes the mechanism only.
1d520f6 to
97cb317
Compare
|
@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
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
… schemas stay within the lock budget
|
@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
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Fix all with cubic | Re-trigger cubic
…ver hides a batch duplicate
|
@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.
No issues found across 13 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
Summary
bump_user_table_rows_versionran as a statement-level trigger inside every row-update statement, so each writer locked its table'suser_table_definitionsrow across the rest of its transaction (executions patch, provenance write and capture, synchronous commit), and concurrent writers to one table queued behind each other. Provenance-only updates bumped it too, even though they don't change the table's contentDEFERRABLE INITIALLY DEFERREDconstraint triggerAFTER UPDATE OF data, order_key, with aWHENon real changes. It bumps once per table per transaction at commit, deduped through one transaction-local setting. INSERT/DELETE statement bumps are unchanged. The only readers (the CSV snapshot cache and mount safety) read outside write transactions, so a commit-time bump keeps their read, scan, re-read checks intactuser_table_rows.table_id→user_table_definitionsFK becomesDEFERRABLE INITIALLY DEFERRED(catalog-only). The write path updates a row twice in one transaction (data, then the provenance marker, which the rolling-deploy demote trigger requires to be separate), and the second update re-checked the FK, taking a key-share lock on the definition row that the commit-time bump then waited behind.ON DELETE CASCADEstays immediate, and nothing catches this FK's error inlineBEGIN, so it stays atomic when an earlier migration in the same deploy used aCOMMITbreakpointCREATE STATISTICS (dependencies) ON workspace_id, table_idplusANALYZE, so the planner stops underestimating per-table row counts and servesdata @>filters from the tenant GINuser_table_unique_value) before its check: the table's unique lock shared, then an exclusive lock per value, keyed on the same normalization the check uses (so'8'and8share a key). Writers of different values, on any column, never wait on each otherjsonobject or array value, or a transaction that would take more than 64 value locks, takes the table's unique lock exclusively instead. Whole-table replace and every import batch always do. A transaction holds at most 1 + 64 unique locks, however many unique columns the table has. 64 is the default per-connection budget of Postgres's shared lock table, so wide schemas and large batches can't exhaust it and fail unrelated queriesupdateRow,batchUpdateRows, and single-row filter updates now runs inside the write transaction, under the locks.batchUpdateRowsalso rejects two updates in one batch that set the same value, and it locks and checks only the unique columns each update changesType of Change
Testing
A/B against staging on identical fresh DBs, real service functions, runs alternated:
updateRowwriters on one table: 943–1071 → 1585–1606 ops/s, p99 77–107 → ~18 ms, lock wait 521–525 → 4–8 backend-sFilter queries on 4,000 tables with skewed sizes: large-table
data @>filters 1.7–5.7 → 0.5 msMigration-during-writes: 0 missed bumps across 3 runs (without the
BEGIN, 40/40 missed)row-writes.integration.ts: data and reorder edits bump once per transaction, including two tables in one transaction; provenance-only, timestamp-only, no-op, and executions-only writes don't bump; a second writer commits while the first is uncommitted; the snapshot re-keys when a writer commits mid-materialization. These fail on the old triggerFresh DB migrate plus replay, secret-provenance and table integration suites, table unit suites, type-check, lint,
check:audits, andcheck:migrations origin/stagingpassUnique races (deterministic, driven by
pg_lockswait states), all ending with one row:'8'vs8updateRowvs insertDifferent values don't wait on each other. Each race test fails on the pre-fix code
Unique-column throughput vs staging is within noise: distinct-value inserts, batch inserts, and batch upserts at 10 and 50 writers.
updateRowkeeps the gains aboveMigration is 0390 on top of staging
Checklist