1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00
unleash.unleash/src/lib/db/transaction.ts

94 lines
3.4 KiB
TypeScript
Raw Normal View History

2023-02-16 08:08:51 +01:00
import { Knex } from 'knex';
import { IUnleashConfig } from 'lib/server-impl';
2023-02-16 08:08:51 +01:00
export type KnexTransaction = Knex.Transaction;
export type MockTransaction = null;
export type UnleashTransaction = KnexTransaction | MockTransaction;
export type TransactionCreator<S> = <T>(
scope: (trx: S) => void | Promise<T>,
) => Promise<T>;
export const createKnexTransactionStarter = (
knex: Knex,
): TransactionCreator<UnleashTransaction> => {
function transaction<T>(
scope: (trx: KnexTransaction) => void | Promise<T>,
) {
if (!knex) {
console.warn(
'It looks like your DB is not provided. Very often it is a test setup problem in setupAppWithCustomConfig',
);
}
2023-02-16 08:08:51 +01:00
return knex.transaction(scope);
}
return transaction;
};
export type DeferredServiceFactory<S> = (db: Knex) => S;
/**
* Services need to be instantiated with a knex instance on a per-transaction basis.
* Limiting the input parameters, makes sure we don't inject already instantiated services
* that might be bound to a different transaction.
*/
export type ServiceFactory<S> = (
config: IUnleashConfig,
) => DeferredServiceFactory<S>;
export type WithTransactional<S> = S & {
transactional: <R>(fn: (service: S) => R) => Promise<R>;
};
chore: handle transactions already started at the controller layer (#4953) ## About the changes This PR adds a method to safeguard us from opening a new transaction while inside another transaction, resulting in two isolated transactions that will not be atomic (if one fails, the other might still complete successfully). https://github.com/knex/knex/blob/bbbe4d4637b3838e4a297a457460cd2c76a700d5/lib/knex-builder/make-knex.js#L143C5-L144C88 We're currently opening transactions at the controller layer https://github.com/Unleash/unleash/blob/2746bd151766f8afbbaa2f640e8ebee6f4f98086/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208 but in some other places, we do it at the store level: https://github.com/Unleash/unleash/blob/2746bd151766f8afbbaa2f640e8ebee6f4f98086/src/lib/db/access-store.ts#L577 ## Alternative We can remove store-level transactions and move them to the controller following this approach: https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/services/index.ts#L282-L284 https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208 This option is more expensive because we have to: 1. Write the factory methods that propagate the transaction to the stores (therefore creating the store factory methods as well) 2. Identify the methods for creating the transactions at the store level and backtrack the calls until the controller layer
2023-10-06 13:38:32 +02:00
/**
* @deprecated this is a temporary solution to deal with transactions at the store level.
chore: handle transactions already started at the controller layer (#4953) ## About the changes This PR adds a method to safeguard us from opening a new transaction while inside another transaction, resulting in two isolated transactions that will not be atomic (if one fails, the other might still complete successfully). https://github.com/knex/knex/blob/bbbe4d4637b3838e4a297a457460cd2c76a700d5/lib/knex-builder/make-knex.js#L143C5-L144C88 We're currently opening transactions at the controller layer https://github.com/Unleash/unleash/blob/2746bd151766f8afbbaa2f640e8ebee6f4f98086/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208 but in some other places, we do it at the store level: https://github.com/Unleash/unleash/blob/2746bd151766f8afbbaa2f640e8ebee6f4f98086/src/lib/db/access-store.ts#L577 ## Alternative We can remove store-level transactions and move them to the controller following this approach: https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/services/index.ts#L282-L284 https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208 This option is more expensive because we have to: 1. Write the factory methods that propagate the transaction to the stores (therefore creating the store factory methods as well) 2. Identify the methods for creating the transactions at the store level and backtrack the calls until the controller layer
2023-10-06 13:38:32 +02:00
* Ideally, we should handle transactions at the service level (each service method should be transactional).
* The controller should define the transactional scope as follows:
* https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208
*
* To be able to use .transactional method, services should be instantiated like this:
* https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/services/index.ts#L282-L284
*
* This function makes sure that `fn` is executed in a transaction.
* If the db is already in a transaction, it will execute `fn` in that transactional scope.
*
* https://github.com/knex/knex/blob/bbbe4d4637b3838e4a297a457460cd2c76a700d5/lib/knex-builder/make-knex.js#L143C5-L144C88
*/
export async function inTransaction<R>(
db: Knex,
fn: (db: Knex) => R,
): Promise<R> {
if (db.isTransaction) {
return fn(db);
}
return db.transaction(async (tx) => fn(tx));
}
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) =>
chore: handle transactions already started at the controller layer (#4953) ## About the changes This PR adds a method to safeguard us from opening a new transaction while inside another transaction, resulting in two isolated transactions that will not be atomic (if one fails, the other might still complete successfully). https://github.com/knex/knex/blob/bbbe4d4637b3838e4a297a457460cd2c76a700d5/lib/knex-builder/make-knex.js#L143C5-L144C88 We're currently opening transactions at the controller layer https://github.com/Unleash/unleash/blob/2746bd151766f8afbbaa2f640e8ebee6f4f98086/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208 but in some other places, we do it at the store level: https://github.com/Unleash/unleash/blob/2746bd151766f8afbbaa2f640e8ebee6f4f98086/src/lib/db/access-store.ts#L577 ## Alternative We can remove store-level transactions and move them to the controller following this approach: https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/services/index.ts#L282-L284 https://github.com/Unleash/unleash/blob/cb034976b93abc799df774858d716a49f645d669/src/lib/features/export-import-toggles/export-import-controller.ts#L206-L208 This option is more expensive because we have to: 1. Write the factory methods that propagate the transaction to the stores (therefore creating the store factory methods as well) 2. Identify the methods for creating the transactions at the store level and backtrack the calls until the controller layer
2023-10-06 13:38:32 +02:00
// Maybe: inTransaction(db, async (trx: Knex.Transaction) => fn(serviceFactory(trx)));
// this assumes that the caller didn't start a transaction already and opens a new one.
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;
}