Skip to content

Commit

Permalink
Stop running prebuilds for inactive projects (10+ weeks)
Browse files Browse the repository at this point in the history
Fixes #8911
  • Loading branch information
jankeromnes committed Apr 11, 2022
1 parent 0d7a2c9 commit cd00c1b
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 12 deletions.
6 changes: 6 additions & 0 deletions components/gitpod-db/src/project-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/

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

export const ProjectDB = Symbol("ProjectDB");
export interface ProjectDB {
Expand All @@ -30,4 +32,8 @@ export interface ProjectDB {
getProjectEnvironmentVariableValues(envVars: ProjectEnvVar[]): Promise<ProjectEnvVarWithValue[]>;
findCachedProjectOverview(projectId: string): Promise<Project.Overview | undefined>;
storeCachedProjectOverview(projectId: string, overview: Project.Overview): Promise<void>;
getProjectUsage(
projectId: string,
): Promise<{ lastWebhookReceived: string; lastWorkspaceStart: string } | undefined>;
updateProjectUsage(projectId: string, usage: DeepPartial<DBProjectUsage>): Promise<void>;
}
26 changes: 26 additions & 0 deletions components/gitpod-db/src/typeorm/entity/db-project-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
* Licensed under the Gitpod Enterprise Source Code License,
* See License.enterprise.txt in the project root folder.
*/

import { Entity, Column, PrimaryColumn } from "typeorm";

import { TypeORM } from "../../typeorm/typeorm";

@Entity()
// on DB but not Typeorm: @Index("ind_dbsync", ["_lastModified"]) // DBSync
export class DBProjectUsage {
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
projectId: string;

@Column("varchar")
lastWebhookReceived: string;

@Column("varchar")
lastWorkspaceStart: string;

// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
@Column()
deleted: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License-AGPL.txt in the project root for license information.
*/

import { MigrationInterface, QueryRunner } from "typeorm";

export class ProjectUsage1649667202321 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"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;",
);
}

public async down(queryRunner: QueryRunner): Promise<void> {}
}
33 changes: 32 additions & 1 deletion components/gitpod-db/src/typeorm/project-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

import { inject, injectable } from "inversify";
import { TypeORM } from "./typeorm";
import { Repository } from "typeorm";
import { DeepPartial, Repository } from "typeorm";
import { v4 as uuidv4 } from "uuid";
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue } from "@gitpod/gitpod-protocol";
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
import { ProjectDB } from "../project-db";
import { DBProject } from "./entity/db-project";
import { DBProjectEnvVar } from "./entity/db-project-env-vars";
import { DBProjectInfo } from "./entity/db-project-info";
import { DBProjectUsage } from "./entity/db-project-usage";

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

protected async getProjectUsageRepo(): Promise<Repository<DBProjectUsage>> {
return (await this.getEntityManager()).getRepository<DBProjectUsage>(DBProjectUsage);
}

public async findProjectById(projectId: string): Promise<Project | undefined> {
const repo = await this.getRepo();
return repo.findOne({ id: projectId, markedDeleted: false });
Expand Down Expand Up @@ -146,6 +151,11 @@ export class ProjectDBImpl implements ProjectDB {
if (info) {
await projectInfoRepo.update(projectId, { deleted: true });
}
const projectUsageRepo = await this.getProjectUsageRepo();
const usage = await projectUsageRepo.findOne({ projectId, deleted: false });
if (usage) {
await projectUsageRepo.update(projectId, { deleted: true });
}
}

public async setProjectEnvironmentVariable(
Expand Down Expand Up @@ -229,4 +239,25 @@ export class ProjectDBImpl implements ProjectDB {
creationTime: new Date().toISOString(),
});
}

public async getProjectUsage(
projectId: string,
): Promise<{ lastWebhookReceived: string; lastWorkspaceStart: string } | undefined> {
const projectUsageRepo = await this.getProjectUsageRepo();
const usage = await projectUsageRepo.findOne({ projectId });
if (usage) {
return {
lastWebhookReceived: usage.lastWebhookReceived,
lastWorkspaceStart: usage.lastWorkspaceStart,
};
}
}

public async updateProjectUsage(projectId: string, usage: DeepPartial<DBProjectUsage>): Promise<void> {
const projectUsageRepo = await this.getProjectUsageRepo();
await projectUsageRepo.save({
projectId,
usage,
});
}
}
10 changes: 8 additions & 2 deletions components/server/ee/src/prebuilds/bitbucket-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export class BitbucketApp {
): Promise<StartPrebuildResult | undefined> {
const span = TraceContext.startSpan("Bitbucket.handlePushHook", ctx);
try {
const projectAndOwner = await this.findProjectAndOwner(data.gitCloneUrl, user);
if (projectAndOwner.project) {
/* no await */ this.projectDB.updateProjectUsage(projectAndOwner.project.id, {
lastWebhookReceived: new Date().toISOString(),
});
}

const contextURL = this.createContextUrl(data);
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
span.setTag("contextURL", contextURL);
Expand All @@ -116,11 +123,10 @@ export class BitbucketApp {
data.commitHash,
);
}
const projectAndOwner = await this.findProjectAndOwner(data.gitCloneUrl, user);
// todo@alex: add branch and project args
const ws = await this.prebuildManager.startPrebuild(
{ span },
{ user, project: projectAndOwner?.project, context, commitInfo },
{ user, project: projectAndOwner.project, context, commitInfo },
);
return ws;
} finally {
Expand Down
12 changes: 11 additions & 1 deletion components/server/ee/src/prebuilds/github-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,13 @@ export class GithubApp {
const installationId = ctx.payload.installation?.id;
const cloneURL = ctx.payload.repository.clone_url;
let { user, project } = await this.findOwnerAndProject(installationId, cloneURL);
const logCtx: LogContext = { userId: user.id };
if (project) {
/* no await */ this.projectDB.updateProjectUsage(project.id, {
lastWebhookReceived: new Date().toISOString(),
});
}

const logCtx: LogContext = { userId: user.id };
if (!!user.blocked) {
log.info(logCtx, `Blocked user tried to start prebuild`, { repo: ctx.payload.repository });
return;
Expand Down Expand Up @@ -347,6 +352,11 @@ export class GithubApp {
const pr = ctx.payload.pull_request;
const contextURL = pr.html_url;
let { user, project } = await this.findOwnerAndProject(installationId, cloneURL);
if (project) {
/* no await */ this.projectDB.updateProjectUsage(project.id, {
lastWebhookReceived: new Date().toISOString(),
});
}

const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
const config = await this.prebuildManager.fetchConfig({ span }, user, context);
Expand Down
12 changes: 9 additions & 3 deletions components/server/ee/src/prebuilds/github-enterprise-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ export class GitHubEnterpriseApp {
): Promise<StartPrebuildResult | undefined> {
const span = TraceContext.startSpan("GitHubEnterpriseApp.handlePushHook", ctx);
try {
const cloneURL = payload.repository.clone_url;
const projectAndOwner = await this.findProjectAndOwner(cloneURL, user);
if (projectAndOwner.project) {
/* no await */ this.projectDB.updateProjectUsage(projectAndOwner.project.id, {
lastWebhookReceived: new Date().toISOString(),
});
}

const contextURL = this.createContextUrl(payload);
span.setTag("contextURL", contextURL);
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
Expand All @@ -127,15 +135,13 @@ export class GitHubEnterpriseApp {

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

const cloneURL = payload.repository.clone_url;
const projectAndOwner = await this.findProjectAndOwner(cloneURL, user);
const commitInfo = await this.getCommitInfo(user, payload.repository.url, payload.after);
const ws = await this.prebuildManager.startPrebuild(
{ span },
{
context,
user: projectAndOwner.user,
project: projectAndOwner?.project,
project: projectAndOwner.project,
commitInfo,
},
);
Expand Down
10 changes: 8 additions & 2 deletions components/server/ee/src/prebuilds/gitlab-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ export class GitLabApp {
span.setTag("contextURL", contextURL);
const context = (await this.contextParser.handle({ span }, user, contextURL)) as CommitContext;
const projectAndOwner = await this.findProjectAndOwner(context.repository.cloneUrl, user);
if (projectAndOwner.project) {
/* no await */ this.projectDB.updateProjectUsage(projectAndOwner.project.id, {
lastWebhookReceived: new Date().toISOString(),
});
}

const config = await this.prebuildManager.fetchConfig({ span }, user, context);
if (!this.prebuildManager.shouldPrebuild(config)) {
log.debug({ userId: user.id }, "GitLab push hook: There is no prebuild config.", {
Expand All @@ -123,8 +129,8 @@ export class GitLabApp {
const ws = await this.prebuildManager.startPrebuild(
{ span },
{
user: projectAndOwner?.user || user,
project: projectAndOwner?.project,
user: projectAndOwner.user || user,
project: projectAndOwner.project,
context,
commitInfo,
},
Expand Down
26 changes: 23 additions & 3 deletions components/server/ee/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,9 @@ export class PrebuildManager {
throw new Error(`Failed to create a prebuild for: ${context.normalizedContextURL}`);
}

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

await this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild);
span.setTag("starting", false);
Expand All @@ -184,9 +183,20 @@ export class PrebuildManager {
prebuildId: prebuild.id,
done: false,
};
};

if (await this.shouldRateLimitPrebuild(span, cloneURL)) {
return await cancelPrebuild(
"Prebuild is rate limited. Please contact Gitpod if you believe this happened in error.",
);
}

if (project) {
if (await this.shouldSkipInactiveProject(project)) {
return await cancelPrebuild(
"Project is inactive. Please start a new workspace for this project to re-enable prebuilds.",
);
}
let aCommitInfo = commitInfo;
if (!aCommitInfo) {
aCommitInfo = await getCommitInfo(
Expand Down Expand Up @@ -347,4 +357,14 @@ export class PrebuildManager {
// Last resort default
return PREBUILD_LIMITER_DEFAULT_LIMIT;
}

private async shouldSkipInactiveProject(project: Project): Promise<boolean> {
const usage = await this.projectService.getProjectUsage(project.id);
const lastWorkspaceStart = usage?.lastWorkspaceStart;
const inactiveProjectTime = 1000 * 60 * 60 * 24 * 7 * 10; // 10 weeks
if (!!lastWorkspaceStart && Date.now() - new Date(lastWorkspaceStart).getTime() > inactiveProjectTime) {
return true;
}
return false;
}
}
6 changes: 6 additions & 0 deletions components/server/ee/src/workspace/workspace-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ export class WorkspaceFactoryEE extends WorkspaceFactory {
projectId = project.id;
}
}
// bump project usage timestamp
if (projectId) {
/* no await */ this.projectDB.updateProjectUsage(projectId, {
lastWorkspaceStart: new Date().toISOString(),
});
}

const id = await this.generateWorkspaceID(context);
const newWs: Workspace = {
Expand Down
6 changes: 6 additions & 0 deletions components/server/src/projects/projects-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,10 @@ export class ProjectsService {
async deleteProjectEnvironmentVariable(variableId: string): Promise<void> {
return this.projectDB.deleteProjectEnvironmentVariable(variableId);
}

async getProjectUsage(
projectId: string,
): Promise<{ lastWebhookReceived: string; lastWorkspaceStart: string } | undefined> {
return this.projectDB.getProjectUsage(projectId);
}
}

0 comments on commit cd00c1b

Please sign in to comment.