fix: dispose races, tunnel guards, retry on stale
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user