From c0df1aa65c8083f3bcbf73910ef88dbf608cd55c Mon Sep 17 00:00:00 2001 From: Jogchum Koerts Date: Thu, 8 Aug 2024 13:49:30 +0200 Subject: [PATCH] Retry transaction also for SQLITE_BUSY (#23243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pascal Jufer Co-authored-by: Hannes Küttner --- .changeset/stale-eyes-complain.md | 5 ++++ api/src/utils/transaction.ts | 50 ++++++++++++++++++++++--------- contributors.yml | 1 + 3 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 .changeset/stale-eyes-complain.md diff --git a/.changeset/stale-eyes-complain.md b/.changeset/stale-eyes-complain.md new file mode 100644 index 0000000000..cc819db4c4 --- /dev/null +++ b/.changeset/stale-eyes-complain.md @@ -0,0 +1,5 @@ +--- +'@directus/api': minor +--- + +Added transaction retry mechanism for SQLite if a `SQLITE_BUSY` errors occurs diff --git a/api/src/utils/transaction.ts b/api/src/utils/transaction.ts index 25d2b44ae9..512af45a71 100644 --- a/api/src/utils/transaction.ts +++ b/api/src/utils/transaction.ts @@ -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 (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 (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 (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)) + ); +} diff --git a/contributors.yml b/contributors.yml index 818ade093d..55885b302e 100644 --- a/contributors.yml +++ b/contributors.yml @@ -147,3 +147,4 @@ - jacobwise - McSundae - danilobuerger +- joggienl