Skip to content

Commit 9a8504c

Browse files
committed
[server] Purge Workspaces at some point
1 parent 54e51b5 commit 9a8504c

File tree

11 files changed

+178
-7
lines changed

11 files changed

+178
-7
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 { daysEarlier } 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 = daysEarlier(now, minContentDeletionTimeInDays).toISOString();
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

+60
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,64 @@ 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+
]);
780+
781+
const wsIds = await this.db.findWorkspacesForPurging(365, 1000, purgeDate);
782+
expect(wsIds).to.deep.equal([
783+
{
784+
id: "1",
785+
ownerId,
786+
},
787+
]);
788+
}
729789
}
730790
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

+22-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import * as chai from "chai";
88
const expect = chai.expect;
99
import { suite, test } from "mocha-typescript";
10-
import { oneMonthLater } from "./timeutil";
10+
import { daysEarlier, oneMonthLater } from "./timeutil";
1111

1212
@suite()
1313
export class TimeutilSpec {
@@ -39,4 +39,25 @@ export class TimeutilSpec {
3939
const later = oneMonthLater(from.toISOString(), day);
4040
expect(later, `expected ${later} to be equal ${expectation}`).to.be.equal(expectation.toISOString());
4141
}
42+
43+
@test
44+
testDaysEarlier() {
45+
const tests: { date: Date; daysEarlier: number; expectation: string }[] = [
46+
{
47+
date: new Date("2021-07-13T00:00:00.000Z"),
48+
daysEarlier: 365,
49+
expectation: "2020-07-13T00:00:00.000Z",
50+
},
51+
{
52+
date: new Date("2019-02-01T00:00:00.000Z"),
53+
daysEarlier: 365,
54+
expectation: "2018-02-01T00:00:00.000Z",
55+
},
56+
];
57+
58+
for (const t of tests) {
59+
const actual = daysEarlier(t.date, t.daysEarlier);
60+
expect(actual.toISOString()).to.equal(t.expectation, `expected ${actual} to be equal ${t.expectation}`);
61+
}
62+
}
4263
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export const orderAsc = (d1: string, d2: string): number => liftDate(d1, d2, (d1
4747
export const liftDate1 = <T>(d1: string, f: (d1: Date) => T): T => f(new Date(d1));
4848
export const liftDate = <T>(d1: string, d2: string, f: (d1: Date, d2: Date) => T): T => f(new Date(d1), new Date(d2));
4949

50+
export const daysEarlier = (fromDate: Date, days: number): Date => {
51+
const t1 = new Date(fromDate.getTime());
52+
t1.setUTCDate(t1.getDate() - days);
53+
return t1;
54+
};
55+
5056
export function hoursBefore(date: string, hours: number): string {
5157
const result = new Date(date);
5258
result.setHours(result.getHours() - hours);

components/server/src/config.ts

+16
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,27 @@ export interface WorkspaceDefaults {
4848
export interface WorkspaceGarbageCollection {
4949
disabled: boolean;
5050
startDate: number;
51+
52+
/** The maximum amount of workspaces that are marked as 'softDeleted' in one go */
5153
chunkLimit: number;
54+
55+
/** The minimal age of a workspace before it is marked as 'softDeleted' (= hidden for the user) */
5256
minAgeDays: number;
57+
58+
/** The minimal age of a prebuild (incl. workspace) before it's content is deleted (+ marked as 'softDeleted') */
5359
minAgePrebuildDays: number;
60+
61+
/** The minimal number of days a workspace has to stay in 'softDeleted' before it's content is deleted */
5462
contentRetentionPeriodDays: number;
63+
64+
/** The maximum amount of workspaces whose content is deleted in one go */
5565
contentChunkLimit: number;
66+
67+
/** The minimal number of days a workspace has to stay in 'contentDeleted' before it's purged from the DB */
68+
purgeRetentionPeriodDays: number;
69+
70+
/** The maximum amount of workspaces which are purged in one go */
71+
purgeChunkLimit: number;
5672
}
5773

5874
/**

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

+32-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export class WorkspaceGarbageCollector {
4444
this.deleteWorkspaceContentAfterRetentionPeriod().catch((err) =>
4545
log.error("wsgc: error during content deletion", err),
4646
);
47+
this.purgeWorkspacesAfterPurgeRetentionPeriod().catch((err) =>
48+
log.error("wsgc: error during hard deletion of workspaces", err),
49+
);
4750
this.deleteOldPrebuilds().catch((err) => log.error("wsgc: error during prebuild deletion", err));
4851
this.deleteOutdatedVolumeSnapshots().catch((err) =>
4952
log.error("wsgc: error during volume snapshot gc deletion", err),
@@ -105,6 +108,34 @@ export class WorkspaceGarbageCollector {
105108
}
106109
}
107110

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

131-
// finds volume snapshots that have been superceded by newer volume snapshot and removes them
162+
// finds volume snapshots that have been superseded by newer volume snapshot and removes them
132163
protected async deleteOutdatedVolumeSnapshots() {
133164
const span = opentracing.globalTracer().startSpan("deleteOutdatedVolumeSnapshots");
134165
try {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1133,7 +1133,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
11331133
try {
11341134
await this.guardAccess({ kind: "workspace", subject: workspace }, "create");
11351135
} catch (err) {
1136-
await this.workspaceDb.trace(ctx).hardDeleteWorkspace(workspace.id);
1136+
await this.workspaceDeletionService.hardDeleteWorkspace(ctx, workspace.id);
11371137
throw err;
11381138
}
11391139

components/server/src/workspace/workspace-deletion-service.ts

+11
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ export class WorkspaceDeletionService {
4646
});
4747
}
4848

49+
/**
50+
* This *hard deletes* the workspace entry and all corresponding workspace-instances, by triggering a db-sync mechanism that purges it from the DB.
51+
* Note: when this function returns that doesn't mean that the entries are actually gone yet, that might still take a short while until db-sync comes
52+
* around to deleting them.
53+
* @param ctx
54+
* @param workspaceId
55+
*/
56+
public async hardDeleteWorkspace(ctx: TraceContext, workspaceId: string): Promise<void> {
57+
await this.db.trace(ctx).hardDeleteWorkspace(workspaceId);
58+
}
59+
4960
/**
5061
* This method garbageCollects a workspace. It deletes its contents and sets the workspaces 'contentDeletedTime'
5162
* @param ctx

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,14 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) {
178178
DefinitelyGpDisabled: ctx.Config.DisableDefinitelyGP,
179179
GitHubApp: githubApp,
180180
WorkspaceGarbageCollection: WorkspaceGarbageCollection{
181-
ChunkLimit: 1000,
182-
ContentChunkLimit: 1000,
183-
ContentRetentionPeriodDays: 21,
184181
Disabled: disableWsGarbageCollection,
185182
MinAgeDays: 14,
186183
MinAgePrebuildDays: 7,
184+
ChunkLimit: 1000,
185+
ContentRetentionPeriodDays: 21,
186+
ContentChunkLimit: 1000,
187+
PurgeRetentionPeriodDays: 365,
188+
PurgeChunkLimit: 1000,
187189
},
188190
EnableLocalApp: enableLocalApp,
189191
AuthProviderConfigFiles: func() []string {

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

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ type WorkspaceGarbageCollection struct {
102102
MinAgePrebuildDays int32 `json:"minAgePrebuildDays"`
103103
ContentRetentionPeriodDays int32 `json:"contentRetentionPeriodDays"`
104104
ContentChunkLimit int32 `json:"contentChunkLimit"`
105+
PurgeRetentionPeriodDays int32 `json:"purgeRetentionPeriodDays"`
106+
PurgeChunkLimit int32 `json:"purgeChunkLimit"`
105107
}
106108

107109
type GitHubApp struct {

0 commit comments

Comments
 (0)