Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/sim/lib/concurrency/leader-lock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>({ 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')
Expand Down
7 changes: 5 additions & 2 deletions apps/sim/lib/concurrency/leader-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
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<T | null>
onFollower: () => Promise<T | null>
Expand All @@ -31,7 +34,7 @@ export async function withLeaderLock<T>(opts: LeaderLockOptions<T>): Promise<T |
key,
ttlSec = DEFAULT_TTL_SEC,
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
maxWaitMs = DEFAULT_MAX_WAIT_MS,
maxWaitMs = ttlSec * 1000,
onLeader,
onFollower,
} = opts
Expand Down