mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(wsl-host): release a failed add's holder and cancel adds the experiment was turned off during (#29889)
A failed or deferred first add kept its detached distro holder with no record to find it; an add in flight when WSL hosts were turned off still finished as a ready host.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* The connect step of a first WSL host Add. Until the Add persists a record, nothing else (Remove,
|
||||
* disabling the experiment, startup adoption) can find this distro's connection or detached
|
||||
* holder, so every way the Add can end without a host must release them here.
|
||||
*/
|
||||
import { errorMessage } from '../../shared/error-message'
|
||||
import type { WslHostAddResult } from '../../shared/wsl-host-types'
|
||||
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
|
||||
import type { WslHostConnector } from './wsl-host-service'
|
||||
import { WslOrcadLaunchDeferredError, type WslOrcadConnection } from './wsl-orcad-connection'
|
||||
|
||||
type WslHostAddRefusal = Extract<WslHostAddResult, { ok: false }>
|
||||
type AddConnector = Pick<WslHostConnector, 'connect' | 'disconnect' | 'releaseHolder'>
|
||||
|
||||
export async function abandonWslHostAdd(connector: AddConnector, distro: string): Promise<void> {
|
||||
await connector.disconnect(distro).catch(() => {})
|
||||
await connector.releaseHolder(distro).catch(() => {})
|
||||
}
|
||||
|
||||
/** `lateRefusal` is re-read once the connect lands: the setting can change while it runs. */
|
||||
export async function connectForWslHostAdd(
|
||||
connector: AddConnector,
|
||||
distro: string,
|
||||
lateRefusal: () => WslHostAddRefusal | null
|
||||
): Promise<{ connection: WslOrcadConnection } | WslHostAddRefusal> {
|
||||
let connection: WslOrcadConnection
|
||||
try {
|
||||
connection = await connector.connect(distro)
|
||||
} catch (error) {
|
||||
await abandonWslHostAdd(connector, distro)
|
||||
if (error instanceof WslOrcadLaunchDeferredError) {
|
||||
return { ok: false, kind: 'deferred', message: error.launch.reason }
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
kind: error instanceof OrcadHostUnsupportedError ? 'unsupported-wsl1' : 'failed',
|
||||
message: errorMessage(error)
|
||||
}
|
||||
}
|
||||
const refusal = lateRefusal()
|
||||
if (refusal) {
|
||||
await abandonWslHostAdd(connector, distro)
|
||||
return refusal
|
||||
}
|
||||
return { connection }
|
||||
}
|
||||
@@ -424,6 +424,57 @@ describe('WslHostService', () => {
|
||||
expect(connector.connect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('releases the holder of a failed or deferred first add, which no record would ever find', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
connector.connect.mockRejectedValueOnce(new Error('deploy failed'))
|
||||
const service = makeService(connector)
|
||||
await expect(service.add({ distro: 'Ubuntu' })).resolves.toMatchObject({ kind: 'failed' })
|
||||
expect(connector.disconnect).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
|
||||
|
||||
vi.clearAllMocks()
|
||||
connector.connect.mockRejectedValueOnce(
|
||||
new WslOrcadLaunchDeferredError({
|
||||
outcome: 'deferred',
|
||||
distro: 'Ubuntu',
|
||||
candidateVersion: '2.0.0',
|
||||
code: 'live-terminals',
|
||||
reason: 'Terminals are still running.',
|
||||
forceable: true
|
||||
})
|
||||
)
|
||||
await expect(service.add({ distro: 'Ubuntu' })).resolves.toMatchObject({ kind: 'deferred' })
|
||||
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(listEnvironments(userDataPath)).toEqual([])
|
||||
})
|
||||
|
||||
it('an add the experiment was turned off during ends without a host, connection or holder', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
let finishConnect: () => void = () => {}
|
||||
connector.connect.mockImplementationOnce(async (distro: string) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
finishConnect = resolve
|
||||
})
|
||||
return connection(distro)
|
||||
})
|
||||
const adding = service.add({ distro: 'Ubuntu' })
|
||||
await vi.waitFor(() => expect(connector.connect).toHaveBeenCalled())
|
||||
|
||||
// Off and back on before the connect lands: the add started under the old setting.
|
||||
enabled = false
|
||||
await service.enabledChanged(false)
|
||||
enabled = true
|
||||
await service.enabledChanged(true)
|
||||
finishConnect()
|
||||
|
||||
await expect(adding).resolves.toMatchObject({ ok: false, kind: 'disabled' })
|
||||
expect(listEnvironments(userDataPath)).toEqual([])
|
||||
expect(connector.disconnect).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(connector.releaseHolder).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(ready).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('treats a distro being added as known, so its first connect may spawn', async () => {
|
||||
const connector = makeConnector([UBUNTU])
|
||||
const service = makeService(connector)
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
type WslHostServerStop,
|
||||
type WslHostStatus
|
||||
} from '../../shared/wsl-host-types'
|
||||
import { OrcadHostUnsupportedError } from '../orcad/orcad-host-unavailable'
|
||||
import type { WslHostDistroCandidate } from './wsl-host-distro-list'
|
||||
import { wslHostSshManagedRefusal } from './wsl-host-ssh-orcad-conflict'
|
||||
import { wslDistroHolderKey } from './wsl-distro-holder-record'
|
||||
@@ -29,9 +28,11 @@ import {
|
||||
sameWslDistro,
|
||||
wslHostNotConnectedMessage,
|
||||
WSL_HOST_REMOVING_MESSAGE,
|
||||
WSL_HOSTS_DISABLED_MESSAGE,
|
||||
wslHostRunRefusal
|
||||
} from './wsl-host-add-admission'
|
||||
import { WslOrcadLaunchDeferredError, type WslOrcadConnection } from './wsl-orcad-connection'
|
||||
import type { WslOrcadConnection } from './wsl-orcad-connection'
|
||||
import { abandonWslHostAdd, connectForWslHostAdd } from './wsl-host-first-connect'
|
||||
import type { WslOrcadConnectionStatus } from './wsl-orcad-connection-status'
|
||||
import { defaultWslOrcadEnvironmentId } from './wsl-orcad-deploy'
|
||||
|
||||
@@ -70,6 +71,8 @@ export class WslHostService {
|
||||
private readonly adding = new Map<string, Promise<WslHostAddResult>>()
|
||||
private visible = new Set<string>()
|
||||
private readonly removing = new Set<string>()
|
||||
// Bumped on disable so an Add that started before it cannot finish as a ready host.
|
||||
private disableGeneration = 0
|
||||
|
||||
constructor(private readonly deps: WslHostServiceDependencies) {}
|
||||
|
||||
@@ -167,6 +170,7 @@ export class WslHostService {
|
||||
this.setVisibleWork([...this.visible], new Set())
|
||||
return
|
||||
}
|
||||
this.disableGeneration += 1
|
||||
for (const record of this.records()) {
|
||||
await connector.disconnect(record.distro)
|
||||
await connector.releaseHolder(record.distro)
|
||||
@@ -264,6 +268,7 @@ export class WslHostService {
|
||||
if (refusal) {
|
||||
return refusal
|
||||
}
|
||||
const generation = this.disableGeneration
|
||||
const admission = admitWslHostDistro({
|
||||
requested: args.distro,
|
||||
candidates: await this.wsl.listDistros(),
|
||||
@@ -282,19 +287,15 @@ export class WslHostService {
|
||||
return sharing
|
||||
}
|
||||
const environmentId = defaultWslOrcadEnvironmentId(distro.name)
|
||||
let connection: WslOrcadConnection
|
||||
try {
|
||||
connection = await this.wsl.connect(distro.name)
|
||||
} catch (error) {
|
||||
if (error instanceof WslOrcadLaunchDeferredError) {
|
||||
return { ok: false, kind: 'deferred', message: error.launch.reason }
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
kind: error instanceof OrcadHostUnsupportedError ? 'unsupported-wsl1' : 'failed',
|
||||
message: errorMessage(error)
|
||||
}
|
||||
const connected = await connectForWslHostAdd(this.wsl, distro.name, () =>
|
||||
generation === this.disableGeneration
|
||||
? wslHostRunRefusal(this.deps)
|
||||
: { ok: false, kind: 'disabled', message: WSL_HOSTS_DISABLED_MESSAGE }
|
||||
)
|
||||
if (!('connection' in connected)) {
|
||||
return connected
|
||||
}
|
||||
const { connection } = connected
|
||||
try {
|
||||
addWslHostEnvironment(userDataPath, {
|
||||
id: environmentId,
|
||||
@@ -309,8 +310,7 @@ export class WslHostService {
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
await this.wsl.disconnect(distro.name).catch(() => {})
|
||||
await this.wsl.releaseHolder(distro.name).catch(() => {})
|
||||
await abandonWslHostAdd(this.wsl, distro.name)
|
||||
return { ok: false, kind: 'failed', message: errorMessage(error) }
|
||||
}
|
||||
const record = this.recordForEnvironment(environmentId)
|
||||
|
||||
Reference in New Issue
Block a user