Skip to content

Commit 76ba165

Browse files
committed
[dashboard] proactively reconnect grpc streams
1 parent 6d02689 commit 76ba165

File tree

5 files changed

+56
-34
lines changed

5 files changed

+56
-34
lines changed

components/dashboard/src/service/public-api.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,28 @@ export function stream<Response>(
277277
try {
278278
for await (const response of factory({
279279
signal: abort.signal,
280+
// GCP timeout is 10 minutes, we timeout 3 mins earlier
281+
// to avoid unknown network errors and reconnect gracefully
282+
timeoutMs: 7 * 60 * 1000,
280283
})) {
281284
backoff = BASE_BACKOFF;
282285
cb(response);
283286
}
284287
} catch (e) {
285288
if (ApplicationError.hasErrorCode(e) && e.code === ErrorCodes.CANCELLED) {
286-
return;
289+
if (abort.signal.aborted) {
290+
// client aborted, don't reconnect, early exit
291+
return;
292+
}
293+
// server aborted, i.e. restart, reconnect with backoff
294+
}
295+
if (ApplicationError.hasErrorCode(e) && e.code === ErrorCodes.DEADLINE_EXCEEDED) {
296+
// timeout is expected, reconnect with base backoff
297+
backoff = BASE_BACKOFF;
298+
} else {
299+
backoff = Math.min(2 * backoff, MAX_BACKOFF);
300+
console.error(e);
287301
}
288-
backoff = Math.min(2 * backoff, MAX_BACKOFF);
289-
console.error("failed to watch prebuild:", e);
290302
}
291303
const jitter = Math.random() * 0.3 * backoff;
292304
const delay = backoff + jitter;

components/dashboard/src/service/service.tsx

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ import {
1313
GitpodServiceImpl,
1414
User,
1515
WorkspaceInfo,
16+
Disposable,
1617
} from "@gitpod/gitpod-protocol";
1718
import { WebSocketConnectionProvider } from "@gitpod/gitpod-protocol/lib/messaging/browser/connection";
1819
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
1920
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
2021
import { IDEFrontendDashboardService } from "@gitpod/gitpod-protocol/lib/frontend-dashboard-service";
2122
import { RemoteTrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
22-
import { helloService, workspaceClient } from "./public-api";
23+
import { helloService, stream, workspaceClient } from "./public-api";
2324
import { getExperimentsClient } from "../experiments/client";
24-
import { ConnectError, Code } from "@connectrpc/connect";
2525
import { instrumentWebSocket } from "./metrics";
26+
import { LotsOfRepliesResponse } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb";
2627

2728
export const gitpodHostUrl = new GitpodHostUrl(window.location.toString());
2829

@@ -122,11 +123,19 @@ function testPublicAPI(service: any): void {
122123
},
123124
});
124125
(async () => {
125-
const MAX_BACKOFF = 60000;
126-
const BASE_BACKOFF = 3000;
127-
let backoff = BASE_BACKOFF;
126+
let previousCount = 0;
127+
const watchLotsOfReplies = () =>
128+
stream<LotsOfRepliesResponse>(
129+
(options) => {
130+
return helloService.lotsOfReplies({ previousCount }, options);
131+
},
132+
(response) => {
133+
previousCount = response.count;
134+
},
135+
);
128136

129137
// emulates server side streaming with public API
138+
let watching: Disposable | undefined;
130139
while (true) {
131140
const isTest =
132141
!!user &&
@@ -135,34 +144,14 @@ function testPublicAPI(service: any): void {
135144
gitpodHost: window.location.host,
136145
}));
137146
if (isTest) {
138-
try {
139-
let previousCount = 0;
140-
for await (const reply of helloService.lotsOfReplies(
141-
{ previousCount },
142-
{
143-
// GCP timeout is 10 minutes, we timeout 3 mins earlier
144-
// to avoid unknown network errors
145-
timeoutMs: 7 * 60 * 1000,
146-
},
147-
)) {
148-
previousCount = reply.count;
149-
backoff = BASE_BACKOFF;
150-
}
151-
} catch (e) {
152-
if (e instanceof ConnectError && e.code === Code.DeadlineExceeded) {
153-
// timeout is expected, continue as usual
154-
backoff = BASE_BACKOFF;
155-
} else {
156-
backoff = Math.min(2 * backoff, MAX_BACKOFF);
157-
console.error(e);
158-
}
147+
if (!watching) {
148+
watching = watchLotsOfReplies();
159149
}
160-
} else {
161-
backoff = BASE_BACKOFF;
150+
} else if (watching) {
151+
watching.dispose();
152+
watching = undefined;
162153
}
163-
const jitter = Math.random() * 0.3 * backoff;
164-
const delay = backoff + jitter;
165-
await new Promise((resolve) => setTimeout(resolve, delay));
154+
await new Promise((resolve) => setTimeout(resolve, 3000));
166155
}
167156
})();
168157
}

components/gitpod-protocol/src/messaging/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export const ErrorCodes = {
113113
// 498 The operation was cancelled, typically by the caller.
114114
CANCELLED: 498 as const,
115115

116+
// 4981 The deadline expired before the operation could complete.
117+
DEADLINE_EXCEEDED: 4981 as const,
118+
116119
// 500 Internal Server Error
117120
INTERNAL_SERVER_ERROR: 500 as const,
118121

components/public-api/typescript-common/src/public-api-converter.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,18 @@ describe("PublicAPIConverter", () => {
455455
expect(appError.message).to.equal("cancelled");
456456
});
457457

458+
it("DEADLINE_EXCEEDED", () => {
459+
const connectError = converter.toError(
460+
new ApplicationError(ErrorCodes.DEADLINE_EXCEEDED, "deadline exceeded"),
461+
);
462+
expect(connectError.code).to.equal(Code.DeadlineExceeded);
463+
expect(connectError.rawMessage).to.equal("deadline exceeded");
464+
465+
const appError = converter.fromError(connectError);
466+
expect(appError.code).to.equal(ErrorCodes.DEADLINE_EXCEEDED);
467+
expect(appError.message).to.equal("deadline exceeded");
468+
});
469+
458470
it("INTERNAL_SERVER_ERROR", () => {
459471
const connectError = converter.toError(
460472
new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, "internal server error"),

components/public-api/typescript-common/src/public-api-converter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ export class PublicAPIConverter {
384384
if (reason.code === ErrorCodes.CANCELLED) {
385385
return new ConnectError(reason.message, Code.Canceled, undefined, undefined, reason);
386386
}
387+
if (reason.code === ErrorCodes.DEADLINE_EXCEEDED) {
388+
return new ConnectError(reason.message, Code.DeadlineExceeded, undefined, undefined, reason);
389+
}
387390
if (reason.code === ErrorCodes.INTERNAL_SERVER_ERROR) {
388391
return new ConnectError(reason.message, Code.Internal, undefined, undefined, reason);
389392
}
@@ -450,6 +453,9 @@ export class PublicAPIConverter {
450453
if (reason.code === Code.Canceled) {
451454
return new ApplicationError(ErrorCodes.CANCELLED, reason.rawMessage);
452455
}
456+
if (reason.code === Code.DeadlineExceeded) {
457+
return new ApplicationError(ErrorCodes.DEADLINE_EXCEEDED, reason.rawMessage);
458+
}
453459
if (reason.code === Code.Internal) {
454460
return new ApplicationError(ErrorCodes.INTERNAL_SERVER_ERROR, reason.rawMessage);
455461
}

0 commit comments

Comments
 (0)