Skip to content

Commit 98ee125

Browse files
csweichelJanKoehnlein
authored andcommitted
[dashboard] Measure workspace cluster region RTT
[server] Test RTT for available workspace cluster only [dashboard] Improve RTT measurement [dashboard] enable RTT measurement
1 parent f64d818 commit 98ee125

File tree

8 files changed

+96
-16
lines changed

8 files changed

+96
-16
lines changed

components/dashboard/src/start/CreateWorkspace.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { openAuthorizeWindow } from "../provider-utils";
1717
import { SelectAccountPayload } from "@gitpod/gitpod-protocol/lib/auth";
1818
import { SelectAccountModal } from "../settings/SelectAccountModal";
1919
import { watchHeadlessLogs } from "../components/PrebuildLogs";
20+
import { measureAndPickWorkspaceClusterRegion } from "./choose-region";
2021

2122
const WorkspaceLogs = React.lazy(() => import('../components/WorkspaceLogs'));
2223

@@ -46,6 +47,9 @@ export default class CreateWorkspace extends React.Component<CreateWorkspaceProp
4647
// Invalidate any previous result.
4748
this.setState({ result: undefined, stillParsing: true });
4849

50+
// Ensure we'll land on a good workspace custer
51+
await measureAndPickWorkspaceClusterRegion();
52+
4953
// We assume anything longer than 3 seconds is no longer just parsing the context URL (i.e. it's now creating a workspace).
5054
let timeout = setTimeout(() => this.setState({ stillParsing: false }), 3000);
5155

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import PendingChangesDropdown from "../components/PendingChangesDropdown";
1515
import { watchHeadlessLogs } from "../components/PrebuildLogs";
1616
import { getGitpodService, gitpodHostUrl } from "../service/service";
1717
import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
18+
import { measureAndPickWorkspaceClusterRegion } from "./choose-region";
1819
const sessionId = v4();
1920

2021
const WorkspaceLogs = React.lazy(() => import('../components/WorkspaceLogs'));
@@ -117,6 +118,9 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
117118
}
118119
}
119120

121+
// Ensure we'll land on a good workspace custer - this call will take max 1 second
122+
await measureAndPickWorkspaceClusterRegion();
123+
120124
const { workspaceId } = this.props;
121125
try {
122126
const result = await getGitpodService().server.startWorkspace(workspaceId, { forceDefaultImage });
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 localStorageKey = "lastRTTMeasurement";
18+
19+
try {
20+
let lastCheck = localStorage.getItem(localStorageKey);
21+
if (!!lastCheck && (Date.now() - Date.parse(lastCheck)) < 6*60*60*1000) {
22+
// we've recently done the check.
23+
return;
24+
}
25+
} catch (err) {
26+
// Date.parse can fail ... in which case we assume we haven't done the RTT measurement recently.
27+
console.log("cannot determine last RTT measurement run", err);
28+
}
29+
30+
const eps = await getGitpodService().server.listWorkspaceClusterRTTEndpoints();
31+
32+
const region = await Promise.race(eps.candidates.map(ep => measureRTT(ep.endpoint, ep.region)));
33+
if (!region) {
34+
console.warn("did not find a prefered workspace cluster region");
35+
return;
36+
}
37+
38+
localStorage.setItem(localStorageKey, new Date().toISOString())
39+
await getGitpodService().server.setWorkspaceClusterPreferences({ region });
40+
}
41+
42+
async function measureRTT(endpoint: string, region: string): Promise<string | undefined> {
43+
const controller = new AbortController();
44+
const abort = setTimeout(() => controller.abort(), 1000);
45+
46+
try {
47+
await Promise.all(Array(5).map(async () => {
48+
try {
49+
await fetch(endpoint, { cache: "no-cache", signal: controller.signal, });
50+
} catch (err) {
51+
// we don't want a single error to abort the race. For example, it's ok
52+
// if the RTT endpoints return 404.
53+
}
54+
}))
55+
} finally {
56+
clearTimeout(abort);
57+
}
58+
59+
return region;
60+
}
61+
62+
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, CreateWorkspaceMode,
1010
Token, UserEnvVarValue, ResolvePluginsParams, PreparePluginUploadParams, Terms,
1111
ResolvedPlugins, Configuration, InstallPluginsParams, UninstallPluginParams, UserInfo, GitpodTokenType,
12-
GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, ProjectEnvVar, WorkspaceClusterPreference
12+
GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, ProjectEnvVar, WorkspaceClusterPreference,
13+
WorkspaceClusterRTTEndpoints
1314
} from './protocol';
1415
import {
1516
Team, TeamMemberInfo,
@@ -240,7 +241,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
240241
trackLocation(event: RemotePageMessage): Promise<void>;
241242
identifyUser(event: RemoteIdentifyMessage): Promise<void>;
242243

243-
listWorkspaceClusterRTTEndpoints(): Promise<{ endpoint: string; region: string }[]>;
244+
listWorkspaceClusterRTTEndpoints(): Promise<WorkspaceClusterRTTEndpoints>;
244245
setWorkspaceClusterPreferences(pref: WorkspaceClusterPreference): Promise<void>;
245246
}
246247

components/gitpod-protocol/src/protocol.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1237,3 +1237,10 @@ export interface Terms {
12371237
export interface WorkspaceClusterPreference {
12381238
region?: string;
12391239
}
1240+
1241+
export interface WorkspaceClusterRTTEndpoints {
1242+
candidates: {
1243+
region: string;
1244+
endpoint: string;
1245+
}[];
1246+
}

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

Lines changed: 9 additions & 7 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, 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, 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';
@@ -57,7 +57,7 @@ import { PartialProject } from '@gitpod/gitpod-protocol/src/teams-projects-proto
5757
import { ClientMetadata } from '../websocket/websocket-connection-manager';
5858
import { ConfigurationService } from '../config/configuration-service';
5959
import { ProjectEnvVar } from '@gitpod/gitpod-protocol/src/protocol';
60-
import { AdmissionPreferenceRegion, WorkspaceClusterDB } from '@gitpod/gitpod-protocol/lib/workspace-cluster';
60+
import { AdmissionPreferenceRegion } from '@gitpod/gitpod-protocol/lib/workspace-cluster';
6161

6262
// shortcut
6363
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -115,8 +115,6 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
115115

116116
@inject(IDEConfigService) protected readonly ideConfigService: IDEConfigService;
117117

118-
@inject(WorkspaceClusterDB) protected readonly workspaceClusterDB: WorkspaceClusterDB;
119-
120118
/** Id the uniquely identifies this server instance */
121119
public readonly uuid: string = uuidv4();
122120
public readonly clientMetadata: ClientMetadata;
@@ -2206,8 +2204,10 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22062204
return ideConfig.ideOptions;
22072205
}
22082206

2209-
async listWorkspaceClusterRTTEndpoints(ctx: TraceContext): Promise<{ region: string; endpoint: string; }[]> {
2210-
const candidates = await this.workspaceClusterDB.findFiltered({state: 'available'});
2207+
async listWorkspaceClusterRTTEndpoints(ctx: TraceContext): Promise<WorkspaceClusterRTTEndpoints> {
2208+
const user = this.checkUser("listWorkspaceClusterRTTEndpoints");
2209+
2210+
const candidates = await this.workspaceManagerClientProvider.getAvailableStartCluster(user);
22112211
const allEndpoints = candidates.flatMap(c => (c.admissionPreferences || []).filter(ap => ap.type === 'region')).map(ap => {
22122212
const rap = ap as AdmissionPreferenceRegion;
22132213
return {
@@ -2216,7 +2216,9 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22162216
};
22172217
});
22182218

2219-
return [...new Set(allEndpoints)];
2219+
return {
2220+
candidates: [...new Set(allEndpoints)]
2221+
};
22202222
}
22212223

22222224
async setWorkspaceClusterPreferences(ctx: TraceContext, pref: WorkspaceClusterPreference): Promise<void> {

components/ws-manager-api/typescript/src/client-provider.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ class TestClientProvider {
5454

5555
@test
5656
public async testGetStarterWorkspaceCluster() {
57-
this.expectInstallations(["a1", "a2", "a3"], await this.provider.getAvailableStartCluster({} as User, {} as Workspace, {} as WorkspaceInstance));
57+
this.expectInstallations(["a1", "a2", "a3"], await this.provider.getAvailableStartCluster({} as User));
5858
this.expectInstallations(["a1", "a2", "a3", "con1"], await this.provider.getAvailableStartCluster({
5959
additionalData: {featurePreview: true}
60-
} as User, {} as Workspace, {} as WorkspaceInstance));
60+
} as User));
6161
this.expectInstallations(["a1", "a2", "a3", "con2"], await this.provider.getAvailableStartCluster({
6262
rolesOrPermissions: ["new-workspace-cluster"]
63-
} as User, {} as Workspace, {} as WorkspaceInstance));
63+
} as User));
6464
}
6565

6666
public async getStartManager() {

components/ws-manager-api/typescript/src/client-provider.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class WorkspaceManagerClientProvider implements Disposable {
4444
* @returns The WorkspaceManagerClient that was chosen to start the next workspace with.
4545
*/
4646
public async getStartManager(user: ExtendedUser, workspace: Workspace, instance: WorkspaceInstance, exceptInstallations?: string[]): Promise<{ manager: PromisifiedWorkspaceManagerClient, installation: string }> {
47-
let availableCluster = await this.getAvailableStartCluster(user, workspace, instance);
47+
let availableCluster = await this.getAvailableStartCluster(user);
4848
if (!!exceptInstallations) {
4949
availableCluster = availableCluster.filter(c => !exceptInstallations?.includes(c.name));
5050
}
@@ -70,9 +70,9 @@ export class WorkspaceManagerClientProvider implements Disposable {
7070
};
7171
}
7272

73-
public async getAvailableStartCluster(user: User, workspace: Workspace, instance: WorkspaceInstance): Promise<WorkspaceClusterWoTLS[]> {
73+
public async getAvailableStartCluster(user: User): Promise<WorkspaceClusterWoTLS[]> {
7474
const allClusters = await this.source.getAllWorkspaceClusters();
75-
const availableClusters = allClusters.filter(c => c.score >= 0 && c.state === "available").filter(admissionConstraintsFilter(user, workspace, instance));
75+
const availableClusters = allClusters.filter(c => c.score >= 0 && c.state === "available").filter(admissionConstraintsFilter(user));
7676
return availableClusters;
7777
}
7878

@@ -176,7 +176,7 @@ function chooseCluster(availableCluster: WorkspaceClusterWoTLS[]): WorkspaceClus
176176
return availableCluster[availableCluster.length - 1];
177177
}
178178

179-
function admissionConstraintsFilter(user: User, workspace: Workspace, instance: WorkspaceInstance): (c: WorkspaceClusterWoTLS) => boolean {
179+
function admissionConstraintsFilter(user: User, workspace?: Workspace, instance?: WorkspaceInstance): (c: WorkspaceClusterWoTLS) => boolean {
180180
return (c: WorkspaceClusterWoTLS) => {
181181
if (!c.admissionConstraints) {
182182
return true;

0 commit comments

Comments
 (0)