1+ /* eslint-disable no-eq-null */
2+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
3+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
14import { Client } from "../.." ;
25import { ServerTask } from "../../features/serverTasks" ;
36import { SpaceServerTaskRepository } from "../serverTasks" ;
47import { ServerTaskRepository } from "../serverTasks" ;
58
9+ export interface ServerTaskWaiterOptions {
10+ maxRetries ?: number ; // Default: 3
11+ retryBackoffMs ?: number ; // Initial backoff in ms, default: 5000
12+ }
13+
614export class ServerTaskWaiter {
7- constructor ( private readonly client : Client , private readonly spaceName : string ) { }
15+ private readonly maxRetries : number ;
16+ private readonly retryBackoffMs : number ;
17+
18+ constructor ( private readonly client : Client , private readonly spaceName : string , options ?: ServerTaskWaiterOptions ) {
19+ this . maxRetries = options ?. maxRetries ?? 3 ;
20+ this . retryBackoffMs = options ?. retryBackoffMs ?? 5000 ;
21+ }
822
923 async waitForServerTasksToComplete (
1024 serverTaskIds : string [ ] ,
1125 statusCheckSleepCycle : number ,
1226 timeout : number ,
1327 pollingCallback ?: ( serverTask : ServerTask ) => void ,
14- cancelOnTimeout : boolean = false ,
28+ cancelOnTimeout : boolean = false
1529 ) : Promise < ServerTask [ ] > {
1630 const spaceServerTaskRepository = new SpaceServerTaskRepository ( this . client , this . spaceName ) ;
17- const serverTaskRepository = new ServerTaskRepository ( this . client )
18-
19- return this . waitForTasks ( spaceServerTaskRepository , serverTaskRepository , serverTaskIds , statusCheckSleepCycle , timeout , cancelOnTimeout , pollingCallback ) ;
31+ const serverTaskRepository = new ServerTaskRepository ( this . client ) ;
32+
33+ return this . waitForTasks (
34+ spaceServerTaskRepository ,
35+ serverTaskRepository ,
36+ serverTaskIds ,
37+ statusCheckSleepCycle ,
38+ timeout ,
39+ cancelOnTimeout ,
40+ pollingCallback
41+ ) ;
2042 }
2143
2244 async waitForServerTaskToComplete (
2345 serverTaskId : string ,
2446 statusCheckSleepCycle : number ,
2547 timeout : number ,
2648 pollingCallback ?: ( serverTask : ServerTask ) => void ,
27- cancelOnTimeout : boolean = false ,
49+ cancelOnTimeout : boolean = false
2850 ) : Promise < ServerTask > {
2951 const spaceServerTaskRepository = new SpaceServerTaskRepository ( this . client , this . spaceName ) ;
30- const serverTaskRepository = new ServerTaskRepository ( this . client )
31- const tasks = await this . waitForTasks ( spaceServerTaskRepository , serverTaskRepository , [ serverTaskId ] , statusCheckSleepCycle , timeout , cancelOnTimeout , pollingCallback ) ;
52+ const serverTaskRepository = new ServerTaskRepository ( this . client ) ;
53+ const tasks = await this . waitForTasks (
54+ spaceServerTaskRepository ,
55+ serverTaskRepository ,
56+ [ serverTaskId ] ,
57+ statusCheckSleepCycle ,
58+ timeout ,
59+ cancelOnTimeout ,
60+ pollingCallback
61+ ) ;
3262 return tasks [ 0 ] ;
3363 }
3464
@@ -61,7 +91,7 @@ export class ServerTaskWaiter {
6191
6292 try {
6393 while ( ! stop ) {
64- const tasks = await spaceServerTaskRepository . getByIds ( serverTaskIds ) ;
94+ const tasks = await this . getTasksWithRetry ( spaceServerTaskRepository , serverTaskIds ) ;
6595
6696 const unknownTaskIds = serverTaskIds . filter ( ( id ) => tasks . filter ( ( t ) => t . Id === id ) . length == 0 ) ;
6797 if ( unknownTaskIds . length ) {
@@ -93,6 +123,7 @@ export class ServerTaskWaiter {
93123
94124 await sleep ( statusCheckSleepCycle ) ;
95125 }
126+
96127 if ( timedOut && cancelOnTimeout && serverTaskIds . length > 0 ) {
97128 await this . cancelTasks ( serverTaskRepository , serverTaskIds ) ;
98129 }
@@ -115,4 +146,72 @@ export class ServerTaskWaiter {
115146 }
116147 }
117148 }
149+
150+ private async getTasksWithRetry ( repository : SpaceServerTaskRepository , taskIds : string [ ] ) : Promise < ServerTask [ ] > {
151+ // eslint-disable-next-line @typescript-eslint/init-declarations
152+ let lastError : any ;
153+
154+ for ( let attempt = 0 ; attempt <= this . maxRetries ; attempt ++ ) {
155+ try {
156+ return await repository . getByIds ( taskIds ) ;
157+ } catch ( error ) {
158+ lastError = error ;
159+ const errorMessage = error instanceof Error ? error . message : String ( error ) ;
160+
161+ const statusCode =
162+ ( error as any ) . StatusCode ||
163+ ( typeof ( error as any ) . code === "number" ? ( error as any ) . code : null ) ||
164+ ( error as any ) . response ?. status ||
165+ ( error as any ) . status ;
166+
167+ const isRetryable = this . isRetryableError ( error , statusCode ) ;
168+
169+ if ( ! isRetryable ) throw error ;
170+
171+ if ( attempt === this . maxRetries )
172+ throw new Error ( `Failed to connect to Octopus server after ${ this . maxRetries } attempts. ` + `Last error: ${ errorMessage } ` ) ;
173+
174+ const backoffDelay = this . retryBackoffMs * Math . pow ( 2 , attempt ) ;
175+ this . client . warn (
176+ `HTTP request failed (attempt ${ attempt + 1 } /${ this . maxRetries } ): ${ errorMessage } ${
177+ statusCode ? ` [${ statusCode } ]` : ""
178+ } . Retrying in ${ backoffDelay } ms...`
179+ ) ;
180+ await new Promise ( ( resolve ) => setTimeout ( resolve , backoffDelay ) ) ;
181+ }
182+ }
183+
184+ // This should never be reached due to throws above, but TypeScript needs it
185+ throw lastError ;
186+ }
187+
188+ private isRetryableError ( error : any , statusCode : number | null ) : boolean {
189+ if ( ! error ) return false ;
190+
191+ if ( statusCode && [ 408 , 429 , 500 , 502 , 503 , 504 ] . includes ( statusCode ) ) {
192+ return true ;
193+ }
194+
195+ try {
196+ const errorStr = String ( error . message || error ) . toLowerCase ( ) ;
197+ const errorCode = error . code ? String ( error . code ) . toLowerCase ( ) : "" ;
198+ const keywords = [
199+ "timeout" ,
200+ "etimedout" ,
201+ "econnreset" ,
202+ "econnrefused" ,
203+ "econnaborted" ,
204+ "enotfound" ,
205+ "eai_again" ,
206+ "epipe" ,
207+ "ehostunreach" ,
208+ "enetunreach" ,
209+ "socket" ,
210+ "network" ,
211+ ] ;
212+ return keywords . some ( ( k ) => errorStr . includes ( k ) || errorCode . includes ( k ) ) ;
213+ } catch {
214+ return false ;
215+ }
216+ }
118217}
0 commit comments