fix(embeddings): the index refresh loses the race it used to deadlock (#3262)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Failing after 55s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Failing after 55s
CI & Build / Build & push image (push) Skipped
An embedding refresh replaces a record's vectors as delete-then-insert, which takes the chunk rows first and the parent row second (via the insert's foreign key). A cascading delete of the parent takes exactly those two locks in the other order. Postgres calls the cycle a deadlock and kills one side: sometimes the detached embedder, silently, and sometimes the user's delete, as a 500 on an operation that should have worked. Both upserts now claim the parent row with FOR KEY SHARE NOWAIT before touching any chunk row. That removes the cycle instead of narrowing it — either the embedder is first and the delete queues behind it, or the delete already holds the row and the embedder loses at once, which is the side designed to lose. FOR KEY SHARE is the lock the insert would take anyway, so an ordinary edit is unaffected. The note twin, recorded as unverified on the issue, has the same shape and the same fix; a trash purge is the hard delete that reaches it. Unit tests pin the ORDER and the lock mode by compiling the statement; the integration pair holds a real delete open in one transaction and proves the embedder returns having written nothing, with a deadline so a regression fails instead of hanging.
This commit is contained in:
+6
-2
@@ -59,14 +59,18 @@ def tool_doc(module: str, name: str) -> str:
|
||||
return _re.sub(r"\s+", " ", fn.__doc__)
|
||||
|
||||
|
||||
def compiled_sql(element) -> str:
|
||||
def compiled_sql(element, dialect=None) -> str:
|
||||
"""A SQLAlchemy clause or statement rendered as literal SQL text.
|
||||
|
||||
For asserting on the shape of a predicate without a database — which is how
|
||||
the visibility clauses and the knowledge facets are both tested. Was a
|
||||
private copy in each of those modules before #3128 needed a third.
|
||||
|
||||
Pass `dialect` when the assertion is about something only one backend
|
||||
renders — a Postgres row-lock mode, say. The generic dialect is enough for
|
||||
a predicate's shape and would quietly drop the rest.
|
||||
"""
|
||||
return str(element.compile(compile_kwargs={"literal_binds": True}))
|
||||
return str(element.compile(dialect=dialect, compile_kwargs={"literal_binds": True}))
|
||||
|
||||
|
||||
def make_mock_session() -> AsyncMock:
|
||||
|
||||
Reference in New Issue
Block a user