From d073065f6d91fecad3c475dbb5fbd3b5e59b44a2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 31 Oct 2026 22:16:08 -0700 Subject: [PATCH] fix(oauth): default a leader-lock follower's wait to the lease so its takeover can land --- apps/sim/lib/concurrency/leader-lock.test.ts | 21 ++++++++++++++++++++ apps/sim/lib/concurrency/leader-lock.ts | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/concurrency/leader-lock.test.ts b/apps/sim/lib/concurrency/leader-lock.test.ts index ce83c6e39bc..4e278f70e5b 100644 --- a/apps/sim/lib/concurrency/leader-lock.test.ts +++ b/apps/sim/lib/concurrency/leader-lock.test.ts @@ -136,6 +136,27 @@ describe('withLeaderLock', () => { expect(redisConfigMockFns.mockReleaseLock).toHaveBeenCalledOnce() }) + it('waits out a dead leader’s lease by default before taking over', async () => { + /** A Redis `SET NX EX` lease on fake time: acquiring succeeds only once the holder's lapses. */ + let leaseExpiresAt = 0 + redisConfigMockFns.mockAcquireLock.mockImplementation(async (_key, _owner, ttlSec) => { + if (Date.now() < leaseExpiresAt) return false + leaseExpiresAt = Date.now() + ttlSec * 1000 + return true + }) + vi.useFakeTimers() + try { + leaseExpiresAt = Date.now() + 10_000 + const onLeader = vi.fn(async () => 'took-over') + const result = withLeaderLock({ key: 'k', onLeader, onFollower: async () => null }) + await vi.advanceTimersByTimeAsync(10_200) + await expect(result).resolves.toBe('took-over') + expect(onLeader).toHaveBeenCalledOnce() + } finally { + vi.useRealTimers() + } + }) + it('returns the follower result when a live holder still has the lease', async () => { redisConfigMockFns.mockAcquireLock.mockResolvedValue(false) const onLeader = vi.fn(async () => 'should-not-run') diff --git a/apps/sim/lib/concurrency/leader-lock.ts b/apps/sim/lib/concurrency/leader-lock.ts index 69949ad98fa..53a29203bdb 100644 --- a/apps/sim/lib/concurrency/leader-lock.ts +++ b/apps/sim/lib/concurrency/leader-lock.ts @@ -8,12 +8,15 @@ const logger = createLogger('LeaderLock') const DEFAULT_TTL_SEC = 10 const DEFAULT_POLL_INTERVAL_MS = 100 -const DEFAULT_MAX_WAIT_MS = 3_000 export interface LeaderLockOptions { key: string ttlSec?: number pollIntervalMs?: number + /** + * How long a follower polls before its takeover try. Defaults to the lease TTL so that try comes + * after a dead leader's lease has lapsed; a shorter wait can only take over a released lease. + */ maxWaitMs?: number onLeader: () => Promise onFollower: () => Promise @@ -31,7 +34,7 @@ export async function withLeaderLock(opts: LeaderLockOptions): Promise