mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(wsl): show a stopped WSL host within a second of its stop, never exec-ing into it (#35823)
After a watch or holder exit, a distro the running list still names is usually stopping. Liveness now re-reads only the running list (never boots) once a second for up to 5 s before any identity exec, and publishes distro-stopped as soon as the list drops it. An exit that arrives during a recovery backoff ends that recovery so the exit re-checks at once (E9).
This commit is contained in:
@@ -26,6 +26,8 @@ export type WslOrcadLivenessDependencies = {
|
||||
readRunning: (target: {
|
||||
distro: string
|
||||
exitedAt?: number | null
|
||||
/** Only a probe started at or after this instant (monotonic) may answer; a cached list won't. */
|
||||
probedAfter?: number
|
||||
}) => Promise<WslRunningDistrosRead>
|
||||
readIdentity: (distro: string, orcadPid: number | null) => Promise<WslGuestIdentity | null>
|
||||
openWatch: (connection: WslOrcadConnection) => Promise<WslOrcadWatch>
|
||||
@@ -125,5 +127,10 @@ export function isWatchOrHolderExit(trigger: Trigger): boolean {
|
||||
export function noteSupervisedExit(entry: Supervised, trigger: Trigger, nowMs: number): void {
|
||||
if (isWatchOrHolderExit(trigger)) {
|
||||
entry.lastExitMs = nowMs
|
||||
// An exit during a recovery backoff ends that recovery, so the exit re-reads the list now.
|
||||
if (entry.busy && entry.wakeWait) {
|
||||
entry.generation += 1
|
||||
entry.wakeWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { WslRunningDistrosRead } from '../wsl-background-distro-gate'
|
||||
import { confirmsRunning, EXIT_SETTLE_MS } from './wsl-orcad-liveness-evidence'
|
||||
import type { Supervised, WslOrcadLivenessDependencies } from './wsl-orcad-liveness-state'
|
||||
|
||||
export const WSL_ORCAD_STOPPING_POLL_MS = 1_000
|
||||
export const WSL_ORCAD_STOPPING_WINDOW_MS = 5_000
|
||||
|
||||
/**
|
||||
* After a watch or holder exit, a distro the list still names is usually stopping: read only the
|
||||
* running list (never boots) until it drops the distro or the window ends, before any exec into
|
||||
* it. An exec while it stops is slow to fail and, landing after the stop, boots it (E9).
|
||||
*/
|
||||
export async function readRunningWhileDistroMayBeStopping(args: {
|
||||
distro: string
|
||||
key: string
|
||||
exitedAt: number | null
|
||||
running: WslRunningDistrosRead
|
||||
now: () => number
|
||||
schedule: Schedule
|
||||
readRunning: WslOrcadLivenessDependencies['readRunning']
|
||||
current: () => boolean
|
||||
}): Promise<WslRunningDistrosRead> {
|
||||
let running = args.running
|
||||
const deadline = args.now() + WSL_ORCAD_STOPPING_WINDOW_MS
|
||||
while (confirmsRunning(running, args.key) && args.now() < deadline) {
|
||||
await new Promise<void>((resolve) => args.schedule(WSL_ORCAD_STOPPING_POLL_MS, resolve))
|
||||
if (!args.current()) {
|
||||
break
|
||||
}
|
||||
running = await args.readRunning({
|
||||
distro: args.distro,
|
||||
exitedAt: args.exitedAt,
|
||||
probedAfter: args.now()
|
||||
})
|
||||
}
|
||||
return running
|
||||
}
|
||||
|
||||
type Schedule = (delayMs: number, task: () => void) => unknown
|
||||
|
||||
/** After an exit: wait out the settle window, re-read the list, then watch it while it still names the distro. */
|
||||
export async function settleExitedDistroRunning(
|
||||
entry: Pick<Supervised, 'distro' | 'key' | 'lastExitMs'>,
|
||||
deps: Pick<WslOrcadLivenessDependencies, 'readRunning'>,
|
||||
current: () => boolean,
|
||||
now: () => number,
|
||||
schedule: Schedule
|
||||
): Promise<WslRunningDistrosRead> {
|
||||
await new Promise<void>((resolve) => schedule(EXIT_SETTLE_MS, resolve))
|
||||
const settled = await deps.readRunning({ distro: entry.distro, exitedAt: entry.lastExitMs })
|
||||
if (!current()) {
|
||||
return settled
|
||||
}
|
||||
return readRunningWhileDistroMayBeStopping({
|
||||
distro: entry.distro,
|
||||
key: entry.key,
|
||||
exitedAt: entry.lastExitMs,
|
||||
running: settled,
|
||||
now,
|
||||
schedule,
|
||||
readRunning: deps.readRunning,
|
||||
current
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,10 @@ import { WSL_ORCAD_IDLE_VERIFY_MS } from './wsl-orcad-liveness-monitor'
|
||||
import { WslDistroNotRunningError } from './wsl-orcad-connection-errors'
|
||||
import { WslOrcadLivenessSupervisor } from './wsl-orcad-liveness-supervisor'
|
||||
import { WSL_ORCAD_RECONNECT_DELAYS_MS } from './wsl-orcad-reconnect-backoff'
|
||||
import {
|
||||
WSL_ORCAD_STOPPING_POLL_MS,
|
||||
WSL_ORCAD_STOPPING_WINDOW_MS
|
||||
} from './wsl-orcad-liveness-stopping-poll'
|
||||
import type { WslOrcadWatch } from './wsl-orcad-watch'
|
||||
|
||||
const CENSUS: OrcadTerminalCensus = {
|
||||
@@ -308,7 +312,8 @@ describe('WslOrcadLivenessSupervisor', () => {
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
h.state.identity = { distroInstance: 'boot:1', orcad: null }
|
||||
h.lastWatch().exit(0)
|
||||
await h.advance(5_000)
|
||||
// The distro stays listed: the list is watched through the stopping window before any exec.
|
||||
await h.advance(5_000 + WSL_ORCAD_STOPPING_WINDOW_MS)
|
||||
const states = h.statuses.map((status) =>
|
||||
status.state === 'reconnecting' ? `${status.state}:${status.reason}` : status.state
|
||||
)
|
||||
@@ -356,8 +361,8 @@ describe('WslOrcadLivenessSupervisor', () => {
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
h.state.identity = { distroInstance: 'boot:1', orcad: null }
|
||||
h.lastWatch().exit(0)
|
||||
// A clean exit is settled (and the list re-read) before the verdict, as a failed one is.
|
||||
await h.advance(EXIT_SETTLE_MS)
|
||||
// A clean exit is settled (and the list watched) before the verdict, as a failed one is.
|
||||
await h.advance(EXIT_SETTLE_MS + WSL_ORCAD_STOPPING_WINDOW_MS)
|
||||
expect(h.statuses.at(-1)).toMatchObject({ state: 'reconnecting' })
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
expect(h.statuses.at(-1)).toEqual({ state: 'ready', census: null })
|
||||
@@ -397,7 +402,7 @@ describe('WslOrcadLivenessSupervisor', () => {
|
||||
const h = harness()
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
h.lastWatch().exit(0)
|
||||
await h.advance(EXIT_SETTLE_MS)
|
||||
await h.advance(EXIT_SETTLE_MS + WSL_ORCAD_STOPPING_WINDOW_MS)
|
||||
expect(h.statuses).toEqual([])
|
||||
expect(h.watches).toHaveLength(2)
|
||||
expect(h.ensure).toHaveBeenCalledTimes(1)
|
||||
@@ -535,7 +540,7 @@ describe('WslOrcadLivenessSupervisor', () => {
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
// A watch riding the desktop's sockets reports no exit code.
|
||||
h.lastWatch().exit(null)
|
||||
await h.advance(5_000)
|
||||
await h.advance(5_000 + WSL_ORCAD_STOPPING_WINDOW_MS)
|
||||
expect(h.readIdentity).toHaveBeenCalledTimes(2)
|
||||
expect(h.statuses.filter((status) => status.state !== 'ready')).toEqual([])
|
||||
expect(h.ensure).toHaveBeenCalledTimes(1)
|
||||
@@ -610,4 +615,67 @@ describe('WslOrcadLivenessSupervisor', () => {
|
||||
await h.advance(1_000)
|
||||
expect(h.statuses.map((status) => status.state)).toEqual(['distro-stopped'])
|
||||
})
|
||||
|
||||
describe('after an exit, while the distro may still be stopping (E9)', () => {
|
||||
const STOPPED: WslRunningDistrosRead = { status: 'confirmed', distros: [] }
|
||||
|
||||
it('publishes distro-stopped within a poll of the list dropping it, never exec-ing into it', async () => {
|
||||
const h = harness()
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
const identityReads = h.readIdentity.mock.calls.length
|
||||
h.lastWatch().exit(null)
|
||||
await h.advance(EXIT_SETTLE_MS + 2_000)
|
||||
// Still listed mid-window: only the list was read, nothing ran inside the distro.
|
||||
expect(h.readIdentity).toHaveBeenCalledTimes(identityReads)
|
||||
expect(h.statuses).toEqual([])
|
||||
|
||||
h.state.running = STOPPED
|
||||
await h.advance(WSL_ORCAD_STOPPING_POLL_MS)
|
||||
expect(h.statuses).toEqual([
|
||||
{ state: 'distro-stopped', terminals: 'exited', recovering: false }
|
||||
])
|
||||
expect(h.readIdentity).toHaveBeenCalledTimes(identityReads)
|
||||
})
|
||||
|
||||
it('falls back to the identity check once the window ends with the distro still listed', async () => {
|
||||
const h = harness()
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
const identityReads = h.readIdentity.mock.calls.length
|
||||
h.supervisor.noteHolderExit('Ubuntu', { code: 1 })
|
||||
await h.advance(EXIT_SETTLE_MS + WSL_ORCAD_STOPPING_WINDOW_MS - WSL_ORCAD_STOPPING_POLL_MS)
|
||||
expect(h.readIdentity).toHaveBeenCalledTimes(identityReads)
|
||||
await h.advance(WSL_ORCAD_STOPPING_POLL_MS)
|
||||
expect(h.readIdentity).toHaveBeenCalledTimes(identityReads + 1)
|
||||
expect(h.statuses).toEqual([])
|
||||
})
|
||||
|
||||
it('adds no running-list reads to heartbeats while the distro runs', async () => {
|
||||
const h = harness()
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
await h.advance(WSL_ORCAD_HEARTBEAT_MS * 3)
|
||||
expect(h.readRunning).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('ends a recovery backoff when the distro then stops, rather than wait it out', async () => {
|
||||
const h = harness()
|
||||
await h.supervisor.connect('Ubuntu')
|
||||
// orcad crashed and keeps failing to relaunch: recovery sits in a long backoff.
|
||||
h.state.identity = { distroInstance: 'boot:1', orcad: null }
|
||||
h.state.ensureFails = 100
|
||||
h.lastWatch().exit(0)
|
||||
await h.advance(EXIT_SETTLE_MS + WSL_ORCAD_STOPPING_WINDOW_MS + 8_000)
|
||||
const waiting = h.statuses.at(-1)
|
||||
expect(waiting).toMatchObject({ state: 'reconnecting' })
|
||||
expect(waiting && 'retryInMs' in waiting ? waiting.retryInMs : 0).toBeGreaterThan(2_000)
|
||||
|
||||
h.state.running = STOPPED
|
||||
h.supervisor.noteHolderExit('Ubuntu', { code: 1 })
|
||||
await h.advance(EXIT_SETTLE_MS + 100)
|
||||
expect(h.statuses.at(-1)).toEqual({
|
||||
state: 'distro-stopped',
|
||||
terminals: 'exited',
|
||||
recovering: false
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
/** D2c/C6 classify watch exits against fresh running lists and guest incarnation, publishing through onChange. Boot only for visible demand; background work waits for running evidence. Use monotonic timers and reverify after resume. */
|
||||
import type { WslOrcadConnection } from './wsl-orcad-connection'
|
||||
import { settleExitedDistroRunning } from './wsl-orcad-liveness-stopping-poll'
|
||||
import { WslDistroNotRunningError } from './wsl-orcad-connection-errors'
|
||||
import type { WslOrcadConnectIntent, WslOrcadConnectionStatus } from './wsl-orcad-connection-status'
|
||||
import { wslDistroHolderKey } from './wsl-distro-holder-record'
|
||||
import { recoverWslOrcad, type WslOrcadRecoveryReason } from './wsl-orcad-liveness-recovery'
|
||||
import {
|
||||
confirmsRunning,
|
||||
EXIT_SETTLE_MS,
|
||||
gatherWslLivenessEvidence
|
||||
} from './wsl-orcad-liveness-evidence'
|
||||
import { confirmsRunning, gatherWslLivenessEvidence } from './wsl-orcad-liveness-evidence'
|
||||
import {
|
||||
attachSupervisedWatch,
|
||||
cancellableWait,
|
||||
@@ -224,10 +221,10 @@ export class WslOrcadLivenessSupervisor {
|
||||
return
|
||||
}
|
||||
if (isWatchOrHolderExit(trigger) && confirmsRunning(running, entry.key)) {
|
||||
// Stopping distros can still appear running; wait before exec accidentally boots them again.
|
||||
await new Promise<void>((resolve) => this.schedule(EXIT_SETTLE_MS, resolve))
|
||||
running = await this.deps.readRunning({ distro: entry.distro, exitedAt: entry.lastExitMs })
|
||||
if (entry.generation !== generation) {
|
||||
// Stopping distros can still appear running; exec into one could boot it again (E9).
|
||||
const current = (): boolean => entry.generation === generation
|
||||
running = await settleExitedDistroRunning(entry, this.deps, current, this.now, this.schedule)
|
||||
if (!current()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,3 +48,12 @@ it('checks a no-boot reconnect against a settled read of its own distro (E9)', a
|
||||
mocks.readSettled.mockResolvedValue({ status: 'confirmed', distros: [] })
|
||||
await expect(mocks.captured.deps?.requireRunning?.('Ubuntu')).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('makes each stopping-window poll take a fresh probe, never the list read before it', async () => {
|
||||
await mocks.captured.deps?.readRunning({ distro: 'Ubuntu', exitedAt: 1_000, probedAfter: 4_000 })
|
||||
expect(mocks.readSettled).toHaveBeenLastCalledWith('Ubuntu', 4_000)
|
||||
|
||||
// A poll is never older than the exit's settle window.
|
||||
await mocks.captured.deps?.readRunning({ distro: 'Ubuntu', exitedAt: 1_000, probedAfter: 1_200 })
|
||||
expect(mocks.readSettled).toHaveBeenLastCalledWith('Ubuntu', 1_000 + EXIT_SETTLE_MS)
|
||||
})
|
||||
|
||||
@@ -47,11 +47,14 @@ export function createWslOrcadLivenessSupervisor(options: {
|
||||
}): WslOrcadLivenessSupervisor {
|
||||
return new WslOrcadLivenessSupervisor({
|
||||
manager: options.manager,
|
||||
readRunning: ({ distro, exitedAt }) =>
|
||||
readSettledConfirmedRunningWslDistros(
|
||||
distro,
|
||||
readRunning: ({ distro, exitedAt, probedAfter }) => {
|
||||
const settled =
|
||||
exitedAt === null || exitedAt === undefined ? undefined : exitedAt + EXIT_SETTLE_MS
|
||||
),
|
||||
return readSettledConfirmedRunningWslDistros(
|
||||
distro,
|
||||
probedAfter === undefined ? settled : Math.max(settled ?? -Infinity, probedAfter)
|
||||
)
|
||||
},
|
||||
readIdentity: (distro, orcadPid) => readWslGuestIdentity(transportFor(distro), orcadPid),
|
||||
openWatch: (connection) =>
|
||||
probeThenWatchWslOrcadBridges(
|
||||
|
||||
Reference in New Issue
Block a user