From d779175b90bc70b7f6c901cbe60c5a674caceb72 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 6 Apr 2024 02:04:28 -0400 Subject: [PATCH] SQlite Retry logic for database lock --- src/db/services/Database.ts | 68 ++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/src/db/services/Database.ts b/src/db/services/Database.ts index 5e1b263..ef31ad2 100644 --- a/src/db/services/Database.ts +++ b/src/db/services/Database.ts @@ -42,14 +42,16 @@ export class DatabaseService { * } */ public static async loadGameState(authorId: number): Promise { - const db = await DatabaseService.getDatabase(); - const sql = `SELECT data FROM game_state WHERE authorId = ? AND active = TRUE`; - const row = await db.get(sql, [authorId]); - if (row) { - return JSON.parse(row.data) as GameState; - } else { - return null; - } + return this.withRetry(async () => { + const db = await DatabaseService.getDatabase(); + const sql = `SELECT data FROM game_state WHERE authorId = ? AND active = TRUE`; + const row = await db.get(sql, [authorId]); + if (row) { + return JSON.parse(row.data) as GameState; + } else { + return null; + } + }); } /** @@ -63,15 +65,17 @@ export class DatabaseService { * await DatabaseService.saveGameState(1, currentGameState); */ public static async saveGameState(authorId: number, gameState: GameState): Promise { - const db = await DatabaseService.getDatabase(); - const data = JSON.stringify(gameState); - const sql = ` + return this.withRetry(async () => { + const db = await DatabaseService.getDatabase(); + const data = JSON.stringify(gameState); + const sql = ` INSERT INTO game_state (authorId, data, active) VALUES (?, ?, TRUE) ON CONFLICT(authorId) DO UPDATE SET data = excluded.data WHERE active = TRUE; - `; - await db.run(sql, [authorId, data]); + `; + await db.run(sql, [authorId, data]); + }); } /** @@ -84,9 +88,41 @@ export class DatabaseService { * await DatabaseService.resetGameState(1); */ public static async resetGameState(authorId: number): Promise { - const db = await DatabaseService.getDatabase(); - const sql = `UPDATE game_state SET active = FALSE WHERE authorId = ? AND active = TRUE`; - await db.run(sql, [authorId]); + return this.withRetry(async () => { + const db = await this.getDatabase(); + const sql = `UPDATE game_state SET active = FALSE WHERE authorId = ? AND active = TRUE`; + await db.run(sql, [authorId]); + }); } + /** + * Attempts to execute a database operation with retries on failure. + * This method is designed to handle SQLITE_BUSY errors by retrying the operation + * with an exponential backoff strategy. This is particularly useful for handling + * cases where the SQLite database is locked due to concurrent access attempts. + * + * @param operation A function that performs the database operation and returns a Promise. + * @param maxRetries The maximum number of retries before giving up. Defaults to 5. + * @param delay The initial delay (in milliseconds) before the first retry. This delay is doubled with each retry. + * @returns A Promise that resolves with the result of the operation, or rejects if the operation fails after all retries. + * @throws Will throw an error if the operation cannot be completed successfully within the allowed number of retries. + */ + private static async withRetry(operation: () => Promise, maxRetries: number = 5, delay: number = 100): Promise { + for (let i = 0; i < maxRetries; i++) { + try { + return await operation(); + } catch (error) { + console.log('Error', error) + // Type check or assertion to handle the error as an object with a 'code' property + const e = error as { code?: string }; + if (e.code === 'SQLITE_BUSY') { + console.log(`Database is busy, retrying... Attempt ${i + 1}`); + await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))); + } else { + throw error; // Rethrow if it's not an SQLITE_BUSY error + } + } + } + throw new Error('Max retries reached for database operation'); + } }