From 1047f545235305e727fb6e301b29b6b0a240ec01 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 20 Dec 2025 11:15:43 +0100 Subject: [PATCH] fix(mutex): prioritize waiting exclusive locks over newly added shared tasks --- .../core/__tests__/repository-mutex.test.ts | 38 +++++++++++++++++++ app/server/core/repository-mutex.ts | 6 ++- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 app/server/core/__tests__/repository-mutex.test.ts diff --git a/app/server/core/__tests__/repository-mutex.test.ts b/app/server/core/__tests__/repository-mutex.test.ts new file mode 100644 index 00000000..623881e0 --- /dev/null +++ b/app/server/core/__tests__/repository-mutex.test.ts @@ -0,0 +1,38 @@ +import { test, describe, expect } from "bun:test"; +import { repoMutex } from "../repository-mutex"; + +describe("RepositoryMutex", () => { + test("should prioritize waiting exclusive locks over new shared locks", async () => { + const repoId = "test-repo"; + const results: string[] = []; + + const releaseShared1 = await repoMutex.acquireShared(repoId, "backup-1"); + results.push("acquired-shared-1"); + + const exclusivePromise = repoMutex.acquireExclusive(repoId, "unlock").then((release) => { + results.push("acquired-exclusive"); + return release; + }); + + const shared2Promise = repoMutex.acquireShared(repoId, "backup-2").then((release) => { + results.push("acquired-shared-2"); + return release; + }); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(results).toEqual(["acquired-shared-1"]); + + releaseShared1(); + + const releaseExclusive = await exclusivePromise; + expect(results).toEqual(["acquired-shared-1", "acquired-exclusive"]); + + releaseExclusive(); + + const releaseShared2 = await shared2Promise; + expect(results).toEqual(["acquired-shared-1", "acquired-exclusive", "acquired-shared-2"]); + + releaseShared2(); + }); +}); diff --git a/app/server/core/repository-mutex.ts b/app/server/core/repository-mutex.ts index 4717c882..a1bb5ad4 100644 --- a/app/server/core/repository-mutex.ts +++ b/app/server/core/repository-mutex.ts @@ -49,7 +49,9 @@ class RepositoryMutex { async acquireShared(repositoryId: string, operation: string): Promise<() => void> { const state = this.getOrCreateState(repositoryId); - if (!state.exclusiveHolder) { + const hasExclusiveInQueue = state.waitQueue.some((item) => item.type === "exclusive"); + + if (!state.exclusiveHolder && !hasExclusiveInQueue) { const lockId = this.generateLockId(); state.sharedHolders.set(lockId, { id: lockId, @@ -60,7 +62,7 @@ class RepositoryMutex { } logger.debug( - `[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation} (exclusive held by: ${state.exclusiveHolder.operation})`, + `[Mutex] Waiting for shared lock on repo ${repositoryId}: ${operation} (exclusive held by: ${state.exclusiveHolder?.operation ?? "none"}, queue: ${state.waitQueue.length})`, ); const lockId = await new Promise((resolve) => { state.waitQueue.push({ type: "shared", operation, resolve });