diff --git a/packages/shared/lib/db/database.ts b/packages/shared/lib/db/database.ts index e771b8ff9c..db98566b3c 100644 --- a/packages/shared/lib/db/database.ts +++ b/packages/shared/lib/db/database.ts @@ -1,5 +1,6 @@ import knex from 'knex'; import type { Knex } from 'knex'; +import { retry } from '../utils/retry.js'; export function getDbConfig({ timeoutMs }: { timeoutMs: number }): Knex.Config { return { @@ -30,7 +31,10 @@ export class KnexDatabase { } async migrate(directory: string): Promise { - return this.knex.migrate.latest({ directory: directory, tableName: '_nango_auth_migrations', schemaName: this.schema() }); + return retry(async () => await this.knex.migrate.latest({ directory: directory, tableName: '_nango_auth_migrations', schemaName: this.schema() }), { + maxAttempts: 4, + delayMs: (attempt) => 500 * attempt + }); } schema() { diff --git a/packages/shared/lib/utils/retry.ts b/packages/shared/lib/utils/retry.ts new file mode 100644 index 0000000000..cf37224813 --- /dev/null +++ b/packages/shared/lib/utils/retry.ts @@ -0,0 +1,21 @@ +interface RetryConfig { + maxAttempts: number; + delayMs: number | ((attempt: number) => number); +} + +export async function retry(fn: () => T, config: RetryConfig): Promise { + const { maxAttempts, delayMs } = config; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return fn(); + } catch (error) { + if (attempt < maxAttempts) { + const delay = typeof delayMs === 'number' ? delayMs : delayMs(attempt); + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + throw new Error('unreachable'); +} diff --git a/packages/shared/lib/utils/retry.unit.test.ts b/packages/shared/lib/utils/retry.unit.test.ts new file mode 100644 index 0000000000..75a4e29d88 --- /dev/null +++ b/packages/shared/lib/utils/retry.unit.test.ts @@ -0,0 +1,40 @@ +import { expect, describe, it } from 'vitest'; +import { retry } from './retry'; + +describe('retry', () => { + it('should retry', async () => { + let count = 0; + const result = await retry( + () => { + count++; + if (count < 3) { + throw new Error('my error'); + } + return count; + }, + { + maxAttempts: 3, + delayMs: () => 0 + } + ); + expect(result).toEqual(3); + }); + + it('should throw error after max attempts', async () => { + let count = 0; + try { + await retry( + () => { + count++; + throw new Error('my error'); + }, + { + maxAttempts: 3, + delayMs: () => 0 + } + ); + } catch (error: any) { + expect(error.message).toEqual('my error'); + } + }); +});