Skip to content

Commit 714047c

Browse files
committed
Add NSCache wrapper to resolve Sendability warnings in Cache impl
1 parent 3993ccb commit 714047c

File tree

2 files changed

+23
-14
lines changed

2 files changed

+23
-14
lines changed

Sources/SWBUtil/Cache.swift

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,49 +41,58 @@ public protocol CacheableValue {
4141
var cost: Int { get }
4242
}
4343

44+
// NSCache insert, remove, and lookup operations are documented as thread safe: https://developer.apple.com/documentation/foundation/nscache
45+
fileprivate final class UnsafeNSCacheSendableWrapper<Key: Hashable, Value> {
46+
init(value: NSCache<KeyWrapper<Key>, ValueWrapper<Value>>) {
47+
self.value = value
48+
}
49+
let value: NSCache<KeyWrapper<Key>, ValueWrapper<Value>>
50+
}
51+
extension UnsafeNSCacheSendableWrapper: @unchecked Sendable where Key: Sendable, Value: Sendable {}
52+
4453
/// A thread-safe cache container.
4554
///
4655
/// This container will automatically evict entries when the system is under memory pressure.
4756
public final class Cache<Key: Hashable, Value>: NSObject, KeyValueStorage, NSCacheDelegate {
4857
/// The underlying cache implementation.
49-
private let cache: NSCache<KeyWrapper<Key>, ValueWrapper<Value>>
58+
private let cache: UnsafeNSCacheSendableWrapper<Key, Value>
5059
private let willEvictCallback: (@Sendable (Value) -> Void)?
5160

5261
public init(willEvictCallback: (@Sendable (Value)->Void)? = nil, totalCostLimit: Int? = nil) {
53-
self.cache = NSCache()
62+
self.cache = .init(value: NSCache())
5463
self.willEvictCallback = willEvictCallback
5564
super.init()
5665
if let totalCostLimit {
57-
self.cache.totalCostLimit = totalCostLimit
66+
self.cache.value.totalCostLimit = totalCostLimit
5867
}
59-
self.cache.delegate = self
68+
self.cache.value.delegate = self
6069
}
6170

6271
/// Remove all objects in the cache.
6372
public func removeAll() {
64-
cache.removeAllObjects()
73+
cache.value.removeAllObjects()
6574
}
6675

6776
/// Remove the entry for a given `key`.
6877
public func remove(_ key: Key) {
69-
cache.removeObject(forKey: KeyWrapper(key))
78+
cache.value.removeObject(forKey: KeyWrapper(key))
7079
}
7180

7281
/// Subscript access to the cache.
7382
public subscript(_ key: Key) -> Value? {
7483
get {
75-
if let wrappedValue = cache.object(forKey: KeyWrapper(key)) {
84+
if let wrappedValue = cache.value.object(forKey: KeyWrapper(key)) {
7685
return wrappedValue.value
7786
}
7887
return nil
7988
}
8089
set {
8190
if let newValue, let cacheableValue = newValue as? (any CacheableValue) {
82-
cache.setObject(ValueWrapper(newValue), forKey: KeyWrapper(key), cost: cacheableValue.cost)
91+
cache.value.setObject(ValueWrapper(newValue), forKey: KeyWrapper(key), cost: cacheableValue.cost)
8392
} else if let newValue = newValue {
84-
cache.setObject(ValueWrapper(newValue), forKey: KeyWrapper(key))
93+
cache.value.setObject(ValueWrapper(newValue), forKey: KeyWrapper(key))
8594
} else {
86-
cache.removeObject(forKey: KeyWrapper(key))
95+
cache.value.removeObject(forKey: KeyWrapper(key))
8796
}
8897
}
8998
}
@@ -98,16 +107,16 @@ public final class Cache<Key: Hashable, Value>: NSObject, KeyValueStorage, NSCac
98107
/// NOTE: The cache *does not* ensure that the block to compute a value is only called once for a given key. In race conditions, it may be called multiple times, even though only the last computed result will be used.
99108
public func getOrInsert(_ key: Key, _ body: () throws -> Value) rethrows -> Value {
100109
let wrappedKey = KeyWrapper(key)
101-
if let wrappedValue = cache.object(forKey: wrappedKey) {
110+
if let wrappedValue = cache.value.object(forKey: wrappedKey) {
102111
return wrappedValue.value
103112
}
104113

105114
let value = try body()
106115
if let cacheableValue = value as? (any CacheableValue) {
107-
cache.setObject(ValueWrapper(value), forKey: wrappedKey, cost: cacheableValue.cost)
116+
cache.value.setObject(ValueWrapper(value), forKey: wrappedKey, cost: cacheableValue.cost)
108117

109118
} else {
110-
cache.setObject(ValueWrapper(value), forKey: wrappedKey)
119+
cache.value.setObject(ValueWrapper(value), forKey: wrappedKey)
111120
}
112121
return value
113122
}

Tests/SWBTaskExecutionTests/BuildDescriptionTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ fileprivate struct BuildDescriptionTests: CoreBasedTests {
781781
}
782782
do {
783783
// Eliminate the dumping of the cache for the `isBuildDirectoryCache` object, which is not relevant for the comparison.
784-
let regex = RegEx(patternLiteral: "▿ isBuildDirectoryCache:.*$\n\\ +- super:.*$\n\\ +- cache:.*$", options: .anchorsMatchLines)
784+
let regex = RegEx(patternLiteral: "▿ isBuildDirectoryCache:.*$\n\\ +- super:.*$\n +▿ cache:.*$\n\\ +- value:.*$\n\\ +- super:.*$", options: .anchorsMatchLines)
785785
_ = regex.replace(in: &s, with: "")
786786
}
787787
return s

0 commit comments

Comments
 (0)