Skip to content

fix(tables): drop the per-table row-order lock from inserts - #8811

Merged
TheodoreSpeaks merged 2 commits into
stagingfrom
fix/drop-row-order-lock
Oct 8, 2026
Merged

TheodoreSpeaks merged 2 commits into
stagingfrom
fix/drop-row-order-lock

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Inserts no longer take the per-table row-order advisory lock (user_table_rows_pos). It was held until commit, so one insert stuck waiting on synchronous replication made every other insert to that table wait behind it and fail at the 3 s lock_timeout. With fix(tables): log row writes to the change log instead of locking the table definition row #8765, a row write no longer holds any lock shared by the whole table.
  • Append keys: appendKeys mints inside the integer range after the last key, narrowed to a random sub-range (32 random halvings). Two appends that read the same last key never collide, a batch stays contiguous instead of interleaving with another writer's, and keys don't grow across appends (8–9 characters after 2,000 sequential appends).
  • Positions stay max + 1, read without a lock, so concurrent appends can share one. The run dispatcher pages on (position, id). When a window is full, it also takes the remaining rows at the last position, so a tie split across windows is never skipped. No schema or API change: the cursor is still the last position processed.
  • Positional inserts take the next key strictly above the lower neighbour, the same rule the neighbour-anchored path already used, so a tied key no longer throws a >= b. A position past the last row now appends; it used to mint a0 and land the row near the top.
  • Upsert: the re-check behind the order lock is gone. The conflict target is always a unique column, and lockUniqueValues already serializes same-value upserts before the lookup.
  • Replace still excludes a concurrent replace through lockUniqueColumns, which every replace takes exclusively, even on tables with no unique columns. An insert that commits during a replace survives it, the same outcome as running right after it.
  • Not changed: the order_key backfill script migration and the collation repair script still take the old lock. Neither concurrent appends nor these scripts are protected by it any more; the scripts only rewrite tables without valid keys.

Benchmark

Local Postgres, 48 concurrent writers on one table through the real row services, 10 s per scenario, with a 20 ms commit delay per transaction to stand in for the replication wait (locks still held). Successful ops/s, p99, failures:

Scenario Op staging + #8765 + #8765 + this
Inserts only insert 17/s · 3115 ms · 13 timeouts 22/s · 2919 ms · 0 1510/s · 80 ms · 0
Mixed insert 2/s · 4856 ms · 39/62 timed out 13/s · 1881 ms · 0 404/s · 84 ms · 0
Mixed + one insert stalled 4 s insert 1/s · 5901 ms · 46/63 timed out 7/s · 3039 ms · 16/86 timed out 461/s · 81 ms · 0
Mixed + one insert stalled 4 s update 7/s · 8011 ms · 0 92/s · 672 ms · 0 384/s · 118 ms · 0

Live row count matched COUNT(*) in every run. Absolute numbers vary between runs; read the columns relative to each other.

Type of Change

  • Bug fix

Testing

New integration tests, each red on the pre-change code:

  • Every insert path (append, at a position, after a row, batch, upsert, import with a new column) commits while another transaction holds the retired lock. Before: each waits out the 3 s lock timeout.
  • 4 batches and 4 single inserts that all read the same last key get distinct keys, and each batch stays contiguous. Red with deterministic append keys: 25 rows, 6 distinct keys.
  • Insert at a position between two rows sharing a key. Before: throws.
  • Insert at a position past the last row lands last. Before: lands near the top.
  • Dispatcher (dispatcher.integration.ts): rows that share positions across a window boundary each run exactly once. Before: tied rows skipped.
  • Two concurrent replaces leave exactly one row set (guards the unique-lock serialization, green before and after).

The unique-value race tests used the order lock to pause inserts mid-flight. They now pause on a definition row held FOR UPDATE, which an insert's foreign-key check waits on after its unique check. The obsolete lock-order unit tests are removed.

Table + TTL integration suites: migrated 161 passed, push-provisioned 131 passed. Table unit tests 987 passed. lint:check and check:audits pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

🤖 Generated with Claude Code

https://claude.ai/code/session_01CJxC2fgucGVpT59RV5Z7xv

@vercel

vercel Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Oct 8, 2026 10:13pm UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files

Turn on auto-fix | Re-trigger cubic

Comment thread apps/sim/lib/table/dispatcher.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[High impact] Removes per-table locking from row inserts and changes position assignment.

The PR appears safe to merge, with a non-blocking concern about oversized dispatcher windows.

Findings

  1. P2 Ties expand queued work ▶

Summary

Removes the per-table insert-order lock and gives appends random key ranges. Positional inserts now handle equal keys and positions past the end. The dispatcher includes rows tied at a window boundary.

  • Unique-value locks still protect upserts, and an exclusive unique lock still separates concurrent replaces.
  • Non-blocking concern: fetching all boundary ties removes the bound on work queued per window.
  • TheodoreSpeaks explicitly accepts inserts surviving a concurrent replace and the repair scripts retaining the retired lock. The PR description states that these scripts only rewrite tables without valid keys.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Concurrent inserts] --> B[Read append anchors without order lock]
  B --> C[Choose random key ranges]
  C --> D[Commit rows with possibly equal positions]
  D --> E[Dispatcher reads a bounded window]
  E --> F[Fetch all remaining boundary ties]
  F --> G[Queue the expanded window and wait]
  G --> H[Advance the position cursor]
Loading

Reviews (2) · Last reviewed commit: "test(tables): order rows bytewise in the..." · Reviewed by Greptile

Comment thread apps/sim/lib/table/dispatcher.ts
Comment thread apps/sim/lib/table/dispatcher.ts
Appends mint keys in a random slot after the last key, so concurrent appends
never share a key and a batch stays contiguous. Positions are left best-effort
and may repeat; the run dispatcher finishes a tied position before advancing
its cursor. Positional inserts skip tied keys. Concurrent replaces stay
serialized by the table's unique lock, which every replace already takes.
@TheodoreSpeaks
TheodoreSpeaks force-pushed the fix/drop-row-order-lock branch from 1329fb9 to f334574 Compare October 8, 2026 22:13
@TheodoreSpeaks
TheodoreSpeaks merged commit cf61888 into staging Oct 8, 2026
46 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the fix/drop-row-order-lock branch October 8, 2026 22:42

This branch was previously deployed

1 inactive deployment
Preview — f334574c Deployed Oct 8, 2026 by vercel[bot]
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