Client-side restart of failed transactions on CockroachDB (#22240)

This commit is contained in:
Pascal Jufer
2024-04-25 14:19:17 +02:00
committed by GitHub
parent dd39af2c29
commit 4b75c375df
2 changed files with 49 additions and 3 deletions

View File

@@ -0,0 +1,5 @@
---
'@directus/api': patch
---
Implemented client-side restart of failed transactions for CockroachDB

View File

@@ -1,4 +1,6 @@
import type { Knex } from 'knex';
import { type Knex } from 'knex';
import { getDatabaseClient } from '../database/index.js';
import { useLogger } from '../logger.js';
/**
* Execute the given handler within the current transaction or a newly created one
@@ -7,10 +9,49 @@ import type { Knex } from 'knex';
* Can be used to ensure the handler is run within a transaction,
* while preventing nested transactions.
*/
export const transaction = <T = unknown>(knex: Knex, handler: (knex: Knex) => Promise<T>): Promise<T> => {
export const transaction = async <T = unknown>(knex: Knex, handler: (knex: Knex) => Promise<T>): Promise<T> => {
if (knex.isTransaction) {
return handler(knex);
} else {
return knex.transaction((trx) => handler(trx));
try {
return await knex.transaction((trx) => handler(trx));
} catch (error: any) {
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;
const MAX_ATTEMPTS = 3;
const BASE_DELAY = 100;
const logger = useLogger();
for (let attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
const delay = 2 ** attempt * BASE_DELAY;
await new Promise((resolve) => setTimeout(resolve, delay));
logger.trace(`Restarting failed transaction (attempt ${attempt + 1}/${MAX_ATTEMPTS})`);
try {
return await knex.transaction((trx) => handler(trx));
} catch (error: any) {
if (error?.code !== COCKROACH_RETRY_ERROR_CODE) throw error;
}
}
/** Initial execution + additional attempts */
const attempts = 1 + MAX_ATTEMPTS;
throw new Error(`Transaction failed after ${attempts} attempts`, { cause: error });
}
}
};