Skip to content

Commit 1d08185

Browse files
srousseyclaude
andcommitted
fix(sqlite): stop SqliteAiVectorStorage.putBulk deadlocking on its own chain slot
`guardedWrite` (added with withConnectionTransaction) routes every public write through `runOnConnection`, but `SqliteAiVectorStorage._putBulkInternal` had taken the shared connection-chain slot itself since before anything above it did. `putBulk` therefore took the slot twice on one handle: the inner `runOnConnection` awaits the outer's promise, which only resolves once the inner returns. The `finally` never runs, so `state.chain` is left unresolved and the handle is poisoned for every later write on it — which is why the suite's `afterEach` `deleteAll()` timed out alongside the three `putBulk` tests. The parent `SqliteTabularStorage._putBulkInternal` does not re-take the chain; this restores that symmetry. Both entry paths already hold the slot: the public `putBulk` via `guardedWrite`, and a call arriving through the `tx` proxy runs inside the transaction that holds it — which is why the removed branch was reachable only from the deadlocking path. `runVectorPutBulkOnHandle` still reads `db.inTransaction` to decide whether to open its own `db.transaction`, so SQLite transaction nesting is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9e3dee6 commit 1d08185

1 file changed

Lines changed: 4 additions & 12 deletions

File tree

providers/sqlite/src/storage/SqliteAiVectorStorage.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
getMetadataProperty,
1818
getVectorProperty,
1919
matchesFilter,
20-
runOnConnection,
2120
validateVectorEntities,
2221
} from "@workglow/storage";
2322
import type {
@@ -487,17 +486,10 @@ export class SqliteAiVectorStorage<
487486
return super._putBulkInternal(entities);
488487
}
489488

490-
// Route the vector-encoding write through the shared connection chain so
491-
// two SqliteAiVectorStorage instances that wrap the same underlying handle
492-
// (or one vector storage and one plain SqliteTabularStorage) queue on the
493-
// same lock instead of racing on the shared connection. Skip when we are
494-
// already inside an outer `withTransaction` — the chain lock is held there.
495-
const dbWithFlag = this.database as unknown as { readonly inTransaction?: boolean };
496-
const alreadyInTx = this.inTransaction || dbWithFlag.inTransaction === true;
497-
const handle = this.connectionHandle();
498-
if (handle !== null && !alreadyInTx) {
499-
return runOnConnection(handle, this, () => this.runVectorPutBulkOnHandle(entities));
500-
}
489+
// The shared connection chain slot is already held by whoever reached
490+
// here: the public `putBulk` takes it in `guardedWrite`, and a call
491+
// arriving through the `tx` proxy runs inside the transaction that holds
492+
// it. Re-taking it here would await a slot only this call can release.
501493
return this.runVectorPutBulkOnHandle(entities);
502494
}
503495

0 commit comments

Comments
 (0)