Skip to content

Commit 1a1ff0f

Browse files
committed
Configuration.persistentReaderConnections
1 parent b2e265d commit 1a1ff0f

File tree

4 files changed

+190
-99
lines changed

4 files changed

+190
-99
lines changed

GRDB/Core/Configuration.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ 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 read-only connections.
305305
///
306306
/// This configuration applies to ``DatabasePool`` only. The default value
307307
/// is 5.
@@ -372,7 +372,28 @@ public struct Configuration {
372372
/// The default is true.
373373
public var automaticMemoryManagement = true
374374
#endif
375-
375+
376+
/// A boolean value indicating whether read-only connections should be
377+
/// kept open as long as they remain in a valid state.
378+
///
379+
/// This configuration applies to ``DatabasePool`` only. The default value
380+
/// is false.
381+
///
382+
/// A `DatabasePool` automatically closes read-only connections on
383+
/// various occasions, in order to spare memory
384+
/// (see ``DatabasePool/releaseMemory()``), or when connections enter an
385+
/// invalid state.
386+
///
387+
/// When this flag is true, only invalid connections are automatically
388+
/// closed. Valid connections, once opened, are kept alive until the
389+
/// `DatabasePool` is deinitialized, or one of those methods is called:
390+
/// ``DatabaseReader/close()``,
391+
/// ``DatabasePool/invalidateReadOnlyConnections()``.
392+
///
393+
/// Consider using this flag when profiling your application reveals
394+
/// that a lot of time is spent opening new SQLite connections.
395+
public var persistentReaderConnections = false
396+
376397
// MARK: - Factory Configuration
377398

378399
/// Creates a factory configuration.

GRDB/Core/DatabasePool.swift

Lines changed: 43 additions & 20 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 reader connections, unless the
177+
/// ``Configuration/persistentReaderConnections`` 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.persistentReaderConnections {
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 reader connections, unless the
211+
/// ``Configuration/persistentReaderConnections`` 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.persistentReaderConnections {
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)
@@ -610,6 +629,10 @@ extension DatabasePool: DatabaseReader {
610629
///
611630
/// Eventual concurrent read-only accesses are not invalidated: they will
612631
/// proceed until completion.
632+
///
633+
/// - This method closes all read-only connections, even if the
634+
/// ``Configuration/persistentReaderConnections`` configuration flag
635+
/// is set.
613636
public func invalidateReadOnlyConnections() {
614637
readerPool?.removeAll()
615638
}

GRDB/Documentation.docc/Extension/Configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ do {
9292
- ``label``
9393
- ``maximumReaderCount``
9494
- ``observesSuspensionNotifications``
95+
- ``persistentReaderConnections``
9596
- ``prepareDatabase(_:)``
9697
- ``publicStatementArguments``
9798
- ``transactionClock``

Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift

Lines changed: 123 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -120,83 +120,129 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase {
120120

121121
#endif
122122

123-
// TODO: fix flaky test
124-
// func testDatabasePoolReleaseMemoryClosesReaderConnections() throws {
125-
// let countQueue = DispatchQueue(label: "GRDB")
126-
// var openConnectionCount = 0
127-
// var totalOpenConnectionCount = 0
128-
//
129-
// dbConfiguration.SQLiteConnectionDidOpen = {
130-
// countQueue.sync {
131-
// totalOpenConnectionCount += 1
132-
// openConnectionCount += 1
133-
// }
134-
// }
135-
//
136-
// dbConfiguration.SQLiteConnectionDidClose = {
137-
// countQueue.sync {
138-
// openConnectionCount -= 1
139-
// }
140-
// }
141-
//
142-
// let dbPool = try makeDatabasePool()
143-
// try dbPool.write { db in
144-
// try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
145-
// for _ in 0..<2 {
146-
// try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
147-
// }
148-
// }
149-
//
150-
// // Block 1 Block 2 Block3
151-
// // SELECT * FROM items
152-
// // step
153-
// // >
154-
// let s1 = DispatchSemaphore(value: 0)
155-
// // SELECT * FROM items
156-
// // step
157-
// // >
158-
// let s2 = DispatchSemaphore(value: 0)
159-
// // step step
160-
// // >
161-
// let s3 = DispatchSemaphore(value: 0)
162-
// // end end releaseMemory
163-
//
164-
// let block1 = { () in
165-
// try! dbPool.read { db in
166-
// let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
167-
// XCTAssertTrue(try cursor.next() != nil)
168-
// s1.signal()
169-
// _ = s2.wait(timeout: .distantFuture)
170-
// XCTAssertTrue(try cursor.next() != nil)
171-
// s3.signal()
172-
// XCTAssertTrue(try cursor.next() == nil)
173-
// }
174-
// }
175-
// let block2 = { () in
176-
// _ = s1.wait(timeout: .distantFuture)
177-
// try! dbPool.read { db in
178-
// let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
179-
// XCTAssertTrue(try cursor.next() != nil)
180-
// s2.signal()
181-
// XCTAssertTrue(try cursor.next() != nil)
182-
// XCTAssertTrue(try cursor.next() == nil)
183-
// }
184-
// }
185-
// let block3 = { () in
186-
// _ = s3.wait(timeout: .distantFuture)
187-
// dbPool.releaseMemory()
188-
// }
189-
// let blocks = [block1, block2, block3]
190-
// DispatchQueue.concurrentPerform(iterations: blocks.count) { index in // FIXME: this crashes sometimes
191-
// blocks[index]()
192-
// }
193-
//
194-
// // Two readers, one writer
195-
// XCTAssertEqual(totalOpenConnectionCount, 3)
196-
//
197-
// // Writer is still open
198-
// XCTAssertEqual(openConnectionCount, 1)
199-
// }
123+
func test_DatabasePool_releaseMemory_closes_reader_connections() throws {
124+
// A complicated test setup that opens multiple reader connections.
125+
let countQueue = DispatchQueue(label: "GRDB")
126+
var openConnectionCount = 0
127+
var totalOpenConnectionCount = 0
128+
129+
dbConfiguration.SQLiteConnectionDidOpen = {
130+
countQueue.sync {
131+
totalOpenConnectionCount += 1
132+
openConnectionCount += 1
133+
}
134+
}
135+
136+
dbConfiguration.SQLiteConnectionDidClose = {
137+
countQueue.sync {
138+
openConnectionCount -= 1
139+
}
140+
}
141+
142+
let dbPool = try makeDatabasePool()
143+
try dbPool.write { db in
144+
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
145+
for _ in 0..<2 {
146+
try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
147+
}
148+
}
149+
150+
// Block 1 Block 2 Block3
151+
// SELECT * FROM items
152+
// step
153+
// >
154+
let s1 = DispatchSemaphore(value: 0)
155+
// SELECT * FROM items
156+
// step
157+
// >
158+
let s2 = DispatchSemaphore(value: 0)
159+
// step step
160+
// >
161+
let s3 = DispatchSemaphore(value: 0)
162+
// end end releaseMemory
163+
164+
let block1 = { () in
165+
try! dbPool.read { db in
166+
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
167+
XCTAssertTrue(try cursor.next() != nil)
168+
s1.signal()
169+
_ = s2.wait(timeout: .distantFuture)
170+
XCTAssertTrue(try cursor.next() != nil)
171+
s3.signal()
172+
XCTAssertTrue(try cursor.next() == nil)
173+
}
174+
}
175+
let block2 = { () in
176+
_ = s1.wait(timeout: .distantFuture)
177+
try! dbPool.read { db in
178+
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
179+
XCTAssertTrue(try cursor.next() != nil)
180+
s2.signal()
181+
XCTAssertTrue(try cursor.next() != nil)
182+
XCTAssertTrue(try cursor.next() == nil)
183+
}
184+
}
185+
let block3 = { () in
186+
_ = s3.wait(timeout: .distantFuture)
187+
dbPool.releaseMemory()
188+
}
189+
let blocks = [block1, block2, block3]
190+
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in // FIXME: this crashes sometimes
191+
blocks[index]()
192+
}
193+
194+
// Two readers, one writer
195+
XCTAssertEqual(totalOpenConnectionCount, 3)
196+
197+
// Writer is still open
198+
XCTAssertEqual(openConnectionCount, 1)
199+
}
200+
201+
func test_DatabasePool_releaseMemory_closes_reader_connections_when_persistentReaderConnections_is_false() throws {
202+
var persistentConnectionCount = 0
203+
204+
dbConfiguration.SQLiteConnectionDidOpen = {
205+
persistentConnectionCount += 1
206+
}
207+
208+
dbConfiguration.SQLiteConnectionDidClose = {
209+
persistentConnectionCount -= 1
210+
}
211+
212+
dbConfiguration.persistentReaderConnections = false
213+
214+
let dbPool = try makeDatabasePool()
215+
XCTAssertEqual(persistentConnectionCount, 1) // writer
216+
217+
try dbPool.read { _ in }
218+
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
219+
220+
dbPool.releaseMemory()
221+
XCTAssertEqual(persistentConnectionCount, 1) // writer
222+
}
223+
224+
func test_DatabasePool_releaseMemory_does_not_close_reader_connections_when_persistentReaderConnections_is_true() throws {
225+
var persistentConnectionCount = 0
226+
227+
dbConfiguration.SQLiteConnectionDidOpen = {
228+
persistentConnectionCount += 1
229+
}
230+
231+
dbConfiguration.SQLiteConnectionDidClose = {
232+
persistentConnectionCount -= 1
233+
}
234+
235+
dbConfiguration.persistentReaderConnections = true
236+
237+
let dbPool = try makeDatabasePool()
238+
XCTAssertEqual(persistentConnectionCount, 1) // writer
239+
240+
try dbPool.read { _ in }
241+
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
242+
243+
dbPool.releaseMemory()
244+
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
245+
}
200246

201247
func testBlocksRetainConnection() throws {
202248
let countQueue = DispatchQueue(label: "GRDB")

0 commit comments

Comments
 (0)