Skip to content

Commit 0736b3b

Browse files
jankeromnesroboquat
authored andcommitted
[dashboard][server] Make Project Overview page faster by pre-fetching and caching Git provider data (branches)
1 parent 17b49d0 commit 0736b3b

File tree

10 files changed

+128
-6
lines changed

10 files changed

+128
-6
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ export interface ProjectDB {
2121
getProjectEnvironmentVariableById(variableId: string): Promise<ProjectEnvVar | undefined>;
2222
deleteProjectEnvironmentVariable(variableId: string): Promise<void>;
2323
getProjectEnvironmentVariableValues(envVars: ProjectEnvVar[]): Promise<ProjectEnvVarWithValue[]>;
24+
findCachedProjectOverview(projectId: string): Promise<Project.Overview | undefined>;
25+
storeCachedProjectOverview(projectId: string, overview: Project.Overview): Promise<void>;
2426
}

components/gitpod-db/src/tables.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ export class GitpodTableDescriptionProvider implements TableDescriptionProvider
251251
deletionColumn: 'deleted',
252252
timeColumn: '_lastModified',
253253
},
254+
{
255+
name: 'd_b_project_info',
256+
primaryKeys: ['projectId'],
257+
deletionColumn: 'deleted',
258+
timeColumn: '_lastModified',
259+
},
254260
/**
255261
* BEWARE
256262
*

components/gitpod-db/src/typeorm/deleted-entry-gc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const tables: TableWithDeletion[] = [
5959
{ deletionColumn: "deleted", name: "d_b_prebuild_info" },
6060
{ deletionColumn: "deleted", name: "d_b_oss_allow_list" },
6161
{ deletionColumn: "deleted", name: "d_b_project_env_var" },
62+
{ deletionColumn: "deleted", name: "d_b_project_info" },
6263
];
6364

6465
interface TableWithDeletion {

components/gitpod-db/src/typeorm/entity/db-prebuild-info-entry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { PrebuildInfo } from "@gitpod/gitpod-protocol";
1010
import { TypeORM } from "../../typeorm/typeorm";
1111

1212
@Entity()
13-
export class DBPrebuildInfo {
13+
export class DBPrebuildInfo {
1414

1515
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
1616
prebuildId: string;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
import { Project } from "@gitpod/gitpod-protocol";
9+
10+
import { TypeORM } from "../../typeorm/typeorm";
11+
12+
@Entity()
13+
// on DB but not Typeorm: @Index("ind_dbsync", ["_lastModified"]) // DBSync
14+
export class DBProjectInfo {
15+
16+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
17+
projectId: string;
18+
19+
@Column({
20+
type: 'simple-json',
21+
transformer: (() => {
22+
return {
23+
to(value: any): any {
24+
return JSON.stringify(value);
25+
},
26+
from(value: any): any {
27+
try {
28+
const obj = JSON.parse(value);
29+
if (Project.Overview.is(obj)) {
30+
return obj;
31+
}
32+
} catch (error) {
33+
}
34+
}
35+
};
36+
})()
37+
})
38+
overview: Project.Overview;
39+
40+
// This column triggers the db-sync deletion mechanism. It's not intended for public consumption.
41+
@Column()
42+
deleted: boolean;
43+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 ProjectInfo1642497869312 implements MigrationInterface {
10+
11+
public async up(queryRunner: QueryRunner): Promise<void> {
12+
await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_project_info` ( `projectId` char(36) NOT NULL, `overview` longtext 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+
public async down(queryRunner: QueryRunner): Promise<void> {
16+
}
17+
18+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryp
1313
import { ProjectDB } from "../project-db";
1414
import { DBProject } from "./entity/db-project";
1515
import { DBProjectEnvVar } from "./entity/db-project-env-vars";
16+
import { DBProjectInfo } from "./entity/db-project-info";
1617

1718
function toProjectEnvVar(envVarWithValue: ProjectEnvVarWithValue): ProjectEnvVar {
1819
const envVar = { ...envVarWithValue };
@@ -37,6 +38,10 @@ export class ProjectDBImpl implements ProjectDB {
3738
return (await this.getEntityManager()).getRepository<DBProjectEnvVar>(DBProjectEnvVar);
3839
}
3940

41+
protected async getProjectInfoRepo(): Promise<Repository<DBProjectInfo>> {
42+
return (await this.getEntityManager()).getRepository<DBProjectInfo>(DBProjectInfo);
43+
}
44+
4045
public async findProjectById(projectId: string): Promise<Project | undefined> {
4146
const repo = await this.getRepo();
4247
return repo.findOne({ id: projectId, markedDeleted: false });
@@ -106,6 +111,12 @@ export class ProjectDBImpl implements ProjectDB {
106111
project.markedDeleted = true;
107112
await repo.save(project);
108113
}
114+
// Delete any additional cached infos about this project
115+
const projectInfoRepo = await this.getProjectInfoRepo();
116+
const info = await projectInfoRepo.findOne({ projectId, deleted: false });
117+
if (info) {
118+
await projectInfoRepo.update(projectId, { deleted: true });
119+
}
109120
}
110121

111122
public async setProjectEnvironmentVariable(projectId: string, name: string, value: string, censored: boolean): Promise<void> {
@@ -164,4 +175,19 @@ export class ProjectDBImpl implements ProjectDB {
164175
const envVarsWithValues = await envVarRepo.findByIds(envVars);
165176
return envVarsWithValues;
166177
}
178+
179+
public async findCachedProjectOverview(projectId: string): Promise<Project.Overview | undefined> {
180+
const projectInfoRepo = await this.getProjectInfoRepo();
181+
const info = await projectInfoRepo.findOne({ projectId });
182+
return info?.overview;
183+
}
184+
185+
public async storeCachedProjectOverview(projectId: string, overview: Project.Overview): Promise<void> {
186+
const projectInfoRepo = await this.getProjectInfoRepo();
187+
await projectInfoRepo.save({
188+
projectId,
189+
overview,
190+
creationTime: new Date().toISOString(),
191+
});
192+
}
167193
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ export namespace Project {
4242
}
4343

4444
export interface Overview {
45-
branches: BranchDetails[]
45+
branches: BranchDetails[];
46+
}
47+
48+
export namespace Overview {
49+
export function is(data?: any): data is Project.Overview {
50+
return Array.isArray(data?.branches);
51+
}
4652
}
4753

4854
export interface BranchDetails {

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,25 @@ export class ProjectsService {
3737
return this.projectDB.findProjectsByCloneUrls(cloneUrls);
3838
}
3939

40-
async getProjectOverview(user: User, project: Project): Promise<Project.Overview | undefined> {
41-
const branches = await this.getBranchDetails(user, project);
42-
return { branches };
40+
async getProjectOverviewCached(user: User, project: Project): Promise<Project.Overview | undefined> {
41+
// Check for a cached project overview (fast!)
42+
const cachedPromise = this.projectDB.findCachedProjectOverview(project.id);
43+
44+
// ...but also refresh the cache on every request (asynchronously / in the background)
45+
const refreshPromise = this.getBranchDetails(user, project).then(branches => {
46+
const overview = { branches };
47+
// No need to await here
48+
this.projectDB.storeCachedProjectOverview(project.id, overview).catch(error => {
49+
log.error(`Could not store cached project overview: ${error}`, { cloneUrl: project.cloneUrl })
50+
});
51+
return overview;
52+
});
53+
54+
const cachedOverview = await cachedPromise;
55+
if (cachedOverview) {
56+
return cachedOverview;
57+
}
58+
return await refreshPromise;
4359
}
4460

4561
protected getRepositoryProvider(project: Project) {
@@ -113,6 +129,10 @@ export class ProjectsService {
113129
}
114130

115131
protected async onDidCreateProject(project: Project, installer: User) {
132+
// Pre-fetch project details in the background -- don't await
133+
this.getProjectOverviewCached(installer, project);
134+
135+
// Install the prebuilds webhook if possible
116136
let { userId, teamId, cloneUrl } = project;
117137
const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl);
118138
const hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1677,7 +1677,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
16771677
}
16781678
await this.guardProjectOperation(user, projectId, "get");
16791679
try {
1680-
return await this.projectsService.getProjectOverview(user, project);
1680+
return await this.projectsService.getProjectOverviewCached(user, project);
16811681
} catch (error) {
16821682
if (UnauthorizedError.is(error)) {
16831683
throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);

0 commit comments

Comments
 (0)