Skip to content

Commit b04b72b

Browse files
committed
[dashboard] Measure workspace cluster region RTT
1 parent 45ee534 commit b04b72b

File tree

6 files changed

+94
-6
lines changed

6 files changed

+94
-6
lines changed

components/dashboard/src/Login.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import fresh from "./images/welcome/fresh.svg";
2222
import prebuild from "./images/welcome/prebuild.svg";
2323
import exclamation from "./images/exclamation.svg";
2424
import { getURLHash } from "./App";
25+
import { measureAndPickWorkspaceClusterRegion } from "./start/choose-region";
2526

2627

2728
function Item(props: { icon: string, iconSize?: string, text: string }) {
@@ -84,6 +85,8 @@ export function Login() {
8485
const authorizeSuccessful = async (payload?: string) => {
8586
updateUser().catch(console.error);
8687

88+
await measureAndPickWorkspaceClusterRegion();
89+
8790
// Check for a valid returnTo in payload
8891
const safeReturnTo = getSafeURLRedirect(payload);
8992
if (safeReturnTo) {

components/dashboard/src/start/StartPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { useEffect } from 'react';
88
import gitpodIcon from '../icons/gitpod.svg';
9+
import { measureAndPickWorkspaceClusterRegion } from './choose-region';
910

1011
export enum StartPhase {
1112
Checking = 0,
@@ -84,6 +85,7 @@ export function StartPage(props: StartPageProps) {
8485
const { phase, error } = props;
8586
let title = props.title || getPhaseTitle(phase, error);
8687
useEffect(() => { document.title = 'Starting — Gitpod' }, []);
88+
measureAndPickWorkspaceClusterRegion();
8789
return <div className="w-screen h-screen align-middle">
8890
<div className="flex flex-col mx-auto items-center text-center h-screen">
8991
<div className="h-1/3"></div>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { getGitpodService } from "../service/service";
8+
9+
/**
10+
*
11+
* measureAndPickWorkspaceClusterRegion attempts multiple fetch calls on all available workspace cluster regions.
12+
* The first region to return those fetch attempts is set as workspace cluster preference.
13+
*
14+
* @returns void
15+
*/
16+
async function measureAndPickWorkspaceClusterRegion(): Promise<void> {
17+
const eps = await getGitpodService().server.listWorkspaceClusterRTTEndpoints();
18+
19+
if (!!eps.lastMeasurement) {
20+
const lrm = Date.parse(eps.lastMeasurement);
21+
if (Date.now() - lrm < 6*60*60*1000) {
22+
// we checked within the last six hours. Nothing to do here.
23+
return;
24+
}
25+
}
26+
27+
const region = await Promise.race(eps.candidates.map(ep => measureRTT(ep.endpoint, ep.region)));
28+
if (!region) {
29+
console.warn("did not find a prefered workspace cluster region");
30+
return;
31+
}
32+
33+
await getGitpodService().server.setWorkspaceClusterPreferences({ region });
34+
}
35+
36+
async function measureRTT(endpoint: string, region: string): Promise<string | undefined> {
37+
const laps = 5;
38+
let count = 0;
39+
for (let i = 0; i < laps; i++) {
40+
const controller = new AbortController();
41+
const abort = setTimeout(() => controller.abort(), 1000);
42+
43+
try {
44+
await fetch(endpoint, {
45+
cache: "no-cache",
46+
signal: controller.signal,
47+
});
48+
count++;
49+
} catch (err) {
50+
console.debug(`failed to fetch RTT endpoint ${endpoint}: ${err}`);
51+
} finally {
52+
clearTimeout(abort);
53+
}
54+
}
55+
56+
if (count < 5) {
57+
// we haven't completed all RTT measurements, hence take a penalty lap.
58+
await new Promise(resolve => setTimeout(resolve, laps * 1000));
59+
return;
60+
}
61+
62+
return region;
63+
}
64+
65+
export { measureAndPickWorkspaceClusterRegion };

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
WhitelistedRepository, WorkspaceImageBuild, AuthProviderInfo, Branding, CreateWorkspaceMode,
1010
Token, UserEnvVarValue, ResolvePluginsParams, PreparePluginUploadParams, Terms,
1111
ResolvedPlugins, Configuration, InstallPluginsParams, UninstallPluginParams, UserInfo, GitpodTokenType,
12-
GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, WorkspaceClusterPreference
12+
GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, WorkspaceClusterPreference,
13+
WorkspaceClusterRTTEndpoints
1314
} from './protocol';
1415
import {
1516
Team, TeamMemberInfo,
@@ -238,7 +239,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
238239
trackLocation(event: RemotePageMessage): Promise<void>;
239240
identifyUser(event: RemoteIdentifyMessage): Promise<void>;
240241

241-
listWorkspaceClusterRTTEndpoints(): Promise<{ endpoint: string; region: string }[]>;
242+
listWorkspaceClusterRTTEndpoints(): Promise<WorkspaceClusterRTTEndpoints>;
242243
setWorkspaceClusterPreferences(pref: WorkspaceClusterPreference): Promise<void>;
243244
}
244245

components/gitpod-protocol/src/protocol.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1254,4 +1254,15 @@ export interface Terms {
12541254

12551255
export interface WorkspaceClusterPreference {
12561256
region?: string;
1257-
}
1257+
// lrm == lastRegionMeasurement. Not writing the full name to save
1258+
// a few 100mb in the database.
1259+
lrm?: string;
1260+
}
1261+
1262+
export interface WorkspaceClusterRTTEndpoints {
1263+
lastMeasurement?: string;
1264+
candidates: {
1265+
region: string;
1266+
endpoint: string;
1267+
}[];
1268+
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { DownloadUrlRequest, DownloadUrlResponse, UploadUrlRequest, UploadUrlResponse } from '@gitpod/content-service/lib/blobs_pb';
88
import { AppInstallationDB, UserDB, UserMessageViewsDB, WorkspaceDB, DBWithTracing, TracedWorkspaceDB, DBGitpodToken, DBUser, UserStorageResourcesDB, TeamDB } from '@gitpod/gitpod-db/lib';
9-
import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient as GitpodApiClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus, StartPrebuildResult, ClientHeaderFields, WorkspaceClusterPreference } from '@gitpod/gitpod-protocol';
9+
import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient as GitpodApiClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus, StartPrebuildResult, ClientHeaderFields, WorkspaceClusterPreference, WorkspaceClusterRTTEndpoints } from '@gitpod/gitpod-protocol';
1010
import { AccountStatement } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
1111
import { AdminBlockUserRequest, AdminGetListRequest, AdminGetListResult, AdminGetWorkspacesRequest, AdminModifyPermanentWorkspaceFeatureFlagRequest, AdminModifyRoleOrPermissionRequest, WorkspaceAndInstance } from '@gitpod/gitpod-protocol/lib/admin-protocol';
1212
import { GetLicenseInfoResult, LicenseFeature, LicenseValidationResult } from '@gitpod/gitpod-protocol/lib/license-protocol';
@@ -2185,7 +2185,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21852185
return ideConfig.ideOptions;
21862186
}
21872187

2188-
async listWorkspaceClusterRTTEndpoints(ctx: TraceContext): Promise<{ region: string; endpoint: string; }[]> {
2188+
async listWorkspaceClusterRTTEndpoints(ctx: TraceContext): Promise<WorkspaceClusterRTTEndpoints> {
2189+
const user = this.checkUser("listWorkspaceClusterRTTEndpoints");
2190+
21892191
const candidates = await this.workspaceClusterDB.findFiltered({state: 'available'});
21902192
const allEndpoints = candidates.flatMap(c => (c.admissionPreferences || []).filter(ap => ap.type === 'region')).map(ap => {
21912193
const rap = ap as AdmissionPreferenceRegion;
@@ -2195,7 +2197,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
21952197
};
21962198
});
21972199

2198-
return [...new Set(allEndpoints)];
2200+
return {
2201+
lastMeasurement: user.additionalData?.clusterPreferences?.lrm,
2202+
candidates: [...new Set(allEndpoints)]
2203+
};
21992204
}
22002205

22012206
async setWorkspaceClusterPreferences(ctx: TraceContext, pref: WorkspaceClusterPreference): Promise<void> {
@@ -2206,6 +2211,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22062211

22072212
// don't just dump `pref` in here, we have no idea what it contains.
22082213
clusterPref.region = pref.region;
2214+
clusterPref.lrm = new Date().toISOString();
22092215

22102216
data.clusterPreferences = clusterPref;
22112217
user.additionalData = data;

0 commit comments

Comments
 (0)