Files
timelimit-server/src/database/utils/serialized.ts
T
2022-11-07 01:00:00 +01:00

108 lines
3.5 KiB
TypeScript

/*
* server component for the TimeLimit App
* Copyright (C) 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sequelize from 'sequelize'
import { configItemIds } from '../config'
import { Database } from '../main'
export class SerializationFeatureCheckException extends Error {}
export function shouldRetryWithException (database: Database, e: unknown): boolean {
if (e instanceof Sequelize.TimeoutError) return true
if (!(e instanceof Sequelize.DatabaseError)) return false
const parent = e.parent
if (database.dialect === 'sqlite') {
if (parent.message.startsWith('SQLITE_BUSY:')) return true
} else if (database.dialect === 'postgres') {
// 40001 = serialization_failure
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((parent as any).code === '40001') return true
// 40P01 = deadlock detected
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((parent as any).code === '40P01') return true
} else if (database.dialect === 'mariadb') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errno = (parent as any).errno
// ER_LOCK_DEADLOCK
// Deadlock found when trying to get lock; try restarting transaction
if (errno === 1213) return true
}
return false
}
export async function assertSerializeableTransactionsAreWorking (database: Database) {
// clean up just for the case
await database.config.destroy({
where: {
id: {
[Sequelize.Op.in]: [ configItemIds.selfTestData, configItemIds.secondSelfTestData ]
}
}
})
// insert specific data
await database.config.bulkCreate([
{
id: configItemIds.selfTestData,
value: '123'
},
{
id: configItemIds.secondSelfTestData,
value: '456'
}
])
try {
// use two parallel transactions
await database.transaction(async (transactionOne) => {
await database.transaction(async (transactionTwo) => {
await database.config.findAll({ transaction: transactionOne })
await database.config.findAll({ transaction: transactionTwo })
await Promise.all([
(async () => {
await database.config.update({ value: 'c' }, { where: { id: configItemIds.selfTestData }, transaction: transactionOne })
})(),
(async () => {
await database.config.update({ value: 'd' }, { where: { id: configItemIds.secondSelfTestData }, transaction: transactionTwo })
})()
])
}, { disableRetry: true })
}, { disableRetry: true })
throw new SerializationFeatureCheckException()
} catch (ex) {
if (!shouldRetryWithException(database, ex)) {
throw new SerializationFeatureCheckException()
}
}
// finish clean up
await database.config.destroy({
where: {
id: {
[Sequelize.Op.in]: [ configItemIds.selfTestData, configItemIds.secondSelfTestData ]
}
}
})
}