Skip to content

Commit

Permalink
chore: revamp transactional impl (#4916)
Browse files Browse the repository at this point in the history
## About the changes
This transactional implementation decorates a service with a
transactional method that removes the need to start transactions in the
method using the service.

This is a gradual rollout with a feature toggle, just because
transactions are not easy.
  • Loading branch information
gastonfournier authored Oct 4, 2023
1 parent 630028a commit 0da48cc
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 108 deletions.
2 changes: 2 additions & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"strictSchemaValidation": false,
"transactionalDecorator": false,
"variantTypeNumber": false,
},
},
Expand Down Expand Up @@ -144,6 +145,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"strictSchemaValidation": false,
"transactionalDecorator": false,
"variantTypeNumber": false,
},
"externalResolver": {
Expand Down
31 changes: 31 additions & 0 deletions src/lib/db/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,34 @@ export const createKnexTransactionStarter = (
}
return transaction;
};

export type DbServiceFactory<S> = (db: Knex) => S;
export type WithTransactional<S> = S & {
transactional: <R>(fn: (service: S) => R) => Promise<R>;
};

export function withTransactional<S>(
serviceFactory: (db: Knex) => S,
db: Knex,
): WithTransactional<S> {
const service = serviceFactory(db) as WithTransactional<S>;

service.transactional = async <R>(fn: (service: S) => R) =>
db.transaction(async (trx: Knex.Transaction) => {
const transactionalService = serviceFactory(trx);
return fn(transactionalService);
});

return service;
}

/** Just for testing purposes */
export function withFakeTransactional<S>(service: S): WithTransactional<S> {
const serviceWithFakeTransactional = service as WithTransactional<S>;

serviceWithFakeTransactional.transactional = async <R>(
fn: (service: S) => R,
) => fn(serviceWithFakeTransactional);

return serviceWithFakeTransactional;
}
213 changes: 113 additions & 100 deletions src/lib/features/export-import-toggles/createExportImportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
createFakePrivateProjectChecker,
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
import { DbServiceFactory } from 'lib/db/transaction';

export const createFakeExportImportTogglesService = (
config: IUnleashConfig,
Expand Down Expand Up @@ -127,109 +128,121 @@ export const createFakeExportImportTogglesService = (
return exportImportService;
};

export const createExportImportTogglesService = (
db: Db,
export const deferredExportImportTogglesService = (
config: IUnleashConfig,
): ExportImportService => {
const { eventBus, getLogger, flagResolver } = config;
const importTogglesStore = new ImportTogglesStore(db);
const featureToggleStore = new FeatureToggleStore(db, eventBus, getLogger);
const tagStore = new TagStore(db, eventBus, getLogger);
const tagTypeStore = new TagTypeStore(db, eventBus, getLogger);
const segmentStore = new SegmentStore(
db,
eventBus,
getLogger,
flagResolver,
);
const projectStore = new ProjectStore(
db,
eventBus,
getLogger,
flagResolver,
);
const featureTagStore = new FeatureTagStore(db, eventBus, getLogger);
const strategyStore = new StrategyStore(db, getLogger);
const contextFieldStore = new ContextFieldStore(
db,
getLogger,
flagResolver,
);
const featureStrategiesStore = new FeatureStrategiesStore(
db,
eventBus,
getLogger,
flagResolver,
);
const featureEnvironmentStore = new FeatureEnvironmentStore(
db,
eventBus,
getLogger,
);
const eventStore = new EventStore(db, getLogger);
const accessService = createAccessService(db, config);
const featureToggleService = createFeatureToggleService(db, config);
const privateProjectChecker = createPrivateProjectChecker(db, config);
): DbServiceFactory<ExportImportService> => {
return (db: Db) => {
const { eventBus, getLogger, flagResolver } = config;
const importTogglesStore = new ImportTogglesStore(db);
const featureToggleStore = new FeatureToggleStore(
db,
eventBus,
getLogger,
);
const tagStore = new TagStore(db, eventBus, getLogger);
const tagTypeStore = new TagTypeStore(db, eventBus, getLogger);
const segmentStore = new SegmentStore(
db,
eventBus,
getLogger,
flagResolver,
);
const projectStore = new ProjectStore(
db,
eventBus,
getLogger,
flagResolver,
);
const featureTagStore = new FeatureTagStore(db, eventBus, getLogger);
const strategyStore = new StrategyStore(db, getLogger);
const contextFieldStore = new ContextFieldStore(
db,
getLogger,
flagResolver,
);
const featureStrategiesStore = new FeatureStrategiesStore(
db,
eventBus,
getLogger,
flagResolver,
);
const featureEnvironmentStore = new FeatureEnvironmentStore(
db,
eventBus,
getLogger,
);
const eventStore = new EventStore(db, getLogger);
const accessService = createAccessService(db, config);
const featureToggleService = createFeatureToggleService(db, config);
const privateProjectChecker = createPrivateProjectChecker(db, config);

const eventService = new EventService(
{
eventStore,
featureTagStore,
},
config,
);
const eventService = new EventService(
{
eventStore,
featureTagStore,
},
config,
);

const featureTagService = new FeatureTagService(
{
tagStore,
featureTagStore,
featureToggleStore,
},
{ getLogger },
eventService,
);
const contextService = new ContextService(
{
projectStore,
contextFieldStore,
featureStrategiesStore,
},
{ getLogger, flagResolver },
eventService,
privateProjectChecker,
);
const strategyService = new StrategyService(
{ strategyStore },
{ getLogger },
eventService,
);
const tagTypeService = new TagTypeService(
{ tagTypeStore },
{ getLogger },
eventService,
);
const exportImportService = new ExportImportService(
{
importTogglesStore,
featureStrategiesStore,
contextFieldStore,
featureToggleStore,
featureTagStore,
segmentStore,
tagTypeStore,
featureEnvironmentStore,
},
config,
{
featureToggleService,
featureTagService,
accessService,
const featureTagService = new FeatureTagService(
{
tagStore,
featureTagStore,
featureToggleStore,
},
{ getLogger },
eventService,
contextService,
strategyService,
tagTypeService,
},
);
);
const contextService = new ContextService(
{
projectStore,
contextFieldStore,
featureStrategiesStore,
},
{ getLogger, flagResolver },
eventService,
privateProjectChecker,
);
const strategyService = new StrategyService(
{ strategyStore },
{ getLogger },
eventService,
);
const tagTypeService = new TagTypeService(
{ tagTypeStore },
{ getLogger },
eventService,
);
const exportImportService = new ExportImportService(
{
importTogglesStore,
featureStrategiesStore,
contextFieldStore,
featureToggleStore,
featureTagStore,
segmentStore,
tagTypeStore,
featureEnvironmentStore,
},
config,
{
featureToggleService,
featureTagService,
accessService,
eventService,
contextService,
strategyService,
tagTypeService,
},
);

return exportImportService;
return exportImportService;
};
};
export const createExportImportTogglesService = (
db: Db,
config: IUnleashConfig,
): ExportImportService => {
const unboundService = deferredExportImportTogglesService(config);
return unboundService(db);
};
48 changes: 42 additions & 6 deletions src/lib/features/export-import-toggles/export-import-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import Controller from '../../routes/controller';
import { Logger } from '../../logger';
import ExportImportService from './export-import-service';
import { OpenApiService } from '../../services';
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
import {
TransactionCreator,
UnleashTransaction,
WithTransactional,
} from '../../db/transaction';
import {
IUnleashConfig,
IUnleashServices,
Expand All @@ -28,25 +32,32 @@ import ApiUser from '../../types/api-user';
class ExportImportController extends Controller {
private logger: Logger;

/** @deprecated gradually rolling out exportImportV2 */
private exportImportService: ExportImportService;

/** @deprecated gradually rolling out exportImportV2 */
private transactionalExportImportService: (
db: UnleashTransaction,
) => ExportImportService;

private exportImportServiceV2: WithTransactional<ExportImportService>;

private openApiService: OpenApiService;

/** @deprecated gradually rolling out exportImportV2 */
private readonly startTransaction: TransactionCreator<UnleashTransaction>;

constructor(
config: IUnleashConfig,
{
exportImportService,
transactionalExportImportService,
exportImportServiceV2,
openApiService,
}: Pick<
IUnleashServices,
| 'exportImportService'
| 'exportImportServiceV2'
| 'openApiService'
| 'transactionalExportImportService'
>,
Expand All @@ -57,6 +68,7 @@ class ExportImportController extends Controller {
this.exportImportService = exportImportService;
this.transactionalExportImportService =
transactionalExportImportService;
this.exportImportServiceV2 = exportImportServiceV2;
this.startTransaction = startTransaction;
this.openApiService = openApiService;
this.route({
Expand Down Expand Up @@ -128,7 +140,13 @@ class ExportImportController extends Controller {
this.verifyExportImportEnabled();
const query = req.body;
const userName = extractUsername(req);
const data = await this.exportImportService.export(query, userName);

const useTransactionalDecorator = this.config.flagResolver.isEnabled(
'transactionalDecorator',
);
const data = useTransactionalDecorator
? await this.exportImportServiceV2.export(query, userName)
: await this.exportImportService.export(query, userName);

this.openApiService.respondWithValidation(
200,
Expand All @@ -145,9 +163,17 @@ class ExportImportController extends Controller {
this.verifyExportImportEnabled();
const dto = req.body;
const { user } = req;
const validation = await this.startTransaction(async (tx) =>
this.transactionalExportImportService(tx).validate(dto, user),

const useTransactionalDecorator = this.config.flagResolver.isEnabled(
'transactionalDecorator',
);
const validation = useTransactionalDecorator
? await this.exportImportServiceV2.transactional((service) =>
service.validate(dto, user),
)
: await this.startTransaction(async (tx) =>
this.transactionalExportImportService(tx).validate(dto, user),
);

this.openApiService.respondWithValidation(
200,
Expand All @@ -172,10 +198,20 @@ class ExportImportController extends Controller {

const dto = req.body;

await this.startTransaction(async (tx) =>
this.transactionalExportImportService(tx).import(dto, user),
const useTransactionalDecorator = this.config.flagResolver.isEnabled(
'transactionalDecorator',
);

if (useTransactionalDecorator) {
await this.exportImportServiceV2.transactional((service) =>
service.import(dto, user),
);
} else {
await this.startTransaction(async (tx) =>
this.transactionalExportImportService(tx).import(dto, user),
);
}

res.status(200).end();
}

Expand Down
Loading

0 comments on commit 0da48cc

Please sign in to comment.