Skip to content

Commit 909955d

Browse files
authored
[db, ts] Introduce a generic db.transaction() (#17980)
* [ts] Upgrade inversify 5.0.1 -> 6.0.1 * [server, db] Introduce TransactionalDB for User, Workspace, Projects * Fix tests
1 parent ab82440 commit 909955d

File tree

18 files changed

+349
-343
lines changed

18 files changed

+349
-343
lines changed

components/content-service-api/typescript/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"dependencies": {
1414
"@grpc/grpc-js": "^1.3.7",
1515
"google-protobuf": "^3.19.1",
16-
"inversify": "^5.0.1",
16+
"inversify": "^6.0.1",
1717
"node-pre-gyp": "^0.17.0",
1818
"opentracing": "^0.14.4"
1919
},

components/gitpod-db/src/container-module.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { ContainerModule } from "inversify";
88

99
import { WorkspaceDB } from "./workspace-db";
10-
import { TypeORMWorkspaceDBImpl, TransactionalWorkspaceDbImpl } from "./typeorm/workspace-db-impl";
10+
import { TypeORMWorkspaceDBImpl } from "./typeorm/workspace-db-impl";
1111
import { TypeORMUserDBImpl } from "./typeorm/user-db-impl";
1212
import { UserDB } from "./user-db";
1313
import { Config } from "./config";
@@ -48,12 +48,11 @@ import { DataCache, DataCacheNoop } from "./data-cache";
4848

4949
// THE DB container module that contains all DB implementations
5050
export const dbContainerModule = (cacheClass = DataCacheNoop) =>
51-
new ContainerModule((bind, unbind, isBound, rebind) => {
51+
new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
5252
bind(Config).toSelf().inSingletonScope();
5353
bind(TypeORM).toSelf().inSingletonScope();
5454
bind(DBWithTracing).toSelf().inSingletonScope();
5555
bind(DataCache).to(cacheClass).inSingletonScope();
56-
bind(TransactionalWorkspaceDbImpl).toSelf().inSingletonScope();
5756

5857
bind(TypeORMBlockedRepositoryDBImpl).toSelf().inSingletonScope();
5958
bind(BlockedRepositoryDB).toService(TypeORMBlockedRepositoryDBImpl);
@@ -78,7 +77,7 @@ export const dbContainerModule = (cacheClass = DataCacheNoop) =>
7877
bind(OneTimeSecretDB).toService(TypeORMOneTimeSecretDBImpl);
7978
bindDbWithTracing(TracedOneTimeSecretDB, bind, OneTimeSecretDB).inSingletonScope();
8079

81-
encryptionModule(bind, unbind, isBound, rebind);
80+
encryptionModule(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation);
8281
bind(KeyProviderConfig)
8382
.toDynamicValue((ctx) => {
8483
const config = ctx.container.get<Config>(Config);

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
*/
66

77
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue, ProjectUsage } from "@gitpod/gitpod-protocol";
8+
import { TransactionalDB } from "./typeorm/transactional-db-impl";
89

910
export const ProjectDB = Symbol("ProjectDB");
10-
export interface ProjectDB {
11+
export interface ProjectDB extends TransactionalDB<ProjectDB> {
1112
findProjectById(projectId: string): Promise<Project | undefined>;
1213
findProjectByCloneUrl(cloneUrl: string): Promise<Project | undefined>;
1314
findProjectsByCloneUrls(cloneUrls: string[]): Promise<(Project & { teamOwners?: string[] })[]>;

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

+12-8
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { inject, injectable } from "inversify";
8-
import { TypeORM } from "./typeorm";
9-
import { Repository } from "typeorm";
7+
import { inject, injectable, optional } from "inversify";
8+
import { EntityManager, Repository } from "typeorm";
109
import { v4 as uuidv4 } from "uuid";
1110
import { PartialProject, Project, ProjectEnvVar, ProjectEnvVarWithValue, ProjectUsage } from "@gitpod/gitpod-protocol";
1211
import { EncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
@@ -15,6 +14,7 @@ import { DBProject } from "./entity/db-project";
1514
import { DBProjectEnvVar } from "./entity/db-project-env-vars";
1615
import { DBProjectInfo } from "./entity/db-project-info";
1716
import { DBProjectUsage } from "./entity/db-project-usage";
17+
import { TransactionalDBImpl, UndefinedEntityManager } from "./transactional-db-impl";
1818

1919
function toProjectEnvVar(envVarWithValue: ProjectEnvVarWithValue): ProjectEnvVar {
2020
const envVar = { ...envVarWithValue };
@@ -23,12 +23,16 @@ function toProjectEnvVar(envVarWithValue: ProjectEnvVarWithValue): ProjectEnvVar
2323
}
2424

2525
@injectable()
26-
export class ProjectDBImpl implements ProjectDB {
27-
@inject(TypeORM) typeORM: TypeORM;
28-
@inject(EncryptionService) protected readonly encryptionService: EncryptionService;
26+
export class ProjectDBImpl extends TransactionalDBImpl<ProjectDB> implements ProjectDB {
27+
constructor(
28+
@inject(EncryptionService) protected readonly encryptionService: EncryptionService,
29+
@inject(UndefinedEntityManager) @optional() transactionalEM: EntityManager | undefined,
30+
) {
31+
super(transactionalEM);
32+
}
2933

30-
protected async getEntityManager() {
31-
return (await this.typeORM.getConnection()).manager;
34+
protected createTransactionalDB(transactionalEM: EntityManager): ProjectDB {
35+
return new ProjectDBImpl(this.encryptionService, transactionalEM);
3236
}
3337

3438
protected async getRepo(): Promise<Repository<DBProject>> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Copyright (c) 2023 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 { EntityManager } from "typeorm";
8+
import { TypeORM } from "./typeorm";
9+
import { inject, injectable, optional } from "inversify";
10+
11+
export interface TransactionalDB<DB> {
12+
transaction<R>(code: (db: DB) => Promise<R>): Promise<R>;
13+
}
14+
15+
// A symbol we never bind to
16+
export const UndefinedEntityManager = Symbol("UndefinedEntityManager");
17+
18+
@injectable()
19+
export abstract class TransactionalDBImpl<DB> implements TransactionalDB<DB> {
20+
@inject(TypeORM) protected readonly typeorm: TypeORM;
21+
22+
constructor(
23+
@optional() protected transactionalEM: EntityManager | undefined, // will be undefined when constructed with inversify, which is inteded
24+
) {}
25+
26+
protected async getEntityManager(): Promise<EntityManager> {
27+
if (this.transactionalEM) {
28+
return this.transactionalEM;
29+
}
30+
return (await this.typeorm.getConnection()).manager;
31+
}
32+
33+
async transaction<R>(code: (db: DB) => Promise<R>): Promise<R> {
34+
const manager = await this.getEntityManager();
35+
return await manager.transaction(async (manager) => {
36+
return await code(this.createTransactionalDB(manager));
37+
});
38+
}
39+
40+
// TODO(gpl) This feels unnecessary because we should already be able to create a new instance using inversify.
41+
// E.g. it could look like: transaction<R>(code: (db1: ..., db2...) => Promise<R>, ...serviceIdentifier: interfaces.ServiceIdentifier<DB>
42+
// But bc this requires some type shennenigans, and we're not sure we need it, we'll stick with this for now
43+
protected abstract createTransactionalDB(transactionalEM: EntityManager): DB;
44+
}

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

+12-29
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
OAuthToken,
2828
OAuthUser,
2929
} from "@jmondi/oauth2-server";
30-
import { inject, injectable, postConstruct } from "inversify";
30+
import { inject, injectable, postConstruct, optional } from "inversify";
3131
import { EntityManager, Repository } from "typeorm";
3232
import { v4 as uuidv4 } from "uuid";
3333
import {
@@ -45,9 +45,9 @@ import { DBUser } from "./entity/db-user";
4545
import { DBUserEnvVar } from "./entity/db-user-env-vars";
4646
import { DBWorkspace } from "./entity/db-workspace";
4747
import { DBUserSshPublicKey } from "./entity/db-user-ssh-public-key";
48-
import { TypeORM } from "./typeorm";
4948
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
5049
import { DataCache } from "../data-cache";
50+
import { TransactionalDBImpl } from "./transactional-db-impl";
5151

5252
// OAuth token expiry
5353
const tokenExpiryInFuture = new DateInterval("7d");
@@ -61,19 +61,23 @@ function getUserCacheKey(id: string): string {
6161
}
6262

6363
@injectable()
64-
export class TypeORMUserDBImpl implements UserDB {
65-
@inject(TypeORM) protected readonly typeorm: TypeORM;
66-
@inject(EncryptionService) protected readonly encryptionService: EncryptionService;
67-
@inject(DataCache) protected readonly cache: DataCache;
64+
export class TypeORMUserDBImpl extends TransactionalDBImpl<UserDB> implements UserDB {
65+
constructor(
66+
@inject(EncryptionService) protected readonly encryptionService: EncryptionService,
67+
@inject(DataCache) protected readonly cache: DataCache,
68+
@optional() transactionalEM: EntityManager,
69+
) {
70+
super(transactionalEM);
71+
}
6872

6973
@postConstruct()
7074
init() {
7175
/** Publish the instance of EncryptionService our entities should use */
7276
encryptionService = this.encryptionService;
7377
}
7478

75-
protected async getEntityManager(): Promise<EntityManager> {
76-
return (await this.typeorm.getConnection()).manager;
79+
protected createTransactionalDB(transactionalEM: EntityManager): UserDB {
80+
return new TypeORMUserDBImpl(this.encryptionService, this.cache, transactionalEM);
7781
}
7882

7983
async getUserRepo(): Promise<Repository<DBUser>> {
@@ -83,13 +87,6 @@ export class TypeORMUserDBImpl implements UserDB {
8387
return (await this.getEntityManager()).getRepository<DBWorkspace>(DBWorkspace);
8488
}
8589

86-
async transaction<T>(code: (db: UserDB) => Promise<T>): Promise<T> {
87-
const manager = await this.getEntityManager();
88-
return await manager.transaction(async (manager) => {
89-
return await code(new TransactionalUserDBImpl(manager, this.cache, this.encryptionService));
90-
});
91-
}
92-
9390
protected async getTokenRepo(): Promise<Repository<DBTokenEntry>> {
9491
return (await this.getEntityManager()).getRepository<DBTokenEntry>(DBTokenEntry);
9592
}
@@ -655,17 +652,3 @@ export class TypeORMUserDBImpl implements UserDB {
655652
return this.mapDBUserToUser(result);
656653
}
657654
}
658-
659-
export class TransactionalUserDBImpl extends TypeORMUserDBImpl {
660-
constructor(
661-
protected readonly manager: EntityManager,
662-
protected readonly cache: DataCache,
663-
protected readonly encryptionService: EncryptionService,
664-
) {
665-
super();
666-
}
667-
668-
async getEntityManager(): Promise<EntityManager> {
669-
return this.manager;
670-
}
671-
}

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

+21-47
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import * as crypto from "crypto";
8-
import { injectable, inject } from "inversify";
8+
import { injectable, inject, optional } from "inversify";
99
import { Repository, EntityManager, DeepPartial, UpdateQueryBuilder, Brackets } from "typeorm";
1010
import {
1111
MaybeWorkspace,
@@ -37,7 +37,6 @@ import {
3737
SnapshotState,
3838
PrebuiltWorkspaceState,
3939
} from "@gitpod/gitpod-protocol";
40-
import { TypeORM } from "./typeorm";
4140
import { DBWorkspace } from "./entity/db-workspace";
4241
import { DBWorkspaceInstance } from "./entity/db-workspace-instance";
4342
import { DBSnapshot } from "./entity/db-snapshot";
@@ -56,6 +55,7 @@ import {
5655
reportWorkspaceInstancePurged,
5756
reportWorkspacePurged,
5857
} from "./metrics";
58+
import { TransactionalDBImpl, UndefinedEntityManager } from "./transactional-db-impl";
5959

6060
type RawTo<T> = (instance: WorkspaceInstance, ws: Workspace) => T;
6161
interface OrderBy {
@@ -64,46 +64,54 @@ interface OrderBy {
6464
}
6565

6666
@injectable()
67-
export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
68-
protected abstract getManager(): Promise<EntityManager>;
67+
export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> implements WorkspaceDB {
68+
constructor(@inject(UndefinedEntityManager) @optional() transactionalEM: EntityManager | undefined) {
69+
super(transactionalEM);
70+
}
71+
72+
protected createTransactionalDB(transactionalEM: EntityManager): WorkspaceDB {
73+
return new TypeORMWorkspaceDBImpl(transactionalEM);
74+
}
6975

7076
protected async getWorkspaceRepo(): Promise<Repository<DBWorkspace>> {
71-
return (await this.getManager()).getRepository<DBWorkspace>(DBWorkspace);
77+
return (await this.getEntityManager()).getRepository<DBWorkspace>(DBWorkspace);
7278
}
7379

7480
protected async getWorkspaceInstanceRepo(): Promise<Repository<DBWorkspaceInstance>> {
75-
return (await this.getManager()).getRepository<DBWorkspaceInstance>(DBWorkspaceInstance);
81+
return (await this.getEntityManager()).getRepository<DBWorkspaceInstance>(DBWorkspaceInstance);
7682
}
7783

7884
protected async getWorkspaceInstanceUserRepo(): Promise<Repository<DBWorkspaceInstanceUser>> {
79-
return (await this.getManager()).getRepository<DBWorkspaceInstanceUser>(DBWorkspaceInstanceUser);
85+
return (await this.getEntityManager()).getRepository<DBWorkspaceInstanceUser>(DBWorkspaceInstanceUser);
8086
}
8187

8288
protected async getRepositoryWhitelist(): Promise<Repository<DBRepositoryWhiteList>> {
83-
return (await this.getManager()).getRepository<DBRepositoryWhiteList>(DBRepositoryWhiteList);
89+
return (await this.getEntityManager()).getRepository<DBRepositoryWhiteList>(DBRepositoryWhiteList);
8490
}
8591

8692
protected async getSnapshotRepo(): Promise<Repository<DBSnapshot>> {
87-
return (await this.getManager()).getRepository<DBSnapshot>(DBSnapshot);
93+
return (await this.getEntityManager()).getRepository<DBSnapshot>(DBSnapshot);
8894
}
8995

9096
protected async getPrebuiltWorkspaceRepo(): Promise<Repository<DBPrebuiltWorkspace>> {
91-
return (await this.getManager()).getRepository<DBPrebuiltWorkspace>(DBPrebuiltWorkspace);
97+
return (await this.getEntityManager()).getRepository<DBPrebuiltWorkspace>(DBPrebuiltWorkspace);
9298
}
9399

94100
protected async getPrebuildInfoRepo(): Promise<Repository<DBPrebuildInfo>> {
95-
return (await this.getManager()).getRepository<DBPrebuildInfo>(DBPrebuildInfo);
101+
return (await this.getEntityManager()).getRepository<DBPrebuildInfo>(DBPrebuildInfo);
96102
}
97103

98104
protected async getPrebuiltWorkspaceUpdatableRepo(): Promise<Repository<DBPrebuiltWorkspaceUpdatable>> {
99-
return (await this.getManager()).getRepository<DBPrebuiltWorkspaceUpdatable>(DBPrebuiltWorkspaceUpdatable);
105+
return (await this.getEntityManager()).getRepository<DBPrebuiltWorkspaceUpdatable>(
106+
DBPrebuiltWorkspaceUpdatable,
107+
);
100108
}
101109

102110
public async connect(maxTries: number = 3, timeout: number = 2000): Promise<void> {
103111
let tries = 1;
104112
while (tries <= maxTries) {
105113
try {
106-
await this.getManager();
114+
await this.getEntityManager();
107115
return;
108116
} catch (err) {
109117
log.error(`DB connection error (attempt ${tries} of ${maxTries})`, err);
@@ -114,10 +122,6 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
114122
throw new Error("Could not establish connection to database!");
115123
}
116124

117-
public async transaction<T>(code: (db: WorkspaceDB) => Promise<T>): Promise<T> {
118-
return code(this);
119-
}
120-
121125
async storeInstance(instance: WorkspaceInstance): Promise<WorkspaceInstance> {
122126
const inst = await this.internalStoreInstance(instance);
123127
return inst;
@@ -1197,34 +1201,4 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB {
11971201
}
11981202
}
11991203

1200-
@injectable()
1201-
export class TypeORMWorkspaceDBImpl extends AbstractTypeORMWorkspaceDBImpl {
1202-
@inject(TypeORM) protected readonly typeorm: TypeORM;
1203-
1204-
protected async getManager() {
1205-
return (await this.typeorm.getConnection()).manager;
1206-
}
1207-
1208-
public async transaction<T>(code: (db: WorkspaceDB) => Promise<T>): Promise<T> {
1209-
const connection = await this.typeorm.getConnection();
1210-
return connection.transaction((manager) => {
1211-
return code(new TransactionalWorkspaceDbImpl(manager));
1212-
});
1213-
}
1214-
}
1215-
1216-
export class TransactionalWorkspaceDbImpl extends AbstractTypeORMWorkspaceDBImpl {
1217-
constructor(protected readonly manager: EntityManager) {
1218-
super();
1219-
}
1220-
1221-
protected async getManager() {
1222-
return this.manager;
1223-
}
1224-
1225-
public async transaction<T>(code: (sb: WorkspaceDB) => Promise<T>): Promise<T> {
1226-
return await code(this);
1227-
}
1228-
}
1229-
12301204
type InstanceJoinResult = DBWorkspace & { instance: WorkspaceInstance };

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

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ import {
1919
import { OAuthTokenRepository, OAuthUserRepository } from "@jmondi/oauth2-server";
2020
import { Repository } from "typeorm";
2121
import { DBUser } from "./typeorm/entity/db-user";
22+
import { TransactionalDB } from "./typeorm/transactional-db-impl";
2223

2324
export type MaybeUser = User | undefined;
2425

2526
export const UserDB = Symbol("UserDB");
26-
export interface UserDB extends OAuthUserRepository, OAuthTokenRepository {
27-
transaction<T>(code: (db: UserDB) => Promise<T>): Promise<T>;
28-
27+
export interface UserDB extends OAuthUserRepository, OAuthTokenRepository, TransactionalDB<UserDB> {
2928
newUser(): Promise<User>;
3029
storeUser(newUser: User): Promise<User>;
3130
updateUserPartial(partial: PartialUserUpdate): Promise<void>;

components/gitpod-protocol/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"cookie": "^0.4.2",
5151
"express": "^4.17.3",
5252
"google-protobuf": "^3.19.1",
53-
"inversify": "^5.1.1",
53+
"inversify": "^6.0.1",
5454
"jaeger-client": "^3.18.1",
5555
"js-yaml": "^3.10.0",
5656
"nice-grpc-common": "^2.0.0",

components/image-builder-api/typescript/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@gitpod/gitpod-protocol": "0.1.5",
1616
"@grpc/grpc-js": "^1.3.7",
1717
"google-protobuf": "^3.19.1",
18-
"inversify": "^5.0.1",
18+
"inversify": "^6.0.1",
1919
"opentracing": "^0.14.4"
2020
},
2121
"devDependencies": {

0 commit comments

Comments
 (0)