Skip to content

perf(insert): shorten lane lock holds for COPY and skip-conflict batches (0.6) - #690

Merged
hardbyte merged 9 commits into
release/0.6.6from
brian/batch-insert-lane-lock-0.6
Nov 8, 2026
Merged

hardbyte merged 9 commits into
release/0.6.6from
brian/batch-insert-lane-lock-0.6

Conversation

@hardbyte

@hardbyte hardbyte commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Backport of #635 to the 0.6 line, for 0.6.15.

On queue storage every enqueue holds a transaction-scoped advisory lock on its (queue, priority, shard) lane until commit. insert_many_copy called awa.insert_job_compat once per row, so the lock was taken at the batch's first ready row and held while the rest of the batch (job ids, unique claims, savepoints, payloads) was processed. Many producers enqueuing large batches into one queue therefore serialised on whole batches, and promotion into that queue waited behind them.

Change

  • Schema patch insert_jobs_compat_batch (awa-model/migrations/patches/insert_jobs_compat_batch.sql): adds awa.insert_jobs_compat_batch, the body of main's v055 without the schema_version row. It does the lane-independent work first (job ids, unique claims in key order, deferred rows, result rows), then, in (queue, priority, shard) order, registers each lane it is the first to use and reserves each lane's range once, and writes ready rows and segments in two statements. The lane lock covers only the registration, the reservation, those writes and the commit.
    • 0.6 adaptation: the ready-ring cursor is read from queue_ring_state, or through ring_cursor('queue') when a later schema provides it, so the 0.6 body stays correct on a 0.7 schema until v055 replaces it. Same signature and return type as v055, so v055's CREATE OR REPLACE applies cleanly over it.
    • Registered in SCHEMA_PATCHES: applied by awa migrate at v40, never writes schema_version, exported as R__insert_jobs_compat_batch.sql by --sql / --extract-to. The probe is catalog-only and matches the body (prosrc registering queue_lanes from v_lane inside the reservation loop), so awa migrate replaces the earlier body that registered lanes up front.
  • insert_many_copy (Rust, and Python insert_many_copy) probes its own connection on every call (has_function_privilege(to_regprocedure(...), 'EXECUTE')) and uses the batch function when installed and executable; otherwise it keeps the existing per-row path. Unique conflicts are skipped on both paths, rows come back in input order.
  • insert_many_skip_unique_conflicts (new Rust API) uses the batch function and returns a validation error naming the patch when it is missing.
  • Generic insert_many is unchanged: one statement, awa.insert_job_compat per row in canonical lane order, on every schema. Its executor cannot be probed first, and there is no process-wide cache, so a process writing to databases with and without the patch never sends unsupported SQL into a caller's transaction.
  • Lock order: both paths register and lock lanes one at a time in one sorted (queue, priority, shard) order. A new lane is registered inside the reservation loop immediately before its reservation, as insert_job_compat and QueueStorage do; head rows are proposed only when missing, because the head tables' row trigger provisions the lane before the conflict check.
  • Shared unique keys: the batch takes all its unique claims before any lane lock, while per-row inserts take claim and lane together, lane by lane. A per-row transaction that holds a lane the batch needs and then inserts a key the batch has claimed but not committed now gets 40P01 instead of a skip or 23505; documented in the CHANGELOG, upgrade guide and troubleshooting, with retry as the remedy. Moving claims into the lane window would undo the change.

Rollout

  • Migrate first: the patch adds one function and alters nothing; released 0.6.13/0.6.14 binaries never call it and keep calling awa.insert_job_compat, which takes the same lane lock. Mixed old/new producers on one lane keep lane_seq dense and each batch contiguous and in input order.
  • Binary first: 0.6.15 COPY stays on the per-row path until the patch commits, then switches on its next call without a restart. insert_many_skip_unique_conflicts errors until then.
  • Roll back: leave the patch in place.
  • Operator notes: docs/upgrade-0.6.14-to-0.6.15.md, CHANGELOG [Unreleased], troubleshooting. Deployments that revoke EXECUTE on new functions must grant it; without it COPY stays per-row.

Tests

New:

  • insert_many_batch_test (9): new-lane ordering (a per-row transaction holding provisioned lane A enqueues into unprovisioned lane B while a batch for A and B waits on A; fails with 40P01 on the earlier body, passes now); lane order, dense lane_seq, one segment per batch and claimability through each of insert_many / skip API / COPY; mixed available + scheduled rows; atomic unique failure; skip semantics (claimed keys, in-batch repeats); canonical storage; the lane lock is no longer held across bookkeeping (with a gate holding the lane lock, a waiting batch already holds its last row's unique claim and has written no ready row); old per-row and new batch producers sharing a lane.
  • insert_many_schema_fallback_test (5): the earlier body (fixture) keeps the patch pending and awa migrate replaces it; binary ahead of the patch falls back, awa migrate applies it (no version row, pending_schema_patches empty), COPY switches on the next call, dropping the function again does not poison later calls; databases with and without the patch in one process inside caller transactions incl. rollback; exported patch applied twice in one explicit transaction and over an installed function; a role without EXECUTE keeps COPY per-row.
  • enqueue_lane_lock_order_test: ported main's canonical-order check (a batch parked on a middle lane holds exactly the earlier lanes) and added the skip API as a producer (9 tests).
  • enqueue_lane_contention_test (ignored benchmark + nightly gate, ported from perf(insert): shorten lane lock holds for COPY and skip-conflict batches #635; AWA_LANE_BENCH_WITHOUT_PATCH=1 measures the per-row COPY path).
  • integration_test: ported perf(insert): shorten lane lock holds for COPY and skip-conflict batches #635's guard serialising the bind-limit test with storage switches (it raced on 0.6 too).

Local results (PostgreSQL 17 and 18 in Docker, full Rust suite via the CI shard layout with --no-fail-fast):

PG17 PG18
rest 320 passed, 3 failed* 321 passed, 2 failed*
heavy 325 passed, 1 failed* 325 passed, 1 failed*
workspace 273 passed, 2 failed* 275 passed
migration_test 44 passed 44 passed

* Not caused by this change: admin_read_cancel_test (2 tests) fails identically on unmodified release/0.6.6 in this environment; test_queue_storage_promotion_does_not_wait_for_the_notify_lock_in_its_lanes needs fsync=on (commit_delay is a no-op without it) and passes on fsync-enabled PG17 and PG18; descriptor_constraints_test (2) and the bind-limit test (fixed here) pass on rerun. Python suite on PG17: 312 passed, 1 skipped.

Released-binary compatibility: the 0.6.14 tree (a8febdc0) copy_test (13), enqueue_lane_lock_order_test (7) and integration_test (47) pass against a patched PG17 database, and the function is still installed after its migrate.

Benchmark

enqueue_lane_contention_test matrix, release build, 185-row unique batches into one lane, fresh database per run, fsync on, two rounds each (local Docker, shared machine, so ranges rather than baselines):

path PG17 1 producer PG17 16 producers PG18 1 producer PG18 16 producers
COPY, before (per-row + savepoints) 856–879 jobs/s, p50 212–218 ms 786–832 jobs/s, p50 3.5–3.8 s 925–994 jobs/s, p50 189–200 ms 902–994 jobs/s, p50 2.9–3.2 s
COPY, after (batch function) 6.2–6.5k jobs/s, p50 26–29 ms 5.2–20.1k jobs/s, p50 113–391 ms 6.7–9.9k jobs/s, p50 18–26 ms 8.1–11.3k jobs/s, p50 255–313 ms
insert_many_skip_unique_conflicts 5.6–7.1k jobs/s 7.1–10.0k jobs/s 7.5–7.9k jobs/s 10.6–11.4k jobs/s
generic insert_many (unchanged) 971–1434 jobs/s 1.2k jobs/s, p50 2.4 s 1.4k jobs/s 1.4k jobs/s, p50 2.0–2.2 s

The nightly gate passes on both: a single 185-row batch takes 22–25 ms against 135–147 ms through the per-row function, and 16 producers on one lane reach 11.4k (PG18) / 17.9k (PG17) jobs/s, 22–23k on four shards.

After the review fix (d2de3738) and merges of release/0.6.6 (#669, #683, #688)

  • PG17 and PG18: insert_many_batch_test 9, insert_many_schema_fallback_test 5, enqueue_lane_lock_order_test 9, copy_test 13, migration_test 44, awa-model migrations unit 9, migrate_patch_cli_test 3, least_privilege_runtime_test 4, lane_ownership_roles_test + dirty_marks_wait_free_test 16, all passing. fmt, offline clippy (all targets/features, -D warnings) and offline workspace build pass.
  • Contention gate A/B on PG18 (fresh database per run, fsync off, two rounds each), earlier body vs fixed body: one producer 12–13 ms vs 11 ms p50 per 185-row batch; 16 producers on one lane 27–29k vs 32–37k jobs/s; four shards 71–79k vs 91–98k jobs/s. Registering a new lane inside the loop costs nothing measurable on existing lanes.

Not covered

  • Rehearsal with published 0.6.13/0.6.14 wheels and CLI against the patched schema under traffic (the 0.6.14 source tree's tests were run instead).
  • No TLA+ model update. Lanes are still registered and locked one at a time in the sorted order; what moves is when the batch takes its unique claims (before any lane), which is the shared-key interaction documented above.

…ts (0.6)

insert_many_copy called awa.insert_job_compat once per row. The function
takes the (queue, priority, shard) lane lock in reserve_enqueue_seq on the
batch's first ready row, and the lock lasts to commit, so it was held while
every remaining row was processed and producers on one lane queued behind
whole batches.

The insert_jobs_compat_batch schema patch (awa 0.7 migration v055, without
its schema_version row) adds awa.insert_jobs_compat_batch. It allocates job
ids, claims unique keys in key order, registers new lanes and builds the
result rows first, then reserves each lane's sequence range once, in
(queue, priority, shard) order, and writes the ready rows and segments in
two statements. It reads the ready-ring cursor through ring_cursor() where
a later schema provides one.

insert_many_copy probes its own connection on every call and uses the
function when it exists and the role may execute it, otherwise the per-row
path. The new insert_many_skip_unique_conflicts requires it. Generic
insert_many keeps its per-row statement on every schema. Released 0.6
binaries never call the function and share the same lane lock.
enqueue_lane_contention_test measures lane throughput and batch latency for
many producers on one lane, across insert paths, unique keys and
enqueue_shards. Its nightly gate runs sixteen producers on one lane and on
four shards, checks lane integrity and compares the batch function's lock
hold time with the per-row function's. AWA_LANE_BENCH_WITHOUT_PATCH drops the
batch function so the matrix can measure the per-row COPY path.
@hardbyte hardbyte added the full-ci Run the full CI matrix (Python build+test, E2E) on this PR label Nov 8, 2026
@coderabbitai

coderabbitai Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8c9dbe45-2350-4270-8405-c5daebd45194

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

…tch-insert-lane-lock-0.6

# Conflicts:
#	CHANGELOG.md
awa.insert_jobs_compat_batch registered every lane it was the first to use
before reserving any. The head tables' row trigger provisions a lane under
its lock and the uncommitted head row blocks other producers of that lane,
so a batch for an existing lane A and a new lane B held B while waiting for
A, and a per-row producer (generic insert_many, single inserts, 0.6.14
binaries) holding A and then enqueuing into B deadlocked with it.

The batch now registers each missing lane inside the reservation loop,
immediately before reserving it, so new lanes follow the register-then-reserve
order every other producer uses. The patch probe matches the new body, so
`awa migrate` replaces the earlier one.

Documents that a per-row transaction sharing a unique key with a batch can
now fail with 40P01 rather than skip the job or raise 23505, and corrects the
lane-order statements in the changelog, upgrade guide and troubleshooting.
…tch-insert-lane-lock-0.6

# Conflicts:
#	CHANGELOG.md
@hardbyte
hardbyte marked this pull request as ready for review November 8, 2026 12:26
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-08T12:29:05.037996Z d300a4a Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…tch-insert-lane-lock-0.6

# Conflicts:
#	awa-model/src/migrations.rs
@hardbyte
hardbyte merged commit 7727795 into release/0.6.6 Nov 8, 2026
62 of 63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run the full CI matrix (Python build+test, E2E) on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant