From a0eb5612b9339fac7e555aa539997936a27bd921 Mon Sep 17 00:00:00 2001 From: coji Date: Mon, 16 Mar 2026 20:50:14 +0900 Subject: [PATCH] perf: skip write mutex for PostgreSQL backend PostgreSQL handles concurrent writes natively via MVCC, advisory locks, and FOR UPDATE SKIP LOCKED. The single-process write mutex was only needed for SQLite/libsql (which opens separate connections for transactions, causing SQLITE_BUSY conflicts). Now the mutex is conditionally applied only when backend !== 'postgres', unlocking true concurrent write throughput for PostgreSQL deployments. Closes #123 Co-Authored-By: Claude Opus 4.6 --- packages/durably/src/storage.ts | 59 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/packages/durably/src/storage.ts b/packages/durably/src/storage.ts index c595cd2..1c12eae 100644 --- a/packages/durably/src/storage.ts +++ b/packages/durably/src/storage.ts @@ -1045,33 +1045,38 @@ export function createKyselyStore( }, } - // Wrap all mutating methods with write lock to prevent SQLITE_BUSY. - // libsql opens separate connections for transactions, so concurrent - // writes from the same Kysely instance can conflict. The mutex - // serializes writes within a single process. Reads are not locked. - const mutatingKeys = [ - 'enqueue', - 'enqueueMany', - 'updateRun', - 'deleteRun', - 'purgeRuns', - 'claimNext', - 'renewLease', - 'releaseExpiredLeases', - 'completeRun', - 'failRun', - 'cancelRun', - 'persistStep', - 'deleteSteps', - 'updateProgress', - 'createLog', - ] as const - - for (const key of mutatingKeys) { - const original = store[key] as (...args: unknown[]) => Promise - ;(store as unknown as Record)[key] = ( - ...args: unknown[] - ): Promise => withWriteLock(() => original.apply(store, args)) + // SQLite/libsql: wrap mutating methods with write lock to prevent SQLITE_BUSY. + // libsql opens separate connections for transactions, so concurrent writes + // from the same Kysely instance can conflict. The mutex serializes writes + // within a single process. Reads are not locked. + // + // PostgreSQL: skip the mutex entirely. PostgreSQL handles concurrent writes + // natively via MVCC, advisory locks, and FOR UPDATE SKIP LOCKED. + if (backend !== 'postgres') { + const mutatingKeys = [ + 'enqueue', + 'enqueueMany', + 'updateRun', + 'deleteRun', + 'purgeRuns', + 'claimNext', + 'renewLease', + 'releaseExpiredLeases', + 'completeRun', + 'failRun', + 'cancelRun', + 'persistStep', + 'deleteSteps', + 'updateProgress', + 'createLog', + ] as const + + for (const key of mutatingKeys) { + const original = store[key] as (...args: unknown[]) => Promise + ;(store as unknown as Record)[key] = ( + ...args: unknown[] + ): Promise => withWriteLock(() => original.apply(store, args)) + } } return store