Skip to content

Commit b9e640c

Browse files
committed
fix(execution): reclaim a lease Redis registers after the local deadline
Addresses review findings on the fallback path. - Always release the lease. The deadline abandons the local wait but cannot cancel the script, so a late completion still registers the lease id; leaving it unreleased kept it counted against the owner for the whole TTL and denied later executions that did have capacity. The id is unique per execution, so removing one that was never registered is a no-op. - Treat a non-positive configured deadline as unconfigured. A timer of zero or less fires immediately, which would leave every acquisition undetermined and silently drop cross-replica enforcement. - Cover both with tests: a lease that completes after the deadline is still released, and a non-positive deadline still lets a real answer land.
1 parent 2018225 commit b9e640c

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

apps/sim/lib/execution/isolated-vm.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
loggerMock,
99
redisConfigMockFns,
1010
} from '@sim/testing'
11+
import { sleep } from '@sim/utils/helpers'
1112
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1213

1314
type MockProc = EventEmitter & {
@@ -580,6 +581,76 @@ describe('isolated-vm scheduler', () => {
580581
expect(result.result).toBe('ok')
581582
})
582583

584+
it('releases a lease that Redis registers after the local deadline', async () => {
585+
const scripts: string[] = []
586+
let completeAcquire!: (value: number) => void
587+
const lateAcquire = new Promise<number>((resolve) => {
588+
completeAcquire = resolve
589+
})
590+
const { executeInIsolatedVM } = await loadExecutionModule({
591+
envOverrides: {
592+
REDIS_URL: 'redis://localhost:6379',
593+
IVM_LEASE_REDIS_DEADLINE_MS: '5',
594+
},
595+
spawns: [() => createReadyProc('ok')],
596+
redisEvalImpl: (...args: unknown[]) => {
597+
const script = String(args[0] ?? '')
598+
scripts.push(script)
599+
// Settles only once the test says so, standing in for a script the
600+
// deadline abandoned locally but that Redis still runs to completion.
601+
if (script.includes('ZREMRANGEBYSCORE')) return lateAcquire
602+
return 1
603+
},
604+
})
605+
606+
const result = await executeInIsolatedVM({
607+
code: 'return "ok"',
608+
params: {},
609+
envVars: {},
610+
contextVariables: {},
611+
timeoutMs: 100,
612+
requestId: 'req-11',
613+
ownerKey: 'user:redis-late',
614+
})
615+
completeAcquire(1)
616+
617+
expect(result.error).toBeUndefined()
618+
expect(scripts.some((script) => script.includes("'ZREM'"))).toBe(true)
619+
})
620+
621+
it('ignores a non-positive configured deadline instead of abandoning every lease', async () => {
622+
const { executeInIsolatedVM } = await loadExecutionModule({
623+
envOverrides: {
624+
IVM_DISTRIBUTED_MAX_INFLIGHT_PER_OWNER: '1',
625+
IVM_LEASE_REDIS_DEADLINE_MS: '-1',
626+
REDIS_URL: 'redis://localhost:6379',
627+
},
628+
spawns: [() => createReadyProc('ok')],
629+
redisEvalImpl: async (...args: unknown[]) => {
630+
const script = String(args[0] ?? '')
631+
if (script.includes('ZREMRANGEBYSCORE')) {
632+
// Arrives after a non-positive timer would already have fired, so the
633+
// answer only lands in time when the default deadline is restored.
634+
await sleep(25)
635+
return 0
636+
}
637+
return 1
638+
},
639+
})
640+
641+
const result = await executeInIsolatedVM({
642+
code: 'return "ok"',
643+
params: {},
644+
envVars: {},
645+
contextVariables: {},
646+
timeoutMs: 100,
647+
requestId: 'req-12',
648+
ownerKey: 'user:negative-deadline',
649+
})
650+
651+
expect(result.error?.message).toContain('Too many concurrent')
652+
})
653+
583654
it('still rejects when Redis answers that the owner is over its lease limit', async () => {
584655
const { executeInIsolatedVM } = await loadExecutionModule({
585656
envOverrides: {

apps/sim/lib/execution/isolated-vm.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,14 @@ const DISTRIBUTED_KEY_PREFIX = 'ivm:fair:v1:owner'
148148
* actually measure is event-loop scheduling, not Redis. A value near normal loop
149149
* latency therefore reports a healthy Redis as unreachable whenever a garbage
150150
* collection pause lands on the call. Keep it well clear of that floor.
151+
*
152+
* A non-positive configured value is treated as unconfigured rather than
153+
* honored: a timer of zero or less fires immediately, which would leave every
154+
* acquisition undetermined and silently drop cross-replica enforcement.
151155
*/
152-
const LEASE_REDIS_DEADLINE_MS = Number.parseInt(env.IVM_LEASE_REDIS_DEADLINE_MS) || 1000
156+
const CONFIGURED_LEASE_REDIS_DEADLINE_MS = Number.parseInt(env.IVM_LEASE_REDIS_DEADLINE_MS)
157+
const LEASE_REDIS_DEADLINE_MS =
158+
CONFIGURED_LEASE_REDIS_DEADLINE_MS > 0 ? CONFIGURED_LEASE_REDIS_DEADLINE_MS : 1000
153159
const QUEUE_RETRY_DELAY_MS = 1000
154160
const DISTRIBUTED_LEASE_GRACE_MS = 30000
155161

@@ -1439,12 +1445,17 @@ export async function executeInIsolatedVM(
14391445
// the per-owner active/queued limits above still bound this work.
14401446

14411447
let settled = false
1442-
const holdsDistributedLease = leaseAcquireResult === 'acquired'
1448+
/**
1449+
* Released even when the acquisition was undetermined. The deadline abandons
1450+
* the local wait but cannot cancel the script, so a late completion still
1451+
* registers this lease id — and unreleased it would count against the owner
1452+
* for the whole TTL, denying later executions that do have capacity. The
1453+
* lease id is unique to this execution, so removing one that was never
1454+
* registered is a no-op.
1455+
*/
14431456
const releaseLease = () => {
14441457
if (settled) return
14451458
settled = true
1446-
// Nothing was registered when the lease was undetermined; skip the round trip.
1447-
if (!holdsDistributedLease) return
14481459
releaseDistributedLease(ownerKey, distributedLeaseId).catch((error) => {
14491460
logger.error('Failed to release distributed lease', { ownerKey, error })
14501461
})

0 commit comments

Comments
 (0)