diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 8cdbcc0aa8..89695e6690 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ 5657AAB91D107001006283EF /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AAB81D107001006283EF /* NSData.swift */; }; 5657AB0F1D10899D006283EF /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB0E1D10899D006283EF /* URL.swift */; }; 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4871EA8D94E004A4992 /* Utils.swift */; }; - 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */; }; + 5659F4901EA8D964004A4992 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */; }; 5659F4981EA8D989004A4992 /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4971EA8D989004A4992 /* Pool.swift */; }; 5664759A1D97D8A000FF74B8 /* SQLCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566475991D97D8A000FF74B8 /* SQLCollection.swift */; }; 566475CC1D981D5E00FF74B8 /* SQLFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566475CA1D981D5E00FF74B8 /* SQLFunctions.swift */; }; @@ -605,7 +605,7 @@ 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSURLTests.swift; sourceTree = ""; }; 5657AB351D108BA9006283EF /* FoundationURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationURLTests.swift; sourceTree = ""; }; 5659F4871EA8D94E004A4992 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteBox.swift; sourceTree = ""; }; + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 5659F4971EA8D989004A4992 /* Pool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pool.swift; sourceTree = ""; }; 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecordTests.swift; sourceTree = ""; }; 565D5D701BBC694D00DC9BD4 /* Row+FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+FoundationTests.swift"; sourceTree = ""; }; @@ -1328,7 +1328,7 @@ 563B8FC424A1D3B9007A48C9 /* OnDemandFuture.swift */, 563EF414215F87EB007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */, + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */, 563B8FB424A1D029007A48C9 /* ReceiveValuesOn.swift */, 56781B0A243F86E600650A83 /* Refinable.swift */, 5659F4871EA8D94E004A4992 /* Utils.swift */, @@ -2185,7 +2185,7 @@ 563B06AB217EF0CC00B38F35 /* ValueObservation.swift in Sources */, 56D110FA28AFC97E00E64463 /* MutablePersistableRecord+DAO.swift in Sources */, 56CEB5111EAA324B00BFAF62 /* FTS3+QueryInterface.swift in Sources */, - 5659F4901EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5659F4901EA8D964004A4992 /* ReadWriteLock.swift in Sources */, 566A841A2041146100E50BFD /* DatabaseSnapshot.swift in Sources */, 569EF0E2200D2D8400A9FA45 /* DatabaseRegion.swift in Sources */, 56CEB4F11EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, diff --git a/GRDB/Record/MutablePersistableRecord+DAO.swift b/GRDB/Record/MutablePersistableRecord+DAO.swift index b63c1ab3a7..89efd4cd7d 100644 --- a/GRDB/Record/MutablePersistableRecord+DAO.swift +++ b/GRDB/Record/MutablePersistableRecord+DAO.swift @@ -273,29 +273,33 @@ private struct InsertQuery: Hashable { } extension InsertQuery { - @ReadWriteBox private static var sqlCache: [InsertQuery: String] = [:] + private static let cacheLock: ReadWriteLock<[InsertQuery: String]> = ReadWriteLock([:]) var sql: String { - if let sql = Self.sqlCache[self] { + if let sql = Self.cacheLock.read({ $0[self] }) { return sql } - let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ") - let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) - let sql: String - switch onConflict { - case .abort: - sql = """ - INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ - default: - sql = """ - INSERT OR \(onConflict.rawValue) \ - INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ - VALUES (\(valuesSQL)) - """ + + return Self.cacheLock.withLock { cache in + let columnsSQL = insertedColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ") + let valuesSQL = databaseQuestionMarks(count: insertedColumns.count) + let sql: String + switch onConflict { + case .abort: + sql = """ + INSERT INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + default: + sql = """ + INSERT OR \(onConflict.rawValue) \ + INTO \(tableName.quotedDatabaseIdentifier) (\(columnsSQL)) \ + VALUES (\(valuesSQL)) + """ + } + + cache[self] = sql + return sql } - Self.sqlCache[self] = sql - return sql } } @@ -309,30 +313,34 @@ private struct UpdateQuery: Hashable { } extension UpdateQuery { - @ReadWriteBox private static var sqlCache: [UpdateQuery: String] = [:] + private static let cacheLock: ReadWriteLock<[UpdateQuery: String]> = ReadWriteLock([:]) var sql: String { - if let sql = Self.sqlCache[self] { + if let sql = Self.cacheLock.read({ $0[self] }) { return sql } - let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") - let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") - let sql: String - switch onConflict { - case .abort: - sql = """ - UPDATE \(tableName.quotedDatabaseIdentifier) \ - SET \(updateSQL) \ - WHERE \(whereSQL) - """ - default: - sql = """ - UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \ - SET \(updateSQL) \ - WHERE \(whereSQL) - """ + + return Self.cacheLock.withLock { cache in + let updateSQL = updatedColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: ", ") + let whereSQL = conditionColumns.map { "\($0.quotedDatabaseIdentifier)=?" }.joined(separator: " AND ") + let sql: String + switch onConflict { + case .abort: + sql = """ + UPDATE \(tableName.quotedDatabaseIdentifier) \ + SET \(updateSQL) \ + WHERE \(whereSQL) + """ + default: + sql = """ + UPDATE OR \(onConflict.rawValue) \(tableName.quotedDatabaseIdentifier) \ + SET \(updateSQL) \ + WHERE \(whereSQL) + """ + } + + cache[self] = sql + return sql } - Self.sqlCache[self] = sql - return sql } } diff --git a/GRDB/Utils/Pool.swift b/GRDB/Utils/Pool.swift index 0a8be5338d..0e38d4550b 100644 --- a/GRDB/Utils/Pool.swift +++ b/GRDB/Utils/Pool.swift @@ -37,7 +37,7 @@ import Dispatch /// got 3 final class Pool: Sendable { private class Item: @unchecked Sendable { - // @unchecked because `isAvailable` is protected by `Pool.content`. + // @unchecked Sendable because `isAvailable` is protected by `contentLock`. let element: T var isAvailable: Bool @@ -59,7 +59,7 @@ final class Pool: Sendable { typealias ElementAndRelease = (element: T, release: @Sendable (PoolCompletion) -> Void) private let makeElement: @Sendable (Int) throws -> T - private let content = ReadWriteBox(wrappedValue: Content(items: [], createdCount: 0)) + private let contentLock = ReadWriteLock(Content(items: [], createdCount: 0)) private let itemsSemaphore: DispatchSemaphore // limits the number of elements private let itemsGroup: DispatchGroup // knows when no element is used private let barrierQueue: DispatchQueue @@ -93,7 +93,7 @@ final class Pool: Sendable { itemsSemaphore.wait() itemsGroup.enter() do { - let item = try content.update { content -> Item in + let item = try contentLock.withLock { content -> Item in if let item = content.items.first(where: \.isAvailable) { item.isAvailable = false return item @@ -142,7 +142,7 @@ final class Pool: Sendable { } private func release(_ item: Item, completion: PoolCompletion) { - content.update { content in + contentLock.withLock { content in switch completion { case .reuse: // This is why Item is a class, not a struct: so that we can @@ -162,7 +162,7 @@ final class Pool: Sendable { /// Performs a block on each pool element, available or not. /// The block is run is some arbitrary dispatch queue. func forEach(_ body: (T) throws -> Void) rethrows { - try content.read { content in + try contentLock.read { content in for item in content.items { try body(item.element) } @@ -172,7 +172,7 @@ final class Pool: Sendable { /// Removes all elements from the pool. /// Currently used elements won't be reused. func removeAll() { - content.update { $0.items.removeAll() } + contentLock.withLock { $0.items.removeAll() } } /// Blocks until no element is used, and runs the `barrier` function before diff --git a/GRDB/Utils/ReadWriteBox.swift b/GRDB/Utils/ReadWriteBox.swift deleted file mode 100644 index 2c0cd88f8c..0000000000 --- a/GRDB/Utils/ReadWriteBox.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Dispatch - -/// A ReadWriteBox grants multiple readers and single-writer guarantees on a -/// value. It is backed by a concurrent DispatchQueue. -@propertyWrapper -final class ReadWriteBox: @unchecked Sendable { - // @unchecked because `_wrappedValue` is protected by `queue` - private var _wrappedValue: T - private var queue: DispatchQueue - - var wrappedValue: T { - get { read { $0 } } - set { update { $0 = newValue } } - } - - var projectedValue: ReadWriteBox { self } - - init(wrappedValue: T) { - _wrappedValue = wrappedValue - queue = DispatchQueue(label: "GRDB.ReadWriteBox", attributes: [.concurrent]) - } - - func read(_ block: (T) throws -> U) rethrows -> U { - try queue.sync { - try block(_wrappedValue) - } - } - - func update(_ block: (inout T) throws -> U) rethrows -> U { - try queue.sync(flags: [.barrier]) { - try block(&_wrappedValue) - } - } -} - -extension ReadWriteBox where T: Numeric { - @discardableResult - func increment() -> T { - update { n in - n += 1 - return n - } - } - - @discardableResult - func decrement() -> T { - update { n in - n -= 1 - return n - } - } -} diff --git a/GRDB/Utils/ReadWriteLock.swift b/GRDB/Utils/ReadWriteLock.swift new file mode 100644 index 0000000000..85f191efb8 --- /dev/null +++ b/GRDB/Utils/ReadWriteLock.swift @@ -0,0 +1,32 @@ +import Dispatch + +/// A ReadWriteLock grants multiple readers and single-writer guarantees on +/// a value. It is backed by a concurrent DispatchQueue. +final class ReadWriteLock { + private var _value: T + private var queue: DispatchQueue + + init(_ value: T) { + _value = value + queue = DispatchQueue(label: "GRDB.ReadWriteLock", attributes: [.concurrent]) + } + + /// Reads the value. + func read(_ body: (T) throws -> U) rethrows -> U { + try queue.sync { + try body(_value) + } + } + + /// Runs the provided closure while holding a lock on the value. + /// + /// - parameter body: A closure that can modify the value. + func withLock(_ body: (inout T) throws -> U) rethrows -> U { + try queue.sync(flags: [.barrier]) { + try body(&_value) + } + } +} + +// @unchecked because `_value` is protected by `queue` +extension ReadWriteLock: @unchecked Sendable where T: Sendable { } diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index e1b707bca6..a4ab361527 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -167,7 +167,7 @@ 5657AB611D108BA9006283EF /* FoundationNSURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */; }; 5657AB691D108BA9006283EF /* FoundationURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB351D108BA9006283EF /* FoundationURLTests.swift */; }; 5659F48A1EA8D94E004A4992 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4871EA8D94E004A4992 /* Utils.swift */; }; - 5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */; }; + 5659F4921EA8D964004A4992 /* ReadWriteLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */; }; 5659F49A1EA8D989004A4992 /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5659F4971EA8D989004A4992 /* Pool.swift */; }; 565EFAF11D0436CE00A8FA9D /* NumericOverflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565EFAED1D0436CE00A8FA9D /* NumericOverflowTests.swift */; }; 5665F868203EF4640084C6C0 /* ColumnInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5665F865203EF4590084C6C0 /* ColumnInfoTests.swift */; }; @@ -635,7 +635,7 @@ 5657AB341D108BA9006283EF /* FoundationNSURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSURLTests.swift; sourceTree = ""; }; 5657AB351D108BA9006283EF /* FoundationURLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationURLTests.swift; sourceTree = ""; }; 5659F4871EA8D94E004A4992 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteBox.swift; sourceTree = ""; }; + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadWriteLock.swift; sourceTree = ""; }; 5659F4971EA8D989004A4992 /* Pool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pool.swift; sourceTree = ""; }; 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchableRecordTests.swift; sourceTree = ""; }; 565D5D701BBC694D00DC9BD4 /* Row+FoundationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Row+FoundationTests.swift"; sourceTree = ""; }; @@ -1349,7 +1349,7 @@ 563B8FBC24A1D388007A48C9 /* OnDemandFuture.swift */, 563EF41E215F8A76007DAACD /* OrderedDictionary.swift */, 5659F4971EA8D989004A4992 /* Pool.swift */, - 5659F48F1EA8D964004A4992 /* ReadWriteBox.swift */, + 5659F48F1EA8D964004A4992 /* ReadWriteLock.swift */, 563B8FB924A1D036007A48C9 /* ReceiveValuesOn.swift */, 56DF37A623D77AA0009AAA05 /* Refinable.swift */, 5659F4871EA8D94E004A4992 /* Utils.swift */, @@ -2009,7 +2009,7 @@ 56012B82257404A400B4925B /* CommonTableExpression.swift in Sources */, 5656A8972295BD56001FF3FF /* SQLRelation.swift in Sources */, 56D110FF28AFC9C600E64463 /* MutablePersistableRecord+DAO.swift in Sources */, - 5659F4921EA8D964004A4992 /* ReadWriteBox.swift in Sources */, + 5659F4921EA8D964004A4992 /* ReadWriteLock.swift in Sources */, 566A842D20413D9A00E50BFD /* DatabaseSnapshot.swift in Sources */, 56CEB4F31EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, 5656A8512295BD56001FF3FF /* SQLInterpolation+QueryInterface.swift in Sources */, diff --git a/TODO.md b/TODO.md index 95d9a5d475..2bed7e01cb 100644 --- a/TODO.md +++ b/TODO.md @@ -131,7 +131,7 @@ - [X] GRDB7: DatabaseCursor has a primary associated type (b11c5dd2) - [ ] GRDB7: Enable Strict Concurrency Checks (6aa43ded) - [X] GRDB7: Sendable: OrderedDictionary (e022c35b) -- [ ] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) +- [X] GRDB7: Rename ReadWriteBox to ReadWriteLock (7f5205ef) - [X] GRDB7: Sendable: DatabaseRegionConvertible (b4677ded) - [ ] GRDB7: Sendable: ValueConcurrentObserver (87b9db65, 5465d056) - [ ] GRDB7: Sendable: ValueWriteOnlyObserver (ff2a7548) diff --git a/Tests/GRDBTests/PoolTests.swift b/Tests/GRDBTests/PoolTests.swift index dbce4868bd..8729ae8171 100644 --- a/Tests/GRDBTests/PoolTests.swift +++ b/Tests/GRDBTests/PoolTests.swift @@ -4,9 +4,9 @@ import XCTest class PoolTests: XCTestCase { /// Returns a Pool whose elements are incremented integers: 1, 2, 3... private func makeCounterPool(maximumCount: Int) -> Pool { - let count = ReadWriteBox(wrappedValue: 0) + let countMutex = Mutex(0) return Pool(maximumCount: maximumCount, makeElement: { _ in - count.increment() + countMutex.increment() }) }