Skip to content

Commit f4eac6d

Browse files
committed
Simplify prebuild ordering
1 parent fd5efea commit f4eac6d

File tree

1 file changed

+70
-54
lines changed

1 file changed

+70
-54
lines changed

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

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import { ImageSourceProvider } from "../workspace/image-source-provider";
2323

2424
const MAX_HISTORY_DEPTH = 100;
2525

26+
enum Match {
27+
None = 0,
28+
Loose = 1,
29+
Exact = 2,
30+
}
31+
2632
@injectable()
2733
export class IncrementalWorkspaceService {
2834
@inject(Config) protected readonly config: Config;
@@ -84,88 +90,86 @@ export class IncrementalWorkspaceService {
8490
// Note: This query returns only not-garbage-collected prebuilds in order to reduce cardinality
8591
// (e.g., at the time of writing, the Gitpod repository has 16K+ prebuilds, but only ~300 not-garbage-collected)
8692
const recentPrebuilds = await this.workspaceDB.findPrebuildsWithWorkspace(projectId);
87-
88-
const sortedRecentPrebuilds = recentPrebuilds
89-
.filter((prebuild) => {
90-
return history.commitHistory?.includes(prebuild.prebuild.commit);
91-
})
92-
.sort((a, b) => {
93-
// instead of the DB-returned creation time we use the commit history to sort the prebuilds
94-
// this way we can return the correct prebuild even if the prebuild was first created for a later commit and then another one for an earlier commit
95-
const aIdx = history.commitHistory?.indexOf(a.prebuild.commit) ?? -1;
96-
const bIdx = history.commitHistory?.indexOf(b.prebuild.commit) ?? -1;
97-
98-
return aIdx - bIdx;
99-
});
10093
const imageSource = await imageSourcePromise;
101-
for (const recentPrebuild of sortedRecentPrebuilds) {
102-
if (
103-
this.isGoodBaseforIncrementalBuild(
94+
95+
// traverse prebuilds by commit history instead of their creationTime, so that we don't match prebuilds created for older revisions but triggered later
96+
for (const commit of history.commitHistory) {
97+
const prebuildsForCommit = recentPrebuilds.filter(({ prebuild }) => prebuild.commit === commit);
98+
for (const entry of prebuildsForCommit) {
99+
const { prebuild, workspace } = entry;
100+
const match = this.isMatchForIncrementalBuild(
104101
history,
105102
config,
106103
imageSource,
107-
recentPrebuild.prebuild,
108-
recentPrebuild.workspace,
104+
prebuild,
105+
workspace,
109106
includeUnfinishedPrebuilds,
110-
)
111-
) {
112-
return recentPrebuild.prebuild;
107+
);
108+
if (match > Match.None) {
109+
console.log("Found base for incremental build", {
110+
prebuild,
111+
workspace,
112+
exactMatch: match === Match.Exact,
113+
});
114+
return prebuild;
115+
}
113116
}
114117
}
115118

116119
return undefined;
117120
}
118121

119-
private isGoodBaseforIncrementalBuild(
122+
private isMatchForIncrementalBuild(
120123
history: WithCommitHistory,
121124
config: WorkspaceConfig,
122125
imageSource: WorkspaceImageSource,
123126
candidatePrebuild: PrebuiltWorkspace,
124127
candidateWorkspace: Workspace,
125128
includeUnfinishedPrebuilds?: boolean,
126-
): boolean {
127-
if (!history.commitHistory || history.commitHistory.length === 0) {
128-
return false;
129+
): Match {
130+
// make typescript happy, we know that history.commitHistory is defined
131+
if (!history.commitHistory) {
132+
return Match.None;
129133
}
130134
if (!CommitContext.is(candidateWorkspace.context)) {
131-
return false;
135+
return Match.None;
132136
}
133137

134138
const acceptableStates: PrebuiltWorkspaceState[] = ["available"];
135139
if (includeUnfinishedPrebuilds) {
136140
acceptableStates.push("building");
137141
acceptableStates.push("queued");
138142
}
139-
140143
if (!acceptableStates.includes(candidatePrebuild.state)) {
141-
return false;
144+
return Match.None;
142145
}
143146

144-
// we are only considering full prebuilds
145-
if (!!candidateWorkspace.basedOnPrebuildId) {
146-
return false;
147+
// we are only considering full prebuilds (we are not building on top of incremental prebuilds)
148+
if (candidateWorkspace.basedOnPrebuildId) {
149+
return Match.None;
147150
}
148151

152+
// check if the amount of additional repositories matches the candidate
149153
if (
150154
candidateWorkspace.context.additionalRepositoryCheckoutInfo?.length !==
151155
history.additionalRepositoryCommitHistories?.length
152156
) {
153-
// different number of repos
154-
return false;
157+
return Match.None;
155158
}
156159

157160
const candidateCtx = candidateWorkspace.context;
161+
162+
// check for overlapping commit history
158163
if (!history.commitHistory.some((sha) => sha === candidateCtx.revision)) {
159-
return false;
164+
return Match.None;
160165
}
161-
162-
// check the commits are included in the commit history
163-
for (const subRepo of candidateWorkspace.context.additionalRepositoryCheckoutInfo || []) {
164-
const matchIngRepo = history.additionalRepositoryCommitHistories?.find(
166+
// check for overlapping git history for each additional repo
167+
for (const subRepo of candidateWorkspace.context.additionalRepositoryCheckoutInfo ?? []) {
168+
const matchingRepo = history.additionalRepositoryCommitHistories?.find(
165169
(repo) => repo.cloneUrl === subRepo.repository.cloneUrl,
166170
);
167-
if (!matchIngRepo || !matchIngRepo.commitHistory.some((sha) => sha === subRepo.revision)) {
168-
return false;
171+
if (!matchingRepo || !matchingRepo.commitHistory.some((sha) => sha === subRepo.revision)) {
172+
return Match.None;
169173
}
170174
}
171175

@@ -175,29 +179,41 @@ export class IncrementalWorkspaceService {
175179
imageSource,
176180
parentImageSource: candidateWorkspace.imageSource,
177181
});
178-
return false;
182+
return Match.None;
179183
}
180184

181185
// ensure the tasks haven't changed
182-
const filterPrebuildTasks = (tasks: TaskConfig[] = []) =>
183-
tasks
184-
.map((task) =>
185-
Object.keys(task)
186-
.filter((key) => ["before", "init", "prebuild"].includes(key))
187-
// @ts-ignore
188-
.reduce((obj, key) => ({ ...obj, [key]: task[key] }), {}),
189-
)
190-
.filter((task) => Object.keys(task).length > 0);
191-
const prebuildTasks = filterPrebuildTasks(config.tasks);
192-
const parentPrebuildTasks = filterPrebuildTasks(candidateWorkspace.config.tasks);
186+
const prebuildTasks = this.filterPrebuildTasks(config.tasks);
187+
const parentPrebuildTasks = this.filterPrebuildTasks(candidateWorkspace.config.tasks);
193188
if (JSON.stringify(prebuildTasks) !== JSON.stringify(parentPrebuildTasks)) {
194189
log.debug(`Skipping parent prebuild: Outdated prebuild tasks`, {
195190
prebuildTasks,
196191
parentPrebuildTasks,
197192
});
198-
return false;
193+
return Match.None;
194+
}
195+
196+
if (candidatePrebuild.commit === history.commitHistory[0]) {
197+
return Match.Exact;
199198
}
200199

201-
return true;
200+
return Match.Loose;
201+
}
202+
203+
/**
204+
* 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
205+
*/
206+
private filterPrebuildTasks(tasks: TaskConfig[] = []): Record<string, string>[] {
207+
return tasks
208+
.map((task) => {
209+
const filteredTask: Record<string, any> = {};
210+
for (const key of Object.keys(task)) {
211+
if (["before", "init", "prebuild"].includes(key)) {
212+
filteredTask[key] = task[key as keyof TaskConfig];
213+
}
214+
}
215+
return filteredTask;
216+
})
217+
.filter((task) => Object.keys(task).length > 0);
202218
}
203219
}

0 commit comments

Comments
 (0)