1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-01 01:18:10 +02:00

chore: revamp transactional impl (#4916)

## 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.
This commit is contained in:
Gastón Fournier 2023-10-04 15:16:37 +02:00 committed by GitHub
parent 630028acba
commit 0da48cc0d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 206 additions and 109 deletions

View File

@ -105,6 +105,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,
"strictSchemaValidation": false, "strictSchemaValidation": false,
"transactionalDecorator": false,
"variantTypeNumber": false, "variantTypeNumber": false,
}, },
}, },
@ -144,6 +145,7 @@ exports[`should create default config 1`] = `
"proPlanAutoCharge": false, "proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false, "responseTimeWithAppNameKillSwitch": false,
"strictSchemaValidation": false, "strictSchemaValidation": false,
"transactionalDecorator": false,
"variantTypeNumber": false, "variantTypeNumber": false,
}, },
"externalResolver": { "externalResolver": {

View File

@ -20,3 +20,34 @@ export const createKnexTransactionStarter = (
} }
return transaction; 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;
}

View File

@ -43,6 +43,7 @@ import {
createFakePrivateProjectChecker, createFakePrivateProjectChecker,
createPrivateProjectChecker, createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker'; } from '../private-project/createPrivateProjectChecker';
import { DbServiceFactory } from 'lib/db/transaction';
export const createFakeExportImportTogglesService = ( export const createFakeExportImportTogglesService = (
config: IUnleashConfig, config: IUnleashConfig,
@ -127,13 +128,17 @@ export const createFakeExportImportTogglesService = (
return exportImportService; return exportImportService;
}; };
export const createExportImportTogglesService = ( export const deferredExportImportTogglesService = (
db: Db,
config: IUnleashConfig, config: IUnleashConfig,
): ExportImportService => { ): DbServiceFactory<ExportImportService> => {
return (db: Db) => {
const { eventBus, getLogger, flagResolver } = config; const { eventBus, getLogger, flagResolver } = config;
const importTogglesStore = new ImportTogglesStore(db); const importTogglesStore = new ImportTogglesStore(db);
const featureToggleStore = new FeatureToggleStore(db, eventBus, getLogger); const featureToggleStore = new FeatureToggleStore(
db,
eventBus,
getLogger,
);
const tagStore = new TagStore(db, eventBus, getLogger); const tagStore = new TagStore(db, eventBus, getLogger);
const tagTypeStore = new TagTypeStore(db, eventBus, getLogger); const tagTypeStore = new TagTypeStore(db, eventBus, getLogger);
const segmentStore = new SegmentStore( const segmentStore = new SegmentStore(
@ -233,3 +238,11 @@ export const createExportImportTogglesService = (
return exportImportService; return exportImportService;
}; };
};
export const createExportImportTogglesService = (
db: Db,
config: IUnleashConfig,
): ExportImportService => {
const unboundService = deferredExportImportTogglesService(config);
return unboundService(db);
};

View File

@ -3,7 +3,11 @@ import Controller from '../../routes/controller';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import ExportImportService from './export-import-service'; import ExportImportService from './export-import-service';
import { OpenApiService } from '../../services'; import { OpenApiService } from '../../services';
import { TransactionCreator, UnleashTransaction } from '../../db/transaction'; import {
TransactionCreator,
UnleashTransaction,
WithTransactional,
} from '../../db/transaction';
import { import {
IUnleashConfig, IUnleashConfig,
IUnleashServices, IUnleashServices,
@ -28,14 +32,19 @@ import ApiUser from '../../types/api-user';
class ExportImportController extends Controller { class ExportImportController extends Controller {
private logger: Logger; private logger: Logger;
/** @deprecated gradually rolling out exportImportV2 */
private exportImportService: ExportImportService; private exportImportService: ExportImportService;
/** @deprecated gradually rolling out exportImportV2 */
private transactionalExportImportService: ( private transactionalExportImportService: (
db: UnleashTransaction, db: UnleashTransaction,
) => ExportImportService; ) => ExportImportService;
private exportImportServiceV2: WithTransactional<ExportImportService>;
private openApiService: OpenApiService; private openApiService: OpenApiService;
/** @deprecated gradually rolling out exportImportV2 */
private readonly startTransaction: TransactionCreator<UnleashTransaction>; private readonly startTransaction: TransactionCreator<UnleashTransaction>;
constructor( constructor(
@ -43,10 +52,12 @@ class ExportImportController extends Controller {
{ {
exportImportService, exportImportService,
transactionalExportImportService, transactionalExportImportService,
exportImportServiceV2,
openApiService, openApiService,
}: Pick< }: Pick<
IUnleashServices, IUnleashServices,
| 'exportImportService' | 'exportImportService'
| 'exportImportServiceV2'
| 'openApiService' | 'openApiService'
| 'transactionalExportImportService' | 'transactionalExportImportService'
>, >,
@ -57,6 +68,7 @@ class ExportImportController extends Controller {
this.exportImportService = exportImportService; this.exportImportService = exportImportService;
this.transactionalExportImportService = this.transactionalExportImportService =
transactionalExportImportService; transactionalExportImportService;
this.exportImportServiceV2 = exportImportServiceV2;
this.startTransaction = startTransaction; this.startTransaction = startTransaction;
this.openApiService = openApiService; this.openApiService = openApiService;
this.route({ this.route({
@ -128,7 +140,13 @@ class ExportImportController extends Controller {
this.verifyExportImportEnabled(); this.verifyExportImportEnabled();
const query = req.body; const query = req.body;
const userName = extractUsername(req); 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( this.openApiService.respondWithValidation(
200, 200,
@ -145,7 +163,15 @@ class ExportImportController extends Controller {
this.verifyExportImportEnabled(); this.verifyExportImportEnabled();
const dto = req.body; const dto = req.body;
const { user } = req; const { user } = req;
const validation = await this.startTransaction(async (tx) =>
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.transactionalExportImportService(tx).validate(dto, user),
); );
@ -172,9 +198,19 @@ class ExportImportController extends Controller {
const dto = req.body; const dto = req.body;
const useTransactionalDecorator = this.config.flagResolver.isEnabled(
'transactionalDecorator',
);
if (useTransactionalDecorator) {
await this.exportImportServiceV2.transactional((service) =>
service.import(dto, user),
);
} else {
await this.startTransaction(async (tx) => await this.startTransaction(async (tx) =>
this.transactionalExportImportService(tx).import(dto, user), this.transactionalExportImportService(tx).import(dto, user),
); );
}
res.status(200).end(); res.status(200).end();
} }

View File

@ -50,8 +50,10 @@ import { Knex } from 'knex';
import { import {
createExportImportTogglesService, createExportImportTogglesService,
createFakeExportImportTogglesService, createFakeExportImportTogglesService,
deferredExportImportTogglesService,
} from '../features/export-import-toggles/createExportImportService'; } from '../features/export-import-toggles/createExportImportService';
import { Db } from '../db/db'; import { Db } from '../db/db';
import { withFakeTransactional, withTransactional } from '../db/transaction';
import { import {
createChangeRequestAccessReadModel, createChangeRequestAccessReadModel,
createFakeChangeRequestAccessService, createFakeChangeRequestAccessService,
@ -274,10 +276,12 @@ export const createServices = (
projectService, projectService,
); );
// TODO: this is a temporary seam to enable packaging by feature
const exportImportService = db const exportImportService = db
? createExportImportTogglesService(db, config) ? createExportImportTogglesService(db, config)
: createFakeExportImportTogglesService(config); : createFakeExportImportTogglesService(config);
const exportImportServiceV2 = db
? withTransactional(deferredExportImportTogglesService(config), db)
: withFakeTransactional(createFakeExportImportTogglesService(config));
const transactionalExportImportService = (txDb: Knex.Transaction) => const transactionalExportImportService = (txDb: Knex.Transaction) =>
createExportImportTogglesService(txDb, config); createExportImportTogglesService(txDb, config);
const transactionalFeatureToggleService = (txDb: Knex.Transaction) => const transactionalFeatureToggleService = (txDb: Knex.Transaction) =>
@ -380,6 +384,7 @@ export const createServices = (
maintenanceService, maintenanceService,
exportImportService, exportImportService,
transactionalExportImportService, transactionalExportImportService,
exportImportServiceV2,
schedulerService, schedulerService,
configurationRevisionService, configurationRevisionService,
transactionalFeatureToggleService, transactionalFeatureToggleService,

View File

@ -31,7 +31,8 @@ export type IFlagKey =
| 'privateProjects' | 'privateProjects'
| 'dependentFeatures' | 'dependentFeatures'
| 'datadogJsonTemplate' | 'datadogJsonTemplate'
| 'disableMetrics'; | 'disableMetrics'
| 'transactionalDecorator';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -147,6 +148,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS, process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
false, false,
), ),
transactionalDecorator: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_TRANSACTIONAL_DECORATOR,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -45,6 +45,7 @@ import ConfigurationRevisionService from '../features/feature-toggle/configurati
import EventAnnouncerService from 'lib/services/event-announcer-service'; import EventAnnouncerService from 'lib/services/event-announcer-service';
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service'; import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
import { WithTransactional } from 'lib/db/transaction';
export interface IUnleashServices { export interface IUnleashServices {
accessService: AccessService; accessService: AccessService;
@ -88,10 +89,13 @@ export interface IUnleashServices {
instanceStatsService: InstanceStatsService; instanceStatsService: InstanceStatsService;
favoritesService: FavoritesService; favoritesService: FavoritesService;
maintenanceService: MaintenanceService; maintenanceService: MaintenanceService;
/** @deprecated prefer exportImportServiceV2, we're doing a gradual rollout */
exportImportService: ExportImportService; exportImportService: ExportImportService;
exportImportServiceV2: WithTransactional<ExportImportService>;
configurationRevisionService: ConfigurationRevisionService; configurationRevisionService: ConfigurationRevisionService;
schedulerService: SchedulerService; schedulerService: SchedulerService;
eventAnnouncerService: EventAnnouncerService; eventAnnouncerService: EventAnnouncerService;
/** @deprecated prefer exportImportServiceV2, we're doing a gradual rollout */
transactionalExportImportService: ( transactionalExportImportService: (
db: Knex.Transaction, db: Knex.Transaction,
) => ExportImportService; ) => ExportImportService;

View File

@ -45,6 +45,7 @@ process.nextTick(async () => {
accessOverview: true, accessOverview: true,
datadogJsonTemplate: true, datadogJsonTemplate: true,
dependentFeatures: true, dependentFeatures: true,
transactionalDecorator: true,
}, },
}, },
authentication: { authentication: {