Skip to content

Commit ba78bd4

Browse files
svenefftingeroboquat
authored andcommitted
[server] abort running prebuilds on same branch
1 parent 3133297 commit ba78bd4

File tree

11 files changed

+145
-87
lines changed

11 files changed

+145
-87
lines changed

components/dashboard/src/projects/Prebuilds.tsx

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ export default function (props: { project?: Project; isAdminDashboard?: boolean
221221
}`}
222222
>
223223
<div>
224-
<div className="text-base text-gray-900 dark:text-gray-50 font-medium uppercase mb-1">
224+
<div
225+
className="text-base text-gray-900 dark:text-gray-50 font-medium uppercase mb-1"
226+
title={getPrebuildStatusDescription(p)}
227+
>
225228
<div className="inline-block align-text-bottom mr-2 w-4 h-4">
226229
{prebuildStatusIcon(p)}
227230
</div>
@@ -329,38 +332,25 @@ export function prebuildStatusIcon(prebuild?: PrebuildWithStatus) {
329332
}
330333
}
331334

332-
function PrebuildStatusDescription(props: { prebuild: PrebuildWithStatus }) {
333-
switch (props.prebuild.status) {
335+
function getPrebuildStatusDescription(prebuild: PrebuildWithStatus): string {
336+
switch (prebuild.status) {
334337
case "queued":
335-
return <span>Prebuild is queued and will be processed when there is execution capacity.</span>;
338+
return `Prebuild is queued and will be processed when there is execution capacity.`;
336339
case "building":
337-
return <span>Prebuild is currently in progress.</span>;
340+
return `Prebuild is currently in progress.`;
338341
case "aborted":
339-
return (
340-
<span>
341-
Prebuild has been cancelled. Either a user cancelled it, or the prebuild rate limit has been
342-
exceeded. {props.prebuild.error}
343-
</span>
344-
);
342+
return `Prebuild has been cancelled. Either a newer commit was pushed to the same branch, a user cancelled it manually, or the prebuild rate limit has been exceeded.`;
345343
case "failed":
346-
return <span>Prebuild failed for system reasons. Please contact support. {props.prebuild.error}</span>;
344+
return `Prebuild failed for system reasons. Please contact support. ${prebuild.error}`;
347345
case "timeout":
348-
return (
349-
<span>
350-
Prebuild timed out. Either the image, or the prebuild tasks took too long. {props.prebuild.error}
351-
</span>
352-
);
346+
return `Prebuild timed out. Either the image, or the prebuild tasks took too long. ${prebuild.error}`;
353347
case "available":
354-
if (props.prebuild?.error) {
355-
return (
356-
<span>
357-
The tasks executed in the prebuild returned a non-zero exit code. {props.prebuild.error}
358-
</span>
359-
);
348+
if (prebuild.error) {
349+
return `The tasks executed in the prebuild returned a non-zero exit code. ${prebuild.error}`;
360350
}
361-
return <span>Prebuild completed successfully.</span>;
351+
return `Prebuild completed successfully.`;
362352
default:
363-
return <span>Unknown prebuild status.</span>;
353+
return `Unknown prebuild status.`;
364354
}
365355
}
366356

@@ -376,7 +366,7 @@ export function PrebuildStatus(props: { prebuild: PrebuildWithStatus }) {
376366
</div>
377367
</div>
378368
<div className="flex space-x-1 items-center text-gray-400">
379-
<PrebuildStatusDescription prebuild={prebuild} />
369+
<span>{getPrebuildStatusDescription(prebuild)}</span>
380370
</div>
381371
</div>
382372
);

components/dashboard/src/projects/ProjectSettings.tsx

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { PageWithSubMenu } from "../components/PageWithSubMenu";
1414
import PillLabel from "../components/PillLabel";
1515
import { ProjectContext } from "./project-context";
1616
import { getExperimentsClient } from "./../experiments/client";
17-
import { DeepPartial } from "@gitpod/gitpod-protocol/lib/util/deep-partial";
1817

1918
export function getProjectSettingsMenu(project?: Project, team?: Team) {
2019
const teamOrUserSlug = !!team ? "t/" + team.slug : "projects";
@@ -56,19 +55,14 @@ export default function () {
5655
const { teams } = useContext(TeamsContext);
5756
const team = getCurrentTeam(location, teams);
5857

59-
const [isLoading, setIsLoading] = useState<boolean>(true);
60-
const [isIncrementalPrebuildsEnabled, setIsIncrementalPrebuildsEnabled] = useState<boolean>(false);
61-
const [isPersistentVolumeClaimEnabled, setIsPersistentVolumeClaimEnabled] = useState<boolean>(false);
6258
const [isShowPersistentVolumeClaim, setIsShowPersistentVolumeClaim] = useState<boolean>(false);
59+
const [projectSettings, setProjectSettings] = useState<ProjectSettings>({});
6360

6461
useEffect(() => {
6562
if (!project) {
6663
return;
6764
}
68-
setIsLoading(false);
69-
setIsIncrementalPrebuildsEnabled(!!project.settings?.useIncrementalPrebuilds);
70-
setIsPersistentVolumeClaimEnabled(!!project.settings?.usePersistentVolumeClaim);
71-
65+
setProjectSettings({ ...project.settings });
7266
(async () => {
7367
const showPersistentVolumeClaim = await getExperimentsClient().getValueAsync(
7468
"persistent_volume_claim",
@@ -84,49 +78,34 @@ export default function () {
8478
})();
8579
}, [project, team, teams]);
8680

87-
const updateProjectPartial = (settings: DeepPartial<ProjectSettings | undefined>) => {
81+
const updateProjectSettings = () => {
8882
if (!project) {
8983
return;
9084
}
91-
92-
return getGitpodService().server.updateProjectPartial({ id: project.id, settings });
85+
setProjectSettings({
86+
...projectSettings,
87+
});
88+
return getGitpodService().server.updateProjectPartial({ id: project.id, settings: projectSettings });
9389
};
9490

9591
const toggleIncrementalPrebuilds = async () => {
96-
if (!project) {
97-
return;
98-
}
99-
setIsLoading(true);
100-
try {
101-
await updateProjectPartial({
102-
useIncrementalPrebuilds: !isIncrementalPrebuildsEnabled,
103-
usePersistentVolumeClaim: isPersistentVolumeClaimEnabled,
104-
});
105-
setIsIncrementalPrebuildsEnabled(!isIncrementalPrebuildsEnabled);
106-
} finally {
107-
setIsLoading(false);
108-
}
92+
projectSettings.useIncrementalPrebuilds = !projectSettings.useIncrementalPrebuilds;
93+
updateProjectSettings();
94+
};
95+
96+
const toggleCancelOutdatedPrebuilds = async () => {
97+
projectSettings.keepOutdatedPrebuildsRunning = !projectSettings.keepOutdatedPrebuildsRunning;
98+
updateProjectSettings();
10999
};
110100

111101
const togglePersistentVolumeClaim = async () => {
112-
if (!project) {
113-
return;
114-
}
115-
setIsLoading(true);
116-
try {
117-
await updateProjectPartial({
118-
useIncrementalPrebuilds: isIncrementalPrebuildsEnabled,
119-
usePersistentVolumeClaim: !isPersistentVolumeClaimEnabled,
120-
});
121-
setIsPersistentVolumeClaimEnabled(!isPersistentVolumeClaimEnabled);
122-
} finally {
123-
setIsLoading(false);
124-
}
102+
projectSettings.usePersistentVolumeClaim = !projectSettings.usePersistentVolumeClaim;
103+
updateProjectSettings();
125104
};
126105

127106
return (
128107
<ProjectSettingsPage project={project}>
129-
<h3>Incremental Prebuilds</h3>
108+
<h3>Prebuilds</h3>
130109
<CheckBox
131110
title={
132111
<span>
@@ -146,10 +125,15 @@ export default function () {
146125
</a>
147126
</span>
148127
}
149-
checked={isIncrementalPrebuildsEnabled}
150-
disabled={isLoading}
128+
checked={!!projectSettings.useIncrementalPrebuilds}
151129
onChange={toggleIncrementalPrebuilds}
152130
/>
131+
<CheckBox
132+
title={<span>Cancel Prebuilds on Outdated Commits </span>}
133+
desc={<span>Cancels all pending and running prebuilds on the branch when a new commit is pushed.</span>}
134+
checked={!projectSettings.keepOutdatedPrebuildsRunning}
135+
onChange={toggleCancelOutdatedPrebuilds}
136+
/>
153137
<br></br>
154138
<h3>Persistent Volume Claim</h3>
155139
<CheckBox
@@ -162,8 +146,8 @@ export default function () {
162146
</span>
163147
}
164148
desc={<span>Experimental feature that is still under development.</span>}
165-
checked={isPersistentVolumeClaimEnabled}
166-
disabled={isLoading || !isShowPersistentVolumeClaim}
149+
checked={!!projectSettings.usePersistentVolumeClaim}
150+
disabled={!isShowPersistentVolumeClaim}
167151
onChange={togglePersistentVolumeClaim}
168152
/>
169153
</ProjectSettingsPage>

components/gitpod-db/src/typeorm/workspace-db-impl.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
WorkspaceAndOwner,
1919
WorkspacePortsAuthData,
2020
WorkspaceOwnerAndSoftDeleted,
21+
PrebuildWithWorkspaceAndInstances,
2122
} from "../workspace-db";
2223
import {
2324
Workspace,
@@ -816,6 +817,38 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
816817
.getOne();
817818
}
818819

820+
public async findActivePrebuiltWorkspacesByBranch(
821+
projectId: string,
822+
branch: string,
823+
): Promise<PrebuildWithWorkspaceAndInstances[]> {
824+
if (!branch) {
825+
return [];
826+
}
827+
const repo = await this.getPrebuiltWorkspaceRepo();
828+
const result = await repo
829+
.createQueryBuilder("pws")
830+
.where(
831+
"(pws.state = 'queued' OR pws.state = 'building') AND pws.projectId = :projectId AND pws.branch = :branch",
832+
{ projectId, branch },
833+
)
834+
.orderBy("pws.creationTime", "DESC")
835+
.innerJoinAndMapOne(
836+
"pws.workspace",
837+
DBWorkspace,
838+
"ws",
839+
"pws.buildWorkspaceId = ws.id and ws.contentDeletedTime = ''",
840+
)
841+
.innerJoinAndMapMany("pws.instances", DBWorkspaceInstance, "wsi", "pws.buildWorkspaceId = wsi.workspaceId")
842+
.getMany();
843+
return result.map((r) => {
844+
return {
845+
prebuild: r,
846+
workspace: (<any>r).workspace,
847+
instances: (<any>r).instances,
848+
};
849+
});
850+
}
851+
819852
public async findPrebuildByWorkspaceID(wsid: string): Promise<PrebuiltWorkspace | undefined> {
820853
const repo = await this.getPrebuiltWorkspaceRepo();
821854
return await repo.createQueryBuilder("pws").where("pws.buildWorkspaceId = :wsid", { wsid }).getOne();

components/gitpod-db/src/workspace-db.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ export interface PrebuildWithWorkspace {
6363
workspace: Workspace;
6464
}
6565

66+
export interface PrebuildWithWorkspaceAndInstances {
67+
prebuild: PrebuiltWorkspace;
68+
workspace: Workspace;
69+
instances: WorkspaceInstance[];
70+
}
71+
6672
export type WorkspaceAndOwner = Pick<Workspace, "id" | "ownerId">;
6773
export type WorkspaceOwnerAndSoftDeleted = Pick<Workspace, "id" | "ownerId" | "softDeleted">;
6874

@@ -170,6 +176,10 @@ export interface WorkspaceDB {
170176

171177
storePrebuiltWorkspace(pws: PrebuiltWorkspace): Promise<PrebuiltWorkspace>;
172178
findPrebuiltWorkspaceByCommit(cloneURL: string, commit: string): Promise<PrebuiltWorkspace | undefined>;
179+
findActivePrebuiltWorkspacesByBranch(
180+
projectId: string,
181+
branch: string,
182+
): Promise<PrebuildWithWorkspaceAndInstances[]>;
173183
findPrebuildsWithWorkpace(cloneURL: string): Promise<PrebuildWithWorkspace[]>;
174184
findPrebuildByWorkspaceID(wsid: string): Promise<PrebuiltWorkspace | undefined>;
175185
findPrebuildByID(pwsid: string): Promise<PrebuiltWorkspace | undefined>;

components/gitpod-protocol/src/teams-projects-protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ProjectConfig {
1515
export interface ProjectSettings {
1616
useIncrementalPrebuilds?: boolean;
1717
usePersistentVolumeClaim?: boolean;
18+
keepOutdatedPrebuildsRunning?: boolean;
1819
}
1920

2021
export interface Project {

components/gitpod-protocol/src/util/logging.ts

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

77
const inspect: (object: any) => string = require("util").inspect; // undefined in frontend
88

9+
let plainLogging: boolean = false; // set to true during development to get non JSON output
910
let jsonLogging: boolean = false;
1011
let component: string | undefined;
1112
let version: string | undefined;
@@ -309,7 +310,7 @@ function makeLogItem(
309310
}
310311

311312
const payload: any = payloadArgs.length == 0 ? undefined : payloadArgs.length == 1 ? payloadArgs[0] : payloadArgs;
312-
const logItem: any = {
313+
const logItem = {
313314
// undefined fields get eliminated in JSON.stringify()
314315
...reportedErrorEvent,
315316
component,
@@ -321,11 +322,19 @@ function makeLogItem(
321322
payload,
322323
loggedViaConsole: calledViaConsole ? true : undefined,
323324
};
325+
if (plainLogging) {
326+
return `[${logItem.severity}] [${logItem.component}] ${logItem.message}
327+
${JSON.stringify(payload || "", undefined, " ")}
328+
${error || ""}
329+
`.trim();
330+
}
324331
let result: string = stringifyLogItem(logItem);
325332

326333
if (result.length > maxAllowedLogItemLength && payload !== undefined) {
327334
delete logItem.payload;
328-
logItem.payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`;
335+
(<any>(
336+
logItem
337+
)).payloadStub = `Payload stripped as log item was longer than ${maxAllowedLogItemLength} characters`;
329338

330339
result = stringifyLogItem(logItem);
331340

components/server/ee/src/prebuilds/gitlab-app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export class GitLabApp {
155155
);
156156

157157
return ws;
158+
} catch (e) {
159+
log.error("error processing GitLab webhook", e, body);
158160
} finally {
159161
span.finish();
160162
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ export class PrebuildManager {
5858
@inject(Config) protected readonly config: Config;
5959
@inject(ProjectsService) protected readonly projectService: ProjectsService;
6060

61+
async abortPrebuildsForBranch(ctx: TraceContext, project: Project, user: User, branch: string): Promise<void> {
62+
const span = TraceContext.startSpan("abortPrebuildsForBranch", ctx);
63+
const prebuilds = await this.workspaceDB
64+
.trace({ span })
65+
.findActivePrebuiltWorkspacesByBranch(project.id, branch);
66+
const results: Promise<any>[] = [];
67+
for (const prebuild of prebuilds) {
68+
for (const instance of prebuild.instances) {
69+
results.push(this.workspaceStarter.stopWorkspaceInstance({ span }, instance.id, instance.region));
70+
}
71+
prebuild.prebuild.state = "aborted";
72+
results.push(this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild.prebuild));
73+
}
74+
await Promise.all(results);
75+
}
76+
6177
async startPrebuild(
6278
ctx: TraceContext,
6379
{ context, project, user, commitInfo }: StartPrebuildParams,
@@ -108,6 +124,13 @@ export class PrebuildManager {
108124
return { prebuildId: existingPB.id, wsid: existingPB.buildWorkspaceId, done: true };
109125
}
110126
}
127+
if (project && context.ref && !project.settings?.keepOutdatedPrebuildsRunning) {
128+
try {
129+
await this.abortPrebuildsForBranch({ span }, project, user, context.ref);
130+
} catch (e) {
131+
console.error("Error aborting prebuilds", e);
132+
}
133+
}
111134

112135
const prebuildContext: StartPrebuildContext = {
113136
title: `Prebuild of "${context.title}"`,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,12 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
591591
(await Promise.all(workspaces.map((workspace) => workspaceDb.findRunningInstance(workspace.id))))
592592
.filter(isDefined)
593593
.forEach((instance) =>
594-
this.internalStopWorkspaceInstance(ctx, instance.id, instance.region, StopWorkspacePolicy.IMMEDIATELY),
594+
this.workspaceStarter.stopWorkspaceInstance(
595+
ctx,
596+
instance.id,
597+
instance.region,
598+
StopWorkspacePolicy.IMMEDIATELY,
599+
),
595600
);
596601

597602
// For some reason, returning the result of `this.userDB.storeUser(target)` does not work. The response never arrives the caller.

0 commit comments

Comments
 (0)