mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-11 00:08:30 +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:
parent
630028acba
commit
0da48cc0d1
@ -105,6 +105,7 @@ exports[`should create default config 1`] = `
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
"strictSchemaValidation": false,
|
||||
"transactionalDecorator": false,
|
||||
"variantTypeNumber": false,
|
||||
},
|
||||
},
|
||||
@ -144,6 +145,7 @@ exports[`should create default config 1`] = `
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
"strictSchemaValidation": false,
|
||||
"transactionalDecorator": false,
|
||||
"variantTypeNumber": false,
|
||||
},
|
||||
"externalResolver": {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
createFakePrivateProjectChecker,
|
||||
createPrivateProjectChecker,
|
||||
} from '../private-project/createPrivateProjectChecker';
|
||||
import { DbServiceFactory } from 'lib/db/transaction';
|
||||
|
||||
export const createFakeExportImportTogglesService = (
|
||||
config: IUnleashConfig,
|
||||
@ -127,109 +128,121 @@ export const createFakeExportImportTogglesService = (
|
||||
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 = (
|
||||
db: Db,
|
||||
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);
|
||||
|
||||
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;
|
||||
const unboundService = deferredExportImportTogglesService(config);
|
||||
return unboundService(db);
|
||||
};
|
||||
|
@ -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,
|
||||
@ -28,14 +32,19 @@ 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(
|
||||
@ -43,10 +52,12 @@ class ExportImportController extends Controller {
|
||||
{
|
||||
exportImportService,
|
||||
transactionalExportImportService,
|
||||
exportImportServiceV2,
|
||||
openApiService,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
| 'exportImportService'
|
||||
| 'exportImportServiceV2'
|
||||
| 'openApiService'
|
||||
| 'transactionalExportImportService'
|
||||
>,
|
||||
@ -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({
|
||||
@ -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,
|
||||
@ -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,
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
@ -50,8 +50,10 @@ import { Knex } from 'knex';
|
||||
import {
|
||||
createExportImportTogglesService,
|
||||
createFakeExportImportTogglesService,
|
||||
deferredExportImportTogglesService,
|
||||
} from '../features/export-import-toggles/createExportImportService';
|
||||
import { Db } from '../db/db';
|
||||
import { withFakeTransactional, withTransactional } from '../db/transaction';
|
||||
import {
|
||||
createChangeRequestAccessReadModel,
|
||||
createFakeChangeRequestAccessService,
|
||||
@ -274,10 +276,12 @@ export const createServices = (
|
||||
projectService,
|
||||
);
|
||||
|
||||
// TODO: this is a temporary seam to enable packaging by feature
|
||||
const exportImportService = db
|
||||
? createExportImportTogglesService(db, config)
|
||||
: createFakeExportImportTogglesService(config);
|
||||
const exportImportServiceV2 = db
|
||||
? withTransactional(deferredExportImportTogglesService(config), db)
|
||||
: withFakeTransactional(createFakeExportImportTogglesService(config));
|
||||
const transactionalExportImportService = (txDb: Knex.Transaction) =>
|
||||
createExportImportTogglesService(txDb, config);
|
||||
const transactionalFeatureToggleService = (txDb: Knex.Transaction) =>
|
||||
@ -380,6 +384,7 @@ export const createServices = (
|
||||
maintenanceService,
|
||||
exportImportService,
|
||||
transactionalExportImportService,
|
||||
exportImportServiceV2,
|
||||
schedulerService,
|
||||
configurationRevisionService,
|
||||
transactionalFeatureToggleService,
|
||||
|
@ -31,7 +31,8 @@ export type IFlagKey =
|
||||
| 'privateProjects'
|
||||
| 'dependentFeatures'
|
||||
| 'datadogJsonTemplate'
|
||||
| 'disableMetrics';
|
||||
| 'disableMetrics'
|
||||
| 'transactionalDecorator';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -147,6 +148,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_DISABLE_METRICS,
|
||||
false,
|
||||
),
|
||||
transactionalDecorator: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_TRANSACTIONAL_DECORATOR,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -45,6 +45,7 @@ import ConfigurationRevisionService from '../features/feature-toggle/configurati
|
||||
import EventAnnouncerService from 'lib/services/event-announcer-service';
|
||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||
import { WithTransactional } from 'lib/db/transaction';
|
||||
|
||||
export interface IUnleashServices {
|
||||
accessService: AccessService;
|
||||
@ -88,10 +89,13 @@ export interface IUnleashServices {
|
||||
instanceStatsService: InstanceStatsService;
|
||||
favoritesService: FavoritesService;
|
||||
maintenanceService: MaintenanceService;
|
||||
/** @deprecated prefer exportImportServiceV2, we're doing a gradual rollout */
|
||||
exportImportService: ExportImportService;
|
||||
exportImportServiceV2: WithTransactional<ExportImportService>;
|
||||
configurationRevisionService: ConfigurationRevisionService;
|
||||
schedulerService: SchedulerService;
|
||||
eventAnnouncerService: EventAnnouncerService;
|
||||
/** @deprecated prefer exportImportServiceV2, we're doing a gradual rollout */
|
||||
transactionalExportImportService: (
|
||||
db: Knex.Transaction,
|
||||
) => ExportImportService;
|
||||
|
@ -45,6 +45,7 @@ process.nextTick(async () => {
|
||||
accessOverview: true,
|
||||
datadogJsonTemplate: true,
|
||||
dependentFeatures: true,
|
||||
transactionalDecorator: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user