import Dispatch /// SchedulingWatchdog makes sure that databases connections are used on correct /// dispatch queues, and warns the user with a fatal error whenever she misuses /// a database connection. /// /// Generally speaking, each connection has its own dispatch queue. But it's not /// enough: users need to use two database connections at the same time: /// . To support this use case, a /// single dispatch queue can be temporarily shared by two or more connections. /// /// - SchedulingWatchdog.makeSerializedQueue(allowingDatabase:) creates a /// dispatch queue that allows one database. /// /// It does so by registering one instance of SchedulingWatchdog as a specific /// of the dispatch queue, a SchedulingWatchdog that allows that database only. /// /// Later on, the queue can be shared by several databases with the method /// inheritingAllowedDatabases(from:execute:). See SerializedDatabase.sync() /// for an example. /// /// - preconditionValidQueue() crashes whenever a database is used in an invalid /// dispatch queue. final class SchedulingWatchdog { private static let watchDogKey = DispatchSpecificKey() private(set) var allowedDatabases: [Database] var databaseObservationBroker: DatabaseObservationBroker? private init(allowedDatabase database: Database) { allowedDatabases = [database] } static func allowDatabase(_ database: Database, onQueue queue: DispatchQueue) { precondition(queue.getSpecific(key: watchDogKey) == nil) let watchdog = SchedulingWatchdog(allowedDatabase: database) queue.setSpecific(key: watchDogKey, value: watchdog) } func inheritingAllowedDatabases(from other: SchedulingWatchdog, execute body: () throws -> T) rethrows -> T { let backup = allowedDatabases allowedDatabases.append(contentsOf: other.allowedDatabases) defer { allowedDatabases = backup } return try body() } static func preconditionValidQueue( _ db: Database, _ message: @autoclosure() -> String = "Database was not used on the correct thread.", file: StaticString = #file, line: UInt = #line) { GRDBPrecondition(allows(db), message(), file: file, line: line) } /// Returns whether the database argument can be used in the current /// dispatch queue. static func allows(_ db: Database) -> Bool { current?.allows(db) ?? false } static var current: SchedulingWatchdog? { DispatchQueue.getSpecific(key: watchDogKey) } func allows(_ db: Database) -> Bool { allowedDatabases.contains { $0 === db } } }