@@ -28,6 +28,8 @@ import { handleRunnerTransportErrorAfterCommandSend } from './runner-command-rec
2828export type PrepareIosRunnerOptions = AppleRunnerPrepareOptions ;
2929export type PrepareIosRunnerResult = AppleRunnerPrepareResult ;
3030
31+ const PREPARE_RUNNER_HEALTH_MAX_SESSION_ATTEMPTS = 2 ;
32+
3133export async function prepareLocalIosRunner (
3234 device : DeviceInfo ,
3335 options : PrepareIosRunnerOptions ,
@@ -36,66 +38,131 @@ export async function prepareLocalIosRunner(
3638 const signal = getRequestSignal ( options . requestId ) ;
3739 const command = withRunnerCommandId ( { command : 'uptime' } ) ;
3840 let session : RunnerSession | undefined ;
39- try {
40- const connectStartedAt = Date . now ( ) ;
41- session = await ensureRunnerSession ( device , options ) ;
42- const connectMs = Date . now ( ) - connectStartedAt ;
43- return recordPrepareResult (
44- device ,
45- await runPrepareHealthCheck ( device , session , command , options , signal , connectMs ) ,
46- ) ;
47- } catch ( err ) {
48- const appErr = err instanceof AppError ? err : new AppError ( 'COMMAND_FAILED' , String ( err ) ) ;
49- if ( ! session || ! shouldRecoverBadCachedRunnerArtifact ( appErr , session ) ) {
50- throw err ;
51- }
52- const reason = appErr . message || 'runner_health_failed' ;
53- await invalidateRunnerSession ( session , 'prepare_cached_runner_health_failed' ) ;
54- await markRunnerXctestrunArtifactBadForRun ( session . xctestrunArtifact , reason ) ;
55- const connectStartedAt = Date . now ( ) ;
56- const rebuiltSession = await ensureRunnerSession ( device , {
57- ...options ,
58- cleanStaleBundles : true ,
59- forceRunnerXctestrunRebuild : true ,
60- } ) ;
61- const connectMs = Date . now ( ) - connectStartedAt ;
41+ let recoveryReason : string | undefined ;
42+ for ( let attempt = 1 ; attempt <= PREPARE_RUNNER_HEALTH_MAX_SESSION_ATTEMPTS ; attempt += 1 ) {
6243 try {
63- const recovered = await runPrepareHealthCheck (
44+ const connectStartedAt = Date . now ( ) ;
45+ session = await ensureRunnerSession ( device , {
46+ ...options ,
47+ cleanStaleBundles : attempt > 1 ? true : options . cleanStaleBundles ,
48+ } ) ;
49+ const connectMs = Date . now ( ) - connectStartedAt ;
50+ return recordPrepareResult (
6451 device ,
65- rebuiltSession ,
66- command ,
67- options ,
68- signal ,
69- connectMs ,
70- { recoveryReason : reason } ,
52+ await runPrepareHealthCheck ( device , session , command , options , signal , connectMs , {
53+ recoveryReason,
54+ } ) ,
7155 ) ;
56+ } catch ( err ) {
57+ const appErr = err instanceof AppError ? err : new AppError ( 'COMMAND_FAILED' , String ( err ) ) ;
58+ if ( ! session ) {
59+ throw err ;
60+ }
61+ if ( shouldRecoverBadCachedRunnerArtifact ( appErr , session ) ) {
62+ return await recoverBadCachedRunnerArtifact ( {
63+ device,
64+ session,
65+ command,
66+ options,
67+ signal,
68+ error : appErr ,
69+ } ) ;
70+ }
71+ if ( ! shouldRetryPrepareRunnerHealthFailure ( appErr ) ) {
72+ throw err ;
73+ }
74+ const reason = appErr . message || 'runner_health_failed' ;
75+ if ( attempt >= PREPARE_RUNNER_HEALTH_MAX_SESSION_ATTEMPTS ) {
76+ await invalidateRunnerSession ( session , 'prepare_runner_health_failed' ) ;
77+ throw err ;
78+ }
79+ recoveryReason ??= reason ;
80+ await invalidateRunnerSession ( session , 'prepare_runner_health_retry' ) ;
7281 emitDiagnostic ( {
73- level : 'info ' ,
74- phase : 'ios_runner_prepare_bad_cache_recovered ' ,
82+ level : 'warn ' ,
83+ phase : 'ios_runner_prepare_health_retry ' ,
7584 data : {
7685 command : command . command ,
7786 commandId : command . commandId ,
78- sessionId : rebuiltSession . sessionId ,
79- xctestrunPath : rebuiltSession . xctestrunArtifact ?. xctestrunPath ,
87+ sessionId : session . sessionId ,
88+ attempt,
89+ maxAttempts : PREPARE_RUNNER_HEALTH_MAX_SESSION_ATTEMPTS ,
8090 reason,
8191 } ,
8292 } ) ;
83- return recordPrepareResult ( device , recovered ) ;
84- } catch ( retryErr ) {
85- await invalidateRunnerSession ( rebuiltSession , 'prepare_rebuilt_runner_health_failed' ) ;
86- const wrapped = wrapPrepareHealthFailure ( retryErr , rebuiltSession , reason ) ;
87- emitPrepareDiagnostic ( device , {
88- cache : rebuiltSession . xctestrunArtifact ?. cache ,
89- artifact : rebuiltSession . xctestrunArtifact ?. artifact ,
90- buildMs : rebuiltSession . xctestrunArtifact ?. buildMs ,
91- connectMs,
92- healthCheckMs : 0 ,
93- xctestrunPath : rebuiltSession . xctestrunArtifact ?. xctestrunPath ,
94- failureReason : wrapped . message ,
95- } ) ;
96- throw wrapped ;
93+ } finally {
94+ session = undefined ;
9795 }
9896 }
97+
98+ throw new AppError ( 'COMMAND_FAILED' , 'iOS runner prepare failed' ) ;
99+ }
100+
101+ async function recoverBadCachedRunnerArtifact ( params : {
102+ device : DeviceInfo ;
103+ session : RunnerSession & {
104+ xctestrunArtifact : NonNullable < RunnerSession [ 'xctestrunArtifact' ] > ;
105+ } ;
106+ command : RunnerCommand ;
107+ options : PrepareIosRunnerOptions ;
108+ signal : AbortSignal | undefined ;
109+ error : AppError ;
110+ } ) : Promise < PrepareIosRunnerResult > {
111+ const { device, session, command, options, signal, error } = params ;
112+ const reason = error . message || 'runner_health_failed' ;
113+ await invalidateRunnerSession ( session , 'prepare_cached_runner_health_failed' ) ;
114+ await markRunnerXctestrunArtifactBadForRun ( session . xctestrunArtifact , reason ) ;
115+ const connectStartedAt = Date . now ( ) ;
116+ const rebuiltSession = await ensureRunnerSession ( device , {
117+ ...options ,
118+ cleanStaleBundles : true ,
119+ forceRunnerXctestrunRebuild : true ,
120+ } ) ;
121+ const connectMs = Date . now ( ) - connectStartedAt ;
122+ try {
123+ const recovered = await runPrepareHealthCheck (
124+ device ,
125+ rebuiltSession ,
126+ command ,
127+ options ,
128+ signal ,
129+ connectMs ,
130+ { recoveryReason : reason } ,
131+ ) ;
132+ emitDiagnostic ( {
133+ level : 'info' ,
134+ phase : 'ios_runner_prepare_bad_cache_recovered' ,
135+ data : {
136+ command : command . command ,
137+ commandId : command . commandId ,
138+ sessionId : rebuiltSession . sessionId ,
139+ xctestrunPath : rebuiltSession . xctestrunArtifact ?. xctestrunPath ,
140+ reason,
141+ } ,
142+ } ) ;
143+ return recordPrepareResult ( device , recovered ) ;
144+ } catch ( retryErr ) {
145+ await invalidateRunnerSession ( rebuiltSession , 'prepare_rebuilt_runner_health_failed' ) ;
146+ const wrapped = wrapPrepareHealthFailure ( retryErr , rebuiltSession , reason ) ;
147+ emitPrepareDiagnostic ( device , {
148+ cache : rebuiltSession . xctestrunArtifact ?. cache ,
149+ artifact : rebuiltSession . xctestrunArtifact ?. artifact ,
150+ buildMs : rebuiltSession . xctestrunArtifact ?. buildMs ,
151+ connectMs,
152+ healthCheckMs : 0 ,
153+ xctestrunPath : rebuiltSession . xctestrunArtifact ?. xctestrunPath ,
154+ failureReason : wrapped . message ,
155+ } ) ;
156+ throw wrapped ;
157+ }
158+ }
159+
160+ function shouldRetryPrepareRunnerHealthFailure ( error : AppError ) : boolean {
161+ return (
162+ isRetryableRunnerError ( error ) ||
163+ shouldRetryRunnerConnectError ( error ) ||
164+ isPrepareHealthTimeout ( error )
165+ ) ;
99166}
100167
101168// fallow-ignore-next-line complexity
0 commit comments