Skip to content

Commit

Permalink
[EMBR-1909] Implement method that recovers from corrupted DB during E…
Browse files Browse the repository at this point in the history
…mbraceStorage 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 <fdraghi@FDraghi-Embrace-MBP.local>
  • Loading branch information
ferdraghi and Fernando Draghi authored Feb 6, 2024
1 parent b8afd0b commit 4749157
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Recover from corrupted DB during EmbraceStorage init.
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ let package = Package(
.testTarget(
name: "EmbraceStorageTests",
dependencies: ["EmbraceStorage", "TestSupport"],
resources: [
.copy("Mocks/")
],
plugins: targetPlugins
),

Expand Down
50 changes: 46 additions & 4 deletions Sources/EmbraceStorage/EmbraceStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
}
Expand All @@ -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
Expand Down
55 changes: 55 additions & 0 deletions Tests/EmbraceStorageTests/EmbraceStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import XCTest
import TestSupport
import GRDB
@testable import EmbraceStorage

class EmbraceStorageTests: XCTestCase {
Expand Down Expand Up @@ -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
}

}
}
Binary file not shown.

0 comments on commit 4749157

Please sign in to comment.