Skip to content

Commit 2cd0670

Browse files
Fix prebuild lookup for prebuilds run in reverse-chronological order (#20360)
* Fix prebuild lookup for prebuilds run in reverse-chronological order * Minor fixes * 🧹 * Simplify prebuild ordering * [server] Prefer exact matches plus order incremental matches by commit history * Do expensive image and prebuild promises in parallel * [server] Prefer exact matches plus order incremental matches by commit history * Fix test comment typo * Do commit index computation once + better logs --------- Co-authored-by: Gero Posmyk-Leinemann <[email protected]>
1 parent 355ece1 commit 2cd0670

File tree

5 files changed

+236
-56
lines changed

5 files changed

+236
-56
lines changed

components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class BitbucketServerRepositoryProvider implements RepositoryProvider {
190190
owner,
191191
repoKind,
192192
repositorySlug: repo,
193-
query: { shaOrRevision: ref, limit: 1000 },
193+
query: { shaOrRevision: ref, limit: 1000 }, // ft: why do we limit to 1000 and not maxDepth?
194194
});
195195

196196
const commits = commitsResult.values || [];

components/server/src/bitbucket/bitbucket-repository-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class BitbucketRepositoryProvider implements RepositoryProvider {
143143
): Promise<string[]> {
144144
const api = await this.apiFactory.create(user);
145145
// TODO(janx): To get more results than Bitbucket API's max pagelen (seems to be 100), pagination should be handled.
146-
// The additional property 'page' may be helfpul.
146+
// The additional property 'page' may be helpful.
147147
const result = await api.repositories.listCommitsAt({
148148
workspace: owner,
149149
repo_slug: repo,

components/server/src/github/github-repository-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export class GithubRepositoryProvider implements RepositoryProvider {
130130
}
131131

132132
// TODO(janx): To get more results than GitHub API's max page size (seems to be 100), pagination should be handled.
133-
// These additional history properties may be helfpul:
133+
// These additional history properties may be helpful:
134134
// totalCount,
135135
// pageInfo {
136136
// haxNextPage,

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

Lines changed: 119 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ import {
1616
} from "@gitpod/gitpod-protocol";
1717
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1818
import { PrebuiltWorkspaceState, WithCommitHistory } from "@gitpod/gitpod-protocol/lib/protocol";
19-
import { WorkspaceDB } from "@gitpod/gitpod-db/lib";
19+
import { PrebuildWithWorkspace, WorkspaceDB } from "@gitpod/gitpod-db/lib";
2020
import { Config } from "../config";
2121
import { HostContextProvider } from "../auth/host-context-provider";
2222
import { ImageSourceProvider } from "../workspace/image-source-provider";
23+
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
24+
import { TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
2325

2426
const MAX_HISTORY_DEPTH = 100;
2527

28+
type IncrementalWorkspaceMatch = "none" | "incremental" | "exact";
29+
2630
@injectable()
2731
export class IncrementalWorkspaceService {
2832
@inject(Config) protected readonly config: Config;
@@ -45,6 +49,7 @@ export class IncrementalWorkspaceService {
4549
context.revision,
4650
maxDepth,
4751
);
52+
4853
history.commitHistory.unshift(context.revision);
4954
if (context.additionalRepositoryCheckoutInfo && context.additionalRepositoryCheckoutInfo.length > 0) {
5055
const histories = context.additionalRepositoryCheckoutInfo.map(async (info) => {
@@ -78,79 +83,128 @@ export class IncrementalWorkspaceService {
7883
return;
7984
}
8085

81-
const imageSourcePromise = this.imageSourceProvider.getImageSource({}, user, context, config);
82-
83-
// Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
84-
// (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
85-
const recentPrebuilds = await this.workspaceDB.findPrebuildsWithWorkspace(projectId);
86-
const imageSource = await imageSourcePromise;
87-
for (const recentPrebuild of recentPrebuilds) {
88-
if (
89-
this.isGoodBaseforIncrementalBuild(
90-
history,
91-
config,
92-
imageSource,
93-
recentPrebuild.prebuild,
94-
recentPrebuild.workspace,
95-
includeUnfinishedPrebuilds,
96-
)
97-
) {
98-
return recentPrebuild.prebuild;
86+
const [recentPrebuilds, imageSource] = await Promise.allSettled([
87+
// Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
88+
// (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
89+
this.workspaceDB.findPrebuildsWithWorkspace(projectId),
90+
this.imageSourceProvider.getImageSource({}, user, context, config),
91+
]);
92+
if (imageSource.status === "rejected") {
93+
log.error("Image source promise was rejected", { reason: imageSource.reason, userId: user.id });
94+
throw new ApplicationError(
95+
ErrorCodes.INTERNAL_SERVER_ERROR,
96+
"Something went wrong when looking up prebuilds",
97+
);
98+
}
99+
if (recentPrebuilds.status === "rejected") {
100+
log.error("Prebuild lookup promise was rejected", { reason: recentPrebuilds.reason, userId: user.id });
101+
throw new ApplicationError(
102+
ErrorCodes.INTERNAL_SERVER_ERROR,
103+
"Something went wrong when looking up prebuilds",
104+
);
105+
}
106+
107+
// traverse prebuilds by commit history instead of their creationTime, so that we don't match prebuilds created for older revisions but triggered later
108+
const candidates: { candidate: PrebuildWithWorkspace; index: number }[] = [];
109+
for (const recentPrebuild of recentPrebuilds.value) {
110+
const { prebuild, workspace } = recentPrebuild;
111+
const { match, index } = this.isMatchForIncrementalBuild(
112+
history,
113+
config,
114+
imageSource.value,
115+
prebuild,
116+
workspace,
117+
includeUnfinishedPrebuilds,
118+
);
119+
if (match === "exact") {
120+
log.info("Found base for incremental build", {
121+
prebuildId: prebuild.id,
122+
match: new TrustedValue({
123+
exact: true,
124+
distanceFromContext: 0,
125+
}),
126+
});
127+
return prebuild;
128+
}
129+
if (match === "incremental") {
130+
candidates.push({ candidate: recentPrebuild, index: index! });
99131
}
100132
}
101-
return undefined;
133+
134+
if (candidates.length === 0) {
135+
return undefined;
136+
}
137+
138+
// Sort by index ASC
139+
candidates.sort((a, b) => a.index - b.index);
140+
const {
141+
candidate: { prebuild },
142+
index,
143+
} = candidates[0];
144+
145+
log.info("Found base for incremental build", {
146+
prebuildId: prebuild.id,
147+
match: {
148+
exact: true,
149+
distanceFromContext: index,
150+
},
151+
});
152+
return prebuild;
102153
}
103154

104-
private isGoodBaseforIncrementalBuild(
155+
private isMatchForIncrementalBuild(
105156
history: WithCommitHistory,
106157
config: WorkspaceConfig,
107158
imageSource: WorkspaceImageSource,
108159
candidatePrebuild: PrebuiltWorkspace,
109160
candidateWorkspace: Workspace,
110161
includeUnfinishedPrebuilds?: boolean,
111-
): boolean {
112-
if (!history.commitHistory || history.commitHistory.length === 0) {
113-
return false;
162+
): { match: Omit<IncrementalWorkspaceMatch, "none">; index: number } | { match: "none"; index?: undefined } {
163+
// make typescript happy, we know that history.commitHistory is defined
164+
if (!history.commitHistory) {
165+
return { match: "none" };
114166
}
115167
if (!CommitContext.is(candidateWorkspace.context)) {
116-
return false;
168+
return { match: "none" };
117169
}
118170

119171
const acceptableStates: PrebuiltWorkspaceState[] = ["available"];
120172
if (includeUnfinishedPrebuilds) {
121173
acceptableStates.push("building");
122174
acceptableStates.push("queued");
123175
}
124-
125176
if (!acceptableStates.includes(candidatePrebuild.state)) {
126-
return false;
177+
return { match: "none" };
127178
}
128179

129-
// we are only considering full prebuilds
130-
if (!!candidateWorkspace.basedOnPrebuildId) {
131-
return false;
180+
// we are only considering full prebuilds (we are not building on top of incremental prebuilds)
181+
if (candidateWorkspace.basedOnPrebuildId) {
182+
return { match: "none" };
132183
}
133184

185+
// check if the amount of additional repositories matches the candidate
134186
if (
135187
candidateWorkspace.context.additionalRepositoryCheckoutInfo?.length !==
136188
history.additionalRepositoryCommitHistories?.length
137189
) {
138-
// different number of repos
139-
return false;
190+
return { match: "none" };
140191
}
141192

142193
const candidateCtx = candidateWorkspace.context;
143-
if (!history.commitHistory.some((sha) => sha === candidateCtx.revision)) {
144-
return false;
194+
195+
// check for overlapping commit history
196+
const commitIndexInHistory = history.commitHistory.indexOf(candidateCtx.revision);
197+
if (commitIndexInHistory === -1) {
198+
return { match: "none" };
145199
}
146200

147-
// check the commits are included in the commit history
148-
for (const subRepo of candidateWorkspace.context.additionalRepositoryCheckoutInfo || []) {
149-
const matchIngRepo = history.additionalRepositoryCommitHistories?.find(
201+
// check for overlapping git history for each additional repo
202+
for (const subRepo of candidateWorkspace.context.additionalRepositoryCheckoutInfo ?? []) {
203+
const matchingRepo = history.additionalRepositoryCommitHistories?.find(
150204
(repo) => repo.cloneUrl === subRepo.repository.cloneUrl,
151205
);
152-
if (!matchIngRepo || !matchIngRepo.commitHistory.some((sha) => sha === subRepo.revision)) {
153-
return false;
206+
if (!matchingRepo || !matchingRepo.commitHistory.some((sha) => sha === subRepo.revision)) {
207+
return { match: "none" };
154208
}
155209
}
156210

@@ -160,29 +214,41 @@ export class IncrementalWorkspaceService {
160214
imageSource,
161215
parentImageSource: candidateWorkspace.imageSource,
162216
});
163-
return false;
217+
return { match: "none" };
164218
}
165219

166220
// ensure the tasks haven't changed
167-
const filterPrebuildTasks = (tasks: TaskConfig[] = []) =>
168-
tasks
169-
.map((task) =>
170-
Object.keys(task)
171-
.filter((key) => ["before", "init", "prebuild"].includes(key))
172-
// @ts-ignore
173-
.reduce((obj, key) => ({ ...obj, [key]: task[key] }), {}),
174-
)
175-
.filter((task) => Object.keys(task).length > 0);
176-
const prebuildTasks = filterPrebuildTasks(config.tasks);
177-
const parentPrebuildTasks = filterPrebuildTasks(candidateWorkspace.config.tasks);
221+
const prebuildTasks = this.filterPrebuildTasks(config.tasks);
222+
const parentPrebuildTasks = this.filterPrebuildTasks(candidateWorkspace.config.tasks);
178223
if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) {
179224
log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, {
180225
prebuildTasks,
181226
parentPrebuildTasks,
182227
});
183-
return false;
228+
return { match: "none" };
184229
}
185230

186-
return true;
231+
if (commitIndexInHistory === 0) {
232+
return { match: "exact", index: commitIndexInHistory };
233+
}
234+
235+
return { match: "incremental", index: commitIndexInHistory };
236+
}
237+
238+
/**
239+
* Given an array of tasks returns only the those which are to run during prebuilds, additionally stripping everything besides the prebuild-related configuration from them
240+
*/
241+
private filterPrebuildTasks(tasks: TaskConfig[] = []): Record<string, string>[] {
242+
return tasks
243+
.map((task) => {
244+
const filteredTask: Record<string, any> = {};
245+
for (const key of Object.keys(task)) {
246+
if (["before", "init", "prebuild"].includes(key)) {
247+
filteredTask[key] = task[key as keyof TaskConfig];
248+
}
249+
}
250+
return filteredTask;
251+
})
252+
.filter((task) => Object.keys(task).length > 0);
187253
}
188254
}

0 commit comments

Comments
 (0)