Skip to content

fix(memory-pgvector): serialize schema init under an advisory lock - #727

Merged
blove merged 1 commit into
mainfrom
blove/pgvector-schema-lock
Sep 18, 2026
Merged

blove merged 1 commit into
mainfrom
blove/pgvector-schema-lock

Conversation

@blove

@blove blove commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #723, which fixed the same defect in @b4run/postgres-storage as issue #709. I found this while fixing that one.

The bug

initSchema created the vector extension, the schema, two tables and six indexes with no lock and no transaction, under this comment:

Idempotent schema init. Safe to call repeatedly (IF NOT EXISTS everywhere).

IF NOT EXISTS does make each statement idempotent. It does not make it concurrency-safe. Two sessions running the same statement race on the catalog and the loser raises 23505 rather than a benign no-op: pg_extension for the extension, pg_namespace for the schema, pg_type for a table, pg_class for an index.

That is a real deployment shape. A serverless deploy scaling from zero cold-starts several isolates that all initialize the same database, and the memoized ready() in pgvector-store.ts only covers one process.

The fix

The whole pass runs in one transaction holding pg_advisory_xact_lock, keyed on the schema and table prefix it builds. Concurrent callers queue, and each one after the first finds the work done and no-ops through it.

A failure now rolls back rather than leaving the caller a connection stuck in an aborted transaction, and a failing rollback cannot mask the original error.

Unlike #723 there is no catalog fast path here. That one needed it because a cold start pays one lock per store component and a test pins that cost; this runs once per store instance behind an existing memo, so an unconditional lock costs one acquisition per process.

Tests

Gated on B4_TEST_PGVECTOR=1, against real pgvector/pgvector:pg16: six concurrent initSchema calls on a virgin schema. Before the change it failed with 23505 on pg_extension_name_index; after it, all six resolve and the schema holds exactly the two expected tables, proving the winner did not leave the losers half-built.

Validation

  • @b4run/memory-pgvector full suite with the gated lane: 5 files, 95 passed, 1 skipped
  • @b4run/memory-pgvector typecheck and lint
  • Root pnpm lint, node scripts/check-docs.mjs, node scripts/check-changesets.mjs
  • api-reference-inventory behavior-contract fingerprints: 252 passed

🤖 Generated with Claude Code

initSchema created the vector extension, the schema, two tables and six
indexes with no lock and no transaction, under a comment claiming IF NOT
EXISTS made it safe to call repeatedly. It does make each statement
idempotent, but not concurrency-safe: two sessions running the same statement
race on the catalog and the loser raises 23505 rather than a no-op — on
pg_extension for the extension, pg_namespace for the schema, pg_type for a
table and pg_class for an index.

That is a real deployment shape, not a theoretical one. A serverless deploy
scaling 0 to N cold-starts N isolates that all initialize the same database,
and the store's memoized ready() covers a single process. This is the defect
@b4run/postgres-storage carried as #709.

The whole pass now runs in one transaction holding pg_advisory_xact_lock
keyed on the schema and prefix it builds, so concurrent callers queue and
every one after the first no-ops through work already done. A failure rolls
back rather than handing the caller a connection stuck in an aborted
transaction, and the rollback cannot mask the original error.

Proven against real Postgres: six concurrent initSchema calls on a virgin
schema failed with 23505 on pg_extension_name_index before the change and
pass after it, leaving exactly the two expected tables.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
b4-run Ready Ready Preview Sep 18, 2026 2:31am UTC

Request Review

@blove
blove enabled auto-merge (squash) September 18, 2026 02:31

@github-actions github-actions 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.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@blove
blove merged commit 026395a into main Sep 18, 2026
30 of 31 checks passed
@blove
blove deleted the blove/pgvector-schema-lock branch September 18, 2026 02:49

This branch was successfully deployed

1 active and 1 inactive deployments
Preview — a6257e29 Deployed Sep 18, 2026 by vercel[bot]
vercel-preview — a6257e29 Deployed Sep 18, 2026 by blove via vercel-native #2073
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