Skip to content

Commit

Permalink
Updates for modern Swift concurrency warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoarment committed Oct 10, 2023
1 parent 2f15d7f commit deaf003
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 86 deletions.
2 changes: 1 addition & 1 deletion Sources/Blackbird/Blackbird.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ extension Blackbird {
}

/// Blackbird's locked-value utility, offered for public use. Useful when conforming to `Sendable`.
public final class Locked<T>: @unchecked Sendable /* unchecked due to use of internal locking */ {
public final class Locked<T: Sendable>: @unchecked Sendable /* unchecked due to use of internal locking */ {
public var value: T {
get {
return lock.withLock { _value }
Expand Down
190 changes: 127 additions & 63 deletions Sources/Blackbird/BlackbirdCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extension Blackbird.Database {
}

internal final class Cache: Sendable {
private class CacheEntry<T> {
private class CacheEntry<T: Sendable> {
typealias AccessTime = UInt64
private let _value: T
var lastAccessed: AccessTime
Expand All @@ -77,9 +77,9 @@ extension Blackbird.Database {
}
}

internal enum CachedQueryResult {
internal enum CachedQueryResult: Sendable {
case miss
case hit(value: Any?)
case hit(value: Sendable?)
}

private let lowMemoryEventSource: DispatchSourceMemoryPressure
Expand All @@ -92,11 +92,7 @@ extension Blackbird.Database {
// or taking precious time to walk the cache contents with the normal prune() operation,
// just dump everything.
//
for (_, cache) in entries {
cache.modelsByPrimaryKey.removeAll(keepingCapacity: false)
cache.cachedQueries.removeAll(keepingCapacity: false)
cache.lowMemoryFlushes += 1
}
for (_, cache) in entries { cache.flushForLowMemory() }
}
}
lowMemoryEventSource.resume()
Expand All @@ -106,22 +102,98 @@ extension Blackbird.Database {
lowMemoryEventSource.cancel()
}

private final class TableCache {
private final class TableCache: @unchecked Sendable { /* unchecked due to internal locking */
private let lock = Blackbird.Lock()

// Cached data
var modelsByPrimaryKey: [Blackbird.Value: CacheEntry<any BlackbirdModel>] = [:]
var cachedQueries: [[Blackbird.Value]: CacheEntry<Any>] = [:]
private var modelsByPrimaryKey: [Blackbird.Value: CacheEntry<any BlackbirdModel>] = [:]
private var cachedQueries: [[Blackbird.Value]: CacheEntry<Sendable>] = [:]

// Performance counters
var hits: Int = 0
var misses: Int = 0
var writes: Int = 0
var rowInvalidations: Int = 0
var queryInvalidations: Int = 0
var tableInvalidations: Int = 0
var evictions: Int = 0
var lowMemoryFlushes: Int = 0
private var hits: Int = 0
private var misses: Int = 0
private var writes: Int = 0
private var rowInvalidations: Int = 0
private var queryInvalidations: Int = 0
private var tableInvalidations: Int = 0
private var evictions: Int = 0
private var lowMemoryFlushes: Int = 0

func get(primaryKey: Blackbird.Value) -> (any BlackbirdModel)? {
lock.lock()
defer { lock.unlock() }

if let hit = modelsByPrimaryKey[primaryKey] {
hit.lastAccessed = mach_absolute_time()
hits += 1
return hit.value()
} else {
misses += 1
return nil
}
}

func get(primaryKeys: [Blackbird.Value]) -> (hits: [any BlackbirdModel], missedKeys: [Blackbird.Value]) {
lock.lock()
defer { lock.unlock() }

var hitResults: [any BlackbirdModel] = []
var missedKeys: [Blackbird.Value] = []
for key in primaryKeys {
if let hit = modelsByPrimaryKey[key] { hitResults.append(hit.value()) } else { missedKeys.append(key) }
}
hits += hitResults.count
misses += missedKeys.count
return (hits: hitResults, missedKeys: missedKeys)
}

func getQuery(cacheKey: [Blackbird.Value]) -> CachedQueryResult {
lock.lock()
defer { lock.unlock() }

if let hit = cachedQueries[cacheKey] {
hits += 1
return .hit(value: hit.value())
} else {
misses += 1
return .miss
}
}

func prune(entryLimit: Int) {
func add(primaryKey: Blackbird.Value, instance: any BlackbirdModel, pruneToLimit: Int? = nil) {
lock.lock()
defer { lock.unlock() }

modelsByPrimaryKey[primaryKey] = CacheEntry(instance)
writes += 1

if let pruneToLimit {
inLock_prune(entryLimit: pruneToLimit)
}
}

func addQuery(cacheKey: [Blackbird.Value], result: Sendable?, pruneToLimit: Int? = nil) {
lock.lock()
defer { lock.unlock() }

cachedQueries[cacheKey] = CacheEntry(result)
writes += 1

if let pruneToLimit {
inLock_prune(entryLimit: pruneToLimit)
}

}

func delete(primaryKey: Blackbird.Value) {
lock.lock()
defer { lock.unlock() }

modelsByPrimaryKey.removeValue(forKey: primaryKey)
writes += 1
}

private func inLock_prune(entryLimit: Int) {
if modelsByPrimaryKey.count + cachedQueries.count <= entryLimit { return }

// As a table hits its entry limit, to avoid running the expensive pruning operation after EVERY addition,
Expand All @@ -148,6 +220,9 @@ extension Blackbird.Database {
}

func invalidate(primaryKeyValue: Blackbird.Value? = nil) {
lock.lock()
defer { lock.unlock() }

if let primaryKeyValue {
if nil != modelsByPrimaryKey.removeValue(forKey: primaryKeyValue) {
rowInvalidations += 1
Expand All @@ -165,13 +240,34 @@ extension Blackbird.Database {
}
}

func flushForLowMemory() {
lock.lock()
defer { lock.unlock() }

modelsByPrimaryKey.removeAll(keepingCapacity: false)
cachedQueries.removeAll(keepingCapacity: false)
lowMemoryFlushes += 1
}

func resetPerformanceMetrics() {
lock.lock()
defer { lock.unlock() }

hits = 0
misses = 0
writes = 0
evictions = 0
rowInvalidations = 0
queryInvalidations = 0
tableInvalidations = 0
lowMemoryFlushes = 0
}

func getPerformanceMetrics() -> CachePerformanceMetrics {
lock.lock()
defer { lock.unlock() }

return CachePerformanceMetrics(hits: hits, misses: misses, writes: writes, rowInvalidations: rowInvalidations, queryInvalidations: queryInvalidations, tableInvalidations: tableInvalidations, evictions: evictions, lowMemoryFlushes: lowMemoryFlushes)
}
}

Expand All @@ -194,18 +290,9 @@ extension Blackbird.Database {
else {
tableCache = TableCache()
$0[tableName] = tableCache
tableCache.misses += 1
return nil
}

if let hit = tableCache.modelsByPrimaryKey[primaryKey] {
hit.lastAccessed = mach_absolute_time()
tableCache.hits += 1
return hit.value()
} else {
tableCache.misses += 1
return nil
}

return tableCache.get(primaryKey: primaryKey)
}
}

Expand All @@ -216,18 +303,9 @@ extension Blackbird.Database {
else {
tableCache = TableCache()
$0[tableName] = tableCache
tableCache.misses += primaryKeys.count
return (hits: [], missedKeys: primaryKeys)
}

var hits: [any BlackbirdModel] = []
var missedKeys: [Blackbird.Value] = []
for key in primaryKeys {
if let hit = tableCache.modelsByPrimaryKey[key] { hits.append(hit.value()) } else { missedKeys.append(key) }
}
tableCache.hits += hits.count
tableCache.misses += missedKeys.count
return (hits: hits, missedKeys: missedKeys)
return tableCache.get(primaryKeys: primaryKeys)
}
}

Expand All @@ -239,10 +317,8 @@ extension Blackbird.Database {
tableCache = TableCache()
$0[tableName] = tableCache
}

tableCache.modelsByPrimaryKey[primaryKey] = CacheEntry(instance)
tableCache.writes += 1
tableCache.prune(entryLimit: entryLimit)

tableCache.add(primaryKey: primaryKey, instance: instance, pruneToLimit: entryLimit)
}
}

Expand All @@ -254,9 +330,8 @@ extension Blackbird.Database {
tableCache = TableCache()
$0[tableName] = tableCache
}

tableCache.modelsByPrimaryKey.removeValue(forKey: primaryKey)
tableCache.writes += 1

tableCache.delete(primaryKey: primaryKey)
}
}

Expand All @@ -267,17 +342,8 @@ extension Blackbird.Database {
else {
tableCache = TableCache()
$0[tableName] = tableCache
tableCache.misses += 1
return .miss
}

if let hit = tableCache.cachedQueries[cacheKey] {
tableCache.hits += 1
return .hit(value: hit.value())
} else {
tableCache.misses += 1
return .miss
}
return tableCache.getQuery(cacheKey: cacheKey)
}
}

Expand All @@ -289,16 +355,14 @@ extension Blackbird.Database {
tableCache = TableCache()
$0[tableName] = tableCache
}

tableCache.cachedQueries[cacheKey] = CacheEntry(result)
tableCache.writes += 1
tableCache.prune(entryLimit: entryLimit)

tableCache.addQuery(cacheKey: cacheKey, result: result, pruneToLimit: entryLimit)
}
}

internal func performanceMetrics() -> [String: CachePerformanceMetrics] {
entriesByTableName.withLock { tableCaches in
tableCaches.mapValues { CachePerformanceMetrics(hits: $0.hits, misses: $0.misses, writes: $0.writes, rowInvalidations: $0.rowInvalidations, queryInvalidations: $0.queryInvalidations, tableInvalidations: $0.tableInvalidations, evictions: $0.evictions, lowMemoryFlushes: $0.lowMemoryFlushes) }
tableCaches.mapValues { $0.getPerformanceMetrics() }
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Blackbird/BlackbirdCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ internal class BlackbirdSQLiteDecoder: Decoder {

fileprivate struct BlackbirdSQLiteSingleValueDecodingContainer: SingleValueDecodingContainer {
public enum Error: Swift.Error {
case invalidEnumValue(typeDescription: String, value: Any)
case invalidEnumValue(typeDescription: String, value: Sendable)
}

var codingPath: [CodingKey] = []
Expand Down
4 changes: 2 additions & 2 deletions Sources/Blackbird/BlackbirdModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ extension BlackbirdModel {
self._deleteCachedInstance(for: database)
}

fileprivate static func _cacheableResult<T>(database: Blackbird.Database, tableName: String, query: String, arguments: [Blackbird.Value], resultFetcher: ((Blackbird.Database) async throws -> T)) async throws -> T {
fileprivate static func _cacheableResult<T: Sendable>(database: Blackbird.Database, tableName: String, query: String, arguments: [Blackbird.Value], resultFetcher: ((Blackbird.Database) async throws -> T)) async throws -> T {
let cacheLimit = Self.cacheLimit
guard cacheLimit > 0 else { return try await resultFetcher(database) }
var cacheKey: [Blackbird.Value] = [.text(query)]
Expand All @@ -1039,7 +1039,7 @@ extension BlackbirdModel {
return result
}

fileprivate static func _cacheableResultIsolated<T>(database: Blackbird.Database, core: isolated Blackbird.Database.Core, tableName: String, query: String, arguments: [Blackbird.Value], resultFetcher: ((Blackbird.Database, isolated Blackbird.Database.Core) throws -> T)) throws -> T {
fileprivate static func _cacheableResultIsolated<T: Sendable>(database: Blackbird.Database, core: isolated Blackbird.Database.Core, tableName: String, query: String, arguments: [Blackbird.Value], resultFetcher: ((Blackbird.Database, isolated Blackbird.Database.Core) throws -> T)) throws -> T {
let cacheLimit = Self.cacheLimit
guard cacheLimit > 0 else { return try resultFetcher(database, core) }
var cacheKey: [Blackbird.Value] = [.text(query)]
Expand Down
16 changes: 8 additions & 8 deletions Sources/Blackbird/BlackbirdModelStructuredQuerying.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ fileprivate struct DecodedStructuredQuery: Sendable {


extension BlackbirdModel {
fileprivate static func _cacheableStructuredResult<T>(database: Blackbird.Database, decoded: DecodedStructuredQuery, resultFetcher: ((Blackbird.Database) async throws -> T)) async throws -> T {
fileprivate static func _cacheableStructuredResult<T: Sendable>(database: Blackbird.Database, decoded: DecodedStructuredQuery, resultFetcher: ((Blackbird.Database) async throws -> T)) async throws -> T {
let cacheLimit = Self.cacheLimit
guard cacheLimit > 0, let cacheKey = decoded.cacheKey else { return try await resultFetcher(database) }

Expand All @@ -199,7 +199,7 @@ extension BlackbirdModel {
return result
}

fileprivate static func _cacheableStructuredResultIsolated<T>(database: Blackbird.Database, core: isolated Blackbird.Database.Core, decoded: DecodedStructuredQuery, resultFetcher: ((Blackbird.Database, isolated Blackbird.Database.Core) throws -> T)) throws -> T {
fileprivate static func _cacheableStructuredResultIsolated<T: Sendable>(database: Blackbird.Database, core: isolated Blackbird.Database.Core, decoded: DecodedStructuredQuery, resultFetcher: ((Blackbird.Database, isolated Blackbird.Database.Core) throws -> T)) throws -> T {
let cacheLimit = Self.cacheLimit
guard cacheLimit > 0, let cacheKey = decoded.cacheKey else { return try resultFetcher(database, core) }

Expand Down Expand Up @@ -375,13 +375,13 @@ extension BlackbirdModel {
/// ```
///
/// If matching against specific primary-key values, use ``update(in:set:forPrimaryKeys:)`` instead.
public static func update(in database: Blackbird.Database, set changes: [BlackbirdColumnKeyPath: Any?], matching: BlackbirdModelColumnExpression<Self>) async throws {
public static func update(in database: Blackbird.Database, set changes: [BlackbirdColumnKeyPath: Sendable?], matching: BlackbirdModelColumnExpression<Self>) async throws {
if changes.isEmpty { return }
try await updateIsolated(in: database, core: database.core, set: changes, matching: matching)
}

/// Synchronous version of ``update(in:set:matching:)`` for use when the database actor is isolated within calls to ``Blackbird/Database/transaction(_:)`` or ``Blackbird/Database/cancellableTransaction(_:)``.
public static func updateIsolated(in database: Blackbird.Database, core: isolated Blackbird.Database.Core, set changes: [BlackbirdColumnKeyPath: Any?], matching: BlackbirdModelColumnExpression<Self>) throws {
public static func updateIsolated(in database: Blackbird.Database, core: isolated Blackbird.Database.Core, set changes: [BlackbirdColumnKeyPath: Sendable?], matching: BlackbirdModelColumnExpression<Self>) throws {
if database.options.contains(.readOnly) { fatalError("Cannot update BlackbirdModels in a read-only database") }
if changes.isEmpty { return }
let table = Self.table
Expand Down Expand Up @@ -437,7 +437,7 @@ extension BlackbirdModel {
/// // "UPDATE Post SET title = 'Hi' WHERE (id = 1 OR id = 2 OR id = 3)"
/// ```
/// For tables with multi-column primary keys, use ``update(in:set:forMulticolumnPrimaryKeys:)``.
public static func update(in database: Blackbird.Database, set changes: [BlackbirdColumnKeyPath: Any?], forPrimaryKeys: [Any]) async throws {
public static func update(in database: Blackbird.Database, set changes: [BlackbirdColumnKeyPath: Sendable?], forPrimaryKeys: [Sendable]) async throws {
if changes.isEmpty { return }
try await updateIsolated(in: database, core: database.core, set: changes, forMulticolumnPrimaryKeys: forPrimaryKeys.map { [$0] })
}
Expand All @@ -463,18 +463,18 @@ extension BlackbirdModel {
/// ```
///
/// For tables with single-column primary keys, ``update(in:set:forPrimaryKeys:)`` may also be used.
public static func update(in database: Blackbird.Database, set changes: [BlackbirdColumnKeyPath: Any?], forMulticolumnPrimaryKeys: [[Any]]) async throws {
public static func update(in database: Blackbird.Database, set changes: [BlackbirdColumnKeyPath: Sendable?], forMulticolumnPrimaryKeys: [[Sendable]]) async throws {
if changes.isEmpty { return }
try await updateIsolated(in: database, core: database.core, set: changes, forMulticolumnPrimaryKeys: forMulticolumnPrimaryKeys)
}

/// Synchronous version of ``update(in:set:forPrimaryKeys:)`` for use when the database actor is isolated within calls to ``Blackbird/Database/transaction(_:)`` or ``Blackbird/Database/cancellableTransaction(_:)``.
public static func updateIsolated(in database: Blackbird.Database, core: isolated Blackbird.Database.Core, set changes: [BlackbirdColumnKeyPath: Any?], forPrimaryKeys: [Any]) throws {
public static func updateIsolated(in database: Blackbird.Database, core: isolated Blackbird.Database.Core, set changes: [BlackbirdColumnKeyPath: Sendable?], forPrimaryKeys: [Sendable]) throws {
try updateIsolated(in: database, core: core, set: changes, forMulticolumnPrimaryKeys: forPrimaryKeys.map { [$0] })
}

/// Synchronous version of ``update(in:set:forMulticolumnPrimaryKeys:)`` for use when the database actor is isolated within calls to ``Blackbird/Database/transaction(_:)`` or ``Blackbird/Database/cancellableTransaction(_:)``.
public static func updateIsolated(in database: Blackbird.Database, core: isolated Blackbird.Database.Core, set changes: [BlackbirdColumnKeyPath: Any?], forMulticolumnPrimaryKeys primaryKeyValues: [[Any]]) throws {
public static func updateIsolated(in database: Blackbird.Database, core: isolated Blackbird.Database.Core, set changes: [BlackbirdColumnKeyPath: Sendable?], forMulticolumnPrimaryKeys primaryKeyValues: [[Sendable]]) throws {
if database.options.contains(.readOnly) { fatalError("Cannot update BlackbirdModels in a read-only database") }
if changes.isEmpty { return }
let primaryKeyValues = Array(primaryKeyValues)
Expand Down
Loading

0 comments on commit deaf003

Please sign in to comment.