Skip to content

Commit deed758

Browse files
committed
[server] WorkspaceGarbageCollector: Purge workspaces after being in deleted for 365 days
1 parent 569954e commit deed758

File tree

8 files changed

+158
-1
lines changed

8 files changed

+158
-1
lines changed

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

+18-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { DBPrebuiltWorkspace } from "./entity/db-prebuilt-workspace";
5252
import { DBPrebuiltWorkspaceUpdatable } from "./entity/db-prebuilt-workspace-updatable";
5353
import { BUILTIN_WORKSPACE_PROBE_USER_ID } from "../user-db";
5454
import { DBPrebuildInfo } from "./entity/db-prebuild-info-entry";
55+
import { daysBefore } from "@gitpod/gitpod-protocol/lib/util/timeutil";
5556

5657
type RawTo<T> = (instance: WorkspaceInstance, ws: Workspace) => T;
5758
interface OrderBy {
@@ -592,6 +593,22 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
592593
return dbResults as WorkspaceAndOwner[];
593594
}
594595

596+
public async findWorkspacesForPurging(
597+
minContentDeletionTimeInDays: number,
598+
limit: number,
599+
now: Date,
600+
): Promise<WorkspaceAndOwner[]> {
601+
const minPurgeTime = daysBefore(now.toISOString(), minContentDeletionTimeInDays);
602+
const repo = await this.getWorkspaceRepo();
603+
const qb = repo
604+
.createQueryBuilder("ws")
605+
.select(["ws.id", "ws.ownerId"])
606+
.where(`ws.contentDeletedTime != ''`)
607+
.andWhere(`ws.contentDeletedTime < :minPurgeTime`, { minPurgeTime })
608+
.limit(limit);
609+
return await qb.getMany();
610+
}
611+
595612
public async findWorkspacesForContentDeletion(
596613
minSoftDeletedTimeInDays: number,
597614
limit: number,
@@ -964,8 +981,8 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
964981
* around to deleting them.
965982
*/
966983
public async hardDeleteWorkspace(workspaceId: string): Promise<void> {
967-
await (await this.getWorkspaceRepo()).update(workspaceId, { deleted: true });
968984
await (await this.getWorkspaceInstanceRepo()).update({ workspaceId }, { deleted: true });
985+
await (await this.getWorkspaceRepo()).update(workspaceId, { deleted: true });
969986
}
970987

971988
public async findAllWorkspaces(

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

+73
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ class WorkspaceDBSpec {
662662
]);
663663
}
664664
}
665+
665666
@test(timeout(10000))
666667
public async testFindVolumeSnapshotWorkspacesForGC() {
667668
await this.threeVolumeSnapshotsForTwoWorkspaces();
@@ -726,5 +727,77 @@ class WorkspaceDBSpec {
726727
workspaceId: workspaceId2,
727728
});
728729
}
730+
731+
@test(timeout(10000))
732+
public async findWorkspacesForPurging() {
733+
const creationTime = "2018-01-01T00:00:00.000Z";
734+
const ownerId = "1221423";
735+
const purgeDate = new Date("2019-02-01T00:00:00.000Z");
736+
const d20180202 = "2018-02-02T00:00:00.000Z";
737+
const d20180201 = "2018-02-01T00:00:00.000Z";
738+
const d20180131 = "2018-01-31T00:00:00.000Z";
739+
await Promise.all([
740+
this.db.store({
741+
id: "1",
742+
creationTime,
743+
description: "something",
744+
contextURL: "http://github.com/myorg/inactive",
745+
ownerId,
746+
context: {
747+
title: "my title",
748+
},
749+
config: {},
750+
type: "regular",
751+
contentDeletedTime: d20180131,
752+
}),
753+
this.db.store({
754+
id: "2",
755+
creationTime,
756+
description: "something",
757+
contextURL: "http://github.com/myorg/active",
758+
ownerId,
759+
context: {
760+
title: "my title",
761+
},
762+
config: {},
763+
type: "regular",
764+
contentDeletedTime: d20180201,
765+
}),
766+
this.db.store({
767+
id: "3",
768+
creationTime,
769+
description: "something",
770+
contextURL: "http://github.com/myorg/active",
771+
ownerId,
772+
context: {
773+
title: "my title",
774+
},
775+
config: {},
776+
type: "regular",
777+
contentDeletedTime: d20180202,
778+
}),
779+
this.db.store({
780+
id: "4",
781+
creationTime,
782+
description: "something",
783+
contextURL: "http://github.com/myorg/active",
784+
ownerId,
785+
context: {
786+
title: "my title",
787+
},
788+
config: {},
789+
type: "regular",
790+
contentDeletedTime: undefined,
791+
}),
792+
]);
793+
794+
const wsIds = await this.db.findWorkspacesForPurging(365, 1000, purgeDate);
795+
expect(wsIds).to.deep.equal([
796+
{
797+
id: "1",
798+
ownerId,
799+
},
800+
]);
801+
}
729802
}
730803
module.exports = new WorkspaceDBSpec();

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

+5
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ export interface WorkspaceDB {
108108
minSoftDeletedTimeInDays: number,
109109
limit: number,
110110
): Promise<WorkspaceOwnerAndSoftDeleted[]>;
111+
findWorkspacesForPurging(
112+
minContentDeletionTimeInDays: number,
113+
limit: number,
114+
now: Date,
115+
): Promise<WorkspaceAndOwner[]>;
111116
findPrebuiltWorkspacesForGC(daysUnused: number, limit: number): Promise<WorkspaceAndOwner[]>;
112117
findAllWorkspaces(
113118
offset: number,

components/gitpod-protocol/src/util/timeutil.spec.ts

+21
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,25 @@ export class TimeutilSpec {
4545
const later = oneMonthLater(from.toISOString(), day);
4646
expect(later, `expected ${later} to be equal ${expectation}`).to.be.equal(expectation.toISOString());
4747
}
48+
49+
@test
50+
testDaysBefore2() {
51+
const tests: { date: Date; daysEarlier: number; expectation: string }[] = [
52+
{
53+
date: new Date("2021-07-13T00:00:00.000Z"),
54+
daysEarlier: 365,
55+
expectation: "2020-07-13T00:00:00.000Z",
56+
},
57+
{
58+
date: new Date("2019-02-01T00:00:00.000Z"),
59+
daysEarlier: 365,
60+
expectation: "2018-02-01T00:00:00.000Z",
61+
},
62+
];
63+
64+
for (const t of tests) {
65+
const actual = daysBefore(t.date.toISOString(), t.daysEarlier);
66+
expect(actual).to.equal(t.expectation, `expected ${actual} to be equal ${t.expectation}`);
67+
}
68+
}
4869
}

components/server/src/config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface WorkspaceGarbageCollection {
6161

6262
/** The maximum amount of workspaces whose content is deleted in one go */
6363
contentChunkLimit: number;
64+
65+
/** The minimal number of days a workspace has to stay in 'contentDeleted' before it's purged from the DB */
66+
purgeRetentionPeriodDays: number;
67+
68+
/** The maximum amount of workspaces which are purged in one go */
69+
purgeChunkLimit: number;
6470
}
6571

6672
/**

components/server/src/workspace/garbage-collector.ts

+31
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export class WorkspaceGarbageCollector {
4747
this.deleteWorkspaceContentAfterRetentionPeriod().catch((err) =>
4848
log.error("wsgc: error during content deletion", err),
4949
);
50+
this.purgeWorkspacesAfterPurgeRetentionPeriod().catch((err) =>
51+
log.error("wsgc: error during hard deletion of workspaces", err),
52+
);
5053
this.deleteOldPrebuilds().catch((err) => log.error("wsgc: error during prebuild deletion", err));
5154
this.deleteOutdatedVolumeSnapshots().catch((err) =>
5255
log.error("wsgc: error during volume snapshot gc deletion", err),
@@ -108,6 +111,34 @@ export class WorkspaceGarbageCollector {
108111
}
109112
}
110113

114+
/**
115+
* This method is meant to purge all traces of a Workspace and it's WorkspaceInstances from the DB
116+
*/
117+
protected async purgeWorkspacesAfterPurgeRetentionPeriod() {
118+
const span = opentracing.globalTracer().startSpan("purgeWorkspacesAfterPurgeRetentionPeriod");
119+
try {
120+
const now = new Date();
121+
const workspaces = await this.workspaceDB
122+
.trace({ span })
123+
.findWorkspacesForPurging(
124+
this.config.workspaceGarbageCollection.purgeRetentionPeriodDays,
125+
this.config.workspaceGarbageCollection.purgeChunkLimit,
126+
now,
127+
);
128+
const deletes = await Promise.all(
129+
workspaces.map((ws) => this.deletionService.hardDeleteWorkspace({ span }, ws.id)),
130+
);
131+
132+
log.info(`wsgc: successfully purged ${deletes.length} workspaces`);
133+
span.addTags({ nrOfCollectedWorkspaces: deletes.length });
134+
} catch (err) {
135+
TraceContext.setError({ span }, err);
136+
throw err;
137+
} finally {
138+
span.finish();
139+
}
140+
}
141+
111142
protected async deleteOldPrebuilds() {
112143
const span = opentracing.globalTracer().startSpan("deleteOldPrebuilds");
113144
try {

install/installer/pkg/components/server/configmap.go

+2
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
192192
ChunkLimit: 1000,
193193
ContentRetentionPeriodDays: 21,
194194
ContentChunkLimit: 1000,
195+
PurgeRetentionPeriodDays: 365,
196+
PurgeChunkLimit: 1000,
195197
},
196198
EnableLocalApp: enableLocalApp,
197199
AuthProviderConfigFiles: func() []string {

install/installer/pkg/components/server/types.go

+2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ type WorkspaceGarbageCollection struct {
9797
MinAgePrebuildDays int32 `json:"minAgePrebuildDays"`
9898
ContentRetentionPeriodDays int32 `json:"contentRetentionPeriodDays"`
9999
ContentChunkLimit int32 `json:"contentChunkLimit"`
100+
PurgeRetentionPeriodDays int32 `json:"purgeRetentionPeriodDays"`
101+
PurgeChunkLimit int32 `json:"purgeChunkLimit"`
100102
}
101103

102104
type GitHubApp struct {

0 commit comments

Comments
 (0)