|
|
|
@ -29,6 +29,9 @@ open class Storage {
|
|
|
|
|
/// When attempting to do a write the transaction will wait this long to acquite a lock before failing
|
|
|
|
|
private static let writeTransactionStartTimeout: TimeInterval = 5
|
|
|
|
|
|
|
|
|
|
/// If a transaction takes longer than this duration then we should fail the transaction rather than keep hanging
|
|
|
|
|
private static let transactionDeadlockTimeoutSeconds: Int = 5
|
|
|
|
|
|
|
|
|
|
private static var sharedDatabaseDirectoryPath: String { "\(FileManager.default.appSharedDataDirectoryPath)/database" }
|
|
|
|
|
private static var databasePath: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)" }
|
|
|
|
|
private static var databasePathShm: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-shm" }
|
|
|
|
@ -322,17 +325,16 @@ open class Storage {
|
|
|
|
|
guard async else { return migrationCompleted(Result(try migrator.migrate(dbWriter))) }
|
|
|
|
|
|
|
|
|
|
migrator.asyncMigrate(dbWriter) { result in
|
|
|
|
|
let finalResult: Swift.Result<Void, Error> = {
|
|
|
|
|
let finalResult: Result<Void, Error> = {
|
|
|
|
|
switch result {
|
|
|
|
|
case .failure(let error): return .failure(error)
|
|
|
|
|
case .success: return .success(())
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Note: We need to dispatch this after a small 0.01 delay to prevent any potential
|
|
|
|
|
// re-entrancy issues since the 'asyncMigrate' returns a result containing a DB instance
|
|
|
|
|
// within a transaction
|
|
|
|
|
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.01, using: dependencies) {
|
|
|
|
|
// Note: We need to dispatch this to the next run toop to prevent blocking if the callback
|
|
|
|
|
// performs subsequent database operations
|
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async(using: dependencies) {
|
|
|
|
|
migrationCompleted(finalResult)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -535,13 +537,16 @@ open class Storage {
|
|
|
|
|
|
|
|
|
|
static func logIfNeeded(_ error: Error, isWrite: Bool) {
|
|
|
|
|
switch error {
|
|
|
|
|
case DatabaseError.SQLITE_ABORT, DatabaseError.SQLITE_INTERRUPT:
|
|
|
|
|
case DatabaseError.SQLITE_ABORT, DatabaseError.SQLITE_INTERRUPT, DatabaseError.SQLITE_ERROR:
|
|
|
|
|
let message: String = ((error as? DatabaseError)?.message ?? "Unknown")
|
|
|
|
|
Log.error("[Storage] Database \(isWrite ? "write" : "read") failed due to error: \(message)")
|
|
|
|
|
|
|
|
|
|
case StorageError.databaseSuspended:
|
|
|
|
|
Log.error("[Storage] Database \(isWrite ? "write" : "read") failed as the database is suspended.")
|
|
|
|
|
|
|
|
|
|
case StorageError.transactionDeadlockTimeout:
|
|
|
|
|
Log.critical("[Storage] Database \(isWrite ? "write" : "read") failed due to a potential synchronous query deadlock timeout.")
|
|
|
|
|
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -557,71 +562,158 @@ open class Storage {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static func perform<T>(
|
|
|
|
|
info: CallInfo,
|
|
|
|
|
updates: @escaping (Database) throws -> T
|
|
|
|
|
) -> (Database) throws -> T {
|
|
|
|
|
return { db in
|
|
|
|
|
guard info.storage?.isSuspended == false else { throw StorageError.databaseSuspended }
|
|
|
|
|
|
|
|
|
|
let timer: TransactionTimer = TransactionTimer.start(duration: Storage.slowTransactionThreshold, info: info)
|
|
|
|
|
defer { timer.stop() }
|
|
|
|
|
|
|
|
|
|
// Get the result
|
|
|
|
|
let result: T = try updates(db)
|
|
|
|
|
|
|
|
|
|
// Update the state flags
|
|
|
|
|
switch info.isWrite {
|
|
|
|
|
case true: info.storage?.hasSuccessfullyWritten = true
|
|
|
|
|
case false: info.storage?.hasSuccessfullyRead = true
|
|
|
|
|
// MARK: - Operations
|
|
|
|
|
|
|
|
|
|
private static func track<T>(
|
|
|
|
|
_ db: Database,
|
|
|
|
|
_ info: CallInfo,
|
|
|
|
|
_ operation: @escaping (Database) throws -> T
|
|
|
|
|
) throws -> T {
|
|
|
|
|
guard info.storage?.isSuspended == false else { throw StorageError.databaseSuspended }
|
|
|
|
|
|
|
|
|
|
let timer: TransactionTimer = TransactionTimer.start(
|
|
|
|
|
duration: Storage.slowTransactionThreshold,
|
|
|
|
|
info: info
|
|
|
|
|
)
|
|
|
|
|
defer { timer.stop() }
|
|
|
|
|
|
|
|
|
|
// Get the result
|
|
|
|
|
let result: T = try operation(db)
|
|
|
|
|
|
|
|
|
|
// Update the state flags
|
|
|
|
|
switch info.isWrite {
|
|
|
|
|
case true: info.storage?.hasSuccessfullyWritten = true
|
|
|
|
|
case false: info.storage?.hasSuccessfullyRead = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// This function manually performs `read`/`write` operations in either a synchronous or asyncronous way using a semaphore to
|
|
|
|
|
/// block the syncrhonous version because `GRDB` has an internal assertion when using it's built-in synchronous `read`/`write`
|
|
|
|
|
/// functions to prevent reentrancy which is unsupported
|
|
|
|
|
///
|
|
|
|
|
/// Unfortunately this results in the code getting messy when trying to chain multiple database transactions (even
|
|
|
|
|
/// when using `db.afterNextTransaction`) which is somewhat unintuitive
|
|
|
|
|
///
|
|
|
|
|
/// The `async` variants don't need to worry about this reentrancy issue so instead we route we use those for all operations instead
|
|
|
|
|
/// and just block the thread when we want to perform a synchronous operation
|
|
|
|
|
@discardableResult private static func performOperation<T>(
|
|
|
|
|
_ info: CallInfo,
|
|
|
|
|
_ operation: @escaping (Database) throws -> T,
|
|
|
|
|
_ completion: ((Result<T, Error>) -> Void)? = nil
|
|
|
|
|
) -> Result<T, Error> {
|
|
|
|
|
var result: Result<T, Error> = .failure(StorageError.invalidQueryResult)
|
|
|
|
|
let semaphore: DispatchSemaphore? = (info.isAsync ? nil : DispatchSemaphore(value: 0))
|
|
|
|
|
let logErrorIfNeeded: (Result<T, Error>) -> () = { result in
|
|
|
|
|
switch result {
|
|
|
|
|
case .success: break
|
|
|
|
|
case .failure(let error): StorageState.logIfNeeded(error, isWrite: info.isWrite)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Perform the actual operation
|
|
|
|
|
switch (StorageState(info.storage), info.isWrite) {
|
|
|
|
|
case (.invalid(let error), _):
|
|
|
|
|
result = .failure(error)
|
|
|
|
|
semaphore?.signal()
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
case (.valid(let dbWriter), true):
|
|
|
|
|
dbWriter.asyncWrite(
|
|
|
|
|
{ db in result = .success(try Storage.track(db, info, operation)) },
|
|
|
|
|
completion: { _, dbResult in
|
|
|
|
|
switch dbResult {
|
|
|
|
|
case .success: break
|
|
|
|
|
case .failure(let error): result = .failure(error)
|
|
|
|
|
}
|
|
|
|
|
semaphore?.signal()
|
|
|
|
|
|
|
|
|
|
if info.isAsync { logErrorIfNeeded(result) }
|
|
|
|
|
completion?(result)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case (.valid(let dbWriter), false):
|
|
|
|
|
dbWriter.asyncRead { dbResult in
|
|
|
|
|
do {
|
|
|
|
|
switch dbResult {
|
|
|
|
|
case .failure(let error): throw error
|
|
|
|
|
case .success(let db): result = .success(try Storage.track(db, info, operation))
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
result = .failure(error)
|
|
|
|
|
}
|
|
|
|
|
semaphore?.signal()
|
|
|
|
|
|
|
|
|
|
if info.isAsync { logErrorIfNeeded(result) }
|
|
|
|
|
completion?(result)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// If this is a synchronous operation then `semaphore` will exist and will block here waiting on the signal from one of the
|
|
|
|
|
/// above closures to be sent
|
|
|
|
|
let semaphoreResult: DispatchTimeoutResult? = semaphore?.wait(timeout: .now() + .seconds(Storage.transactionDeadlockTimeoutSeconds))
|
|
|
|
|
|
|
|
|
|
/// If the transaction timed out then log the error and report a failure
|
|
|
|
|
guard semaphoreResult != .timedOut else {
|
|
|
|
|
StorageState.logIfNeeded(StorageError.transactionDeadlockTimeout, isWrite: info.isWrite)
|
|
|
|
|
return .failure(StorageError.transactionDeadlockTimeout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !info.isAsync { logErrorIfNeeded(result) }
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func performPublisherOperation<T>(
|
|
|
|
|
_ fileName: String,
|
|
|
|
|
_ functionName: String,
|
|
|
|
|
_ lineNumber: Int,
|
|
|
|
|
isWrite: Bool,
|
|
|
|
|
_ operation: @escaping (Database) throws -> T
|
|
|
|
|
) -> AnyPublisher<T, Error> {
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: false)
|
|
|
|
|
case .valid:
|
|
|
|
|
/// **Note:** GRDB does have `readPublisher`/`writePublisher` functions but it appears to asynchronously
|
|
|
|
|
/// trigger both the `output` and `complete` closures at the same time which causes a lot of unexpected
|
|
|
|
|
/// behaviours (this behaviour is apparently expected but still causes a number of odd behaviours in our code
|
|
|
|
|
/// for more information see https://github.com/groue/GRDB.swift/issues/1334)
|
|
|
|
|
///
|
|
|
|
|
/// Instead of this we are just using `Deferred { Future {} }` which is executed on the specified scheduled
|
|
|
|
|
/// which behaves in a much more expected way than the GRDB `readPublisher`/`writePublisher` does
|
|
|
|
|
let info: CallInfo = CallInfo(self, fileName, functionName, lineNumber, .syncWrite)
|
|
|
|
|
return Deferred {
|
|
|
|
|
Future { resolver in
|
|
|
|
|
resolver(Storage.performOperation(info, operation))
|
|
|
|
|
}
|
|
|
|
|
}.eraseToAnyPublisher()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Functions
|
|
|
|
|
|
|
|
|
|
@discardableResult public func write<T>(
|
|
|
|
|
fileName: String = #file,
|
|
|
|
|
functionName: String = #function,
|
|
|
|
|
lineNumber: Int = #line,
|
|
|
|
|
fileName file: String = #file,
|
|
|
|
|
functionName funcN: String = #function,
|
|
|
|
|
lineNumber line: Int = #line,
|
|
|
|
|
using dependencies: Dependencies = Dependencies(),
|
|
|
|
|
updates: @escaping (Database) throws -> T?
|
|
|
|
|
) -> T? {
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: true)
|
|
|
|
|
case .valid(let dbWriter):
|
|
|
|
|
let info: CallInfo = CallInfo(fileName, functionName, lineNumber, true, self)
|
|
|
|
|
do { return try dbWriter.write(Storage.perform(info: info, updates: updates)) }
|
|
|
|
|
catch { return StorageState.logIfNeeded(error, isWrite: true) }
|
|
|
|
|
switch Storage.performOperation(CallInfo(self, file, funcN, line, .syncWrite), updates) {
|
|
|
|
|
case .failure: return nil
|
|
|
|
|
case .success(let result): return result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open func writeAsync<T>(
|
|
|
|
|
fileName: String = #file,
|
|
|
|
|
functionName: String = #function,
|
|
|
|
|
lineNumber: Int = #line,
|
|
|
|
|
fileName file: String = #file,
|
|
|
|
|
functionName funcN: String = #function,
|
|
|
|
|
lineNumber line: Int = #line,
|
|
|
|
|
using dependencies: Dependencies = Dependencies(),
|
|
|
|
|
updates: @escaping (Database) throws -> T,
|
|
|
|
|
completion: @escaping (Database, Swift.Result<T, Error>) throws -> Void = { _, _ in }
|
|
|
|
|
completion: @escaping (Result<T, Error>) -> Void = { _ in }
|
|
|
|
|
) {
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: true)
|
|
|
|
|
case .valid(let dbWriter):
|
|
|
|
|
let info: CallInfo = CallInfo(fileName, functionName, lineNumber, true, self)
|
|
|
|
|
dbWriter.asyncWrite(
|
|
|
|
|
Storage.perform(info: info, updates: updates),
|
|
|
|
|
completion: { db, result in
|
|
|
|
|
switch result {
|
|
|
|
|
case .failure(let error): StorageState.logIfNeeded(error, isWrite: true)
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try? completion(db, result)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
Storage.performOperation(CallInfo(self, file, funcN, line, .asyncWrite), updates, completion)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open func writePublisher<T>(
|
|
|
|
@ -631,50 +723,19 @@ open class Storage {
|
|
|
|
|
using dependencies: Dependencies = Dependencies(),
|
|
|
|
|
updates: @escaping (Database) throws -> T
|
|
|
|
|
) -> AnyPublisher<T, Error> {
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: true)
|
|
|
|
|
case .valid:
|
|
|
|
|
/// **Note:** GRDB does have a `writePublisher` method but it appears to asynchronously trigger
|
|
|
|
|
/// both the `output` and `complete` closures at the same time which causes a lot of unexpected
|
|
|
|
|
/// behaviours (this behaviour is apparently expected but still causes a number of odd behaviours in our code
|
|
|
|
|
/// for more information see https://github.com/groue/GRDB.swift/issues/1334)
|
|
|
|
|
///
|
|
|
|
|
/// Instead of this we are just using `Deferred { Future {} }` which is executed on the specified scheduled
|
|
|
|
|
/// which behaves in a much more expected way than the GRDB `writePublisher` does
|
|
|
|
|
let info: CallInfo = CallInfo(fileName, functionName, lineNumber, true, self)
|
|
|
|
|
return Deferred {
|
|
|
|
|
Future { [weak self] resolver in
|
|
|
|
|
/// The `StorageState` may have changed between the creation of the publisher and it actually
|
|
|
|
|
/// being executed so we need to check again
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: true)
|
|
|
|
|
case .valid(let dbWriter):
|
|
|
|
|
do {
|
|
|
|
|
resolver(Result.success(try dbWriter.write(Storage.perform(info: info, updates: updates))))
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
StorageState.logIfNeeded(error, isWrite: true)
|
|
|
|
|
resolver(Result.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.eraseToAnyPublisher()
|
|
|
|
|
}
|
|
|
|
|
return performPublisherOperation(fileName, functionName, lineNumber, isWrite: true, updates)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@discardableResult public func read<T>(
|
|
|
|
|
fileName: String = #file,
|
|
|
|
|
functionName: String = #function,
|
|
|
|
|
lineNumber: Int = #line,
|
|
|
|
|
fileName file: String = #file,
|
|
|
|
|
functionName funcN: String = #function,
|
|
|
|
|
lineNumber line: Int = #line,
|
|
|
|
|
using dependencies: Dependencies = Dependencies(),
|
|
|
|
|
_ value: @escaping (Database) throws -> T?
|
|
|
|
|
) -> T? {
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: false)
|
|
|
|
|
case .valid(let dbWriter):
|
|
|
|
|
let info: CallInfo = CallInfo(fileName, functionName, lineNumber, false, self)
|
|
|
|
|
do { return try dbWriter.read(Storage.perform(info: info, updates: value)) }
|
|
|
|
|
catch { return StorageState.logIfNeeded(error, isWrite: false) }
|
|
|
|
|
switch Storage.performOperation(CallInfo(self, file, funcN, line, .syncRead), value) {
|
|
|
|
|
case .failure: return nil
|
|
|
|
|
case .success(let result): return result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -685,35 +746,7 @@ open class Storage {
|
|
|
|
|
using dependencies: Dependencies = Dependencies(),
|
|
|
|
|
value: @escaping (Database) throws -> T
|
|
|
|
|
) -> AnyPublisher<T, Error> {
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: false)
|
|
|
|
|
case .valid:
|
|
|
|
|
/// **Note:** GRDB does have a `readPublisher` method but it appears to asynchronously trigger
|
|
|
|
|
/// both the `output` and `complete` closures at the same time which causes a lot of unexpected
|
|
|
|
|
/// behaviours (this behaviour is apparently expected but still causes a number of odd behaviours in our code
|
|
|
|
|
/// for more information see https://github.com/groue/GRDB.swift/issues/1334)
|
|
|
|
|
///
|
|
|
|
|
/// Instead of this we are just using `Deferred { Future {} }` which is executed on the specified scheduled
|
|
|
|
|
/// which behaves in a much more expected way than the GRDB `readPublisher` does
|
|
|
|
|
let info: CallInfo = CallInfo(fileName, functionName, lineNumber, false, self)
|
|
|
|
|
return Deferred {
|
|
|
|
|
Future { [weak self] resolver in
|
|
|
|
|
/// The `StorageState` may have changed between the creation of the publisher and it actually
|
|
|
|
|
/// being executed so we need to check again
|
|
|
|
|
switch StorageState(self) {
|
|
|
|
|
case .invalid(let error): return StorageState.logIfNeeded(error, isWrite: false)
|
|
|
|
|
case .valid(let dbWriter):
|
|
|
|
|
do {
|
|
|
|
|
resolver(Result.success(try dbWriter.read(Storage.perform(info: info, updates: value))))
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
StorageState.logIfNeeded(error, isWrite: false)
|
|
|
|
|
resolver(Result.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.eraseToAnyPublisher()
|
|
|
|
|
}
|
|
|
|
|
return performPublisherOperation(fileName, functionName, lineNumber, isWrite: false, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rever to the `ValueObservation.start` method for full documentation
|
|
|
|
@ -904,11 +937,18 @@ public extension Storage {
|
|
|
|
|
|
|
|
|
|
private extension Storage {
|
|
|
|
|
class CallInfo {
|
|
|
|
|
enum Behaviour {
|
|
|
|
|
case syncRead
|
|
|
|
|
case asyncRead
|
|
|
|
|
case syncWrite
|
|
|
|
|
case asyncWrite
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
weak var storage: Storage?
|
|
|
|
|
let file: String
|
|
|
|
|
let function: String
|
|
|
|
|
let line: Int
|
|
|
|
|
let isWrite: Bool
|
|
|
|
|
weak var storage: Storage?
|
|
|
|
|
let behaviour: Behaviour
|
|
|
|
|
|
|
|
|
|
var callInfo: String {
|
|
|
|
|
let fileInfo: String = (file.components(separatedBy: "/").last.map { "\($0):\(line) - " } ?? "")
|
|
|
|
@ -916,18 +956,31 @@ private extension Storage {
|
|
|
|
|
return "\(fileInfo)\(function)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var isWrite: Bool {
|
|
|
|
|
switch behaviour {
|
|
|
|
|
case .syncWrite, .asyncWrite: return true
|
|
|
|
|
case .syncRead, .asyncRead: return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var isAsync: Bool {
|
|
|
|
|
switch behaviour {
|
|
|
|
|
case .asyncRead, .asyncWrite: return true
|
|
|
|
|
case .syncRead, .syncWrite: return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init(
|
|
|
|
|
_ storage: Storage?,
|
|
|
|
|
_ file: String,
|
|
|
|
|
_ function: String,
|
|
|
|
|
_ line: Int,
|
|
|
|
|
_ isWrite: Bool,
|
|
|
|
|
_ storage: Storage?
|
|
|
|
|
_ behaviour: Behaviour
|
|
|
|
|
) {
|
|
|
|
|
self.storage = storage
|
|
|
|
|
self.file = file
|
|
|
|
|
self.function = function
|
|
|
|
|
self.line = line
|
|
|
|
|
self.isWrite = isWrite
|
|
|
|
|
self.storage = storage
|
|
|
|
|
self.behaviour = behaviour
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|