Retry transaction also for SQLITE_BUSY (#23243)

Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
Co-authored-by: Hannes Küttner <kuettner.hannes@gmail.com>
This commit is contained in:
Jogchum Koerts
2024-08-08 13:49:30 +02:00
committed by GitHub
parent 7d22f32443
commit c0df1aa65c
3 changed files with 42 additions and 14 deletions

View File

@@ -0,0 +1,5 @@
---
'@directus/api': minor
---
Added transaction retry mechanism for SQLite if a `SQLITE_BUSY` errors occurs

View File

@@ -1,6 +1,8 @@
import { isObject } from '@directus/utils';
import { type Knex } from 'knex';
import { getDatabaseClient } from '../database/index.js';
import { useLogger } from '../logger/index.js';
import type { DatabaseClient } from '../types/index.js';
/**
* Execute the given handler within the current transaction or a newly created one
@@ -15,20 +17,10 @@ export const transaction = async <T = unknown>(knex: Knex, handler: (knex: Knex)
} else {
try {
return await knex.transaction((trx) => handler(trx));
} catch (error: any) {
} catch (error) {
const client = getDatabaseClient(knex);
/**
* This error code indicates that the transaction failed due to another
* concurrent or recent transaction attempting to write to the same data.
* This can usually be solved by restarting the transaction on client-side
* after a short delay, so that it is executed against the latest state.
*
* @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
*/
const COCKROACH_RETRY_ERROR_CODE = '40001';
if (client !== 'cockroachdb' || error?.code !== COCKROACH_RETRY_ERROR_CODE) throw error;
if (!shouldRetryTransaction(client, error)) throw error;
const MAX_ATTEMPTS = 3;
const BASE_DELAY = 100;
@@ -44,8 +36,8 @@ export const transaction = async <T = unknown>(knex: Knex, handler: (knex: Knex)
try {
return await knex.transaction((trx) => handler(trx));
} catch (error: any) {
if (error?.code !== COCKROACH_RETRY_ERROR_CODE) throw error;
} catch (error) {
if (!shouldRetryTransaction(client, error)) throw error;
}
}
@@ -55,3 +47,33 @@ export const transaction = async <T = unknown>(knex: Knex, handler: (knex: Knex)
}
}
};
function shouldRetryTransaction(client: DatabaseClient, error: unknown): boolean {
/**
* This error code indicates that the transaction failed due to another
* concurrent or recent transaction attempting to write to the same data.
* This can usually be solved by restarting the transaction on client-side
* after a short delay, so that it is executed against the latest state.
*
* @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
*/
const COCKROACH_RETRY_ERROR_CODE = '40001';
/**
* SQLITE_BUSY is an error code returned by SQLite when an operation can't be
* performed due to a locked database file. This often arises due to multiple
* processes trying to simultaneously access the database, causing potential
* data inconsistencies. There are a few mechanisms to handle this case,
* one of which is to retry the complete transaction again
* on client-side after a short delay.
*
* @link https://www.sqlite.org/rescode.html#busy
*/
const SQLITE_BUSY_ERROR_CODE = 'SQLITE_BUSY';
return (
isObject(error) &&
((client === 'cockroachdb' && error['code'] === COCKROACH_RETRY_ERROR_CODE) ||
(client === 'sqlite' && error['code'] === SQLITE_BUSY_ERROR_CODE))
);
}

View File

@@ -147,3 +147,4 @@
- jacobwise
- McSundae
- danilobuerger
- joggienl