Skip to content

Commit 5adeaea

Browse files
authored
Wrap supervisor api with error and retry (#85)
* Wrap supervisor api with error and retry * Change error code * Remove client side signal
1 parent 7bb2927 commit 5adeaea

File tree

2 files changed

+37
-7
lines changed

2 files changed

+37
-7
lines changed

src/local-ssh/ipc/extensionServiceServer.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
4874
class 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,

src/local-ssh/proxy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ class WebSocketSSHProxy {
124124
setTimeout(() => process.exit(0), 50);
125125
}
126126
});
127+
sshStream.on('end', () => {
128+
setTimeout(() => process.exit(0), 50);
129+
});
130+
sshStream.on('close', () => {
131+
setTimeout(() => process.exit(0), 50);
132+
});
127133

128134
// This is expected to never throw as key is hardcoded
129135
const keys = await importKeyBytes(getHostKey());

0 commit comments

Comments
 (0)