mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2026-08-31 19:03:45 +02:00
108 lines
3.5 KiB
TypeScript
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 ]
|
|
}
|
|
}
|
|
})
|
|
}
|