1
0
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:
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,
"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": {

View File

@ -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;
}

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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,

View File

@ -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 = {

View File

@ -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;

View File

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