Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift
T

356 lines
12 KiB
Swift

import XCTest
@testable import GRDB
class DatabasePoolReleaseMemoryTests: GRDBTestCase {
func testDatabasePoolDeinitClosesAllConnections() throws {
let countQueue = DispatchQueue(label: "GRDB")
var openConnectionCount = 0
var totalOpenConnectionCount = 0
dbConfiguration.SQLiteConnectionDidOpen = {
countQueue.sync {
totalOpenConnectionCount += 1
openConnectionCount += 1
}
}
dbConfiguration.SQLiteConnectionDidClose = {
countQueue.sync {
openConnectionCount -= 1
}
}
// write & read
do {
// Create and release DatabasePool
let dbPool = try makeDatabasePool()
// Writer connection
try dbPool.write { db in
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
}
// Reader connection
try dbPool.read { _ in }
}
// One reader, one writer
XCTAssertEqual(totalOpenConnectionCount, 2)
// All connections are closed
XCTAssertEqual(openConnectionCount, 0)
}
#if os(iOS)
func testDatabasePoolReleasesMemoryOnPressureEvent() throws {
// Create a database pool, and expect a reader connection to be closed
let expectation = self.expectation(description: "Reader connection closed")
var configuration = Configuration()
configuration.SQLiteConnectionWillClose = { conn in
if sqlite3_db_readonly(conn, nil) != 0 {
expectation.fulfill()
}
}
let dbPool = try makeDatabasePool(configuration: configuration)
// Precondition: there is one reader.
try dbPool.read { _ in }
// Simulate memory warning.
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
// Postcondition: reader connection was closed
withExtendedLifetime(dbPool) { _ in
waitForExpectations(timeout: 0.5)
}
}
func testDatabasePoolDoesNotReleaseMemoryOnPressureEventIfDisabled() throws {
// Create a database pool, and do not expect any reader connection to be closed
let expectation = self.expectation(description: "Reader connection closed")
expectation.isInverted = true
var configuration = Configuration()
configuration.automaticMemoryManagement = false
configuration.SQLiteConnectionWillClose = { conn in
if sqlite3_db_readonly(conn, nil) != 0 {
expectation.fulfill()
}
}
let dbPool = try makeDatabasePool(configuration: configuration)
// Precondition: there is one reader.
try dbPool.read { _ in }
// Simulate memory warning.
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
// Postcondition: no reader connection was closed
withExtendedLifetime(dbPool) { _ in
waitForExpectations(timeout: 0.5)
}
}
// Regression test for <https://github.com/groue/GRDB.swift/pull/1253#issuecomment-1177166630>
func testDatabasePoolDoesNotPreventConcurrentReadsOnPressureEvent() throws {
let dbPool = try makeDatabasePool()
// Start a read that blocks
let semaphore = DispatchSemaphore(value: 0)
dbPool.asyncRead { _ in
semaphore.wait()
}
// Simulate memory warning.
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
// Make sure we can read
try dbPool.read { _ in }
// Cleanup
semaphore.signal()
}
#endif
func test_DatabasePool_releaseMemory_closes_reader_connections() throws {
// A complicated test setup that opens multiple reader connections.
let countQueue = DispatchQueue(label: "GRDB")
var openConnectionCount = 0
var totalOpenConnectionCount = 0
dbConfiguration.SQLiteConnectionDidOpen = {
countQueue.sync {
totalOpenConnectionCount += 1
openConnectionCount += 1
}
}
dbConfiguration.SQLiteConnectionDidClose = {
countQueue.sync {
openConnectionCount -= 1
}
}
let dbPool = try makeDatabasePool()
try dbPool.write { db in
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
for _ in 0..<2 {
try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
}
}
// Block 1 Block 2 Block3
// SELECT * FROM items
// step
// >
let s1 = DispatchSemaphore(value: 0)
// SELECT * FROM items
// step
// >
let s2 = DispatchSemaphore(value: 0)
// step step
// >
let s3 = DispatchSemaphore(value: 0)
// end end releaseMemory
let block1 = { () in
try! dbPool.read { db in
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
XCTAssertTrue(try cursor.next() != nil)
s1.signal()
_ = s2.wait(timeout: .distantFuture)
XCTAssertTrue(try cursor.next() != nil)
s3.signal()
XCTAssertTrue(try cursor.next() == nil)
}
}
let block2 = { () in
_ = s1.wait(timeout: .distantFuture)
try! dbPool.read { db in
let cursor = try Row.fetchCursor(db, sql: "SELECT * FROM items")
XCTAssertTrue(try cursor.next() != nil)
s2.signal()
XCTAssertTrue(try cursor.next() != nil)
XCTAssertTrue(try cursor.next() == nil)
}
}
let block3 = { () in
_ = s3.wait(timeout: .distantFuture)
dbPool.releaseMemory()
}
let blocks = [block1, block2, block3]
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in // FIXME: this crashes sometimes
blocks[index]()
}
// Two readers, one writer
XCTAssertEqual(totalOpenConnectionCount, 3)
// Writer is still open
XCTAssertEqual(openConnectionCount, 1)
}
func test_DatabasePool_releaseMemory_closes_reader_connections_when_persistentReadOnlyConnections_is_false() throws {
var persistentConnectionCount = 0
dbConfiguration.SQLiteConnectionDidOpen = {
persistentConnectionCount += 1
}
dbConfiguration.SQLiteConnectionDidClose = {
persistentConnectionCount -= 1
}
dbConfiguration.persistentReadOnlyConnections = false
let dbPool = try makeDatabasePool()
XCTAssertEqual(persistentConnectionCount, 1) // writer
try dbPool.read { _ in }
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
dbPool.releaseMemory()
XCTAssertEqual(persistentConnectionCount, 1) // writer
}
func test_DatabasePool_releaseMemory_does_not_close_reader_connections_when_persistentReadOnlyConnections_is_true() throws {
var persistentConnectionCount = 0
dbConfiguration.SQLiteConnectionDidOpen = {
persistentConnectionCount += 1
}
dbConfiguration.SQLiteConnectionDidClose = {
persistentConnectionCount -= 1
}
dbConfiguration.persistentReadOnlyConnections = true
let dbPool = try makeDatabasePool()
XCTAssertEqual(persistentConnectionCount, 1) // writer
try dbPool.read { _ in }
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
dbPool.releaseMemory()
XCTAssertEqual(persistentConnectionCount, 2) // writer + reader
}
func testBlocksRetainConnection() throws {
let countQueue = DispatchQueue(label: "GRDB")
var openConnectionCount = 0
var totalOpenConnectionCount = 0
dbConfiguration.SQLiteConnectionDidOpen = {
countQueue.sync {
totalOpenConnectionCount += 1
openConnectionCount += 1
}
}
dbConfiguration.SQLiteConnectionDidClose = {
countQueue.sync {
openConnectionCount -= 1
}
}
// Block 1 Block 2
// read {
// >
let s1 = DispatchSemaphore(value: 0)
// dbPool = nil
// >
let s2 = DispatchSemaphore(value: 0)
// use database
// }
let (block1, block2) = { () -> (() -> (), () -> ()) in
var dbPool: DatabasePool? = try! self.makeDatabasePool()
try! dbPool!.write { db in
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
}
let block1 = { () in
_ = s1.wait(timeout: .distantFuture)
dbPool = nil
s2.signal()
}
let block2 = { [weak dbPool] () in
if let dbPool {
try! dbPool.read { db in
s1.signal()
_ = s2.wait(timeout: .distantFuture)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items"), 0)
}
} else {
XCTFail("expect non nil dbPool")
}
}
return (block1, block2)
}()
let blocks = [block1, block2]
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
blocks[index]()
}
// one writer, one reader
XCTAssertEqual(totalOpenConnectionCount, 2)
// All connections are closed
XCTAssertEqual(openConnectionCount, 0)
}
func testStatementDoNotRetainDatabaseConnection() throws {
// Block 1 Block 2
// create statement INSERT
// >
let s1 = DispatchSemaphore(value: 0)
// dbPool = nil
// >
let s2 = DispatchSemaphore(value: 0)
// dbPool is nil
let (block1, block2) = { () -> (() -> (), () -> ()) in
var dbPool: DatabasePool? = try! self.makeDatabasePool()
let block1 = { () in
_ = s1.wait(timeout: .distantFuture)
dbPool = nil
s2.signal()
}
let block2 = { [weak dbPool] () in
var statement: Statement? = nil
do {
if let dbPool {
do {
try dbPool.write { db in
statement = try db.makeStatement(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
s1.signal()
}
} catch {
XCTFail("error: \(error)")
}
} else {
XCTFail("expect non nil dbPool")
}
}
_ = s2.wait(timeout: .distantFuture)
XCTAssertTrue(statement != nil)
XCTAssertTrue(dbPool == nil)
}
return (block1, block2)
}()
let blocks = [block1, block2]
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
blocks[index]()
}
}
}