Skip to content

Commit df51c30

Browse files
authored
Update for Swift 6 with thread safety improvements (#20)
* Update to Swift 6 and improve thread safety * Update to Swift 6 * Update ExampleTests.swift * Update to fix tests * Add concurrency stress tests (#21) * Add thread safety tests * Update ThreadSafetyTests.swift * Update ThreadSafetyTests.swift * Update ThreadSafetyTests.swift
1 parent 0829fe1 commit df51c30

21 files changed

+144
-49
lines changed

.github/workflows/macOS.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ name: macOS
22

33
on:
44
push:
5-
branches: [ "main" ]
6-
pull_request:
7-
branches: [ "main" ]
5+
branches: ["**"]
86

97
jobs:
108
build:
119
runs-on: macos-latest
12-
1310
steps:
11+
- uses: maxim-lobanov/setup-xcode@v1
12+
with:
13+
xcode-version: 16.0
1414
- uses: actions/checkout@v3
1515
- name: Build
1616
run: swift build -v

.github/workflows/ubuntu.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ name: Ubuntu
22

33
on:
44
push:
5-
branches: [ "main" ]
6-
pull_request:
7-
branches: [ "main" ]
5+
branches: ["**"]
86

97
jobs:
108
build:
119
runs-on: ubuntu-latest
1210

1311
steps:
14-
- uses: actions/checkout@v3
15-
- name: Build
16-
run: swift build -v
17-
- name: Run tests
18-
run: swift test -v
12+
- uses: sersoft-gmbh/swifty-linux-action@v3
13+
with:
14+
release-version: 6.0.1
15+
- uses: actions/checkout@v3
16+
- name: Build for release
17+
run: swift build -v -c release
18+
- name: Test
19+
run: swift test -v

.github/workflows/windows.yml

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ name: Windows
22

33
on:
44
push:
5-
branches: [ "main" ]
6-
pull_request:
7-
branches: [ "main" ]
5+
branches: ["**"]
86

97
jobs:
108
build:
119
runs-on: windows-latest
1210
steps:
13-
- uses: compnerd/gha-setup-swift@main
11+
- uses: actions/checkout@v4
12+
13+
# ① Install Swift for Windows
14+
- name: Set up Swift 6.1
15+
uses: compnerd/gha-setup-swift@main
1416
with:
15-
branch: swift-5.8-release
16-
tag: 5.8-RELEASE
17-
18-
- uses: actions/checkout@v2
17+
branch: swift-6.1-release # release branch
18+
tag: 6.1-RELEASE # exact toolchain tag
19+
20+
# ② Build & test
21+
- run: swift --version # sanity-check
1922
- run: swift build
20-
- run: swift test
23+
- run: swift test

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.6
1+
// swift-tools-version: 6.0
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
*A simple, lightweight caching library for Swift.*
44

5+
Requires **Swift 6.0** or later.
6+
57
## What is Cache?
68

79
Cache is a Swift library for caching arbitrary data types in memory. It provides a simple and intuitive API for storing, retrieving, and removing objects from the cache.

Sources/Cache/Cache/AnyCacheable.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#if !os(Windows)
2-
public class AnyCacheable: Cacheable {
2+
import Foundation
3+
public class AnyCacheable: Cacheable, @unchecked Sendable {
34
public typealias Key = AnyHashable
45
public typealias Value = Any
56

7+
private let lock = NSRecursiveLock()
68
private var cache: any Cacheable
79

810
private var cacheGet: ((AnyHashable) -> Any?)!
@@ -93,6 +95,7 @@ public class AnyCacheable: Cacheable {
9395
_ key: AnyHashable,
9496
as: Output.Type = Output.self
9597
) -> Output? {
98+
lock.lock(); defer { lock.unlock() }
9699
guard let value = cacheGet(key) else {
97100
return nil
98101
}
@@ -108,6 +111,7 @@ public class AnyCacheable: Cacheable {
108111
_ key: AnyHashable,
109112
as: Output.Type = Output.self
110113
) throws -> Output {
114+
lock.lock(); defer { lock.unlock() }
111115
let resolvedValue = try cacheResolve(key)
112116

113117
guard let output = resolvedValue as? Output else {
@@ -121,31 +125,37 @@ public class AnyCacheable: Cacheable {
121125
}
122126

123127
public func set(value: Value, forKey key: AnyHashable) {
128+
lock.lock(); defer { lock.unlock() }
124129
cacheSet(value, key)
125130
}
126131

127132
public func remove(_ key: AnyHashable) {
133+
lock.lock(); defer { lock.unlock() }
128134
cacheRemove(key)
129135
}
130136

131137
public func contains(_ key: AnyHashable) -> Bool {
132-
cacheContains(key)
138+
lock.lock(); defer { lock.unlock() }
139+
return cacheContains(key)
133140
}
134141

135142
public func require(keys: Set<AnyHashable>) throws -> Self {
143+
lock.lock(); defer { lock.unlock() }
136144
try cacheRequireKeys(keys)
137145

138146
return self
139147
}
140148

141149
public func require(_ key: AnyHashable) throws -> Self {
150+
lock.lock(); defer { lock.unlock() }
142151
try cacheRequireKey(key)
143152

144153
return self
145154
}
146155

147156
public func values<Output>(ofType: Output.Type) -> [AnyHashable: Output] {
148-
cacheValues().compactMapValues { value in
157+
lock.lock(); defer { lock.unlock() }
158+
return cacheValues().compactMapValues { value in
149159
value as? Output
150160
}
151161
}

Sources/Cache/Cache/Cache.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Foundation
1313
let age = cache.get("age") // age is now 100
1414
```
1515
*/
16-
open class Cache<Key: Hashable, Value>: Cacheable {
16+
open class Cache<Key: Hashable, Value>: Cacheable, @unchecked Sendable {
1717

1818
/// Lock to synchronize the access to the cache dictionary.
1919
fileprivate var lock: NSLock

Sources/Cache/Cache/ComposableCache.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#if !os(Windows)
2-
public struct ComposableCache<Key: Hashable>: Cacheable {
2+
import Foundation
3+
public struct ComposableCache<Key: Hashable>: Cacheable, @unchecked Sendable {
4+
private let lock = NSRecursiveLock()
35
private let caches: [AnyCacheable]
46

57
public init(caches: [any Cacheable]) {
@@ -14,6 +16,7 @@ public struct ComposableCache<Key: Hashable>: Cacheable {
1416
_ key: Key,
1517
as: Output.Type = Output.self
1618
) -> Output? {
19+
lock.lock(); defer { lock.unlock() }
1720
for cache in caches {
1821
guard
1922
let output = cache.get(key, as: Output.self)
@@ -31,6 +34,7 @@ public struct ComposableCache<Key: Hashable>: Cacheable {
3134
_ key: Key,
3235
as: Output.Type = Output.self
3336
) throws -> Output {
37+
lock.lock(); defer { lock.unlock() }
3438
for cache in caches {
3539
guard
3640
let output = try? cache.resolve(key, as: Output.self)
@@ -45,18 +49,21 @@ public struct ComposableCache<Key: Hashable>: Cacheable {
4549
}
4650

4751
public func set(value: Any, forKey key: Key) {
52+
lock.lock(); defer { lock.unlock() }
4853
for cache in caches {
4954
cache.set(value: value, forKey: key)
5055
}
5156
}
5257

5358
public func remove(_ key: Key) {
59+
lock.lock(); defer { lock.unlock() }
5460
for cache in caches {
5561
cache.remove(key)
5662
}
5763
}
5864

5965
public func contains(_ key: Key) -> Bool {
66+
lock.lock(); defer { lock.unlock() }
6067
for cache in caches {
6168
if cache.contains(key) {
6269
return true
@@ -67,6 +74,7 @@ public struct ComposableCache<Key: Hashable>: Cacheable {
6774
}
6875

6976
public func require(keys: Set<Key>) throws -> ComposableCache<Key> {
77+
lock.lock(); defer { lock.unlock() }
7078
for cache in caches {
7179
_ = try cache.require(keys: keys)
7280
}
@@ -75,6 +83,7 @@ public struct ComposableCache<Key: Hashable>: Cacheable {
7583
}
7684

7785
public func require(_ key: Key) throws -> ComposableCache<Key> {
86+
lock.lock(); defer { lock.unlock() }
7887
for cache in caches {
7988
_ = try cache.require(key)
8089
}
@@ -83,6 +92,7 @@ public struct ComposableCache<Key: Hashable>: Cacheable {
8392
}
8493

8594
public func values<Output>(ofType: Output.Type) -> [Key: Output] {
95+
lock.lock(); defer { lock.unlock() }
8696
for cache in caches {
8797
let values = cache.values(ofType: Output.self).compactMapKeys { $0 as? Key }
8898

Sources/Cache/Cache/ExpiringCache.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import Foundation
99

1010
Objects stored in the cache are automatically removed when their expiration duration has passed.
1111
*/
12-
public class ExpiringCache<Key: Hashable, Value>: Cacheable {
12+
public class ExpiringCache<Key: Hashable, Value>: Cacheable, @unchecked Sendable {
1313
/// `Error` that reports expired values
14-
public struct ExpiriedValueError: LocalizedError {
14+
public struct ExpiriedValueError: LocalizedError, @unchecked Sendable {
1515
/// Expired key
1616
public let key: Key
1717

@@ -71,7 +71,7 @@ public class ExpiringCache<Key: Hashable, Value>: Cacheable {
7171
}
7272
}
7373

74-
private struct ExpiringValue {
74+
private struct ExpiringValue: @unchecked Sendable {
7575
let expriation: Date
7676
let value: Value
7777
}

Sources/Cache/Cache/LRUCache.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ Error Handling: The set(value:forKey:) function does not throw any error. Instea
1111

1212
The `LRUCache` class is a subclass of the `Cache` class. You can use its `capacity` property to specify the maximum number of key-value pairs that the cache can hold.
1313
*/
14-
public class LRUCache<Key: Hashable, Value>: Cache<Key, Value> {
14+
public class LRUCache<Key: Hashable, Value>: Cache<Key, Value>, @unchecked Sendable {
15+
private let lock = NSRecursiveLock()
1516
private var keys: [Key]
1617

1718
/// The maximum capacity of the cache.
@@ -46,6 +47,7 @@ public class LRUCache<Key: Hashable, Value>: Cache<Key, Value> {
4647
}
4748

4849
public override func get<Output>(_ key: Key, as: Output.Type = Output.self) -> Output? {
50+
lock.lock(); defer { lock.unlock() }
4951
guard let value = super.get(key, as: Output.self) else {
5052
return nil
5153
}
@@ -56,13 +58,15 @@ public class LRUCache<Key: Hashable, Value>: Cache<Key, Value> {
5658
}
5759

5860
public override func set(value: Value, forKey key: Key) {
61+
lock.lock(); defer { lock.unlock() }
5962
super.set(value: value, forKey: key)
6063

6164
updateKeys(recentlyUsed: key)
6265
checkCapacity()
6366
}
6467

6568
public override func remove(_ key: Key) {
69+
lock.lock(); defer { lock.unlock() }
6670
super.remove(key)
6771

6872
if let index = keys.firstIndex(of: key) {
@@ -71,6 +75,7 @@ public class LRUCache<Key: Hashable, Value>: Cache<Key, Value> {
7175
}
7276

7377
public override func contains(_ key: Key) -> Bool {
78+
lock.lock(); defer { lock.unlock() }
7479
guard super.contains(key) else {
7580
return false
7681
}

0 commit comments

Comments
 (0)