fix: dispose races, tunnel guards, retry on stale

This commit is contained in:
smartass
2026-06-12 01:48:25 +05:00
parent 1fecb1cce4
commit 32143566d4
15 changed files with 412 additions and 61 deletions
+74
View File
@@ -225,6 +225,32 @@ describe('createManager', () => {
expect(tunnels[1].closed).toBe(false)
})
it('retries instead of throwing when a build fails after ownership is lost', async () => {
let release: () => void = () => {}
const gate = new Promise<void>((resolve) => {
release = resolve
})
// First build is gated and then rejects; later builds succeed.
const gatedTunnel = vi
.fn(createTunnel)
.mockImplementationOnce(async () => {
await gate
throw new Error('stale build boom')
})
.mockImplementation(createTunnel)
resolved = { config: config(sshExtra()), source: 'store', hash: 'ssh1' }
const manager = createManager(registry, { createDriver, createTunnel: gatedTunnel })
const pending = manager.get('c')
// Drop ownership of the in-flight slot (synchronous cache.delete) before
// its build rejects; do not await invalidate or it blocks on the gate.
const invalidated = manager.invalidate('c')
release()
// The stale build's error is swallowed; get retries and resolves fresh.
const managed = await pending
expect(managed.driver).toBe(drivers[0])
await invalidated
})
it('disposeAll disposes everything', async () => {
const manager = createManager(registry, { createDriver, createTunnel })
await manager.get('c')
@@ -233,4 +259,52 @@ describe('createManager', () => {
const again = await manager.get('c')
expect(again.driver).toBe(drivers[1])
})
it('disposeAll waits for an in-flight rotation dispose', async () => {
let release: () => void = () => {}
const gate = new Promise<void>((resolve) => {
release = resolve
})
// The first driver's dispose is gated; rotation fires it and forgets it.
const gatingCreateDriver = (target: DriverTarget): Driver => {
targets.push(target)
const first = drivers.length === 0
const driver = fakeDriver()
if (first) {
driver.dispose = vi.fn(async () => {
await gate
driver.disposed = true
})
}
drivers.push(driver)
return driver
}
const manager = createManager(registry, {
createDriver: gatingCreateDriver,
createTunnel
})
await manager.get('c')
// Hash change rotates the old entry out: its dispose is fire-and-forget.
resolved = { config: config({ port: 9999 }), source: 'store', hash: 'h2' }
await manager.get('c')
expect(drivers[0].disposed).toBe(false)
let resolvedAll = false
const all = manager.disposeAll().then(() => {
resolvedAll = true
})
// Flush all currently-queued microtasks: disposeAll must still be
// pending because the gated rotation dispose has not completed.
for (let i = 0; i < 10; i += 1) {
await Promise.resolve()
}
expect(resolvedAll).toBe(false)
expect(drivers[0].disposed).toBe(false)
release()
await all
// disposeAll only resolves once the rotated-out driver finished disposing.
expect(drivers[0].disposed).toBe(true)
expect(drivers[1].disposed).toBe(true)
})
})