From 47491574600c67b354cc53373f207353dd491063 Mon Sep 17 00:00:00 2001 From: Fernando Draghi <44760248+ferdraghi@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:58:39 -0300 Subject: [PATCH] [EMBR-1909] Implement method that recovers from corrupted DB during EmbraceStorage init (#154) * Implemented method to recover from corrupted DB curing EmbraceStorage creation. * added changelog, fix broken test * print unknown error to consoleLog * Addressing PR feedback * addressing more PR feedback. * some cleanup --------- Co-authored-by: Fernando Draghi --- CHANGELOG.md | 1 + Package.swift | 3 + Sources/EmbraceStorage/EmbraceStorage.swift | 50 ++++++++++++++-- .../EmbraceStorageTests.swift | 55 ++++++++++++++++++ .../Mocks/db_corrupted.sqlite | Bin 0 -> 49376 bytes 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 Tests/EmbraceStorageTests/Mocks/db_corrupted.sqlite diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29b..67634c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Recover from corrupted DB during EmbraceStorage init. diff --git a/Package.swift b/Package.swift index 85efc1f7..c78aa54c 100644 --- a/Package.swift +++ b/Package.swift @@ -163,6 +163,9 @@ let package = Package( .testTarget( name: "EmbraceStorageTests", dependencies: ["EmbraceStorage", "TestSupport"], + resources: [ + .copy("Mocks/") + ], plugins: targetPlugins ), diff --git a/Sources/EmbraceStorage/EmbraceStorage.swift b/Sources/EmbraceStorage/EmbraceStorage.swift index 3ab874d5..65242e7a 100644 --- a/Sources/EmbraceStorage/EmbraceStorage.swift +++ b/Sources/EmbraceStorage/EmbraceStorage.swift @@ -3,12 +3,12 @@ // import Foundation +import EmbraceCommon import GRDB /// Class in charge of storing all the data captured by the Embrace SDK. /// It provides an abstraction layer over a GRDB SQLite database. public class EmbraceStorage { - public private(set) var options: Options public private(set) var dbQueue: DatabaseQueue @@ -21,14 +21,14 @@ public class EmbraceStorage { if case let .inMemory(name) = options.storageMechanism { dbQueue = try DatabaseQueue(named: name) - } else if case let .onDisk(baseURL, fileName) = options.storageMechanism { // create base directory if necessary try FileManager.default.createDirectory(at: baseURL, withIntermediateDirectories: true) // create sqlite file - let filepath = baseURL.appendingPathComponent(fileName).path - dbQueue = try DatabaseQueue(path: filepath) + let filepath = baseURL.appendingPathComponent(fileName) + + dbQueue = try EmbraceStorage.getDBQueueIfPossible(at: filepath) } else { fatalError("Unsupported storage mechansim added") } @@ -40,6 +40,48 @@ public class EmbraceStorage { try ResourceRecord.defineTable(db: db) } } + + /// Will attempt to create or open the DB File. If first attempt fails due to GRDB error, it'll assume the existing DB is corruped and try again after deleting the existing DB file. + private static func getDBQueueIfPossible(at fileURL: URL) throws -> DatabaseQueue { + do { + return try DatabaseQueue(path: fileURL.path) + } catch { + if let dbError = error as? DatabaseError { + ConsoleLog.error(""" +GRDB Failed to initialize EmbraceStorage. +Will attempt to remove existing file and create a new DB. +Message: \(dbError.message ?? "[empty message]"), +Result Code: \(dbError.resultCode), +SQLite Extended Code: \(dbError.extendedResultCode) +""") + } else { + ConsoleLog.error(""" +Unknown error while trying to initialize EmbraceStorage: \(error) +Will attempt to recover by deleting existing DB. +""") + } + } + + try EmbraceStorage.deleteDBFile(at: fileURL) + + return try DatabaseQueue(path: fileURL.path) + } + + /// Will attempt to delete the provided file. + private static func deleteDBFile(at fileURL: URL) throws { + do { + let fileURL = URL(fileURLWithPath: fileURL.path) + try FileManager.default.removeItem(at: fileURL) + } catch let error { + ConsoleLog.error(""" +EmbraceStorage failed to remove DB file. +Domain: \(error._domain) +Code: \(error._code) +Filepath: \(fileURL) +""") + } + } + } // MARK: - Sync operations diff --git a/Tests/EmbraceStorageTests/EmbraceStorageTests.swift b/Tests/EmbraceStorageTests/EmbraceStorageTests.swift index 315e50b6..823ca3e8 100644 --- a/Tests/EmbraceStorageTests/EmbraceStorageTests.swift +++ b/Tests/EmbraceStorageTests/EmbraceStorageTests.swift @@ -4,6 +4,7 @@ import XCTest import TestSupport +import GRDB @testable import EmbraceStorage class EmbraceStorageTests: XCTestCase { @@ -152,4 +153,58 @@ class EmbraceStorageTests: XCTestCase { wait(for: [expectation2], timeout: .defaultTimeout) } + + func test_corruptedDbAction() { + guard let corruptedDb = EmbraceStorageTests.prepareCorruptedDBForTest() else { + return XCTFail("\(#function): Failed to create corrupted DB for test") + } + + let dbBaseUrl = corruptedDb.deletingLastPathComponent() + let dbFile = corruptedDb.lastPathComponent + + /// Make sure the target DB is corrupted and GRDB returns the expected result when trying to load it. + let corruptedAttempt = Result(catching: { try DatabaseQueue(path: corruptedDb.absoluteString) }) + if case let .failure(error as DatabaseError) = corruptedAttempt { + XCTAssertEqual(error.resultCode, .SQLITE_CORRUPT) + } else { + XCTFail("\(#function): Failed to load a corrupted db for test.") + } + + /// Attempting to create an EmbraceStorage with the corrupted DB should result in a valid storage creation + let storeCreationAttempt = Result(catching: { + try EmbraceStorage(options: .init(baseUrl: dbBaseUrl, fileName: dbFile)) + }) + if case let .failure(error) = storeCreationAttempt { + XCTFail("\(#function): EmbraceStorage failed to recover from corrupted existing DB: \(error)") + } + + /// Then the corrupted DB should've been corrected and now GRDB should be able to load it. + let fixedAttempt = Result(catching: { try DatabaseQueue(path: corruptedDb.absoluteString) }) + if case let .failure(error) = fixedAttempt { + XCTFail("\(#function): DB Is still corrupted after it should've been fixed: \(error)") + } + } + + static func prepareCorruptedDBForTest() -> URL? { + guard + let resourceUrl = Bundle.module.path(forResource: "db_corrupted", ofType: "sqlite", inDirectory: "Mocks"), + let corruptedDbPath = URL(string: "file://\(resourceUrl)") + else { + return nil + } + + let copyCorruptedPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("db.sqlite") + + do { + if FileManager.default.fileExists(atPath: copyCorruptedPath.path) { + try FileManager.default.removeItem(at: copyCorruptedPath) + } + try FileManager.default.copyItem(at: corruptedDbPath, to: copyCorruptedPath) + return copyCorruptedPath + } catch let e { + print("\(#function): error creating corrupt db: \(e)") + return nil + } + + } } diff --git a/Tests/EmbraceStorageTests/Mocks/db_corrupted.sqlite b/Tests/EmbraceStorageTests/Mocks/db_corrupted.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..e205fddf938bb2b3e2900ec20bfbefc6910d45f0 GIT binary patch literal 49376 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 w($LDu;jDnh%AIpa&_do;5?UwQNR literal 0 HcmV?d00001