1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01: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,109 +128,121 @@ export const createFakeExportImportTogglesService = (
return exportImportService; return exportImportService;
}; };
export const deferredExportImportTogglesService = (
config: IUnleashConfig,
): 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 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,
eventService,
contextService,
strategyService,
tagTypeService,
},
);
return exportImportService;
};
};
export const createExportImportTogglesService = ( export const createExportImportTogglesService = (
db: Db, db: Db,
config: IUnleashConfig, config: IUnleashConfig,
): ExportImportService => { ): ExportImportService => {
const { eventBus, getLogger, flagResolver } = config; const unboundService = deferredExportImportTogglesService(config);
const importTogglesStore = new ImportTogglesStore(db); return unboundService(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 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,
eventService,
contextService,
strategyService,
tagTypeService,
},
);
return exportImportService;
}; };

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,9 +163,17 @@ 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) =>
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( this.openApiService.respondWithValidation(
200, 200,
@ -172,10 +198,20 @@ class ExportImportController extends Controller {
const dto = req.body; const dto = req.body;
await this.startTransaction(async (tx) => const useTransactionalDecorator = this.config.flagResolver.isEnabled(
this.transactionalExportImportService(tx).import(dto, user), '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(); 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: {