@@ -45,6 +45,32 @@ const phaseMap: Record<WorkspaceInstanceStatus_Phase, WorkspaceInstancePhase | u
45
45
[ WorkspaceInstanceStatus_Phase . UNSPECIFIED ] : undefined ,
46
46
} ;
47
47
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
+
48
74
class ExtensionServiceImpl implements ExtensionServiceImplementation {
49
75
constructor (
50
76
private logService : ILogService ,
@@ -56,22 +82,20 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
56
82
57
83
}
58
84
59
- private async getWorkspaceSSHKey ( ownerToken : string , workspaceId : string , workspaceHost : string ) {
85
+ private async getWorkspaceSSHKey ( ownerToken : string , workspaceId : string , workspaceHost : string , signal : AbortSignal ) {
60
86
const workspaceUrl = `https://${ workspaceId } .${ workspaceHost } ` ;
61
87
const metadata = new BrowserHeaders ( ) ;
62
88
metadata . append ( 'x-gitpod-owner-token' , ownerToken ) ;
63
89
const client = new ControlServiceClient ( `${ workspaceUrl } /_supervisor/v1` , { transport : NodeHttpTransport ( ) } ) ;
64
90
65
- const privateKey = await new Promise < string > ( ( resolve , reject ) => {
91
+ const privateKey = await wrapSupervisorAPIError ( ( ) => new Promise < string > ( ( resolve , reject ) => {
66
92
client . createSSHKeyPair ( new CreateSSHKeyPairRequest ( ) , metadata , ( err , resp ) => {
67
93
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 ) ;
71
95
}
72
96
resolve ( resp ! . toObject ( ) . privateKey ) ;
73
97
} ) ;
74
- } ) ;
98
+ } ) , { signal } ) ;
75
99
76
100
const parsedResult = ssh2 . utils . parseKey ( privateKey ) ;
77
101
if ( parsedResult instanceof Error || ! parsedResult ) {
@@ -117,7 +141,7 @@ class ExtensionServiceImpl implements ExtensionServiceImplementation {
117
141
const workspaceHost = url . host . substring ( url . host . indexOf ( '.' ) + 1 ) ;
118
142
instanceId = ( usePublicApi ? ( workspace as Workspace ) . status ?. instance ?. instanceId : ( workspace as WorkspaceInfo ) . latestInstance ?. id ) as string ;
119
143
120
- const sshkey = phase === 'running' ? ( await this . getWorkspaceSSHKey ( ownerToken , workspaceId , workspaceHost ) ) : '' ;
144
+ const sshkey = phase === 'running' ? ( await this . getWorkspaceSSHKey ( ownerToken , workspaceId , workspaceHost , _context . signal ) ) : '' ;
121
145
122
146
return {
123
147
gitpodHost,
0 commit comments