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 6b8ebad commit 93a960c
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 15 deletions.
4 changes: 3 additions & 1 deletion components/gitpod-db/src/project-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* See License.enterprise.txt in the project root folder.
*/

import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue } from "@gitpod/gitpod-protocol";
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue, ProjectUsage } from "@gitpod/gitpod-protocol";

export const ProjectDB = Symbol("ProjectDB");
export interface ProjectDB {
Expand All @@ -30,4 +30,6 @@ 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<ProjectUsage | undefined>;
updateProjectUsage(projectId: string, usage: Partial<ProjectUsage>): Promise<void>;
}
6 changes: 6 additions & 0 deletions components/gitpod-db/src/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ export class GitpodTableDescriptionProvider implements TableDescriptionProvider
deletionColumn: "deleted",
timeColumn: "_lastModified",
},
{
name: "d_b_project_usage",
primaryKeys: ["projectId"],
deletionColumn: "deleted",
timeColumn: "_lastModified",
},
/**
* BEWARE
*
Expand Down
1 change: 1 addition & 0 deletions components/gitpod-db/src/typeorm/deleted-entry-gc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const tables: TableWithDeletion[] = [
{ deletionColumn: "deleted", name: "d_b_oss_allow_list" },
{ deletionColumn: "deleted", name: "d_b_project_env_var" },
{ deletionColumn: "deleted", name: "d_b_project_info" },
{ deletionColumn: "deleted", name: "d_b_project_usage" },
];

interface TableWithDeletion {
Expand Down
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) 2022 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 DEFAULT '', `lastWorkspaceStart` varchar(255) NOT NULL DEFAULT '', `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> {}
}
31 changes: 30 additions & 1 deletion components/gitpod-db/src/typeorm/project-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { inject, injectable } from "inversify";
import { TypeORM } from "./typeorm";
import { Repository } from "typeorm";
import { v4 as uuidv4 } from "uuid";
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue } from "@gitpod/gitpod-protocol";
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue, ProjectUsage } 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,23 @@ export class ProjectDBImpl implements ProjectDB {
creationTime: new Date().toISOString(),
});
}

public async getProjectUsage(projectId: string): Promise<ProjectUsage | 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: Partial<ProjectUsage>): Promise<void> {
const projectUsageRepo = await this.getProjectUsageRepo();
await projectUsageRepo.save({
projectId,
...usage,
});
}
}
5 changes: 5 additions & 0 deletions components/gitpod-protocol/src/teams-projects-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export namespace Project {

export type PartialProject = DeepPartial<Project> & Pick<Project, "id">;

export interface ProjectUsage {
lastWebhookReceived: string;
lastWorkspaceStart: string;
}

export interface PrebuildWithStatus {
info: PrebuildInfo;
status: PrebuiltWorkspaceState;
Expand Down
11 changes: 9 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,14 @@ 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) {
/* tslint:disable-next-line */
/** 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 +124,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
14 changes: 13 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,14 @@ 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) {
/* tslint:disable-next-line */
/** 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 +353,12 @@ export class GithubApp {
const pr = ctx.payload.pull_request;
const contextURL = pr.html_url;
let { user, project } = await this.findOwnerAndProject(installationId, cloneURL);
if (project) {
/* tslint:disable-next-line */
/** 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
13 changes: 10 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,15 @@ 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) {
/* tslint:disable-next-line */
/** 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 +136,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
11 changes: 9 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,13 @@ 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) {
/* tslint:disable-next-line */
/** 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 +130,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
27 changes: 24 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,15 @@ 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);
if (!usage?.lastWorkspaceStart) {
return false;
}
const now = Date.now();
const lastUse = new Date(usage.lastWorkspaceStart).getTime();
const inactiveProjectTime = 1000 * 60 * 60 * 24 * 7 * 10; // 10 weeks
return now - lastUse > inactiveProjectTime;
}
}
6 changes: 5 additions & 1 deletion components/server/src/projects/projects-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { HostContextProvider } from "../auth/host-context-provider";
import { RepoURL } from "../repohost";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
import { PartialProject } from "@gitpod/gitpod-protocol/src/teams-projects-protocol";
import { PartialProject, ProjectUsage } from "@gitpod/gitpod-protocol/src/teams-projects-protocol";

@injectable()
export class ProjectsService {
Expand Down Expand Up @@ -248,4 +248,8 @@ export class ProjectsService {
async deleteProjectEnvironmentVariable(variableId: string): Promise<void> {
return this.projectDB.deleteProjectEnvironmentVariable(variableId);
}

async getProjectUsage(projectId: string): Promise<ProjectUsage | undefined> {
return this.projectDB.getProjectUsage(projectId);
}
}
2 changes: 1 addition & 1 deletion components/server/src/user/user-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ export class UserController {
await this.userService.updateUserEnvVarsOnLogin(user, envVars);
await this.userService.acceptCurrentTerms(user);

/* no await */ trackSignup(user, req, this.analytics).catch((err) =>
/** no await */ trackSignup(user, req, this.analytics).catch((err) =>
log.warn({ userId: user.id }, "trackSignup", err),
);

Expand Down
7 changes: 7 additions & 0 deletions components/server/src/workspace/workspace-starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ export class WorkspaceStarter {
const span = TraceContext.startSpan("WorkspaceStarter.startWorkspace", ctx);
span.setTag("workspaceId", workspace.id);

if (workspace.projectId && workspace.type === "regular") {
/* tslint:disable-next-line */
/** no await */ this.projectDB.updateProjectUsage(workspace.projectId, {
lastWorkspaceStart: new Date().toISOString(),
});
}

options = options || {};
try {
// Some workspaces do not have an image source.
Expand Down

0 comments on commit 93a960c

Please sign in to comment.