Skip to content

fix(v2): take the vector extension advisory lock in ainit_vectorstore_table - #335

Open
Mrxh (x1247897956) wants to merge 1 commit into
langchain-ai:mainfrom
x1247897956:fix/v2-create-extension-advisory-lock
Open

Mrxh (x1247897956) wants to merge 1 commit into
langchain-ai:mainfrom
x1247897956:fix/v2-create-extension-advisory-lock

Conversation

@x1247897956

Copy link
Copy Markdown

What

PGEngine.ainit_vectorstore_table now takes the same transaction-level advisory lock that the legacy PGVector path already uses, before running CREATE EXTENSION IF NOT EXISTS vector.

Why

CREATE EXTENSION IF NOT EXISTS is not safe against concurrent sessions. Two sessions can both pass the existence check and then collide on pg_extension_name_index:

duplicate key value violates unique constraint "pg_extension_name_index"

The legacy path already guards against this — _create_vector_extension in langchain_postgres/vectorstores.py takes pg_advisory_xact_lock(1573678846307946496) — but the v2 path ran the same statement with no lock at all, so it provided no protection, including against a v2 init racing a legacy init.

This can be hit whenever a store is initialized concurrently, for example:

  • two asyncio.gather'd ainit_vectorstore_table calls
  • a PGVectorStore init racing a legacy PGVector init

Fixes #329

How

I deliberately reuse the legacy lock key rather than introducing a v2-specific one, so the two code paths serialize against each other instead of only within themselves. The lock is taken as a separate execute() before CREATE EXTENSION: since pg_advisory_xact_lock is held until the surrounding transaction ends, it covers the CREATE EXTENSION below and the commit() that follows it.

Happy to move the key into a shared constant if you would rather not have it defined in both modules.

Testing

New regression test: TestEngineAsync::test_init_table_waits_for_vector_extension_lock. It holds the advisory lock on a second session, asserts that ainit_vectorstore_table blocks until the lock is released, then asserts the table was created.

It fails on unpatched main, i.e. it does reproduce the missing serialization:

with pytest.raises(asyncio.TimeoutError):
E   Failed: DID NOT RAISE <class 'TimeoutError'>

Commands run locally:

  • make test → 754 passed, 25 skipped
  • uv run pytest --disable-socket --allow-unix-socket tests/unit_tests/v2/test_engine.py → 19 passed
  • uv run ruff format --diff / uv run ruff check → clean
  • uv run mypy langchain_postgres/v2/engine.py → Success: no issues found in 1 source file

`PGEngine.ainit_vectorstore_table` ran `CREATE EXTENSION IF NOT EXISTS
vector` without the advisory lock that the legacy `PGVector` path takes in
`_create_vector_extension`. Postgres lets two concurrent sessions both pass
the existence check and then collide on `pg_extension_name_index`, so
concurrent initializations could fail with a duplicate key error. Take the
same lock key as the legacy path so both code paths serialize against each
other.

This branch has not been deployed

No deployments
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.

Race condition on CREATE EXTENSION vector in PGEngine (missing advisory lock)

1 participant