Skip to content

Commit c6e220a

Browse files
authored
Merge pull request #1350 from groue/dev/long-lived-read-only-transaction
DatabasePool won't close read-only connections if requested, and ValueObservation no longer opens a new database connection when it starts
2 parents 5d7fdea + 600d1b8 commit c6e220a

File tree

11 files changed

+630
-194
lines changed

11 files changed

+630
-194
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
107107

108108
---
109109

110+
## Next Release
111+
112+
- **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.
113+
110114
## 6.9.2
111115

112116
Released March 14, 2023 • [diff](https://github.com/groue/GRDB.swift/compare/v6.9.1...v6.9.2)

GRDB.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@
281281
56D110D828AFC84000E64463 /* PersistableRecord+Insert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D110D728AFC84000E64463 /* PersistableRecord+Insert.swift */; };
282282
56D110DD28AFC8B400E64463 /* PersistableRecord+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D110DC28AFC8B400E64463 /* PersistableRecord+Save.swift */; };
283283
56D110FA28AFC97E00E64463 /* MutablePersistableRecord+DAO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D110F928AFC97E00E64463 /* MutablePersistableRecord+DAO.swift */; };
284+
56D3332029C38D6700430680 /* WALSnapshotTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */; };
284285
56D496541D812F5B008276D7 /* SQLExpressionLiteralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A4CDAF1D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift */; };
285286
56D496551D812F83008276D7 /* FoundationDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB2F1D108BA9006283EF /* FoundationDataTests.swift */; };
286287
56D496571D81303E008276D7 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; };
@@ -753,6 +754,7 @@
753754
56D110D728AFC84000E64463 /* PersistableRecord+Insert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Insert.swift"; sourceTree = "<group>"; };
754755
56D110DC28AFC8B400E64463 /* PersistableRecord+Save.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Save.swift"; sourceTree = "<group>"; };
755756
56D110F928AFC97E00E64463 /* MutablePersistableRecord+DAO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MutablePersistableRecord+DAO.swift"; sourceTree = "<group>"; };
757+
56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WALSnapshotTransaction.swift; sourceTree = "<group>"; };
756758
56D5075D1F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryKeyInfoTests.swift; sourceTree = "<group>"; };
757759
56D51CFF1EA789FA0074638A /* FetchableRecord+TableRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FetchableRecord+TableRecord.swift"; sourceTree = "<group>"; };
758760
56D91AA12205E03700770D8D /* SQLRelation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLRelation.swift; sourceTree = "<group>"; };
@@ -1466,6 +1468,7 @@
14661468
566B9C1F25C6CC24004542CF /* RowDecodingError.swift */,
14671469
56BB6EA81D3009B100A1CA52 /* SchedulingWatchdog.swift */,
14681470
560A37A61C8FF6E500949E71 /* SerializedDatabase.swift */,
1471+
56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */,
14691472
56E9FAD7221053DC00C703A8 /* SQL.swift */,
14701473
569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */,
14711474
56FBFED82210731A00945324 /* SQLRequest.swift */,
@@ -2157,6 +2160,7 @@
21572160
5653EC122098738B00F46237 /* SQLGenerationContext.swift in Sources */,
21582161
560D92471C672C4B00F4F92B /* MutablePersistableRecord.swift in Sources */,
21592162
5674A6E41F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift in Sources */,
2163+
56D3332029C38D6700430680 /* WALSnapshotTransaction.swift in Sources */,
21602164
5653EB0C20944C7C00F46237 /* HasManyAssociation.swift in Sources */,
21612165
5657AAB91D107001006283EF /* NSData.swift in Sources */,
21622166
560D92421C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */,

GRDB/Core/Configuration.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,15 @@ public struct Configuration {
301301
/// If nil, GRDB picks a default one.
302302
var readonlyBusyMode: Database.BusyMode? = nil
303303

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

326329
/// The quality of service of database accesses.
@@ -372,7 +375,22 @@ public struct Configuration {
372375
/// The default is true.
373376
public var automaticMemoryManagement = true
374377
#endif
375-
378+
379+
/// A boolean value indicating whether read-only connections should be
380+
/// kept open.
381+
///
382+
/// This configuration flag applies to ``DatabasePool`` only. The
383+
/// default value is false.
384+
///
385+
/// When the flag is false, a `DatabasePool` closes read-only
386+
/// connections when requested to dispose non-essential memory with
387+
/// ``DatabasePool/releaseMemory()``. When true, those connections are
388+
/// kept open.
389+
///
390+
/// Consider setting this flag to true when profiling your application
391+
/// reveals that a lot of time is spent opening new SQLite connections.
392+
public var persistentReadOnlyConnections = false
393+
376394
// MARK: - Factory Configuration
377395

378396
/// Creates a factory configuration.

GRDB/Core/DatabasePool.swift

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -168,47 +168,66 @@ extension DatabasePool {
168168

169169
// MARK: - Memory management
170170

171-
/// Frees as much memory as possible, by disposing non-essential memory from
172-
/// the writer connection, and closing all reader connections.
171+
/// Frees as much memory as possible, by disposing non-essential memory.
173172
///
174173
/// This method is synchronous, and blocks the current thread until all
175174
/// database accesses are completed.
176175
///
176+
/// This method closes all read-only connections, unless the
177+
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
178+
/// is set.
179+
///
177180
/// - warning: This method can prevent concurrent reads from executing,
178181
/// until it returns. Prefer ``releaseMemoryEventually()`` if you intend
179182
/// to keep on using the database while releasing memory.
180183
public func releaseMemory() {
181184
// Release writer memory
182185
writer.sync { $0.releaseMemory() }
183186

184-
// Release readers memory by closing all connections.
185-
//
186-
// We must use a barrier in order to guarantee that memory has been
187-
// freed (reader connections closed) when the method exits, as
188-
// documented.
189-
//
190-
// Without the barrier, connections would only close _eventually_ (after
191-
// their eventual concurrent jobs have completed).
192-
readerPool?.barrier {
193-
readerPool?.removeAll()
187+
if configuration.persistentReadOnlyConnections {
188+
// Keep existing readers
189+
readerPool?.forEach { reader in
190+
reader.sync { $0.releaseMemory() }
191+
}
192+
} else {
193+
// Release readers memory by closing all connections.
194+
//
195+
// We must use a barrier in order to guarantee that memory has been
196+
// freed (reader connections closed) when the method exits, as
197+
// documented.
198+
//
199+
// Without the barrier, connections would only close _eventually_ (after
200+
// their eventual concurrent jobs have completed).
201+
readerPool?.barrier {
202+
readerPool?.removeAll()
203+
}
194204
}
195205
}
196206

197-
/// Eventually frees as much memory as possible, by disposing non-essential
198-
/// memory from the writer connection, and closing all reader connections.
207+
/// Eventually frees as much memory as possible, by disposing
208+
/// non-essential memory.
209+
///
210+
/// This method eventually closes all read-only connections, unless the
211+
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
212+
/// is set.
199213
///
200214
/// Unlike ``releaseMemory()``, this method does not prevent concurrent
201215
/// database accesses when it is executing. But it does not notify when
202216
/// non-essential memory has been freed.
203217
public func releaseMemoryEventually() {
204-
// Release readers memory by eventually closing all reader connections
205-
// (they will close after their current jobs have completed).
206-
readerPool?.removeAll()
218+
if configuration.persistentReadOnlyConnections {
219+
// Keep existing readers
220+
readerPool?.forEach { reader in
221+
reader.async { $0.releaseMemory() }
222+
}
223+
} else {
224+
// Release readers memory by eventually closing all reader connections
225+
// (they will close after their current jobs have completed).
226+
readerPool?.removeAll()
227+
}
207228

208229
// Release writer memory eventually.
209-
writer.async { db in
210-
db.releaseMemory()
211-
}
230+
writer.async { $0.releaseMemory() }
212231
}
213232

214233
#if os(iOS)
@@ -608,8 +627,12 @@ extension DatabasePool: DatabaseReader {
608627
/// After this method is called, read-only database access methods will use
609628
/// new SQLite connections.
610629
///
611-
/// Eventual concurrent read-only accesses are not invalidated: they will
630+
/// Eventual concurrent read-only accesses are not interrupted, and
612631
/// proceed until completion.
632+
///
633+
/// - This method closes all read-only connections, even if the
634+
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
635+
/// is set.
613636
public func invalidateReadOnlyConnections() {
614637
readerPool?.removeAll()
615638
}
@@ -640,6 +663,50 @@ extension DatabasePool: DatabaseReader {
640663
return readers.first { $0.onValidQueue }
641664
}
642665

666+
// MARK: - WAL Snapshot Transactions
667+
668+
// swiftlint:disable:next line_length
669+
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
670+
/// Returns a long-lived WAL snapshot transaction on a reader connection.
671+
func walSnapshotTransaction() throws -> WALSnapshotTransaction {
672+
guard let readerPool else {
673+
throw DatabaseError.connectionIsClosed()
674+
}
675+
676+
let (reader, releaseReader) = try readerPool.get()
677+
return try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
678+
// Discard the connection if the transaction could not be
679+
// properly ended. If we'd reuse it, the next read would
680+
// fail because we'd fail starting a read transaction.
681+
releaseReader(isInsideTransaction ? .discard : .reuse)
682+
})
683+
}
684+
685+
/// Returns a long-lived WAL snapshot transaction on a reader connection.
686+
///
687+
/// - important: The `completion` argument is executed in a serial
688+
/// dispatch queue, so make sure you use the transaction asynchronously.
689+
func asyncWALSnapshotTransaction(_ completion: @escaping (Result<WALSnapshotTransaction, Error>) -> Void) {
690+
guard let readerPool else {
691+
completion(.failure(DatabaseError.connectionIsClosed()))
692+
return
693+
}
694+
695+
readerPool.asyncGet { result in
696+
completion(result.flatMap { reader, releaseReader in
697+
Result {
698+
try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
699+
// Discard the connection if the transaction could not be
700+
// properly ended. If we'd reuse it, the next read would
701+
// fail because we'd fail starting a read transaction.
702+
releaseReader(isInsideTransaction ? .discard : .reuse)
703+
})
704+
}
705+
})
706+
}
707+
}
708+
#endif
709+
643710
// MARK: - Database Observation
644711

645712
public func _add<Reducer: ValueReducer>(

GRDB/Core/SerializedDatabase.swift

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ final class SerializedDatabase {
1414
/// The dispatch queue
1515
private let queue: DispatchQueue
1616

17+
/// If true, overrides `configuration.allowsUnsafeTransactions`.
18+
private var allowsUnsafeTransactions = false
19+
1720
init(
1821
path: String,
1922
configuration: Configuration = Configuration(),
@@ -76,10 +79,25 @@ final class SerializedDatabase {
7679
}
7780
}
7881

79-
/// Synchronously executes a block the serialized dispatch queue, and
80-
/// returns its result.
82+
/// Executes database operations, returns their result after they have
83+
/// finished executing, and allows or forbids long-lived transactions.
84+
///
85+
/// This method is not reentrant.
86+
///
87+
/// - parameter allowingLongLivedTransaction: When true, the
88+
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
89+
/// ignored until this method is called again with false.
90+
func sync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
91+
try sync { db in
92+
self.allowsUnsafeTransactions = allowingLongLivedTransaction
93+
return try body(db)
94+
}
95+
}
96+
97+
/// Executes database operations, and returns their result after they
98+
/// have finished executing.
8199
///
82-
/// This method is *not* reentrant.
100+
/// This method is not reentrant.
83101
func sync<T>(_ block: (Database) throws -> T) rethrows -> T {
84102
// Three different cases:
85103
//
@@ -122,8 +140,23 @@ final class SerializedDatabase {
122140
}
123141
}
124142

125-
/// Synchronously executes a block the serialized dispatch queue, and
126-
/// returns its result.
143+
/// Executes database operations, returns their result after they have
144+
/// finished executing, and allows or forbids long-lived transactions.
145+
///
146+
/// This method is reentrant.
147+
///
148+
/// - parameter allowingLongLivedTransaction: When true, the
149+
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
150+
/// ignored until this method is called again with false.
151+
func reentrantSync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
152+
try reentrantSync { db in
153+
self.allowsUnsafeTransactions = allowingLongLivedTransaction
154+
return try body(db)
155+
}
156+
}
157+
158+
/// Executes database operations, and returns their result after they
159+
/// have finished executing.
127160
///
128161
/// This method is reentrant.
129162
func reentrantSync<T>(_ block: (Database) throws -> T) rethrows -> T {
@@ -189,7 +222,7 @@ final class SerializedDatabase {
189222
}
190223
}
191224

192-
/// Asynchronously executes a block in the serialized dispatch queue.
225+
/// Schedules database operations for execution, and returns immediately.
193226
func async(_ block: @escaping (Database) -> Void) {
194227
queue.async {
195228
block(self.db)
@@ -242,7 +275,7 @@ final class SerializedDatabase {
242275
line: UInt = #line)
243276
{
244277
GRDBPrecondition(
245-
configuration.allowsUnsafeTransactions || !db.isInsideTransaction,
278+
allowsUnsafeTransactions || configuration.allowsUnsafeTransactions || !db.isInsideTransaction,
246279
message(),
247280
file: file,
248281
line: line)

0 commit comments

Comments
 (0)