Skip to content

Commit 64dd3cf

Browse files
committed
fix: share prepare ios-runner timeout budget
1 parent ef95fee commit 64dd3cf

7 files changed

Lines changed: 108 additions & 47 deletions

File tree

src/daemon/client/daemon-client.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import {
1111
resolveClientSettings,
1212
} from './daemon-client-lifecycle.ts';
1313
import { sendRequest } from './daemon-client-transport.ts';
14+
import {
15+
DAEMON_REQUEST_TIMEOUT_MS,
16+
INSTALL_REQUEST_TIMEOUT_MS,
17+
PREPARE_REQUEST_TIMEOUT_MS,
18+
} from '../request-timeouts.ts';
1419

1520
export { computeDaemonCodeSignature } from '../code-signature.ts';
1621
export { downloadRemoteArtifact } from '../../remote/daemon-artifacts.ts';
@@ -23,12 +28,6 @@ export { shouldResetDaemonAfterRequestTimeout } from './daemon-client-timeout.ts
2328
export type DaemonRequest = SharedDaemonRequest;
2429
export type DaemonResponse = SharedDaemonResponse;
2530

26-
const REQUEST_TIMEOUT_MS = 90_000;
27-
const PREPARE_REQUEST_TIMEOUT_MS = 240_000;
28-
// Keep this above the longest platform install subprocess timeout so the client
29-
// envelope does not abort a still-progressing device install first.
30-
const INSTALL_REQUEST_TIMEOUT_MS = 180_000;
31-
3231
export async function sendToDaemon(req: Omit<DaemonRequest, 'token'>): Promise<DaemonResponse> {
3332
const requestId = req.meta?.requestId ?? createRequestId();
3433
const debug = Boolean(req.meta?.debug || req.flags?.verbose);
@@ -123,7 +122,7 @@ export function resolveDaemonRequestTimeoutMs(
123122
}
124123
if (req.command === PUBLIC_COMMANDS.prepare) return PREPARE_REQUEST_TIMEOUT_MS;
125124
if (isInstallLikeCommand(req.command)) return INSTALL_REQUEST_TIMEOUT_MS;
126-
return REQUEST_TIMEOUT_MS;
125+
return DAEMON_REQUEST_TIMEOUT_MS;
127126
}
128127

129128
function isExplicitTimeoutCommand(command: string | undefined): boolean {

src/daemon/handlers/__tests__/session.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2509,8 +2509,13 @@ test('prepare ios-runner starts the XCTest runner on an explicit iOS selector',
25092509
expect.objectContaining({
25102510
cleanStaleBundles: true,
25112511
buildTimeoutMs: 240000,
2512-
healthTimeoutMs: 120000,
2512+
healthTimeoutMs: 240000,
25132513
logPath: expect.stringMatching(/daemon\.log$/),
2514+
prepareDeadline: expect.objectContaining({
2515+
elapsedMs: expect.any(Function),
2516+
isExpired: expect.any(Function),
2517+
remainingMs: expect.any(Function),
2518+
}),
25142519
requestId: 'prepare-request',
25152520
startupTimeoutMs: 240000,
25162521
}),
@@ -2562,7 +2567,12 @@ test('prepare ios-runner starts the XCTest runner on an explicit macOS selector'
25622567
expect.objectContaining({ platform: 'macos', id: 'host-macos-local' }),
25632568
expect.objectContaining({
25642569
buildTimeoutMs: 240000,
2565-
healthTimeoutMs: 120000,
2570+
healthTimeoutMs: 240000,
2571+
prepareDeadline: expect.objectContaining({
2572+
elapsedMs: expect.any(Function),
2573+
isExpired: expect.any(Function),
2574+
remainingMs: expect.any(Function),
2575+
}),
25662576
requestId: 'prepare-macos-request',
25672577
}),
25682578
);

src/daemon/handlers/session.ts

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,8 @@ import { handleSessionObservabilityCommands } from './session-observability.ts';
3838
import { handleSessionReplayCommands } from './session-replay.ts';
3939
import { getSessionCommandKind } from '../daemon-command-registry.ts';
4040
import { LeaseRegistry } from '../lease-registry.ts';
41-
42-
const PREPARE_IOS_RUNNER_MIN_STARTUP_TIMEOUT_MS = 45_000;
43-
const PREPARE_IOS_RUNNER_DEFAULT_BUILD_TIMEOUT_MS = 5 * 60_000;
44-
const PREPARE_IOS_RUNNER_HEALTH_TIMEOUT_MS = 90_000;
45-
const PREPARE_IOS_RUNNER_MAX_HEALTH_TIMEOUT_MS = 3 * 60_000;
41+
import { PREPARE_REQUEST_TIMEOUT_MS } from '../request-timeouts.ts';
42+
import { Deadline } from '../../utils/retry.ts';
4643

4744
async function handlePrepareCommand(params: {
4845
req: DaemonRequest;
@@ -76,7 +73,7 @@ async function handlePrepareCommand(params: {
7673
const startedAtMs = Date.now();
7774
const result = await prepareIosRunner(
7875
device,
79-
buildPrepareIosRunnerOptions(req, session, logPath),
76+
buildPrepareIosRunnerOptions(req, session, logPath, startedAtMs),
8077
);
8178
const durationMs = Math.max(0, Date.now() - startedAtMs);
8279
return {
@@ -89,51 +86,28 @@ function buildPrepareIosRunnerOptions(
8986
req: DaemonRequest,
9087
session: SessionState | undefined,
9188
logPath: string,
89+
startedAtMs: number,
9290
): Parameters<typeof prepareIosRunner>[1] {
93-
const buildTimeoutMs = readPrepareIosRunnerBuildTimeoutMs(req);
91+
const timeoutMs = readPrepareIosRunnerTimeoutMs(req);
9492
return {
9593
...buildAppleRunnerRequestOptions({
9694
req,
9795
logPath,
9896
traceLogPath: session?.trace?.outPath,
9997
}),
10098
cleanStaleBundles: true,
101-
startupTimeoutMs: resolvePrepareIosRunnerStartupTimeoutMs(req.flags?.timeoutMs),
102-
buildTimeoutMs,
103-
healthTimeoutMs: resolvePrepareIosRunnerHealthTimeoutMs(req.flags?.timeoutMs, buildTimeoutMs),
99+
buildTimeoutMs: timeoutMs,
100+
healthTimeoutMs: timeoutMs,
101+
prepareDeadline: Deadline.fromTimeoutMs(timeoutMs, startedAtMs),
102+
startupTimeoutMs: timeoutMs,
104103
};
105104
}
106105

107-
function resolvePrepareIosRunnerStartupTimeoutMs(timeoutMs: unknown): number | undefined {
108-
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
109-
return undefined;
110-
}
111-
return Math.max(PREPARE_IOS_RUNNER_MIN_STARTUP_TIMEOUT_MS, Math.floor(timeoutMs));
112-
}
113-
114-
function readPrepareIosRunnerBuildTimeoutMs(req: DaemonRequest): number {
106+
function readPrepareIosRunnerTimeoutMs(req: DaemonRequest): number {
115107
const value = req.flags?.timeoutMs;
116108
return typeof value === 'number' && Number.isFinite(value) && value > 0
117109
? value
118-
: PREPARE_IOS_RUNNER_DEFAULT_BUILD_TIMEOUT_MS;
119-
}
120-
121-
function resolvePrepareIosRunnerHealthTimeoutMs(
122-
timeoutMs: unknown,
123-
buildTimeoutMs: number,
124-
): number {
125-
const configuredTimeout =
126-
typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
127-
? Math.floor(timeoutMs)
128-
: undefined;
129-
if (!configuredTimeout) {
130-
return Math.min(buildTimeoutMs, PREPARE_IOS_RUNNER_HEALTH_TIMEOUT_MS);
131-
}
132-
return Math.min(
133-
buildTimeoutMs,
134-
PREPARE_IOS_RUNNER_MAX_HEALTH_TIMEOUT_MS,
135-
Math.max(PREPARE_IOS_RUNNER_HEALTH_TIMEOUT_MS, Math.floor(configuredTimeout / 2)),
136-
);
110+
: PREPARE_REQUEST_TIMEOUT_MS;
137111
}
138112

139113
function prepareIosRunnerResponseData(

src/daemon/request-timeouts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const DAEMON_REQUEST_TIMEOUT_MS = 90_000;
2+
export const PREPARE_REQUEST_TIMEOUT_MS = 240_000;
3+
4+
// Keep this above the longest platform install subprocess timeout so the client
5+
// envelope does not abort a still-progressing device install first.
6+
export const INSTALL_REQUEST_TIMEOUT_MS = 180_000;

src/platforms/ios/__tests__/runner-command-retry.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
33
import { IOS_SIMULATOR } from '../../../__tests__/test-utils/index.ts';
44
import { clearRequestCanceled, markRequestCanceled } from '../../../daemon/request-cancel.ts';
55
import { AppError } from '../../../kernel/errors.ts';
6+
import { Deadline } from '../../../utils/retry.ts';
67
import type { RunnerSession } from '../runner-session-types.ts';
78

89
const {
@@ -174,6 +175,35 @@ test('prepareIosRunner retries a fresh launch session when the health check cann
174175
);
175176
});
176177

178+
test('prepareIosRunner spends one shared deadline across setup and health check', async () => {
179+
vi.useFakeTimers();
180+
try {
181+
vi.setSystemTime(1_000);
182+
const session = makeRunnerSession({ port: 8100 });
183+
const prepareDeadline = Deadline.fromTimeoutMs(100_000);
184+
185+
mockEnsureRunnerSession.mockImplementationOnce(async () => {
186+
vi.setSystemTime(31_000);
187+
return session;
188+
});
189+
mockExecuteRunnerCommandWithSession.mockResolvedValueOnce({ uptimeMs: 42 });
190+
191+
const result = await prepareIosRunner(IOS_SIMULATOR, {
192+
buildTimeoutMs: 100_000,
193+
healthTimeoutMs: 100_000,
194+
prepareDeadline,
195+
startupTimeoutMs: 100_000,
196+
});
197+
198+
assert.deepEqual(result.runner, { uptimeMs: 42 });
199+
assert.equal(mockEnsureRunnerSession.mock.calls[0]?.[1]?.buildTimeoutMs, 100_000);
200+
assert.equal(mockEnsureRunnerSession.mock.calls[0]?.[1]?.startupTimeoutMs, 100_000);
201+
assert.equal(mockExecuteRunnerCommandWithSession.mock.calls[0]?.[4], 70_000);
202+
} finally {
203+
vi.useRealTimers();
204+
}
205+
});
206+
177207
test('prewarmIosRunnerSession proves cached runner health with uptime', async () => {
178208
const session = makeRunnerSession({ port: 8100 });
179209
mockEnsureRunnerSession.mockResolvedValueOnce(session);

src/platforms/ios/runner-lifecycle.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export type PrepareIosRunnerResult = AppleRunnerPrepareResult;
3030

3131
const PREPARE_RUNNER_HEALTH_MAX_SESSION_ATTEMPTS = 2;
3232

33+
type PrepareDeadlinePhaseTimeouts = Partial<
34+
Pick<PrepareIosRunnerOptions, 'buildTimeoutMs' | 'startupTimeoutMs'>
35+
>;
36+
3337
export async function prepareLocalIosRunner(
3438
device: DeviceInfo,
3539
options: PrepareIosRunnerOptions,
@@ -72,6 +76,7 @@ async function runPrepareAttempt(params: {
7276
const session = await ensureRunnerSession(device, {
7377
...options,
7478
cleanStaleBundles: attempt > 1 ? true : options.cleanStaleBundles,
79+
...readPrepareDeadlinePhaseTimeouts(options, 'runner_session'),
7580
});
7681
const connectMs = Date.now() - connectStartedAt;
7782
try {
@@ -169,6 +174,7 @@ async function recoverBadCachedRunnerArtifact(params: {
169174
...options,
170175
cleanStaleBundles: true,
171176
forceRunnerXctestrunRebuild: true,
177+
...readPrepareDeadlinePhaseTimeouts(options, 'runner_rebuild'),
172178
});
173179
const connectMs = Date.now() - connectStartedAt;
174180
try {
@@ -364,12 +370,17 @@ async function runPrepareHealthCheck(
364370
reason?: { recoveryReason?: string; failureReason?: string },
365371
): Promise<PrepareIosRunnerResult> {
366372
const healthStartedAt = Date.now();
373+
const timeoutMs = readPreparePhaseTimeoutMs(
374+
options.prepareDeadline,
375+
options.healthTimeoutMs,
376+
'runner_health',
377+
);
367378
const runner = await executeRunnerCommandWithSession(
368379
device,
369380
session,
370381
command,
371382
options.logPath,
372-
options.healthTimeoutMs,
383+
timeoutMs,
373384
signal,
374385
);
375386
return buildPrepareIosRunnerResult(
@@ -381,6 +392,35 @@ async function runPrepareHealthCheck(
381392
);
382393
}
383394

395+
function readPrepareDeadlinePhaseTimeouts(
396+
options: PrepareIosRunnerOptions,
397+
phase: string,
398+
): PrepareDeadlinePhaseTimeouts {
399+
if (!options.prepareDeadline) return {};
400+
const timeoutMs = readPreparePhaseTimeoutMs(
401+
options.prepareDeadline,
402+
options.buildTimeoutMs,
403+
phase,
404+
);
405+
return { buildTimeoutMs: timeoutMs, startupTimeoutMs: timeoutMs };
406+
}
407+
408+
function readPreparePhaseTimeoutMs(
409+
deadline: PrepareIosRunnerOptions['prepareDeadline'],
410+
fallbackTimeoutMs: number | undefined,
411+
phase: string,
412+
): number {
413+
if (!deadline) return fallbackTimeoutMs ?? RUNNER_STARTUP_TIMEOUT_MS;
414+
const remainingMs = Math.floor(deadline.remainingMs());
415+
if (remainingMs <= 0) {
416+
throw new AppError('COMMAND_FAILED', 'prepare ios-runner timed out', {
417+
phase,
418+
reason: 'prepare_deadline_expired',
419+
});
420+
}
421+
return remainingMs;
422+
}
423+
384424
function shouldRecoverBadCachedRunnerArtifact(
385425
error: AppError,
386426
session: RunnerSession,

src/platforms/ios/runner-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AsyncLocalStorage } from 'node:async_hooks';
22
import type { RunnerLogicalLeaseContext } from '../../core/runner-lease-context.ts';
33
import type { DeviceInfo } from '../../kernel/device.ts';
4+
import type { Deadline } from '../../utils/retry.ts';
45
import type { RunnerCommand } from './runner-contract.ts';
56
import type {
67
RunnerXctestrunArtifactState,
@@ -27,6 +28,7 @@ export type AppleRunnerPrewarmOptions = AppleRunnerLifecycleOptions;
2728

2829
export type AppleRunnerPrepareOptions = AppleRunnerLifecycleOptions & {
2930
healthTimeoutMs: number;
31+
prepareDeadline?: Deadline;
3032
};
3133

3234
export type AppleRunnerPrepareResult = {

0 commit comments

Comments
 (0)