blob: 67ec9425deed19dddf2bc31168227f671317f411 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import {
prepareStorageRootControlDirectory,
resolveStorageRoot,
StorageRootAuthorityError,
} from '@maka/storage/root-authority';
import { readStateRootCompositionBinding } from '@maka/storage/state-root-composition';
import { performance } from 'node:perf_hooks';
import {
requireClientInstanceId,
requireHostCompositionId,
validateProtocolRange,
type HostRegistration,
type HostIncompatible,
type ProtocolRange,
} from '../protocol/index.js';
import {
connectResolvedRuntimeHost,
type ConnectRuntimeHostResult,
type RuntimeHostConnection,
type RuntimeHostConnectionFailure,
} from './connection.js';
import {
launchDetachedRuntimeHostCandidate,
launchOwnedRuntimeHostCandidate,
type CandidateExitDetails,
type CandidateProcessExit,
type CandidateLauncher,
type DetachedCandidateAttempt,
type OwnedCandidateAttempt,
} from './launcher.js';
export type { CandidateExitDetails } from './launcher.js';
import {
isPermanentCandidateStartupFailure,
type CandidateStartupFailure,
type CandidateStartupFailureReport,
} from '../candidate-startup-failure.js';
import {
clearCandidateStartupDiagnostic,
selectCandidateStartupDiagnostic,
} from '../control/startup-diagnostic.js';
import {
decodeRuntimeHostManagedLaunchClaim,
readRuntimeHostManagedDeploymentConfig,
runtimeHostManagedLaunchRejection,
RuntimeHostManagedDeploymentError,
type RuntimeHostManagedDeploymentAuthorityOptions,
type RuntimeHostManagedLaunchClaim,
} from '../operator/managed-deployment.js';
import { abortable, waitForRuntimeHostReady } from './wait-for-ready.js';
// Candidate readiness includes the Windows named-pipe ACL helper, whose
// fail-closed ceiling is 60s. Leave enough room for election and connection
// bookkeeping after that helper returns.
const DEFAULT_ELECTION_DEADLINE_MS = 75_000;
const DEFAULT_BACKOFF_MIN_MS = 20;
const DEFAULT_BACKOFF_MAX_MS = 250;
const MIN_CANDIDATE_INTERVAL_MS = 250;
export const ELECTION_DEADLINE_MS_ENV_VAR = 'MAKA_RUNTIME_HOST_ELECTION_DEADLINE_MS';
export const IDLE_GRACE_MS_ENV_VAR = 'MAKA_RUNTIME_HOST_IDLE_GRACE_MS';
export interface ConnectOrSpawnRuntimeHostInput {
rootPath: string;
protocol: ProtocolRange;
compositionId: string;
generation?: string;
takeoverHostEpoch?: string;
clientInstanceId?: string;
electionDeadlineMs?: number;
idleGraceMs?: number;
connectTimeoutMs?: number;
handshakeTimeoutMs?: number;
candidateEntrypoint: string | URL;
candidateExecutable?: string;
managedLaunchClaim?: RuntimeHostManagedLaunchClaim;
signal?: AbortSignal;
/** Existing authority lease inherited by a launch-owner-supervised Candidate. */
inheritableAuthorityLeaseFd?: number;
/** Close a newly spawned ephemeral Candidate if this launcher exits. */
closeOnLauncherExit?: boolean;
/** Candidate-exit sink forwarded to the launcher; the embedder owns the sink. */
onExit?: (details: CandidateExitDetails) => void;
}
interface ConnectOrSpawnRuntimeHostDependencies {
launchCandidate: CandidateLauncher;
random(): number;
/** Defaults to `process.env`; injected so tests never mutate the real environment. */
env?: NodeJS.ProcessEnv;
connectHost?: typeof connectResolvedRuntimeHost;
/** Authority-location override for tests and embedded runtimes. */
managedDeploymentAuthority?: RuntimeHostManagedDeploymentAuthorityOptions;
}
type ElectionConnectionResult = Awaited<ReturnType<typeof connectResolvedRuntimeHost>>;
const defaultDependencies: ConnectOrSpawnRuntimeHostDependencies = {
launchCandidate: launchDetachedRuntimeHostCandidate,
random: Math.random,
};
// Invalid values fail closed: a silently ignored typo would look like a changed window.
function durationMsFromEnvironment(
rawValue: string | undefined,
envVar: string,
minimum: number,
): number | undefined {
if (rawValue === undefined || rawValue.trim() === '') return undefined;
const parsed = Number(rawValue);
requireOptionalTimeout(parsed, envVar, minimum);
return parsed;
}
/**
* Resolves the operator override for the client election deadline. Large
* workspaces can legitimately take longer than the default window on their
* first start after an upgrade, so the deadline must be raisable without a
* code change.
*/
export function electionDeadlineMsFromEnvironment(
rawValue: string | undefined,
): number | undefined {
return durationMsFromEnvironment(rawValue, ELECTION_DEADLINE_MS_ENV_VAR, 1);
}
export type ConnectOrSpawnRuntimeHostResult =
| {
kind: 'connected';
connection: RuntimeHostConnection;
registration: Extract<ConnectRuntimeHostResult, { kind: 'connected' }>['registration'];
spawnedProcess?: RuntimeHostSpawnedProcess;
}
| Extract<ConnectRuntimeHostResult, { kind: 'upgrade_required' }>
| Extract<ConnectRuntimeHostResult, { kind: 'incompatible' }>
| {
kind: 'failed';
reason: 'composition_mismatch';
requiredCompositionId: string;
diagnostic?: RuntimeHostElectionDiagnostic;
}
| {
kind: 'failed';
reason: CandidateStartupFailure['reason'] | 'startup_timeout' | 'host_unresponsive';
diagnostic?: RuntimeHostElectionDiagnostic;
};
export interface RuntimeHostElectionDiagnostic {
readonly deadlineMs: number;
readonly elapsedMs: number;
readonly candidateLaunches: number;
readonly sawEndpointConnected: boolean;
readonly lastConnectionFailure?: RuntimeHostConnectionFailure;
readonly observations: {
readonly totalResults: number;
readonly notRegistered: number;
readonly connectFailed: number;
readonly handshakeFailed: number;
readonly connected: number;
readonly readyWaitFailed: number;
readonly deadlineElapsed: number;
readonly otherResults: number;
};
readonly lastRegistration?: {
readonly pid: number;
readonly state: HostRegistration['state'];
readonly lifecycleMode: HostRegistration['lifecycleMode'];
readonly generation?: string;
};
readonly latestCandidate?: {
readonly pid: number;
readonly startupAttemptId?: string;
readonly state: 'running' | 'exited' | 'unknown';
readonly exitCode?: number | null;
readonly signal?: NodeJS.Signals | null;
};
}
interface MutableElectionObservations {
totalResults: number;
notRegistered: number;
connectFailed: number;
handshakeFailed: number;
connected: number;
readyWaitFailed: number;
deadlineElapsed: number;
otherResults: number;
}
interface ObservedCandidateAttempt {
readonly attempt: DetachedCandidateAttempt;
exit?: CandidateProcessExit;
}
export interface RuntimeHostSpawnedProcess {
readonly pid: number;
readonly exited: Promise<CandidateProcessExit>;
}
export async function connectOrSpawnRuntimeHost(
input: ConnectOrSpawnRuntimeHostInput,
): Promise<ConnectOrSpawnRuntimeHostResult> {
return connectOrSpawnRuntimeHostWithDependencies(input, defaultDependencies);
}
export type ConnectOwnedRuntimeHostResult =
| { kind: 'connected'; connection: RuntimeHostConnection; host: OwnedCandidateAttempt }
| Exclude<ConnectOrSpawnRuntimeHostResult, { kind: 'connected' }>
| { kind: 'failed'; reason: 'existing_host' }
| { kind: 'failed'; reason: 'startup_failed'; detail: string };
interface ConnectOwnedRuntimeHostDependencies {
launchCandidate: typeof launchOwnedRuntimeHostCandidate;
}
const defaultOwnedDependencies: ConnectOwnedRuntimeHostDependencies = {
launchCandidate: launchOwnedRuntimeHostCandidate,
};
export async function connectOwnedRuntimeHost(
input: OwnedRuntimeHostInput,
): Promise<ConnectOwnedRuntimeHostResult> {
return connectOwnedRuntimeHostWithDependencies(input, defaultOwnedDependencies);
}
export async function connectOwnedRuntimeHostWithDependencies(
input: OwnedRuntimeHostInput,
dependencies: ConnectOwnedRuntimeHostDependencies,
): Promise<ConnectOwnedRuntimeHostResult> {
let launch: ReturnType<typeof launchOwnedRuntimeHostCandidate> | undefined;
let connection: RuntimeHostConnection | undefined;
try {
const result = await connectOrSpawnRuntimeHostWithDependencies(
{
...input,
candidateEntrypoint: new URL('../execution-candidate-main.js', import.meta.url),
idleGraceMs: 0,
},
{
launchCandidate(candidate) {
launch ??= dependencies.launchCandidate({
...candidate,
// Proxy passwords belong in the child environment, never process arguments.
env: {
MAKA_HOSTED_INITIALIZATION: input.initialization
? JSON.stringify(input.initialization)
: '',
},
});
return launch;
},
random: Math.random,
},
);
if (result.kind !== 'connected') {
releaseOwnedLaunch(launch);
return result;
}
const host = await launch?.spawned.catch(() => undefined);
if (!host) {
await result.connection.close();
return {
kind: 'failed',
reason: launch ? 'host_unresponsive' : 'existing_host',
};
}
const ownedConnection = result.connection;
connection = ownedConnection;
const diagnostics = await abortable(
() => ownedConnection.request('host.diagnostics.query', {}),
input.signal,
);
if (diagnostics.pid !== host.pid) {
await connection.close();
connection = undefined;
await host.settle(1_000);
return { kind: 'failed', reason: 'existing_host' };
}
return { kind: 'connected', connection: ownedConnection, host };
} catch (error) {
await connection?.close().catch(() => undefined);
releaseOwnedLaunch(launch);
const code =
error instanceof StorageRootAuthorityError ? error.code : 'internal_startup_failure';
const cause = error instanceof Error ? error.cause : undefined;
const causeCode =
cause instanceof Error && 'code' in cause && typeof cause.code === 'string'
? cause.code
: undefined;
return {
kind: 'failed',
reason: 'startup_failed',
detail: `${code}${causeCode && /^[A-Z0-9_]{1,64}$/.test(causeCode) ? ` (${causeCode})` : ''}`,
};
}
}
export interface HostedRuntimeInitialization {
readonly incognito: true;
readonly proxyUrl?: string;
}
type OwnedRuntimeHostInput = Omit<
ConnectOrSpawnRuntimeHostInput,
'candidateEntrypoint' | 'idleGraceMs'
> & {
readonly initialization?: HostedRuntimeInitialization;
};
function releaseOwnedLaunch(
launch: ReturnType<typeof launchOwnedRuntimeHostCandidate> | undefined,
): void {
if (!launch) return;
void launch.spawned
.then((host) => {
host.releaseToEnvironment();
})
.catch(() => undefined);
}
export async function connectOrSpawnRuntimeHostWithDependencies(
input: ConnectOrSpawnRuntimeHostInput,
dependencies: ConnectOrSpawnRuntimeHostDependencies,
): Promise<ConnectOrSpawnRuntimeHostResult> {
const environment = dependencies.env ?? process.env;
const deadlineMs =
input.electionDeadlineMs ??
electionDeadlineMsFromEnvironment(environment[ELECTION_DEADLINE_MS_ENV_VAR]) ??
DEFAULT_ELECTION_DEADLINE_MS;
const idleGraceMs =
input.idleGraceMs ??
durationMsFromEnvironment(environment[IDLE_GRACE_MS_ENV_VAR], IDLE_GRACE_MS_ENV_VAR, 0);
requireOptionalTimeout(input.electionDeadlineMs, 'electionDeadlineMs', 1);
requireOptionalTimeout(input.idleGraceMs, 'idleGraceMs', 0);
validateProtocolRange(input.protocol);
requireHostCompositionId(input.compositionId);
requireOptionalTimeout(input.connectTimeoutMs, 'connectTimeoutMs', 1);
requireOptionalTimeout(input.handshakeTimeoutMs, 'handshakeTimeoutMs', 1);
const managedLaunchClaim =
input.managedLaunchClaim === undefined
? undefined
: decodeRuntimeHostManagedLaunchClaim(input.managedLaunchClaim);
input.signal?.throwIfAborted();
const clientInstanceId = requireClientInstanceId(input.clientInstanceId ?? randomUUID());
const capability = await resolveStorageRoot({ path: input.rootPath, kind: 'interactive' });
const composition = await readStateRootCompositionBinding(capability.canonicalPath);
if (composition && composition.compositionId !== input.compositionId) {
return {
kind: 'failed',
reason: 'composition_mismatch',
requiredCompositionId: composition.compositionId,
};
}
const { controlDirectory } = await prepareStorageRootControlDirectory(capability);
// Root authority initialization must settle before the bounded election window begins.
const startedAt = performance.now();
const deadline = startedAt + deadlineMs;
let nextCandidateAt = startedAt;
let backoffMs = DEFAULT_BACKOFF_MIN_MS;
let sawUnresponsiveEndpoint = false;
let startupFailure: CandidateStartupFailureReport | undefined;
let pendingCandidateReports = 0;
let electionSettled = false;
let candidateInFlight = false;
const candidateLaunches = new Set<ReturnType<CandidateLauncher>>();
let sawEndpointConnected = false;
let lastRegistration: HostRegistration | undefined;
let lastConnectionFailure: RuntimeHostConnectionFailure | undefined;
let latestCandidate: ObservedCandidateAttempt | undefined;
const observations: MutableElectionObservations = {
totalResults: 0,
notRegistered: 0,
connectFailed: 0,
handshakeFailed: 0,
connected: 0,
readyWaitFailed: 0,
deadlineElapsed: 0,
otherResults: 0,
};
try {
while (performance.now() < deadline) {
input.signal?.throwIfAborted();
const result = await (dependencies.connectHost ?? connectResolvedRuntimeHost)({
capability,
controlDirectory,
protocol: input.protocol,
compositionId: input.compositionId,
...(input.generation === undefined ? {} : { generation: input.generation }),
...(input.takeoverHostEpoch === undefined
? {}
: { takeoverHostEpoch: input.takeoverHostEpoch }),
clientInstanceId,
connectTimeoutMs: input.connectTimeoutMs,
handshakeTimeoutMs: input.handshakeTimeoutMs,
electionDeadline: deadline,
});
const observed = recordElectionResult(result, observations);
if (result.kind === 'unavailable' && result.connectionFailure) {
lastConnectionFailure = result.connectionFailure;
}
if (observed.registration) lastRegistration = observed.registration;
if (observed.endpointConnected) sawEndpointConnected = true;
if (result.kind === 'election_deadline_elapsed') {
if (result.endpointConnected) sawUnresponsiveEndpoint = true;
break;
}
if (result.kind === 'connected') {
const remaining = deadline - performance.now();
if (remaining <= 0) {
await result.connection.close().catch(() => undefined);
break;
}
try {
await waitForRuntimeHostReady(
result.connection,
Math.max(1, Math.ceil(remaining)),
input.signal,
);
electionSettled = true;
await retireCandidateStartupDiagnostic(capability.rootId, startupFailure);
const selected = latestCandidate?.attempt;
const spawnedProcess =
selected?.pid === result.registration.pid && selected.exited
? { pid: selected.pid, exited: selected.exited }
: undefined;
return spawnedProcess ? { ...result, spawnedProcess } : result;
} catch {
observations.readyWaitFailed += 1;
await result.connection.close().catch(() => undefined);
}
input.signal?.throwIfAborted();
sawUnresponsiveEndpoint = true;
}
if (result.kind === 'upgrade_required') return result;
if (result.kind === 'unavailable' && result.reason === 'handshake_failed') {
sawUnresponsiveEndpoint = true;
}
if (isBlockingIncompatibility(result)) {
return result;
}
if (isPermanentCandidateStartupFailure(startupFailure) && pendingCandidateReports === 0) {
const selectedFailure = startupFailure;
electionSettled = true;
await selectCandidateStartupDiagnostic(
capability.rootId,
selectedFailure.startupAttemptId,
).catch(() => undefined);
return { kind: 'failed', reason: selectedFailure.reason };
}
const now = performance.now();
if (
shouldLaunchCandidate(result) &&
!isPermanentCandidateStartupFailure(startupFailure) &&
!candidateInFlight &&
now >= nextCandidateAt
) {
let managedDeployment;
try {
managedDeployment = await readRuntimeHostManagedDeploymentConfig(
capability,
dependencies.managedDeploymentAuthority,
);
} catch (error) {
if (
error instanceof RuntimeHostManagedDeploymentError &&
error.code === 'invalid_config'
) {
return { kind: 'failed', reason: 'deployment_record_invalid' };
}
throw error;
}
const managedLaunchRejection = runtimeHostManagedLaunchRejection(
managedDeployment,
managedLaunchClaim,
'on_demand',
);
if (managedLaunchRejection !== undefined) {
// A managed endpoint that accepted a connection but did not answer
// is temporarily unavailable, not evidence that the client needs a
// new operator. Keep reconnecting without ever launching a replacement.
return {
kind: 'failed',
reason:
managedLaunchRejection === 'managed_root_requires_operator' && sawUnresponsiveEndpoint
? 'host_unresponsive'
: managedLaunchRejection,
};
}
try {
const remaining = deadline - performance.now();
if (remaining <= 0) break;
const launch = dependencies.launchCandidate({
rootPath: capability.canonicalPath,
expectedRootId: capability.rootId,
entrypoint: input.candidateEntrypoint,
...(input.candidateExecutable === undefined
? {}
: { executable: input.candidateExecutable }),
initialConnectionTimeoutMs: Math.ceil(remaining),
...(idleGraceMs === undefined ? {} : { idleGraceMs }),
...(input.generation === undefined ? {} : { generation: input.generation }),
...(managedLaunchClaim === undefined ? {} : { managedLaunchClaim }),
...(input.onExit === undefined ? {} : { onExit: input.onExit }),
...(input.inheritableAuthorityLeaseFd === undefined
? {}
: {
inheritableAuthorityLeaseFd: input.inheritableAuthorityLeaseFd,
launchOwnerClientInstanceId: clientInstanceId,
}),
...(input.closeOnLauncherExit === undefined
? {}
: { closeOnLauncherExit: input.closeOnLauncherExit }),
});
candidateLaunches.add(launch);
const attempt = await settleBeforeDeadline(launch.spawned, deadline, input.signal);
candidateInFlight = true;
const candidate: ObservedCandidateAttempt = { attempt };
latestCandidate = candidate;
if (attempt.exited) {
void attempt.exited.then(
(exit) => {
candidate.exit = exit;
candidateInFlight = false;
},
() => {
candidateInFlight = false;
},
);
}
if (attempt.startupFailure) {
pendingCandidateReports += 1;
void attempt.startupFailure
.then(
(failure) => {
if (!failure) return;
if (electionSettled) {
void clearCandidateStartupDiagnostic(
capability.rootId,
failure.startupAttemptId,
).catch(() => undefined);
return;
}
const replace =
!startupFailure ||
(!isPermanentCandidateStartupFailure(startupFailure) &&
isPermanentCandidateStartupFailure(failure));
const obsolete = replace ? startupFailure : failure;
if (replace) {
startupFailure = failure;
}
if (obsolete) {
void clearCandidateStartupDiagnostic(
capability.rootId,
obsolete.startupAttemptId,
).catch(() => undefined);
}
},
() => undefined,
)
.finally(() => {
pendingCandidateReports -= 1;
});
}
} catch {
// A failed Candidate attempt is ordinary election evidence; discovery continues.
}
nextCandidateAt = now + MIN_CANDIDATE_INTERVAL_MS;
}
const remaining = deadline - performance.now();
if (remaining <= 0) break;
const random = dependencies.random();
const jitter = 0.75 + Math.min(1, Math.max(0, Number.isFinite(random) ? random : 0.5)) * 0.5;
await sleep(Math.min(remaining, Math.max(1, Math.round(backoffMs * jitter))), input.signal);
backoffMs = Math.min(DEFAULT_BACKOFF_MAX_MS, backoffMs * 2);
}
if (startupFailure) {
const selectedFailure = startupFailure;
electionSettled = true;
await selectCandidateStartupDiagnostic(
capability.rootId,
selectedFailure.startupAttemptId,
).catch(() => undefined);
return { kind: 'failed', reason: selectedFailure.reason };
}
return {
kind: 'failed',
reason: sawUnresponsiveEndpoint ? 'host_unresponsive' : 'startup_timeout',
diagnostic: createElectionDiagnostic({
deadlineMs,
startedAt,
candidateLaunches: candidateLaunches.size,
sawEndpointConnected,
observations,
lastRegistration,
lastConnectionFailure,
latestCandidate,
}),
};
} finally {
electionSettled = true;
}
}
function recordElectionResult(
result: ElectionConnectionResult,
observations: MutableElectionObservations,
): { readonly endpointConnected: boolean; readonly registration?: HostRegistration } {
observations.totalResults += 1;
const registration = 'registration' in result ? result.registration : undefined;
if (result.kind === 'election_deadline_elapsed') {
observations.deadlineElapsed += 1;
return {
endpointConnected: result.endpointConnected,
...(result.registration ? { registration: result.registration } : {}),
};
}
if (result.kind === 'connected') {
observations.connected += 1;
return { endpointConnected: true, registration };
}
if (result.kind !== 'unavailable') {
observations.otherResults += 1;
return {
endpointConnected: electionResultReachedEndpoint(result),
...(registration ? { registration } : {}),
};
}
switch (result.reason) {
case 'not_registered':
observations.notRegistered += 1;
break;
case 'connect_failed':
observations.connectFailed += 1;
break;
case 'handshake_failed':
observations.handshakeFailed += 1;
break;
default:
observations.otherResults += 1;
break;
}
return {
endpointConnected: electionResultReachedEndpoint(result),
...(registration ? { registration } : {}),
};
}
function electionResultReachedEndpoint(
result: Exclude<ElectionConnectionResult, { kind: 'election_deadline_elapsed' }>,
): boolean {
switch (result.kind) {
case 'connected':
case 'draining':
case 'incompatible':
case 'upgrade_required':
return true;
case 'unavailable':
return result.endpointConnected;
}
}
function createElectionDiagnostic(input: {
readonly deadlineMs: number;
readonly startedAt: number;
readonly candidateLaunches: number;
readonly sawEndpointConnected: boolean;
readonly observations: MutableElectionObservations;
readonly lastRegistration: HostRegistration | undefined;
readonly lastConnectionFailure: RuntimeHostConnectionFailure | undefined;
readonly latestCandidate: ObservedCandidateAttempt | undefined;
}): RuntimeHostElectionDiagnostic {
const candidate = input.latestCandidate;
return {
deadlineMs: input.deadlineMs,
elapsedMs: Math.max(0, Math.round(performance.now() - input.startedAt)),
candidateLaunches: input.candidateLaunches,
sawEndpointConnected: input.sawEndpointConnected,
observations: { ...input.observations },
...(input.lastConnectionFailure ? { lastConnectionFailure: input.lastConnectionFailure } : {}),
...(input.lastRegistration
? {
lastRegistration: {
pid: input.lastRegistration.pid,
state: input.lastRegistration.state,
lifecycleMode: input.lastRegistration.lifecycleMode,
...(input.lastRegistration.generation === undefined
? {}
: { generation: input.lastRegistration.generation }),
},
}
: {}),
...(candidate
? {
latestCandidate: {
pid: candidate.attempt.pid,
...(candidate.attempt.startupAttemptId === undefined
? {}
: { startupAttemptId: candidate.attempt.startupAttemptId }),
state: candidate.exit ? 'exited' : candidate.attempt.exited ? 'running' : 'unknown',
...(candidate.exit
? { exitCode: candidate.exit.code, signal: candidate.exit.signal }
: {}),
},
}
: {}),
};
}
async function retireCandidateStartupDiagnostic(
rootId: string,
startupFailure: CandidateStartupFailureReport | undefined,
): Promise<void> {
await Promise.all([
clearCandidateStartupDiagnostic(rootId),
...(startupFailure
? [clearCandidateStartupDiagnostic(rootId, startupFailure.startupAttemptId)]
: []),
]).catch(() => undefined);
}
function isBlockingIncompatibility(
result: ConnectRuntimeHostResult,
): result is Extract<ConnectRuntimeHostResult, { kind: 'incompatible' }> {
return result.kind === 'incompatible' && result.handshake.replacement === 'blocked_by_residency';
}
function shouldLaunchCandidate(result: ConnectRuntimeHostResult): boolean {
return result.kind === 'unavailable' || result.kind === 'draining';
}
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
if (signal?.aborted) return Promise.reject(signal.reason);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
signal?.removeEventListener('abort', onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(signal?.reason);
};
signal?.addEventListener('abort', onAbort, { once: true });
});
}
function settleBeforeDeadline<T>(
operation: Promise<T>,
deadline: number,
signal?: AbortSignal,
): Promise<T> {
const remaining = deadline - performance.now();
if (remaining <= 0) return Promise.reject(new Error('Runtime Host election deadline elapsed'));
if (signal?.aborted) return Promise.reject(signal.reason);
return new Promise((resolve, reject) => {
const settle = (operation: () => void) => {
clearTimeout(timer);
signal?.removeEventListener('abort', onAbort);
operation();
};
const timer = setTimeout(() => {
settle(() => reject(new Error('Runtime Host election deadline elapsed')));
}, remaining);
const onAbort = () => settle(() => reject(signal?.reason));
signal?.addEventListener('abort', onAbort, { once: true });
operation.then(
(value) => settle(() => resolve(value)),
(error: unknown) => settle(() => reject(error)),
);
});
}
function requireOptionalTimeout(value: number | undefined, label: string, minimum: number): void {
if (value === undefined) return;
if (!Number.isSafeInteger(value) || value < minimum || value > 120_000) {
throw new RangeError(`${label} must be an integer between ${minimum} and 120000`);
}
}