import Dispatch import Foundation #if os(iOS) import UIKit #endif public final class DatabasePool { private let writer: SerializedDatabase /// The pool of reader connections. /// It is constant, until close() sets it to nil. private var readerPool: Pool? @LockedBox var databaseSnapshotCount = 0 /// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers. private var suspensionObservers: [NSObjectProtocol] = [] // MARK: - Database Information public var configuration: Configuration { writer.configuration } /// The path to the database. public var path: String { writer.path } // MARK: - Initializer /// Opens or creates an SQLite database. /// /// For example: /// /// ```swift /// let dbPool = try DatabasePool(path: "/path/to/database.sqlite") /// ``` /// /// The SQLite connections are closed when the database pool /// gets deallocated. /// /// - parameters: /// - path: The path to the database file. /// - configuration: A configuration. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public init(path: String, configuration: Configuration = Configuration()) throws { GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1") // Writer writer = try SerializedDatabase( path: path, configuration: configuration, defaultLabel: "GRDB.DatabasePool", purpose: "writer") // Readers var readerConfiguration = DatabasePool.readerConfiguration(configuration) // Readers can't allow dangling transactions because there's no // guarantee that one can get the same reader later in order to close // an opened transaction. readerConfiguration.allowsUnsafeTransactions = false var readerCount = 0 readerPool = Pool( maximumCount: configuration.maximumReaderCount, qos: configuration.readQoS, makeElement: { readerCount += 1 // protected by Pool (TODO: document this protection behavior) return try SerializedDatabase( path: path, configuration: readerConfiguration, defaultLabel: "GRDB.DatabasePool", purpose: "reader.\(readerCount)") }) // Set up journal mode unless readonly if !configuration.readonly { switch configuration.journalMode { case .default, .wal: try writer.sync { try $0.setUpWALMode() } } } setupSuspension() // Be a nice iOS citizen, and don't consume too much memory // See https://github.com/groue/GRDB.swift/#memory-management #if os(iOS) if configuration.automaticMemoryManagement { setupMemoryManagement() } #endif } deinit { // Remove block-based Notification observers. suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:)) // Undo job done in setupMemoryManagement() // // https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error // Explicit unregistration is required before macOS 10.11. NotificationCenter.default.removeObserver(self) // Close reader connections before the writer connection. // Context: https://github.com/groue/GRDB.swift/issues/739 readerPool = nil } /// Returns a Configuration suitable for readonly connections on a /// WAL database. private static func readerConfiguration(_ configuration: Configuration) -> Configuration { var configuration = configuration configuration.readonly = true // Readers use deferred transactions by default. // Other transaction kinds are forbidden by SQLite in read-only connections. configuration.defaultTransactionKind = .deferred // // > But there are some obscure cases where a query against a WAL-mode // > database can return SQLITE_BUSY, so applications should be prepared // > for that happenstance. // > // > - If another database connection has the database mode open in // > exclusive locking mode [...] // > - When the last connection to a particular database is closing, // > that connection will acquire an exclusive lock for a short time // > while it cleans up the WAL and shared-memory files [...] // > - If the last connection to a database crashed, then the first new // > connection to open the database will start a recovery process. An // > exclusive lock is held during recovery. [...] // // The whole point of WAL readers is to avoid SQLITE_BUSY, so let's // setup a busy handler for pool readers, in order to workaround those // "obscure cases" that may happen when the database is shared between // multiple processes. if configuration.readonlyBusyMode == nil { configuration.readonlyBusyMode = .timeout(10) } return configuration } } // @unchecked because of databaseSnapshotCount, readerPool and suspensionObservers extension DatabasePool: @unchecked Sendable { } extension DatabasePool { // MARK: - Memory management /// Frees as much memory as possible, by disposing non-essential memory. /// /// This method is synchronous, and blocks the current thread until all /// database accesses are completed. /// /// This method closes all read-only connections, unless the /// ``Configuration/persistentReadOnlyConnections`` configuration flag /// is set. /// /// - warning: This method can prevent concurrent reads from executing, /// until it returns. Prefer ``releaseMemoryEventually()`` if you intend /// to keep on using the database while releasing memory. public func releaseMemory() { // Release writer memory writer.sync { $0.releaseMemory() } if configuration.persistentReadOnlyConnections { // Keep existing readers readerPool?.forEach { reader in reader.sync { $0.releaseMemory() } } } else { // Release readers memory by closing all connections. // // We must use a barrier in order to guarantee that memory has been // freed (reader connections closed) when the method exits, as // documented. // // Without the barrier, connections would only close _eventually_ (after // their eventual concurrent jobs have completed). readerPool?.barrier { readerPool?.removeAll() } } } /// Eventually frees as much memory as possible, by disposing /// non-essential memory. /// /// This method eventually closes all read-only connections, unless the /// ``Configuration/persistentReadOnlyConnections`` configuration flag /// is set. /// /// Unlike ``releaseMemory()``, this method does not prevent concurrent /// database accesses when it is executing. But it does not notify when /// non-essential memory has been freed. public func releaseMemoryEventually() { if configuration.persistentReadOnlyConnections { // Keep existing readers readerPool?.forEach { reader in reader.async { $0.releaseMemory() } } } else { // Release readers memory by eventually closing all reader connections // (they will close after their current jobs have completed). readerPool?.removeAll() } // Release writer memory eventually. writer.async { $0.releaseMemory() } } #if os(iOS) /// Listens to UIApplicationDidEnterBackgroundNotification and /// UIApplicationDidReceiveMemoryWarningNotification in order to release /// as much memory as possible. private func setupMemoryManagement() { let center = NotificationCenter.default center.addObserver( self, selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) center.addObserver( self, selector: #selector(DatabasePool.applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) } @objc private func applicationDidEnterBackground(_ notification: NSNotification) { guard let application = notification.object as? UIApplication else { return } let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil) if task == .invalid { // Release memory synchronously releaseMemory() } else { // Release memory eventually. // // We don't know when reader connections will be closed (because // they may be currently in use), so we don't quite know when // reader memory will be freed (which would be the ideal timing for // ending our background task). // // So let's just end the background task after the writer connection // has freed its memory. That's better than nothing. releaseMemoryEventually() writer.async { _ in application.endBackgroundTask(task) } } } @objc private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) { releaseMemoryEventually() } #endif } extension DatabasePool: DatabaseReader { public func close() throws { try readerPool?.barrier { // Close writer connection first. If we can't close it, // don't close readers. // // This allows us to exit this method as fully closed (read and // writes fail), or not closed at all (reads and writes succeed). // // Unfortunately, this introduces a regression for // https://github.com/groue/GRDB.swift/issues/739. // TODO: fix this regression. try writer.sync { try $0.close() } // OK writer is closed. Now close readers and // eventually prevent any future read access defer { readerPool = nil } try readerPool?.forEach { reader in try reader.sync { try $0.close() } } } } // MARK: - Interrupting Database Operations public func interrupt() { writer.interrupt() readerPool?.forEach { $0.interrupt() } } // MARK: - Database Suspension func suspend() { if configuration.readonly { // read-only WAL connections can't acquire locks and do not need to // be suspended. return } writer.suspend() } func resume() { if configuration.readonly { // read-only WAL connections can't acquire locks and do not need to // be suspended. return } writer.resume() } private func setupSuspension() { if configuration.observesSuspensionNotifications { let center = NotificationCenter.default suspensionObservers.append(center.addObserver( forName: Database.suspendNotification, object: nil, queue: nil, using: { [weak self] _ in self?.suspend() } )) suspensionObservers.append(center.addObserver( forName: Database.resumeNotification, object: nil, queue: nil, using: { [weak self] _ in self?.resume() } )) } } // MARK: - Reading from Database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func read(_ value: (Database) throws -> T) throws -> T { GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") guard let readerPool else { throw DatabaseError.connectionIsClosed() } return try readerPool.get { reader in try reader.sync { db in try db.isolated { try db.clearSchemaCacheIfNeeded() return try value(db) } } } } public func asyncRead(_ value: @escaping (Result) -> Void) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return } readerPool.asyncGet { result in do { let (reader, releaseReader) = try result.get() // Second async jump because that's how `Pool.async` has to be used. reader.async { db in defer { try? db.commit() // Ignore commit error releaseReader(.reuse) } do { // The block isolation comes from the DEFERRED transaction. try db.beginTransaction(.deferred) try db.clearSchemaCacheIfNeeded() value(.success(db)) } catch { value(.failure(error)) } } } catch { value(.failure(error)) } } } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) throws -> T { GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.") guard let readerPool else { throw DatabaseError.connectionIsClosed() } return try readerPool.get { reader in try reader.sync { db in try db.clearSchemaCacheIfNeeded() return try value(db) } } } public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { guard let readerPool else { value(.failure(DatabaseError.connectionIsClosed())) return } readerPool.asyncGet { result in do { let (reader, releaseReader) = try result.get() // Second async jump because that's how `Pool.async` has to be used. reader.async { db in defer { releaseReader(.reuse) } do { try db.clearSchemaCacheIfNeeded() value(.success(db)) } catch { value(.failure(error)) } } } catch { value(.failure(error)) } } } public func unsafeReentrantRead(_ value: (Database) throws -> T) throws -> T { if let reader = currentReader { return try reader.reentrantSync(value) } else if writer.onValidQueue { return try writer.execute(value) } else { guard let readerPool else { throw DatabaseError.connectionIsClosed() } return try readerPool.get { reader in try reader.sync { db in try db.clearSchemaCacheIfNeeded() return try value(db) } } } } public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { // The semaphore that blocks until futureResult is defined: let futureSemaphore = DispatchSemaphore(value: 0) var futureResult: Result? = nil asyncConcurrentRead { dbResult in // Fetch and release the future futureResult = dbResult.flatMap { db in Result { try value(db) } } futureSemaphore.signal() } return DatabaseFuture { // Block the future until results are fetched _ = futureSemaphore.wait(timeout: .distantFuture) return try futureResult!.get() } } public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { asyncConcurrentRead(value) } /// Performs an asynchronous read access. /// /// This method must be called from the writer dispatch queue, outside of /// any transaction. You'll get a fatal error otherwise. /// /// The `value` function is guaranteed to see the database in the last /// committed state at the moment this method is called. Eventual /// concurrent database updates are not visible from the function. /// /// This method returns as soon as the isolation guarantee described above /// has been established. /// /// In the example below, the number of players is fetched concurrently with /// the player insertion. Yet it is guaranteed to be zero: /// /// ```swift /// try writer.asyncWriteWithoutTransaction { db in /// // Delete all players /// try Player.deleteAll() /// /// // Count players concurrently /// writer.asyncConcurrentRead { dbResult in /// do { /// let db = try dbResult.get() /// // Guaranteed to be zero /// let count = try Player.fetchCount(db) /// } catch { /// // Handle error /// } /// } /// /// // Insert a player /// try Player(...).insert(db) /// } /// ``` /// /// - parameter value: A function that accesses the database. public func asyncConcurrentRead(_ value: @escaping (Result) -> Void) { // Check that we're on the writer queue... writer.execute { db in // ... and that no transaction is opened. GRDBPrecondition(!db.isInsideTransaction, """ must not be called from inside a transaction. \ If this error is raised from a DatabasePool.write block, use \ DatabasePool.writeWithoutTransaction instead (and use \ transactions when needed). """) } // The semaphore that blocks the writing dispatch queue until snapshot // isolation has been established: let isolationSemaphore = DispatchSemaphore(value: 0) do { guard let readerPool else { throw DatabaseError.connectionIsClosed() } let (reader, releaseReader) = try readerPool.get() reader.async { db in defer { try? db.commit() // Ignore commit error releaseReader(.reuse) } do { // https://www.sqlite.org/isolation.html // // > In WAL mode, SQLite exhibits "snapshot isolation". When // > a read transaction starts, that reader continues to see // > an unchanging "snapshot" of the database file as it // > existed at the moment in time when the read transaction // > started. Any write transactions that commit while the // > read transaction is active are still invisible to the // > read transaction, because the reader is seeing a // > snapshot of database file from a prior moment in time. // // That's exactly what we need. But what does "when read // transaction starts" mean? // // http://www.sqlite.org/lang_transaction.html // // > Deferred [transaction] means that no locks are acquired // > on the database until the database is first accessed. // > [...] Locks are not acquired until the first read or // > write operation. [...] Because the acquisition of locks // > is deferred until they are needed, it is possible that // > another thread or process could create a separate // > transaction and write to the database after the BEGIN // > on the current thread has executed. // // Now that's precise enough: SQLite defers snapshot // isolation until the first SELECT: // // Reader Writer // BEGIN DEFERRED TRANSACTION // UPDATE ... (1) // Here the change (1) is visible from the reader // SELECT ... // UPDATE ... (2) // Here the change (2) is not visible from the reader // // We thus have to perform a select that establishes the // snapshot isolation before we release the writer queue: // // Reader Writer // BEGIN DEFERRED TRANSACTION // SELECT anything // UPDATE ... (1) // Here the change (1) is not visible from the reader // // Since any select goes, use `PRAGMA schema_version`. try db.beginTransaction(.deferred) try db.clearSchemaCacheIfNeeded() } catch { isolationSemaphore.signal() value(.failure(error)) return } // Now that we have an isolated snapshot of the last commit, we // can release the writer queue. isolationSemaphore.signal() value(.success(db)) } } catch { isolationSemaphore.signal() value(.failure(error)) } // Block the writer queue until snapshot isolation success or error _ = isolationSemaphore.wait(timeout: .distantFuture) } /// Invalidates open read-only SQLite connections. /// /// After this method is called, read-only database access methods will use /// new SQLite connections. /// /// Eventual concurrent read-only accesses are not interrupted, and /// proceed until completion. /// /// - This method closes all read-only connections, even if the /// ``Configuration/persistentReadOnlyConnections`` configuration flag /// is set. public func invalidateReadOnlyConnections() { readerPool?.removeAll() } /// Returns a reader that can be used from the current dispatch queue, /// if any. private var currentReader: SerializedDatabase? { guard let readerPool else { return nil } var readers: [SerializedDatabase] = [] readerPool.forEach { reader in // We can't check for reader.onValidQueue here because // Pool.forEach() runs its closure argument in some arbitrary // dispatch queue. We thus extract the reader so that we can query // it below. readers.append(reader) } // Now the readers array contains some readers. The pool readers may // already be different, because some other thread may have started // a new read, for example. // // This doesn't matter: the reader we are looking for is already on // its own dispatch queue. If it exists, is still in use, thus still // in the pool, and thus still relevant for our check: return readers.first { $0.onValidQueue } } // MARK: - WAL Snapshot Transactions // swiftlint:disable:next line_length #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) /// Returns a long-lived WAL snapshot transaction on a reader connection. func walSnapshotTransaction() throws -> WALSnapshotTransaction { guard let readerPool else { throw DatabaseError.connectionIsClosed() } let (reader, releaseReader) = try readerPool.get() return try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in // Discard the connection if the transaction could not be // properly ended. If we'd reuse it, the next read would // fail because we'd fail starting a read transaction. releaseReader(isInsideTransaction ? .discard : .reuse) }) } /// Returns a long-lived WAL snapshot transaction on a reader connection. /// /// - important: The `completion` argument is executed in a serial /// dispatch queue, so make sure you use the transaction asynchronously. func asyncWALSnapshotTransaction(_ completion: @escaping (Result) -> Void) { guard let readerPool else { completion(.failure(DatabaseError.connectionIsClosed())) return } readerPool.asyncGet { result in completion(result.flatMap { reader, releaseReader in Result { try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in // Discard the connection if the transaction could not be // properly ended. If we'd reuse it, the next read would // fail because we'd fail starting a read transaction. releaseReader(isInsideTransaction ? .discard : .reuse) }) } }) } } #endif // MARK: - Database Observation public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, onChange: @escaping (Reducer.Value) -> Void) -> AnyDatabaseCancellable { if configuration.readonly { // The easy case: the database does not change return _addReadOnly( observation: observation, scheduling: scheduler, onChange: onChange) } else if observation.requiresWriteAccess { // Observe from the writer database connection. return _addWriteOnly( observation: observation, scheduling: scheduler, onChange: onChange) } else { // DatabasePool can perform concurrent observation return _addConcurrent( observation: observation, scheduling: scheduler, onChange: onChange) } } /// A concurrent observation fetches the initial value without waiting for /// the writer. private func _addConcurrent( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, onChange: @escaping (Reducer.Value) -> Void) -> AnyDatabaseCancellable { assert(!configuration.readonly, "Use _addReadOnly(observation:) instead") assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead") let observer = ValueConcurrentObserver( dbPool: self, scheduler: scheduler, trackingMode: observation.trackingMode, reducer: observation.makeReducer(), events: observation.events, onChange: onChange) return observer.start() } } extension DatabasePool: DatabaseWriter { // MARK: - Writing in Database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func writeWithoutTransaction(_ updates: (Database) throws -> T) rethrows -> T { try writer.sync(updates) } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { guard let readerPool else { throw DatabaseError.connectionIsClosed() } return try readerPool.barrier { try writer.sync(updates) } } public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { guard let readerPool else { updates(.failure(DatabaseError.connectionIsClosed())) return } readerPool.asyncBarrier { self.writer.sync { updates(.success($0)) } } } /// Wraps database operations inside a database transaction. /// /// The `updates` function runs in the writer dispatch queue, serialized /// with all database updates. /// /// If `updates` throws an error, the transaction is rollbacked and the /// error is rethrown. If it returns /// ``Database/TransactionCompletion/rollback``, the transaction is also /// rollbacked, but no error is thrown. /// /// For example: /// /// ```swift /// try dbPool.writeInTransaction { db in /// try Player(name: "Arthur").insert(db) /// try Player(name: "Barbara").insert(db) /// return .commit /// } /// ``` /// /// - precondition: This method is not reentrant. /// - parameters: /// - kind: The transaction type (default nil). If nil, the transaction /// type is the ``Configuration/defaultTransactionKind`` of the /// the ``configuration``. /// - updates: A function that updates the database. /// - throws: The error thrown by `updates`, or by the wrapping transaction. public func writeInTransaction( _ kind: Database.TransactionKind? = nil, _ updates: (Database) throws -> Database.TransactionCompletion) throws { try writer.sync { db in try db.inTransaction(kind) { try updates(db) } } } public func unsafeReentrantWrite(_ updates: (Database) throws -> T) rethrows -> T { try writer.reentrantSync(updates) } public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { writer.async(updates) } } extension DatabasePool { // MARK: - Snapshots /// Creates a database snapshot that serializes accesses to an unchanging /// database content, as it exists at the moment the snapshot is created. /// /// It is a programmer error to create a snapshot from the writer dispatch /// queue when a transaction is opened: /// /// ```swift /// try dbPool.write { db in /// try Player.deleteAll() /// /// // fatal error: makeSnapshot() must not be called from inside a transaction /// let snapshot = try dbPool.makeSnapshot() /// } /// ``` /// /// To avoid this fatal error, create the snapshot *before* or *after* /// the transaction: /// /// ```swift /// let snapshot = try dbPool.makeSnapshot() // OK /// /// try dbPool.writeWithoutTransaction { db in /// let snapshot = try dbPool.makeSnapshot() // OK /// /// try db.inTransaction { /// try Player.deleteAll() /// return .commit /// } /// /// // OK /// let snapshot = try dbPool.makeSnapshot() // OK /// } /// /// let snapshot = try dbPool.makeSnapshot() // OK /// ``` public func makeSnapshot() throws -> DatabaseSnapshot { // Sanity check if writer.onValidQueue { writer.execute { db in GRDBPrecondition( !db.isInsideTransaction, "makeSnapshot() must not be called from inside a transaction.") } } return try DatabaseSnapshot( path: path, configuration: DatabasePool.readerConfiguration(writer.configuration), defaultLabel: "GRDB.DatabasePool", purpose: "snapshot.\($databaseSnapshotCount.increment())") } // swiftlint:disable:next line_length #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) /// Creates a database snapshot that allows concurrent accesses to an /// unchanging database content, as it exists at the moment the snapshot /// is created. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite /// database is not in the [WAL mode](https://www.sqlite.org/wal.html), /// or if this method is called from a write transaction, or if the /// wal file is missing or truncated (size zero). /// /// Related SQLite documentation: public func makeSnapshotPool() throws -> DatabaseSnapshotPool { try unsafeReentrantRead { db in try DatabaseSnapshotPool(db) } } #endif }