Skip to content

Commit e7a47d9

Browse files
[prebuilds, workspace-gc] Fix and add tests for not showing soft-dele… (#20093)
* [prebuilds, workspace-gc] Fix and add tests for not showing soft-deleted prebuilds anymore * [workspace-gc] Add more explanatory comments * Apply suggestions from code review Co-authored-by: Filip Troníček <[email protected]> --------- Co-authored-by: Filip Troníček <[email protected]>
1 parent 5ebe1cc commit e7a47d9

File tree

4 files changed

+78
-123
lines changed

4 files changed

+78
-123
lines changed

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

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
498498
ws.ownerId AS ownerId
499499
FROM d_b_workspace AS ws
500500
WHERE ws.deleted = 0
501-
AND ws.type= ?
501+
AND ws.type = ?
502502
AND ws.softDeleted IS NULL
503503
AND ws.softDeletedTime = ''
504504
AND ws.pinned = 0
@@ -556,35 +556,6 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
556556
return dbResults as WorkspaceOwnerAndSoftDeleted[];
557557
}
558558

559-
/**
560-
* @deprecated delete me end of June 2024
561-
*/
562-
public async findPrebuiltWorkspacesForGC(daysUnused: number, limit: number): Promise<WorkspaceAndOwner[]> {
563-
const workspaceRepo = await this.getWorkspaceRepo();
564-
const dbResults = await workspaceRepo.query(
565-
`
566-
SELECT ws.id AS id,
567-
ws.ownerId AS ownerId
568-
FROM d_b_workspace AS ws,
569-
d_b_prebuilt_workspace AS pb
570-
LEFT OUTER JOIN d_b_workspace AS usages ON usages.basedOnPrebuildId = pb.id
571-
WHERE
572-
pb.buildworkspaceId = ws.id
573-
AND ws.type = 'prebuild'
574-
AND ws.contentDeletedTime = ''
575-
AND ws.deletionEligibilityTime = ''
576-
AND ws.pinned = 0
577-
AND ws.creationTime < NOW() - INTERVAL ? DAY
578-
GROUP BY ws.id, ws.ownerId
579-
HAVING
580-
max(usages.creationTime) IS NULL or max(usages.creationTime) < NOW() - INTERVAL ? DAY
581-
LIMIT ?;
582-
`,
583-
[daysUnused, daysUnused, limit],
584-
);
585-
return dbResults as WorkspaceAndOwner[];
586-
}
587-
588559
private async doJoinInstanceWithWorkspace<T>(
589560
conditions: string[],
590561
conditionParams: {},
@@ -722,11 +693,22 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
722693

723694
public async findPrebuildByWorkspaceID(wsid: string): Promise<PrebuiltWorkspace | undefined> {
724695
const repo = await this.getPrebuiltWorkspaceRepo();
725-
return await repo.createQueryBuilder("pws").where("pws.buildWorkspaceId = :wsid", { wsid }).getOne();
696+
return await repo
697+
.createQueryBuilder("pws")
698+
.innerJoin(DBWorkspace, "ws", "pws.buildWorkspaceId = ws.id")
699+
.where("pws.buildWorkspaceId = :wsid", { wsid })
700+
.andWhere("ws.contentDeletedTime = ''")
701+
.getOne();
726702
}
703+
727704
public async findPrebuildByID(pwsid: string): Promise<PrebuiltWorkspace | undefined> {
728705
const repo = await this.getPrebuiltWorkspaceRepo();
729-
return await repo.findOne(pwsid);
706+
return await repo
707+
.createQueryBuilder("pws")
708+
.innerJoin(DBWorkspace, "ws", "pws.buildWorkspaceId = ws.id")
709+
.where("pws.id = :pwsid", { pwsid })
710+
.andWhere("ws.contentDeletedTime = ''")
711+
.getOne();
730712
}
731713

732714
public async findPrebuildsWithWorkspace(projectId: string): Promise<PrebuildWithWorkspace[]> {
@@ -1063,6 +1045,7 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
10631045
.innerJoinAndMapOne("pws.project", DBProject, "project", "pws.projectId = project.id")
10641046
.where("project.markedDeleted = false")
10651047
.andWhere("project.id IS NOT NULL")
1048+
.andWhere("ws.contentDeletedTime = ''")
10661049
.skip(pagination.offset)
10671050
.take(pagination.limit)
10681051
.orderBy("pws.creationTime", sort.order); // todo: take sort field into account
@@ -1130,7 +1113,8 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
11301113
.createQueryBuilder("pws")
11311114
.orderBy("pws.creationTime", "DESC")
11321115
.innerJoinAndMapOne("pws.workspace", DBWorkspace, "ws", "pws.buildWorkspaceId = ws.id")
1133-
.andWhere("pws.id = :id", { id });
1116+
.andWhere("pws.id = :id", { id })
1117+
.andWhere("ws.contentDeletedTime = ''");
11341118

11351119
return query.getOne();
11361120
}

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

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -235,42 +235,50 @@ class WorkspaceDBSpec {
235235
}
236236

237237
@test(timeout(10000))
238-
public async testFindPrebuildsForGC_oldPrebuildNoUsage() {
239-
await this.createPrebuild(2);
240-
const dbResult = await this.db.findPrebuiltWorkspacesForGC(1, 10);
241-
expect(dbResult.length).to.eq(1);
242-
expect(dbResult[0].id).to.eq("12345");
243-
expect(dbResult[0].ownerId).to.eq("1221423");
238+
public async testFindEligableWorkspacesForSoftDeletion_markedEligable_Prebuild() {
239+
const { ws } = await this.createPrebuild(20, 15);
240+
const dbResult = await this.db.findEligibleWorkspacesForSoftDeletion(new Date(), 10, "prebuild");
241+
expect(dbResult.length).to.equal(1);
242+
expect(dbResult[0].id).to.eq(ws.id);
243+
expect(dbResult[0].ownerId).to.eq(ws.ownerId);
244244
}
245245

246246
@test(timeout(10000))
247-
public async testFindPrebuildsForGC_newPrebuildNoUsage() {
248-
await this.createPrebuild(0);
249-
const dbResult = await this.db.findPrebuiltWorkspacesForGC(1, 10);
247+
public async testFindEligableWorkspacesForSoftDeletion_notMarkedEligable_Prebuild() {
248+
await this.createPrebuild(20, -7);
249+
const dbResult = await this.db.findEligibleWorkspacesForSoftDeletion(new Date(), 10, "prebuild");
250250
expect(dbResult.length).to.eq(0);
251251
}
252252

253253
@test(timeout(10000))
254-
public async testFindPrebuildsForGC_oldPrebuildOldUsage() {
255-
await this.createPrebuild(2, 2);
256-
const dbResult = await this.db.findPrebuiltWorkspacesForGC(1, 10);
257-
expect(dbResult.length).to.eq(1);
258-
expect(dbResult[0].id).to.eq("12345");
259-
expect(dbResult[0].ownerId).to.eq("1221423");
260-
}
254+
public async testPrebuildGarbageCollection() {
255+
const { pbws } = await this.createPrebuild(20, 15);
256+
257+
// mimick the behavior of the Garbage Collector
258+
const gcWorkspaces = await this.db.findEligibleWorkspacesForSoftDeletion(new Date(), 10, "prebuild");
259+
expect(gcWorkspaces.length).to.equal(1);
260+
261+
const now = new Date().toISOString();
262+
await this.db.updatePartial(gcWorkspaces[0].id, {
263+
contentDeletedTime: now,
264+
softDeletedTime: now,
265+
softDeleted: "gc",
266+
});
261267

262-
@test(timeout(10000))
263-
public async testFindPrebuildsForGC_oldPrebuildNewUsage() {
264-
await this.createPrebuild(12, 0);
265-
const dbResult = await this.db.findPrebuiltWorkspacesForGC(1, 10);
266-
expect(dbResult.length).to.eq(0);
268+
// next cycle is empty
269+
const nextGcCycle = await this.db.findEligibleWorkspacesForSoftDeletion(new Date(), 10, "prebuild");
270+
expect(nextGcCycle.length).to.equal(0);
271+
272+
// prebuild can't be discovered anymore because it's workspace has been GC'ed
273+
const prebuild = await this.db.findPrebuildByID(pbws.id);
274+
expect(prebuild).to.be.undefined;
267275
}
268276

269-
protected async createPrebuild(createdDaysAgo: number, usageDaysAgo?: number) {
277+
protected async createPrebuild(createdDaysAgo: number, deletionEligibilityTimeDaysAgo?: number) {
270278
const now = new Date();
271279
now.setDate(now.getDate() - createdDaysAgo);
272280
const creationTime = now.toISOString();
273-
await this.db.store({
281+
const ws = await this.db.store({
274282
id: "12345",
275283
creationTime,
276284
description: "something",
@@ -283,33 +291,23 @@ class WorkspaceDBSpec {
283291
config: {},
284292
type: "prebuild",
285293
});
286-
await this.db.storePrebuiltWorkspace({
294+
const pbws = await this.db.storePrebuiltWorkspace({
287295
id: "prebuild123",
288296
buildWorkspaceId: "12345",
289297
creationTime,
290-
cloneURL: "",
298+
cloneURL: "https://github.com/foo/bar",
291299
commit: "",
292300
state: "available",
293301
statusVersion: 0,
294302
});
295-
if (usageDaysAgo !== undefined) {
296-
const now = new Date();
297-
now.setDate(now.getDate() - usageDaysAgo);
298-
await this.db.store({
299-
id: "usage-of-12345",
300-
creationTime: now.toISOString(),
301-
description: "something",
302-
contextURL: "https://github.com/foo/bar",
303-
ownerId: "1221423",
304-
organizationId: "org123",
305-
context: {
306-
title: "my title",
307-
},
308-
config: {},
309-
basedOnPrebuildId: "prebuild123",
310-
type: "regular",
311-
});
303+
304+
if (deletionEligibilityTimeDaysAgo !== undefined) {
305+
const deletionEligibilityTime = new Date();
306+
deletionEligibilityTime.setDate(deletionEligibilityTime.getDate() - deletionEligibilityTimeDaysAgo);
307+
await this.db.updatePartial(ws.id, { deletionEligibilityTime: deletionEligibilityTime.toISOString() });
312308
}
309+
310+
return { ws, pbws };
313311
}
314312

315313
@test(timeout(10000))

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ export interface WorkspaceDB {
112112
limit: number,
113113
now: Date,
114114
): Promise<WorkspaceAndOwner[]>;
115-
findPrebuiltWorkspacesForGC(daysUnused: number, limit: number): Promise<WorkspaceAndOwner[]>;
116115
findAllWorkspaces(
117116
offset: number,
118117
limit: number,

components/server/src/jobs/workspace-gc.ts

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ import { SYSTEM_USER_ID } from "../authorization/authorizer";
2222
import { StorageClient } from "../storage/storage-client";
2323

2424
/**
25-
* The WorkspaceGarbageCollector has two tasks:
26-
* - mark old, unused workspaces as 'softDeleted = "gc"' after a certain period (initially: 21)
27-
* - actually delete softDeleted workspaces if they are older than a configured time (initially: 7)
25+
* The WorkspaceGarbageCollector is responsible for moving workspaces (also affecting Prebuilds) through the following state machine:
26+
* - every action (create, start, stop, use as prebuild) updates the workspace.deletionEligibilityTime, which is set to some date into the future
27+
* - the GC has multiple sub-tasks to:
28+
* - find _regular_ workspaces "to delete" (with deletionEligibilityTime < now) -> move to "softDeleted"
29+
* - find _any_ workspace "softDeleted" for long enough -> move to "contentDeleted"
30+
* - find _any_ workspace "contentDeleted" for long enough -> move to "purged"
31+
* - prebuilds are special in that:
32+
* - the GC has a dedicated sub-task to move workspace of type "prebuid" from "to delete" (with a different threshold) -> to "contentDeleted" directly
33+
* - the "purging" takes care of all Prebuild-related sub-resources, too
2834
*/
2935
@injectable()
3036
export class WorkspaceGarbageCollector implements Job {
@@ -49,31 +55,33 @@ export class WorkspaceGarbageCollector implements Job {
4955
return;
5056
}
5157

58+
// Move eligible "regular" workspace -> softDeleted
5259
try {
5360
await this.softDeleteEligibleWorkspaces();
5461
} catch (error) {
5562
log.error("workspace-gc: error during eligible workspace deletion", error);
5663
}
64+
65+
// Move softDeleted workspaces -> contentDeleted
5766
try {
5867
await this.deleteWorkspaceContentAfterRetentionPeriod();
5968
} catch (error) {
6069
log.error("workspace-gc: error during content deletion", error);
6170
}
71+
72+
// Move eligible "prebuild" workspaces -> contentDeleted (jumping over softDeleted)
73+
// At this point, Prebuilds are no longer visible nor usable.
6274
try {
63-
await this.purgeWorkspacesAfterPurgeRetentionPeriod();
64-
} catch (err) {
65-
log.error("workspace-gc: error during hard deletion of workspaces", err);
66-
}
67-
try {
68-
//TODO (se) delete this end of June 2024
69-
await this.deleteOldPrebuilds();
75+
await this.deleteEligiblePrebuilds();
7076
} catch (err) {
71-
log.error("workspace-gc: error during prebuild deletion", err);
77+
log.error("workspace-gc: error during eligible prebuild deletion", err);
7278
}
79+
80+
// Move contentDeleted workspaces -> purged (calling workspaceService.hardDeleteWorkspace)
7381
try {
74-
await this.deleteEligiblePrebuilds();
82+
await this.purgeWorkspacesAfterPurgeRetentionPeriod();
7583
} catch (err) {
76-
log.error("workspace-gc: error during eligible prebuild deletion", err);
84+
log.error("workspace-gc: error during hard deletion of workspaces", err);
7785
}
7886

7987
return undefined;
@@ -230,40 +238,6 @@ export class WorkspaceGarbageCollector implements Job {
230238
}
231239
}
232240

233-
private async deleteOldPrebuilds() {
234-
const span = opentracing.globalTracer().startSpan("deleteOldPrebuilds");
235-
try {
236-
const now = new Date();
237-
const workspaces = await this.workspaceDB
238-
.trace({ span })
239-
.findPrebuiltWorkspacesForGC(
240-
this.config.workspaceGarbageCollection.minAgePrebuildDays,
241-
this.config.workspaceGarbageCollection.chunkLimit,
242-
);
243-
const afterSelect = new Date();
244-
log.info(`workspace-gc: about to delete ${workspaces.length} prebuilds`);
245-
for (const ws of workspaces) {
246-
try {
247-
await this.garbageCollectPrebuild({ span }, ws);
248-
} catch (err) {
249-
log.error({ workspaceId: ws.id }, "workspace-gc: failed to delete prebuild", err);
250-
}
251-
}
252-
const afterDelete = new Date();
253-
254-
log.info(`workspace-gc: successfully deleted ${workspaces.length} prebuilds`, {
255-
selectionTimeMs: afterSelect.getTime() - now.getTime(),
256-
deletionTimeMs: afterDelete.getTime() - afterSelect.getTime(),
257-
});
258-
span.addTags({ nrOfCollectedPrebuilds: workspaces.length });
259-
} catch (err) {
260-
TraceContext.setError({ span }, err);
261-
throw err;
262-
} finally {
263-
span.finish();
264-
}
265-
}
266-
267241
/**
268242
* This method garbageCollects a workspace. It deletes its contents and sets the workspaces 'contentDeletedTime'
269243
* @param ctx

0 commit comments

Comments
 (0)