SQLite gains in-process mutex for database lock parity

The `@n8n/db` lock service now provides the same serialization guarantees for SQLite as Postgres advisory locks, preventing concurrent async callers from interleaving inside critical sections.
SQLite databases previously relied solely on database transactions for serialization—a gap that could cause race conditions when multiple concurrent async calls within the same process attempted to enter a critical section simultaneously. Each call ran its own transaction, but transactions alone don't block other callers in the same process.
This PR adds an in-process async mutex for SQLite that acquires before entering the transaction, matching the behavior of Postgres advisory locks. The implementation uses an ownership-token model: each lock acquisition creates an opaque token, and the release function only acts if its token still matches the current holder. This prevents double-release bugs and stale releases from corrupting a subsequent acquisition's lock state.
Locks are per lock-ID rather than global, so different lock IDs don't block each other. FIFO ordering is maintained via a queue array, and timeout support mirrors Postgres behavior using setTimeout with queue cleanup on expiry. Ownership transfers atomically on release—new tokens are assigned before the waiter's promise resolves, eliminating a microtask-width window where a tryAcquireLock could bypass the queue.
The Postgres code path remains unchanged but is slightly cleaner after removing the unnecessary conditional nesting inside the transaction callback.
This change lives in the @n8n/db package and ensures consistent locking behavior across both database types used by n8n.
View Original GitHub Description
Summary
DbLockService previously relied solely on database transactions for SQLite serialization, which is insufficient — concurrent async callers within the same process can interleave inside the critical section even when each runs its own transaction. This PR adds an in-process async mutex for SQLite that provides the same serialization guarantees as Postgres advisory locks (pg_advisory_xact_lock / pg_try_advisory_xact_lock).
What changed:
withLockandtryWithLocknow acquire an in-process mutex before entering the transaction on SQLite, while Postgres continues to use advisory locks as before- The mutex uses an ownership-token model: each acquisition creates an opaque token, and the release function only acts if its token still matches the current holder — making double-release a safe no-op
- Ownership transfers atomically on release (new token assigned to
heldbefore the waiter's promise resolves), eliminating a microtask-width window wheretryAcquireLockcould bypass the queue - The Postgres code path is unchanged but slightly simplified by removing the now-unnecessary
if (type === 'postgresdb')nesting inside the transaction callback
Key implementation decisions:
- The mutex is per-lock-ID (not global) so different lock IDs don't block each other
- FIFO ordering is maintained via a queue array
- Timeout support mirrors the Postgres
SET LOCAL lock_timeoutbehavior withsetTimeout+ queue cleanup on expiry
Related Links
<!-- Add Linear ticket link if applicable -->Review checklist
- PR title and summary are descriptive. (conventions)
- Docs updated or follow-up ticket created.
- Tests included.
- PR Labeled with
Backport to Beta,Backport to Stable, orBackport to v1(if the PR is an urgent fix that needs to be backported)