@@ -45,6 +45,32 @@ const phaseMap: Record<WorkspaceInstanceStatus_Phase, WorkspaceInstancePhase | u
4545 [ WorkspaceInstanceStatus_Phase . UNSPECIFIED ] : undefined ,
4646} ;
4747
48+ function wrapSupervisorAPIError < T > ( callback : ( ) => Promise < T > , opts ?: { maxRetries ?: number ; signal ?: AbortSignal } ) : Promise < T > {
49+ const maxRetries = opts ?. maxRetries ?? 5 ;
50+ let retries = 0 ;
51+
52+ const onError : ( err : any ) => Promise < T > = async ( err ) => {
53+ if ( ! isServiceError ( err ) ) {
54+ throw err ;
55+ }
56+
57+ const shouldRetry = opts ?. signal ? ! opts . signal . aborted : retries ++ < maxRetries ;
58+ const isNetworkProblem = err . message . includes ( 'Response closed without' ) ;
59+ if ( shouldRetry && ( err . code === Code . Unavailable || err . code === Code . Aborted || isNetworkProblem ) ) {
60+ await timeout ( 1000 ) ;
61+ return callback ( ) . catch ( onError ) ;
62+ }
63+ if ( isNetworkProblem ) {
64+ err . code = Code . Unavailable ;
65+ }
66+ // codes of grpc-web are align with grpc and connect
67+ // see https://github.com/improbable-eng/grpc-web/blob/1d9bbb09a0990bdaff0e37499570dbc7d6e58ce8/client/grpc-web/src/Code.ts#L1
68+ throw new WrapError ( 'Failed to call supervisor API' , err , 'SupervisorAPI:' + Code [ err . code ] ) ;
69+ } ;
70+
71+ return callback ( ) . catch ( onError ) ;
72+ }
73+
4874class ExtensionServiceImpl implements ExtensionServiceImplementation {
4975 constructor (
5076 private logService : ILogService ,
@@ -56,22 +82,20 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
5682
5783 }
5884
59- private async getWorkspaceSSHKey ( ownerToken : string , workspaceId : string , workspaceHost : string ) {
85+ private async getWorkspaceSSHKey ( ownerToken : string , workspaceId : string , workspaceHost : string , signal : AbortSignal ) {
6086 const workspaceUrl = `https://${ workspaceId } .${ workspaceHost } ` ;
6187 const metadata = new BrowserHeaders ( ) ;
6288 metadata . append ( 'x-gitpod-owner-token' , ownerToken ) ;
6389 const client = new ControlServiceClient ( `${ workspaceUrl } /_supervisor/v1` , { transport : NodeHttpTransport ( ) } ) ;
6490
65- const privateKey = await new Promise < string > ( ( resolve , reject ) => {
91+ const privateKey = await wrapSupervisorAPIError ( ( ) => new Promise < string > ( ( resolve , reject ) => {
6692 client . createSSHKeyPair ( new CreateSSHKeyPairRequest ( ) , metadata , ( err , resp ) => {
6793 if ( err ) {
68- // codes of grpc-web are align with grpc and connect
69- // see https://github.com/improbable-eng/grpc-web/blob/1d9bbb09a0990bdaff0e37499570dbc7d6e58ce8/client/grpc-web/src/Code.ts#L1
70- return reject ( new WrapError ( 'Failed to call supervisor API' , err , 'SupervisorAPI:' + Code [ err . code ] ) ) ;
94+ return reject ( err ) ;
7195 }
7296 resolve ( resp ! . toObject ( ) . privateKey ) ;
7397 } ) ;
74- } ) ;
98+ } ) , { signal } ) ;
7599
76100 const parsedResult = ssh2 . utils . parseKey ( privateKey ) ;
77101 if ( parsedResult instanceof Error || ! parsedResult ) {
@@ -117,7 +141,7 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
117141 const workspaceHost = url . host . substring ( url . host . indexOf ( '.' ) + 1 ) ;
118142 instanceId = ( usePublicApi ? ( workspace as Workspace ) . status ?. instance ?. instanceId : ( workspace as WorkspaceInfo ) . latestInstance ?. id ) as string ;
119143
120- const sshkey = phase === 'running' ? ( await this . getWorkspaceSSHKey ( ownerToken , workspaceId , workspaceHost ) ) : '' ;
144+ const sshkey = phase === 'running' ? ( await this . getWorkspaceSSHKey ( ownerToken , workspaceId , workspaceHost , _context . signal ) ) : '' ;
121145
122146 return {
123147 gitpodHost,
0 commit comments