Skip to content

Commit cd00c1b

Browse files
committed
Stop running prebuilds for inactive projects (10+ weeks)
Fixes #8911
1 parent 0d7a2c9 commit cd00c1b

File tree

11 files changed

+152
-12
lines changed

11 files changed

+152
-12
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66

77
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue } from "@gitpod/gitpod-protocol";
8+
import { DeepPartial } from "typeorm";
9+
import { DBProjectUsage } from "./typeorm/entity/db-project-usage";
810

911
export const ProjectDB = Symbol("ProjectDB");
1012
export interface ProjectDB {
@@ -30,4 +32,8 @@ export interface ProjectDB {
3032
getProjectEnvironmentVariableValues(envVars: ProjectEnvVar[]): Promise<ProjectEnvVarWithValue[]>;
3133
findCachedProjectOverview(projectId: string): Promise<Project.Overview | undefined>;
3234
storeCachedProjectOverview(projectId: string, overview: Project.Overview): Promise<void>;
35+
getProjectUsage(
36+
projectId: string,
37+
): Promise<{ lastWebhookReceived: string; lastWorkspaceStart: string } | undefined>;
38+
updateProjectUsage(projectId: string, usage: DeepPartial<DBProjectUsage>): Promise<void>;
3339
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the Gitpod Enterprise Source Code License,
4+
* See License.enterprise.txt in the project root folder.
5+
*/
6+
7+
import { Entity, Column, PrimaryColumn } from "typeorm";
8+
9+
import { TypeORM } from "../../typeorm/typeorm";
10+
11+
@Entity()
12+
// on DB but not Typeorm: @Index("ind_dbsync", ["_lastModified"]) // DBSync
13+
export class DBProjectUsage {
14+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
15+
projectId: string;
16+
17+
@Column("varchar")
18+
lastWebhookReceived: string;
19+
20+
@Column("varchar")
21+
lastWorkspaceStart: string;
22+
23+
// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
24+
@Column()
25+
deleted: boolean;
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 { MigrationInterface, QueryRunner } from "typeorm";
8+
9+
export class ProjectUsage1649667202321 implements MigrationInterface {
10+
public async up(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(
12+
"CREATE TABLE IF NOT EXISTS `d_b_project_usage` ( `projectId` char(36) NOT NULL, `lastWebhookReceived` varchar(255) NOT NULL, `lastWorkspaceStart` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`projectId`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
13+
);
14+
}
15+
16+
public async down(queryRunner: QueryRunner): Promise<void> {}
17+
}

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66

77
import { inject, injectable } from "inversify";
88
import { TypeORM } from "./typeorm";
9-
import { Repository } from "typeorm";
9+
import { DeepPartial, Repository } from "typeorm";
1010
import { v4 as uuidv4 } from "uuid";
1111
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue } from "@gitpod/gitpod-protocol";
1212
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
1313
import { ProjectDB } from "../project-db";
1414
import { DBProject } from "./entity/db-project";
1515
import { DBProjectEnvVar } from "./entity/db-project-env-vars";
1616
import { DBProjectInfo } from "./entity/db-project-info";
17+
import { DBProjectUsage } from "./entity/db-project-usage";
1718

1819
function toProjectEnvVar(envVarWithValue: ProjectEnvVarWithValue): ProjectEnvVar {
1920
const envVar = { ...envVarWithValue };
@@ -42,6 +43,10 @@ export class ProjectDBImpl implements ProjectDB {
4243
return (await this.getEntityManager()).getRepository<DBProjectInfo>(DBProjectInfo);
4344
}
4445

46+
protected async getProjectUsageRepo(): Promise<Repository<DBProjectUsage>> {
47+
return (await this.getEntityManager()).getRepository<DBProjectUsage>(DBProjectUsage);
48+
}
49+
4550
public async findProjectById(projectId: string): Promise<Project | undefined> {
4651
const repo = await this.getRepo();
4752
return repo.findOne({ id: projectId, markedDeleted: false });
@@ -146,6 +151,11 @@ export class ProjectDBImpl implements ProjectDB {
146151
if (info) {
147152
await projectInfoRepo.update(projectId, { deleted: true });
148153
}
154+
const projectUsageRepo = await this.getProjectUsageRepo();
155+
const usage = await projectUsageRepo.findOne({ projectId, deleted: false });
156+
if (usage) {
157+
await projectUsageRepo.update(projectId, { deleted: true });
158+
}
149159
}
150160

151161
public async setProjectEnvironmentVariable(
@@ -229,4 +239,25 @@ export class ProjectDBImpl implements ProjectDB {
229239
creationTime: new Date().toISOString(),
230240
});
231241
}
242+
243+
public async getProjectUsage(
244+
projectId: string,
245+
): Promise<{ lastWebhookReceived: string; lastWorkspaceStart: string } | undefined> {
246+
const projectUsageRepo = await this.getProjectUsageRepo();
247+
const usage = await projectUsageRepo.findOne({ projectId });
248+
if (usage) {
249+
return {
250+
lastWebhookReceived: usage.lastWebhookReceived,
251+
lastWorkspaceStart: usage.lastWorkspaceStart,
252+
};
253+
}
254+
}
255+
256+
public async updateProjectUsage(projectId: string, usage: DeepPartial<DBProjectUsage>): Promise<void> {
257+
const projectUsageRepo = await this.getProjectUsageRepo();
258+
await projectUsageRepo.save({
259+
projectId,
260+
usage,
261+
});
262+
}
232263
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ export class BitbucketApp {
9595
): Promise<StartPrebuildResult | undefined> {
9696
const span = TraceContext.startSpan("Bitbucket.handlePushHook", ctx);
9797
try {
98+
const projectAndOwner = await this.findProjectAndOwner(data.gitCloneUrl, user);
99+
if (projectAndOwner.project) {
100+
/* no await */ this.projectDB.updateProjectUsage(projectAndOwner.project.id, {
101+
lastWebhookReceived: new Date().toISOString(),
102+
});
103+
}
104+
98105
const contextURL = this.createContextUrl(data);
99106
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
100107
span.setTag("contextURL", contextURL);
@@ -116,11 +123,10 @@ export class BitbucketApp {
116123
data.commitHash,
117124
);
118125
}
119-
const projectAndOwner = await this.findProjectAndOwner(data.gitCloneUrl, user);
120126
// todo@alex: add branch and project args
121127
const ws = await this.prebuildManager.startPrebuild(
122128
{ span },
123-
{ user, project: projectAndOwner?.project, context, commitInfo },
129+
{ user, project: projectAndOwner.project, context, commitInfo },
124130
);
125131
return ws;
126132
} finally {

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,13 @@ export class GithubApp {
240240
const installationId = ctx.payload.installation?.id;
241241
const cloneURL = ctx.payload.repository.clone_url;
242242
let { user, project } = await this.findOwnerAndProject(installationId, cloneURL);
243-
const logCtx: LogContext = { userId: user.id };
243+
if (project) {
244+
/* no await */ this.projectDB.updateProjectUsage(project.id, {
245+
lastWebhookReceived: new Date().toISOString(),
246+
});
247+
}
244248

249+
const logCtx: LogContext = { userId: user.id };
245250
if (!!user.blocked) {
246251
log.info(logCtx, `Blocked user tried to start prebuild`, { repo: ctx.payload.repository });
247252
return;
@@ -347,6 +352,11 @@ export class GithubApp {
347352
const pr = ctx.payload.pull_request;
348353
const contextURL = pr.html_url;
349354
let { user, project } = await this.findOwnerAndProject(installationId, cloneURL);
355+
if (project) {
356+
/* no await */ this.projectDB.updateProjectUsage(project.id, {
357+
lastWebhookReceived: new Date().toISOString(),
358+
});
359+
}
350360

351361
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
352362
const config = await this.prebuildManager.fetchConfig({ span }, user, context);

components/server/ee/src/prebuilds/github-enterprise-app.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ export class GitHubEnterpriseApp {
116116
): Promise<StartPrebuildResult | undefined> {
117117
const span = TraceContext.startSpan("GitHubEnterpriseApp.handlePushHook", ctx);
118118
try {
119+
const cloneURL = payload.repository.clone_url;
120+
const projectAndOwner = await this.findProjectAndOwner(cloneURL, user);
121+
if (projectAndOwner.project) {
122+
/* no await */ this.projectDB.updateProjectUsage(projectAndOwner.project.id, {
123+
lastWebhookReceived: new Date().toISOString(),
124+
});
125+
}
126+
119127
const contextURL = this.createContextUrl(payload);
120128
span.setTag("contextURL", contextURL);
121129
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
@@ -127,15 +135,13 @@ export class GitHubEnterpriseApp {
127135

128136
log.debug("GitHub Enterprise push event: Starting prebuild.", { contextURL });
129137

130-
const cloneURL = payload.repository.clone_url;
131-
const projectAndOwner = await this.findProjectAndOwner(cloneURL, user);
132138
const commitInfo = await this.getCommitInfo(user, payload.repository.url, payload.after);
133139
const ws = await this.prebuildManager.startPrebuild(
134140
{ span },
135141
{
136142
context,
137143
user: projectAndOwner.user,
138-
project: projectAndOwner?.project,
144+
project: projectAndOwner.project,
139145
commitInfo,
140146
},
141147
);

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export class GitLabApp {
108108
span.setTag("contextURL", contextURL);
109109
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
110110
const projectAndOwner = await this.findProjectAndOwner(context.repository.cloneUrl, user);
111+
if (projectAndOwner.project) {
112+
/* no await */ this.projectDB.updateProjectUsage(projectAndOwner.project.id, {
113+
lastWebhookReceived: new Date().toISOString(),
114+
});
115+
}
116+
111117
const config = await this.prebuildManager.fetchConfig({ span }, user, context);
112118
if (!this.prebuildManager.shouldPrebuild(config)) {
113119
log.debug({ userId: user.id }, "GitLab push hook: There is no prebuild config.", {
@@ -123,8 +129,8 @@ export class GitLabApp {
123129
const ws = await this.prebuildManager.startPrebuild(
124130
{ span },
125131
{
126-
user: projectAndOwner?.user || user,
127-
project: projectAndOwner?.project,
132+
user: projectAndOwner.user || user,
133+
project: projectAndOwner.project,
128134
context,
129135
commitInfo,
130136
},

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,9 @@ export class PrebuildManager {
171171
throw new Error(`Failed to create a prebuild for: ${context.normalizedContextURL}`);
172172
}
173173

174-
if (await this.shouldRateLimitPrebuild(span, cloneURL)) {
174+
const cancelPrebuild = async (message: string) => {
175175
prebuild.state = "aborted";
176-
prebuild.error =
177-
"Prebuild is rate limited. Please contact Gitpod if you believe this happened in error.";
176+
prebuild.error = message;
178177

179178
await this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild);
180179
span.setTag("starting", false);
@@ -184,9 +183,20 @@ export class PrebuildManager {
184183
prebuildId: prebuild.id,
185184
done: false,
186185
};
186+
};
187+
188+
if (await this.shouldRateLimitPrebuild(span, cloneURL)) {
189+
return await cancelPrebuild(
190+
"Prebuild is rate limited. Please contact Gitpod if you believe this happened in error.",
191+
);
187192
}
188193

189194
if (project) {
195+
if (await this.shouldSkipInactiveProject(project)) {
196+
return await cancelPrebuild(
197+
"Project is inactive. Please start a new workspace for this project to re-enable prebuilds.",
198+
);
199+
}
190200
let aCommitInfo = commitInfo;
191201
if (!aCommitInfo) {
192202
aCommitInfo = await getCommitInfo(
@@ -347,4 +357,14 @@ export class PrebuildManager {
347357
// Last resort default
348358
return PREBUILD_LIMITER_DEFAULT_LIMIT;
349359
}
360+
361+
private async shouldSkipInactiveProject(project: Project): Promise<boolean> {
362+
const usage = await this.projectService.getProjectUsage(project.id);
363+
const lastWorkspaceStart = usage?.lastWorkspaceStart;
364+
const inactiveProjectTime = 1000 * 60 * 60 * 24 * 7 * 10; // 10 weeks
365+
if (!!lastWorkspaceStart && Date.now() - new Date(lastWorkspaceStart).getTime() > inactiveProjectTime) {
366+
return true;
367+
}
368+
return false;
369+
}
350370
}

components/server/ee/src/workspace/workspace-factory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ export class WorkspaceFactoryEE extends WorkspaceFactory {
310310
projectId = project.id;
311311
}
312312
}
313+
// bump project usage timestamp
314+
if (projectId) {
315+
/* no await */ this.projectDB.updateProjectUsage(projectId, {
316+
lastWorkspaceStart: new Date().toISOString(),
317+
});
318+
}
313319

314320
const id = await this.generateWorkspaceID(context);
315321
const newWs: Workspace = {

components/server/src/projects/projects-service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,10 @@ export class ProjectsService {
248248
async deleteProjectEnvironmentVariable(variableId: string): Promise<void> {
249249
return this.projectDB.deleteProjectEnvironmentVariable(variableId);
250250
}
251+
252+
async getProjectUsage(
253+
projectId: string,
254+
): Promise<{ lastWebhookReceived: string; lastWorkspaceStart: string } | undefined> {
255+
return this.projectDB.getProjectUsage(projectId);
256+
}
251257
}

0 commit comments

Comments
 (0)