Skip to content

Commit 8e8a5db

Browse files
[PAPI] Retry steams upon not receiving data for 10 seconds (#20172)
* [PAPI] Retry steams upon not receiving data for 10 seconds * [PAPI] Retry steams upon not receiving data for 10 seconds (#20176) * debug message * Do not listen for ws updates globally and do not timeout for those calls * Timeout signal as a getter * use ff for disablement * accesor fn => getter * send empty initial data in papi for global ws watch requests --------- Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent 414ee45 commit 8e8a5db

File tree

8 files changed

+39
-31
lines changed

8 files changed

+39
-31
lines changed

components/dashboard/src/app/AppRoutes.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import { Blocked } from "./Blocked";
3333
import { BlockedRepositories } from "../admin/BlockedRepositories";
3434
import PersonalAccessTokenCreateView from "../user-settings/PersonalAccessTokensCreateView";
3535
import { CreateWorkspacePage } from "../workspaces/CreateWorkspacePage";
36-
import { WebsocketClients } from "./WebsocketClients";
3736
import { BlockedEmailDomains } from "../admin/BlockedEmailDomains";
3837
import { AppNotifications } from "../AppNotifications";
3938
import { projectsPathInstallGitHubApp } from "../projects/projects.routes";
@@ -263,7 +262,6 @@ export const AppRoutes = () => {
263262
/>
264263
</Switch>
265264
</div>
266-
<WebsocketClients />
267265
</Route>
268266
</Switch>
269267
</Route>

components/dashboard/src/app/WebsocketClients.tsx

-15
This file was deleted.

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

+16-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { VerificationService } from "@gitpod/public-api/lib/gitpod/v1/verificati
4141
import { JsonRpcInstallationClient } from "./json-rpc-installation-client";
4242
import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installation_connect";
4343
import { JsonRpcUserClient } from "./json-rpc-user-client";
44+
import { Timeout } from "@gitpod/gitpod-protocol/lib/util/timeout";
4445

4546
const transport = createConnectTransport({
4647
baseUrl: `${window.location.protocol}//${window.location.host}/public-api`,
@@ -273,14 +274,26 @@ export function stream<Response>(
273274
let backoff = BASE_BACKOFF;
274275
const abort = new AbortController();
275276
(async () => {
277+
// Only timeout after 10 seconds with no data in some environments
278+
const experiments = getExperimentsClient();
279+
const enableTimeout = await experiments.getValueAsync("supervisor_check_ready_retry", false, {});
280+
276281
while (!abort.signal.aborted) {
282+
const connectionTimeout = new Timeout(10_000, () => enableTimeout);
277283
try {
284+
connectionTimeout.start();
285+
connectionTimeout.signal?.addEventListener("abort", () => {
286+
console.error("Connection timed out after no response for 10s");
287+
});
288+
278289
for await (const response of factory({
279-
signal: abort.signal,
290+
signal: AbortSignal.any([abort.signal, connectionTimeout.signal!]),
280291
// GCP timeout is 10 minutes, we timeout 3 mins earlier
281292
// to avoid unknown network errors and reconnect gracefully
282293
timeoutMs: 7 * 60 * 1000,
283294
})) {
295+
connectionTimeout.clear(); // connection is alive now, clear timeout
296+
284297
backoff = BASE_BACKOFF;
285298
cb(response);
286299
}
@@ -302,6 +315,8 @@ export function stream<Response>(
302315
backoff = Math.min(2 * backoff, MAX_BACKOFF);
303316
console.error(e);
304317
}
318+
} finally {
319+
connectionTimeout.clear();
305320
}
306321
const jitter = Math.random() * 0.3 * backoff;
307322
const delay = backoff + jitter;

components/dashboard/src/workspaces/Workspaces.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@ import { BookOpen, Code } from "lucide-react";
2424
import { ReactComponent as GitpodStrokedSVG } from "../icons/gitpod-stroked.svg";
2525
import { isGitpodIo } from "../utils";
2626
import PersonalizedContent from "./PersonalizedContent";
27+
import { useListenToWorkspacesWSMessages as useListenToWorkspacesStatusUpdates } from "../data/workspaces/listen-to-workspace-ws-messages";
2728

2829
const WorkspacesPage: FunctionComponent = () => {
2930
const [limit, setLimit] = useState(50);
3031
const [searchTerm, setSearchTerm] = useState("");
3132
const [showInactive, setShowInactive] = useState(false);
3233
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
34+
3335
const { data, isLoading } = useListWorkspacesQuery({ limit });
3436
const deleteInactiveWorkspaces = useDeleteInactiveWorkspacesMutation();
37+
useListenToWorkspacesStatusUpdates();
38+
3539
const { toast } = useToast();
3640

3741
// Sort workspaces into active/inactive groups

components/gitpod-protocol/src/util/timeout.spec.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,44 @@ export class TimeoutSpec {
1616
const timeout = new Timeout(1);
1717
timeout.start();
1818
await timeout.await();
19-
expect(timeout.signal()?.aborted).to.be.true;
19+
expect(timeout.signal?.aborted).to.be.true;
2020
}
2121

2222
@test
2323
async testSimpleRunNotStarted() {
2424
const timeout = new Timeout(1);
2525
await timeout.await();
26-
expect(timeout.signal()).to.be.undefined;
26+
expect(timeout.signal).to.be.undefined;
2727
}
2828

2929
@test
3030
async testRestart() {
3131
const timeout = new Timeout(20);
3232
timeout.start();
3333
await timeout.await();
34-
expect(timeout.signal()?.aborted).to.be.true;
34+
expect(timeout.signal?.aborted).to.be.true;
3535

3636
timeout.restart();
37-
expect(timeout.signal()).to.not.be.undefined;
38-
expect(timeout.signal()?.aborted).to.be.false;
37+
expect(timeout.signal).to.not.be.undefined;
38+
expect(timeout.signal?.aborted).to.be.false;
3939
await timeout.await();
40-
expect(timeout.signal()?.aborted).to.be.true;
40+
expect(timeout.signal?.aborted).to.be.true;
4141
}
4242

4343
@test
4444
async testClear() {
4545
const timeout = new Timeout(1000);
4646
timeout.restart();
4747
timeout.clear();
48-
expect(timeout.signal()).to.be.undefined;
48+
expect(timeout.signal).to.be.undefined;
4949
}
5050

5151
@test
5252
async testAbortCondition() {
5353
const timeout = new Timeout(1, () => false); // will never trigger abort
5454
timeout.start();
5555
await new Promise((resolve) => setTimeout(resolve, 50));
56-
expect(timeout.signal()).to.not.be.undefined;
57-
expect(timeout.signal()?.aborted).to.be.false;
56+
expect(timeout.signal).to.not.be.undefined;
57+
expect(timeout.signal?.aborted).to.be.false;
5858
}
5959
}

components/gitpod-protocol/src/util/timeout.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export class Timeout {
3434

3535
const abortController = new AbortController();
3636
this._abortController = abortController;
37+
if (this.timeout === Infinity) {
38+
return;
39+
}
3740
this._timer = setTimeout(() => {
3841
if (this.abortCondition && !this.abortCondition()) {
3942
return;
@@ -74,7 +77,7 @@ export class Timeout {
7477
/**
7578
* @returns The AbortSignal of the current timeout, if one is active.
7679
*/
77-
public signal(): AbortSignal | undefined {
80+
get signal() {
7881
return this._abortController?.signal;
7982
}
8083
}

components/server/src/workspace/workspace-service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,9 @@ export class WorkspaceService {
11331133
response.status = status;
11341134
yield response;
11351135
}
1136+
} else {
1137+
// we initially send an empty status update to let clients know the connection is alive
1138+
yield {} as WatchWorkspaceStatusResponse;
11361139
}
11371140
const it = this.watchWorkspaceStatus(userId, opts);
11381141
for await (const instance of it) {

components/supervisor/frontend/src/ide/supervisor-service-client.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export class SupervisorServiceClient {
114114
});
115115
const response = await fetch(wsSupervisorStatusUrl.toString(), {
116116
credentials: "include",
117-
signal: timeout.signal(),
117+
signal: timeout.signal,
118118
});
119119
let result;
120120
if (response.ok) {
@@ -140,11 +140,11 @@ export class SupervisorServiceClient {
140140

141141
// we want to track this kind of errors, as they are on the critical path (of revealing the workspace)
142142
isError = true;
143-
trackCheckReady({ aborted: timeout.signal()?.aborted }, e);
143+
trackCheckReady({ aborted: timeout.signal?.aborted }, e);
144144
} finally {
145145
if (!isError) {
146146
// make sure we don't track twice in case of an error
147-
trackCheckReady({ aborted: timeout.signal()?.aborted });
147+
trackCheckReady({ aborted: timeout.signal?.aborted });
148148
}
149149
timeout.clear();
150150
}

0 commit comments

Comments
 (0)