From f5198c7c0b1bbd7e6703406d1b639930650820af Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 7 Nov 2022 01:00:00 +0100 Subject: [PATCH] Retry transactions --- src/database/main.ts | 48 ++++++++++++++++++++++++++++---- src/database/utils/serialized.ts | 4 +-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/database/main.ts b/src/database/main.ts index 0ad7465..8da2766 100644 --- a/src/database/main.ts +++ b/src/database/main.ts @@ -42,6 +42,7 @@ import { createU2fKeyModel, U2fKeyModelStatic } from './u2fkey' import { createUsedTimeModel, UsedTimeModelStatic } from './usedtime' import { createUserModel, UserModelStatic } from './user' import { createUserLimitLoginCategoryModel, UserLimitLoginCategoryModelStatic } from './userlimitlogincategory' +import { shouldRetryWithException } from './utils/serialized' export type Transaction = Sequelize.Transaction @@ -71,10 +72,18 @@ export interface Database { usedTime: UsedTimeModelStatic user: UserModelStatic userLimitLoginCategory: UserLimitLoginCategoryModelStatic - transaction: (autoCallback: (t: Transaction) => Promise, options?: { transaction: Transaction }) => Promise + transaction: ( + autoCallback: (t: Transaction) => Promise, + options?: TransactionOptions + ) => Promise dialect: string } +interface TransactionOptions { + transaction?: Transaction + disableRetry?: boolean +} + const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({ addDeviceToken: createAddDeviceTokenModel(sequelize), authtoken: createAuthtokenModel(sequelize), @@ -101,10 +110,39 @@ const createDatabase = (sequelize: Sequelize.Sequelize): Database => ({ usedTime: createUsedTimeModel(sequelize), user: createUserModel(sequelize), userLimitLoginCategory: createUserLimitLoginCategoryModel(sequelize), - transaction: (autoCallback: (transaction: Transaction) => Promise, options?: { transaction: Transaction }) => (sequelize.transaction({ - isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE, - transaction: options?.transaction - }, autoCallback)) as Promise, + async transaction( + autoCallback: (transaction: Transaction) => Promise, + options?: TransactionOptions + ): Promise { + const runAttempt = () => sequelize.transaction({ + isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE, + transaction: options?.transaction + }, autoCallback) + + const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time)) + + try { + return await runAttempt() + } catch (ex) { + if ( + options?.disableRetry || + options?.transaction || + !shouldRetryWithException(this, ex) + ) throw ex + } + + await delay(10 * (1 + Math.random())) + + try { + return await runAttempt() + } catch (ex) { + if (!shouldRetryWithException(this, ex)) throw ex + } + + await delay(100 * (1 + Math.random())) + + return await runAttempt() + }, dialect: sequelize.getDialect() }) diff --git a/src/database/utils/serialized.ts b/src/database/utils/serialized.ts index feaf579..c05d340 100644 --- a/src/database/utils/serialized.ts +++ b/src/database/utils/serialized.ts @@ -86,8 +86,8 @@ export async function assertSerializeableTransactionsAreWorking (database: Datab await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo }) })() ]) - }) - }) + }, { disableRetry: true }) + }, { disableRetry: true }) throw new SerializationFeatureCheckException() } catch (ex) {