Skip to content

Commit 76a302b

Browse files
committed
[server] Refactor common incremental prebuilds code into a IncrementalPrebuildsService
1 parent 13b6b6d commit 76a302b

File tree

7 files changed

+260
-250
lines changed

7 files changed

+260
-250
lines changed

components/gitpod-protocol/src/protocol.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,13 +1111,16 @@ export namespace SnapshotContext {
11111111
}
11121112
}
11131113

1114-
export interface StartPrebuildContext extends WorkspaceContext {
1115-
actual: WorkspaceContext;
1114+
export interface WithCommitHistories {
11161115
commitHistory?: string[];
11171116
additionalRepositoryCommitHistories?: {
11181117
cloneUrl: string;
11191118
commitHistory: string[];
11201119
}[];
1120+
}
1121+
1122+
export interface StartPrebuildContext extends WorkspaceContext, WithCommitHistories {
1123+
actual: WorkspaceContext;
11211124
project?: Project;
11221125
branch?: string;
11231126
}
@@ -1200,7 +1203,7 @@ export namespace AdditionalContentContext {
12001203
}
12011204
}
12021205

1203-
export interface CommitContext extends WorkspaceContext, GitCheckoutInfo {
1206+
export interface CommitContext extends WorkspaceContext, GitCheckoutInfo, WithCommitHistories {
12041207
/** @deprecated Moved to .repository.cloneUrl, left here for backwards-compatibility for old workspace contextes in the DB */
12051208
cloneUrl?: string;
12061209

components/server/ee/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { PrebuildStatusMaintainer } from "./prebuilds/prebuilt-status-maintainer
2222
import { GitLabApp } from "./prebuilds/gitlab-app";
2323
import { BitbucketApp } from "./prebuilds/bitbucket-app";
2424
import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app";
25+
import { IncrementalPrebuildsService } from "./prebuilds/incremental-prebuilds-service";
2526
import { IPrefixContextParser } from "../../src/workspace/context-parser";
2627
import { StartPrebuildContextParser } from "./prebuilds/start-prebuild-context-parser";
2728
import { WorkspaceFactory } from "../../src/workspace/workspace-factory";
@@ -83,6 +84,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
8384
bind(BitbucketAppSupport).toSelf().inSingletonScope();
8485
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
8586
bind(BitbucketServerApp).toSelf().inSingletonScope();
87+
bind(IncrementalPrebuildsService).toSelf().inSingletonScope();
8688

8789
bind(UserCounter).toSelf().inSingletonScope();
8890

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* Copyright (c) 2022 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 { inject, injectable } from "inversify";
8+
import {
9+
CommitContext,
10+
PrebuiltWorkspace,
11+
TaskConfig,
12+
User,
13+
Workspace,
14+
WorkspaceConfig,
15+
WorkspaceImageSource,
16+
} from "@gitpod/gitpod-protocol";
17+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
18+
import { WithCommitHistories } from "@gitpod/gitpod-protocol/src/protocol";
19+
import { WorkspaceDB } from "@gitpod/gitpod-db/lib";
20+
import { Config } from "../../../src/config";
21+
import { ConfigProvider } from "../../../src/workspace/config-provider";
22+
import { HostContextProvider } from "../../../src/auth/host-context-provider";
23+
import { ImageSourceProvider } from "../../../src/workspace/image-source-provider";
24+
25+
@injectable()
26+
export class IncrementalPrebuildsService {
27+
@inject(Config) protected readonly config: Config;
28+
@inject(ConfigProvider) protected readonly configProvider: ConfigProvider;
29+
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
30+
@inject(ImageSourceProvider) protected readonly imageSourceProvider: ImageSourceProvider;
31+
@inject(WorkspaceDB) protected readonly workspaceDB: WorkspaceDB;
32+
33+
public async getCommitHistories(context: CommitContext, user: User): Promise<WithCommitHistories> {
34+
const maxDepth = this.config.incrementalPrebuilds.commitHistory;
35+
const hostContext = this.hostContextProvider.get(context.repository.host);
36+
const repoProvider = hostContext?.services?.repositoryProvider;
37+
if (!repoProvider) {
38+
return {};
39+
}
40+
const result: WithCommitHistories = {};
41+
result.commitHistory = await repoProvider.getCommitHistory(
42+
user,
43+
context.repository.owner,
44+
context.repository.name,
45+
context.revision,
46+
maxDepth,
47+
);
48+
if (context.additionalRepositoryCheckoutInfo && context.additionalRepositoryCheckoutInfo.length > 0) {
49+
const histories = context.additionalRepositoryCheckoutInfo.map(async (info) => {
50+
const commitHistory = await repoProvider.getCommitHistory(
51+
user,
52+
info.repository.owner,
53+
info.repository.name,
54+
info.revision,
55+
maxDepth,
56+
);
57+
return {
58+
cloneUrl: info.repository.cloneUrl,
59+
commitHistory,
60+
};
61+
});
62+
result.additionalRepositoryCommitHistories = await Promise.all(histories);
63+
}
64+
return result;
65+
}
66+
67+
public async findGoodBaseForIncrementalBuild(
68+
context: WithCommitHistories,
69+
commitContext: CommitContext,
70+
user: User,
71+
): Promise<PrebuiltWorkspace | undefined> {
72+
if (!context.commitHistory || context.commitHistory.length < 1) {
73+
return;
74+
}
75+
76+
const { config } = await this.configProvider.fetchConfig({}, user, commitContext);
77+
const imageSource = await this.imageSourceProvider.getImageSource({}, user, commitContext, config);
78+
79+
// Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
80+
// (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
81+
const recentPrebuilds = await this.workspaceDB.findPrebuildsWithWorkpace(commitContext.repository.cloneUrl);
82+
for (const recentPrebuild of recentPrebuilds) {
83+
if (
84+
await this.isGoodBaseforIncrementalBuild(
85+
context,
86+
config,
87+
imageSource,
88+
recentPrebuild.prebuild,
89+
recentPrebuild.workspace,
90+
)
91+
) {
92+
return recentPrebuild.prebuild;
93+
}
94+
}
95+
}
96+
97+
protected async isGoodBaseforIncrementalBuild(
98+
context: WithCommitHistories,
99+
config: WorkspaceConfig,
100+
imageSource: WorkspaceImageSource,
101+
candidatePrebuild: PrebuiltWorkspace,
102+
candidateWorkspace: Workspace,
103+
): Promise<boolean> {
104+
if (!context.commitHistory || context.commitHistory.length === 0) {
105+
log.info("Disqualified: no commitHistory");
106+
return false;
107+
}
108+
if (!CommitContext.is(candidateWorkspace.context)) {
109+
log.info("Disqualified: candiate workspace context not a commit context");
110+
return false;
111+
}
112+
113+
// we are only considering available prebuilds
114+
if (candidatePrebuild.state !== "available") {
115+
log.info("Disqualified: candidate prebuild not available");
116+
return false;
117+
}
118+
119+
// we are only considering full prebuilds
120+
if (!!candidateWorkspace.basedOnPrebuildId) {
121+
log.info("Disqualified: candidate is based on another prebuild");
122+
return false;
123+
}
124+
125+
if (
126+
candidateWorkspace.context.additionalRepositoryCheckoutInfo?.length !==
127+
context.additionalRepositoryCommitHistories?.length
128+
) {
129+
// different number of repos
130+
log.info(
131+
"Disqualified: candidate context additional repository checkout info !== context additional repo histories",
132+
);
133+
return false;
134+
}
135+
136+
const candidateCtx = candidateWorkspace.context;
137+
if (!context.commitHistory.some((sha) => sha === candidateCtx.revision)) {
138+
log.info("Disqualified: candidate revision not in commit history");
139+
return false;
140+
}
141+
142+
// check the commits are included in the commit history
143+
for (const subRepo of candidateWorkspace.context.additionalRepositoryCheckoutInfo || []) {
144+
const matchIngRepo = context.additionalRepositoryCommitHistories?.find(
145+
(repo) => repo.cloneUrl === subRepo.repository.cloneUrl,
146+
);
147+
if (!matchIngRepo || !matchIngRepo.commitHistory.some((sha) => sha === subRepo.revision)) {
148+
log.info("Disqualified: something about matchIngRepo");
149+
return false;
150+
}
151+
}
152+
153+
// ensure the image source hasn't changed (skips older images)
154+
if (JSON.stringify(imageSource) !== JSON.stringify(candidateWorkspace.imageSource)) {
155+
log.debug(`Skipping parent prebuild: Outdated image`, {
156+
imageSource,
157+
parentImageSource: candidateWorkspace.imageSource,
158+
});
159+
log.info("Disqualified: image source has changed");
160+
return false;
161+
}
162+
163+
// ensure the tasks haven't changed
164+
const filterPrebuildTasks = (tasks: TaskConfig[] = []) =>
165+
tasks
166+
.map((task) =>
167+
Object.keys(task)
168+
.filter((key) => ["before", "init", "prebuild"].includes(key))
169+
// @ts-ignore
170+
.reduce((obj, key) => ({ ...obj, [key]: task[key] }), {}),
171+
)
172+
.filter((task) => Object.keys(task).length > 0);
173+
const prebuildTasks = filterPrebuildTasks(config.tasks);
174+
const parentPrebuildTasks = filterPrebuildTasks(candidateWorkspace.config.tasks);
175+
if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) {
176+
log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, {
177+
prebuildTasks,
178+
parentPrebuildTasks,
179+
});
180+
log.info("Disqualified: prebuild tasks have changed");
181+
return false;
182+
}
183+
184+
return true;
185+
}
186+
}

components/server/ee/src/prebuilds/prebuild-manager.ts

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { inject, injectable } from "inversify";
3333
import * as opentracing from "opentracing";
3434
import { StopWorkspacePolicy } from "@gitpod/ws-manager/lib";
3535
import { error } from "console";
36+
import { IncrementalPrebuildsService } from "./incremental-prebuilds-service";
3637

3738
export class WorkspaceRunningError extends Error {
3839
constructor(msg: string, public instance: WorkspaceInstance) {
@@ -59,6 +60,7 @@ export class PrebuildManager {
5960
@inject(ConfigProvider) protected readonly configProvider: ConfigProvider;
6061
@inject(Config) protected readonly config: Config;
6162
@inject(ProjectsService) protected readonly projectService: ProjectsService;
63+
@inject(IncrementalPrebuildsService) protected readonly incrementalPrebuildsService: IncrementalPrebuildsService;
6264

6365
async abortPrebuildsForBranch(ctx: TraceContext, project: Project, user: User, branch: string): Promise<void> {
6466
const span = TraceContext.startSpan("abortPrebuildsForBranch", ctx);
@@ -172,36 +174,13 @@ export class PrebuildManager {
172174
};
173175

174176
if (this.shouldPrebuildIncrementally(context.repository.cloneUrl, project)) {
175-
const maxDepth = this.config.incrementalPrebuilds.commitHistory;
176-
const hostContext = this.hostContextProvider.get(context.repository.host);
177-
const repoProvider = hostContext?.services?.repositoryProvider;
178-
if (repoProvider) {
179-
prebuildContext.commitHistory = await repoProvider.getCommitHistory(
180-
user,
181-
context.repository.owner,
182-
context.repository.name,
183-
context.revision,
184-
maxDepth,
185-
);
186-
if (
187-
context.additionalRepositoryCheckoutInfo &&
188-
context.additionalRepositoryCheckoutInfo.length > 0
189-
) {
190-
const histories = context.additionalRepositoryCheckoutInfo.map(async (info) => {
191-
const commitHistory = await repoProvider.getCommitHistory(
192-
user,
193-
info.repository.owner,
194-
info.repository.name,
195-
info.revision,
196-
maxDepth,
197-
);
198-
return {
199-
cloneUrl: info.repository.cloneUrl,
200-
commitHistory,
201-
};
202-
});
203-
prebuildContext.additionalRepositoryCommitHistories = await Promise.all(histories);
204-
}
177+
const { commitHistory, additionalRepositoryCommitHistories } =
178+
await this.incrementalPrebuildsService.getCommitHistories(context, user);
179+
if (commitHistory) {
180+
prebuildContext.commitHistory = commitHistory;
181+
}
182+
if (additionalRepositoryCommitHistories) {
183+
prebuildContext.additionalRepositoryCommitHistories = additionalRepositoryCommitHistories;
205184
}
206185
}
207186

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

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import {
4747
TeamMemberRole,
4848
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
4949
PrebuildEvent,
50-
StartPrebuildContext,
5150
} from "@gitpod/gitpod-protocol";
5251
import { ResponseError } from "vscode-jsonrpc";
5352
import {
@@ -116,10 +115,12 @@ import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billin
116115
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode";
117116
import { BillingModes } from "../billing/billing-mode";
118117
import { UsageServiceDefinition } from "@gitpod/usage-api/lib/usage/v1/usage.pb";
118+
import { IncrementalPrebuildsService } from "../prebuilds/incremental-prebuilds-service";
119119

120120
@injectable()
121121
export class GitpodServerEEImpl extends GitpodServerImpl {
122122
@inject(PrebuildManager) protected readonly prebuildManager: PrebuildManager;
123+
@inject(IncrementalPrebuildsService) protected readonly incrementalPrebuildsService: IncrementalPrebuildsService;
123124
@inject(LicenseDB) protected readonly licenseDB: LicenseDB;
124125
@inject(LicenseKeySource) protected readonly licenseKeySource: LicenseKeySource;
125126

@@ -970,58 +971,25 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
970971
const logPayload = { mode, cloneUrl, commit: commitSHAs, prebuiltWorkspace };
971972
log.debug(logCtx, "Looking for prebuilt workspace: ", logPayload);
972973
if (prebuiltWorkspace?.state !== "available" && mode === CreateWorkspaceMode.UseLastSuccessfulPrebuild) {
973-
const maxDepth = this.config.incrementalPrebuilds.commitHistory;
974-
const hostContext = this.hostContextProvider.get(context.repository.host);
975-
const repoProvider = hostContext?.services?.repositoryProvider;
976-
if (repoProvider) {
977-
(context as any).commitHistory = await repoProvider.getCommitHistory(
978-
user,
979-
context.repository.owner,
980-
context.repository.name,
981-
context.revision,
982-
maxDepth,
983-
);
984-
log.info("findPrebuiltWorkspace: incremental workspace", {
985-
commitHistory: (context as any).commitHistory,
986-
});
987-
// {"component":"server","severity":"INFO","time":"2022-10-12T12:57:06.447Z","message":"findPrebuiltWorkspace: incremental workspace","payload":{"commitHistory":["95b666410f37b5237bf416feb748fb1e8aab8fd4","ab01b49c669ce41e0fef1cb306b1ba648ff4ea6a","b3d8f3ada8e729a99b334f75b45eacf240350afa","71d1a3cae0121b2fea14a69b97bcdddce44809ac","218dd0667a7eff7ed657f27b570431919cd87be7","feb2d8b057f751ccd13a7aec07ecfd85757cbb09","9270c10e3b1c23f0a65253cd516aae55120550e9","a70117ff7ae75de4de6c922ab9953e299c711046","24c9dcb64f582ae696797dc617b9af1352a6f01a","6f2aff3ebf94cf40a0c96e87bfd00b2917fc7adb","98b4bfb6190a3bb9fb0f513ffc2bce4009177960","35c9687543411e9e1ced865a9fcd69e1003fe3ed","61d913686e3f5ac17bcd490774097dc6302c92bf","f93921910f80c085fafc5157f6d6b7205b19a4fa"]}}
988-
989-
// Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
990-
// (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
991-
const recentPrebuilds = await this.workspaceDb
992-
.trace(ctx)
993-
.findPrebuildsWithWorkpace(context.repository.cloneUrl);
994-
995-
const { config } = await this.workspaceFactory.configProvider.fetchConfig(ctx, user, context);
996-
const imageSource = await this.workspaceFactory.imageSourceProvider.getImageSource(
997-
ctx,
998-
user,
999-
context,
1000-
config,
1001-
);
1002-
1003-
for (const recentPrebuild of recentPrebuilds) {
1004-
if (
1005-
!(await this.workspaceFactory.isGoodBaseforIncrementalPrebuild(
1006-
context as any as StartPrebuildContext,
1007-
config,
1008-
imageSource,
1009-
recentPrebuild.prebuild,
1010-
recentPrebuild.workspace,
1011-
))
1012-
) {
1013-
log.info({ userId: user.id }, "Not using incremental workspace prebuild", {
1014-
candidatePrebuild: recentPrebuild.prebuild,
1015-
});
1016-
continue;
1017-
}
1018-
log.info({ userId: user.id }, "Using incremental workspace prebuild", {
1019-
prebuild: recentPrebuild.prebuild,
1020-
});
1021-
prebuiltWorkspace = recentPrebuild.prebuild;
1022-
break;
1023-
}
974+
const { commitHistory, additionalRepositoryCommitHistories } =
975+
await this.incrementalPrebuildsService.getCommitHistories(context, user);
976+
if (commitHistory) {
977+
context.commitHistory = commitHistory;
978+
}
979+
if (additionalRepositoryCommitHistories) {
980+
context.additionalRepositoryCommitHistories = additionalRepositoryCommitHistories;
1024981
}
982+
log.info("findPrebuiltWorkspace: incremental workspace", {
983+
commitHistory: context.commitHistory,
984+
additionalRepositoryCommitHistories: context.additionalRepositoryCommitHistories,
985+
});
986+
// {"component":"server","severity":"INFO","time":"2022-10-12T12:57:06.447Z","message":"findPrebuiltWorkspace: incremental workspace","payload":{"commitHistory":["95b666410f37b5237bf416feb748fb1e8aab8fd4","ab01b49c669ce41e0fef1cb306b1ba648ff4ea6a","b3d8f3ada8e729a99b334f75b45eacf240350afa","71d1a3cae0121b2fea14a69b97bcdddce44809ac","218dd0667a7eff7ed657f27b570431919cd87be7","feb2d8b057f751ccd13a7aec07ecfd85757cbb09","9270c10e3b1c23f0a65253cd516aae55120550e9","a70117ff7ae75de4de6c922ab9953e299c711046","24c9dcb64f582ae696797dc617b9af1352a6f01a","6f2aff3ebf94cf40a0c96e87bfd00b2917fc7adb","98b4bfb6190a3bb9fb0f513ffc2bce4009177960","35c9687543411e9e1ced865a9fcd69e1003fe3ed","61d913686e3f5ac17bcd490774097dc6302c92bf","f93921910f80c085fafc5157f6d6b7205b19a4fa"]}}
987+
988+
prebuiltWorkspace = await this.incrementalPrebuildsService.findGoodBaseForIncrementalBuild(
989+
context,
990+
context,
991+
user,
992+
);
1025993
}
1026994
if (!prebuiltWorkspace) {
1027995
return;

0 commit comments

Comments
 (0)