Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:

---

## Next Release

- **New**: [#1350](https://github.com/groue/GRDB.swift/pull/1350) by [@groue](https://github.com/groue): DatabasePool won't close read-only connections if requested, and ValueObservation no longer opens a new database connection when it starts.

## 6.9.2

Released March 14, 2023 • [diff](https://github.com/groue/GRDB.swift/compare/v6.9.1...v6.9.2)
Expand Down
4 changes: 4 additions & 0 deletions GRDB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@
56D110D828AFC84000E64463 /* PersistableRecord+Insert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D110D728AFC84000E64463 /* PersistableRecord+Insert.swift */; };
56D110DD28AFC8B400E64463 /* PersistableRecord+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D110DC28AFC8B400E64463 /* PersistableRecord+Save.swift */; };
56D110FA28AFC97E00E64463 /* MutablePersistableRecord+DAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D110F928AFC97E00E64463 /* MutablePersistableRecord+DAO.swift */; };
56D3332029C38D6700430680 /* WALSnapshotTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */; };
56D496541D812F5B008276D7 /* SQLExpressionLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */; };
56D496551D812F83008276D7 /* FoundationDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB2F1D108BA9006283EF /* FoundationDataTests.swift */; };
56D496571D81303E008276D7 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; };
Expand Down Expand Up @@ -753,6 +754,7 @@
56D110D728AFC84000E64463 /* PersistableRecord+Insert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Insert.swift"; sourceTree = "<group>"; };
56D110DC28AFC8B400E64463 /* PersistableRecord+Save.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Save.swift"; sourceTree = "<group>"; };
56D110F928AFC97E00E64463 /* MutablePersistableRecord+DAO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MutablePersistableRecord+DAO.swift"; sourceTree = "<group>"; };
56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WALSnapshotTransaction.swift; sourceTree = "<group>"; };
56D5075D1F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryKeyInfoTests.swift; sourceTree = "<group>"; };
56D51CFF1EA789FA0074638A /* FetchableRecord+TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FetchableRecord+TableRecord.swift"; sourceTree = "<group>"; };
56D91AA12205E03700770D8D /* SQLRelation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLRelation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1466,6 +1468,7 @@
566B9C1F25C6CC24004542CF /* RowDecodingError.swift */,
56BB6EA81D3009B100A1CA52 /* SchedulingWatchdog.swift */,
560A37A61C8FF6E500949E71 /* SerializedDatabase.swift */,
56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */,
56E9FAD7221053DC00C703A8 /* SQL.swift */,
569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */,
56FBFED82210731A00945324 /* SQLRequest.swift */,
Expand Down Expand Up @@ -2157,6 +2160,7 @@
5653EC122098738B00F46237 /* SQLGenerationContext.swift in Sources */,
560D92471C672C4B00F4F92B /* MutablePersistableRecord.swift in Sources */,
5674A6E41F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift in Sources */,
56D3332029C38D6700430680 /* WALSnapshotTransaction.swift in Sources */,
5653EB0C20944C7C00F46237 /* HasManyAssociation.swift in Sources */,
5657AAB91D107001006283EF /* NSData.swift in Sources */,
560D92421C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */,
Expand Down
28 changes: 23 additions & 5 deletions GRDB/Core/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,15 @@ public struct Configuration {
/// If nil, GRDB picks a default one.
var readonlyBusyMode: Database.BusyMode? = nil

/// The maximum number of concurrent readers.
/// The maximum number of concurrent reader connections.
///
/// This configuration applies to ``DatabasePool`` only. The default value
/// is 5.
/// This configuration has effect on ``DatabasePool`` and
/// ``DatabaseSnapshotPool`` only. The default value is 5.
///
/// You can query this value at runtime in order to get the actual capacity
/// for concurrent reads of any ``DatabaseReader``. For example:
/// for concurrent reads of any ``DatabaseReader``. In this context,
/// ``DatabaseQueue`` and ``DatabaseSnapshot`` have a capacity of 1,
/// because they can't perform two concurrent reads. For example:
///
/// ```swift
/// var config = Configuration()
Expand All @@ -321,6 +323,7 @@ public struct Configuration {
/// print(dbQueue.configuration.maximumReaderCount) // 1
/// print(dbPool.configuration.maximumReaderCount) // 5
/// print(dbSnapshot.configuration.maximumReaderCount) // 1
/// ```
public var maximumReaderCount: Int = 5

/// The quality of service of database accesses.
Expand Down Expand Up @@ -372,7 +375,22 @@ public struct Configuration {
/// The default is true.
public var automaticMemoryManagement = true
#endif


/// A boolean value indicating whether read-only connections should be
/// kept open.
///
/// This configuration flag applies to ``DatabasePool`` only. The
/// default value is false.
///
/// When the flag is false, a `DatabasePool` closes read-only
/// connections when requested to dispose non-essential memory with
/// ``DatabasePool/releaseMemory()``. When true, those connections are
/// kept open.
///
/// Consider setting this flag to true when profiling your application
/// reveals that a lot of time is spent opening new SQLite connections.
public var persistentReadOnlyConnections = false

// MARK: - Factory Configuration

/// Creates a factory configuration.
Expand Down
109 changes: 88 additions & 21 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,47 +168,66 @@ extension DatabasePool {

// MARK: - Memory management

/// Frees as much memory as possible, by disposing non-essential memory from
/// the writer connection, and closing all reader connections.
/// 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() }

// 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()
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 from the writer connection, and closing all reader connections.
/// 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() {
// Release readers memory by eventually closing all reader connections
// (they will close after their current jobs have completed).
readerPool?.removeAll()
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 { db in
db.releaseMemory()
}
writer.async { $0.releaseMemory() }
}

#if os(iOS)
Expand Down Expand Up @@ -608,8 +627,12 @@ extension DatabasePool: DatabaseReader {
/// After this method is called, read-only database access methods will use
/// new SQLite connections.
///
/// Eventual concurrent read-only accesses are not invalidated: they will
/// 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()
}
Expand Down Expand Up @@ -640,6 +663,50 @@ extension DatabasePool: DatabaseReader {
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<WALSnapshotTransaction, Error>) -> 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<Reducer: ValueReducer>(
Expand Down
47 changes: 40 additions & 7 deletions GRDB/Core/SerializedDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ final class SerializedDatabase {
/// The dispatch queue
private let queue: DispatchQueue

/// If true, overrides `configuration.allowsUnsafeTransactions`.
private var allowsUnsafeTransactions = false

init(
path: String,
configuration: Configuration = Configuration(),
Expand Down Expand Up @@ -76,10 +79,25 @@ final class SerializedDatabase {
}
}

/// Synchronously executes a block the serialized dispatch queue, and
/// returns its result.
/// Executes database operations, returns their result after they have
/// finished executing, and allows or forbids long-lived transactions.
///
/// This method is not reentrant.
///
/// - parameter allowingLongLivedTransaction: When true, the
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
/// ignored until this method is called again with false.
func sync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
try sync { db in
self.allowsUnsafeTransactions = allowingLongLivedTransaction
return try body(db)
}
}

/// Executes database operations, and returns their result after they
/// have finished executing.
///
/// This method is *not* reentrant.
/// This method is not reentrant.
func sync<T>(_ block: (Database) throws -> T) rethrows -> T {
// Three different cases:
//
Expand Down Expand Up @@ -122,8 +140,23 @@ final class SerializedDatabase {
}
}

/// Synchronously executes a block the serialized dispatch queue, and
/// returns its result.
/// Executes database operations, returns their result after they have
/// finished executing, and allows or forbids long-lived transactions.
///
/// This method is reentrant.
///
/// - parameter allowingLongLivedTransaction: When true, the
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
/// ignored until this method is called again with false.
func reentrantSync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
try reentrantSync { db in
self.allowsUnsafeTransactions = allowingLongLivedTransaction
return try body(db)
}
}

/// Executes database operations, and returns their result after they
/// have finished executing.
///
/// This method is reentrant.
func reentrantSync<T>(_ block: (Database) throws -> T) rethrows -> T {
Expand Down Expand Up @@ -189,7 +222,7 @@ final class SerializedDatabase {
}
}

/// Asynchronously executes a block in the serialized dispatch queue.
/// Schedules database operations for execution, and returns immediately.
func async(_ block: @escaping (Database) -> Void) {
queue.async {
block(self.db)
Expand Down Expand Up @@ -242,7 +275,7 @@ final class SerializedDatabase {
line: UInt = #line)
{
GRDBPrecondition(
configuration.allowsUnsafeTransactions || !db.isInsideTransaction,
allowsUnsafeTransactions || configuration.allowsUnsafeTransactions || !db.isInsideTransaction,
message(),
file: file,
line: line)
Expand Down
Loading