Skip to content

Commit 63fed91

Browse files
committed
fix: fix potential data race handling actor reentrancy
1 parent 81c2c9c commit 63fed91

23 files changed

+478
-204
lines changed

Package.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ let package = Package(
3636
),
3737
.testTarget(
3838
name: "AsyncObjectsTests",
39-
dependencies: ["AsyncObjects"]
39+
dependencies: ["AsyncObjects"],
40+
swiftSettings: testingSwiftSettings
4041
),
4142
]
4243
)
@@ -77,3 +78,22 @@ var swiftSettings: [SwiftSetting] {
7778

7879
return swiftSettings
7980
}
81+
82+
var testingSwiftSettings: [SwiftSetting] {
83+
var swiftSettings: [SwiftSetting] = []
84+
85+
if ProcessInfo.processInfo.environment[
86+
"SWIFTCI_CONCURRENCY_CHECKS"
87+
] != nil {
88+
swiftSettings.append(
89+
.unsafeFlags([
90+
"-Xfrontend",
91+
"-warn-concurrency",
92+
"-enable-actor-data-race-checks",
93+
"-require-explicit-sendable",
94+
])
95+
)
96+
}
97+
98+
return swiftSettings
99+
}

Sources/AsyncObjects/AsyncCountdownEvent.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#if swift(>=5.7)
2+
import Foundation
3+
#else
14
@preconcurrency import Foundation
5+
#endif
26
import OrderedCollections
37

48
/// An event object that controls access to a resource between high and low priority tasks
@@ -53,6 +57,11 @@ public actor AsyncCountdownEvent: AsyncObject {
5357
_ continuation: Continuation,
5458
withKey key: UUID
5559
) {
60+
guard !isSet, continuations.isEmpty else {
61+
currentCount += 1
62+
continuation.resume()
63+
return
64+
}
5665
continuations[key] = continuation
5766
}
5867

@@ -71,6 +80,7 @@ public actor AsyncCountdownEvent: AsyncObject {
7180
/// - Parameter number: The number to decrement count by.
7281
@inlinable
7382
func _decrementCount(by number: UInt = 1) {
83+
defer { _resumeContinuations() }
7484
guard currentCount > 0 else { return }
7585
currentCount -= number
7686
}
@@ -94,15 +104,17 @@ public actor AsyncCountdownEvent: AsyncObject {
94104
///
95105
/// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error.
96106
@inlinable
97-
func _withPromisedContinuation() async throws {
107+
nonisolated func _withPromisedContinuation() async throws {
98108
let key = UUID()
99109
try await withTaskCancellationHandler { [weak self] in
100110
Task { [weak self] in
101111
await self?._removeContinuation(withKey: key)
102112
}
103113
} operation: { () -> Continuation.Success in
104114
try await Continuation.with { continuation in
105-
self._addContinuation(continuation, withKey: key)
115+
Task { [weak self] in
116+
await self?._addContinuation(continuation, withKey: key)
117+
}
106118
}
107119
}
108120
}
@@ -176,7 +188,6 @@ public actor AsyncCountdownEvent: AsyncObject {
176188
/// - Parameter count: The number of signals to register.
177189
public func signal(repeat count: UInt) {
178190
_decrementCount(by: count)
179-
_resumeContinuations()
180191
}
181192

182193
/// Waits for, or increments, a countdown event.
@@ -187,7 +198,7 @@ public actor AsyncCountdownEvent: AsyncObject {
187198
/// Use this to wait for high priority tasks completion to start low priority ones.
188199
@Sendable
189200
public func wait() async {
190-
if isSet { currentCount += 1; return }
201+
guard !isSet else { currentCount += 1; return }
191202
try? await _withPromisedContinuation()
192203
}
193204
}

Sources/AsyncObjects/AsyncEvent.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#if swift(>=5.7)
2+
import Foundation
3+
#else
14
@preconcurrency import Foundation
5+
#endif
26

37
/// An object that controls execution of tasks depending on the signal state.
48
///
@@ -14,7 +18,8 @@ public actor AsyncEvent: AsyncObject {
1418
@usableFromInline
1519
private(set) var continuations: [UUID: Continuation] = [:]
1620
/// Indicates whether current state of event is signalled.
17-
private var signalled: Bool
21+
@usableFromInline
22+
var signalled: Bool
1823

1924
// MARK: Internal
2025

@@ -28,6 +33,7 @@ public actor AsyncEvent: AsyncObject {
2833
_ continuation: Continuation,
2934
withKey key: UUID
3035
) {
36+
guard !signalled else { continuation.resume(); return }
3137
continuations[key] = continuation
3238
}
3339

@@ -50,15 +56,17 @@ public actor AsyncEvent: AsyncObject {
5056
///
5157
/// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error.
5258
@inlinable
53-
func _withPromisedContinuation() async throws {
59+
nonisolated func _withPromisedContinuation() async throws {
5460
let key = UUID()
5561
try await withTaskCancellationHandler { [weak self] in
5662
Task { [weak self] in
5763
await self?._removeContinuation(withKey: key)
5864
}
5965
} operation: { () -> Continuation.Success in
6066
try await Continuation.with { continuation in
61-
self._addContinuation(continuation, withKey: key)
67+
Task { [weak self] in
68+
await self?._addContinuation(continuation, withKey: key)
69+
}
6270
}
6371
}
6472
}

Sources/AsyncObjects/AsyncSemaphore.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#if swift(>=5.7)
2+
import Foundation
3+
#else
14
@preconcurrency import Foundation
5+
#endif
26
import OrderedCollections
37

48
/// An object that controls access to a resource across multiple task contexts through use of a traditional counting semaphore.
@@ -37,6 +41,7 @@ public actor AsyncSemaphore: AsyncObject {
3741
_ continuation: Continuation,
3842
withKey key: UUID
3943
) {
44+
guard count <= 0 else { continuation.resume(); return }
4045
continuations[key] = continuation
4146
}
4247

@@ -67,15 +72,17 @@ public actor AsyncSemaphore: AsyncObject {
6772
///
6873
/// - Throws: If `resume(throwing:)` is called on the continuation, this function throws that error.
6974
@inlinable
70-
func _withPromisedContinuation() async throws {
75+
nonisolated func _withPromisedContinuation() async throws {
7176
let key = UUID()
7277
try await withTaskCancellationHandler { [weak self] in
7378
Task { [weak self] in
7479
await self?._removeContinuation(withKey: key)
7580
}
7681
} operation: { () -> Continuation.Success in
7782
try await Continuation.with { continuation in
78-
self._addContinuation(continuation, withKey: key)
83+
Task { [weak self] in
84+
await self?._addContinuation(continuation, withKey: key)
85+
}
7986
}
8087
}
8188
}
@@ -117,7 +124,7 @@ public actor AsyncSemaphore: AsyncObject {
117124
@Sendable
118125
public func wait() async {
119126
count -= 1
120-
if count > 0 { return }
127+
guard count <= 0 else { return }
121128
try? await _withPromisedContinuation()
122129
}
123130
}

Sources/AsyncObjects/CancellationSource.swift

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,66 @@ public actor CancellationSource {
5252
linkedSources.append(source)
5353
}
5454

55+
/// Propagate cancellation to linked cancellation sources.
56+
@inlinable
57+
nonisolated func _propagateCancellation() async {
58+
await withTaskGroup(of: Void.self) { group in
59+
let linkedSources = await linkedSources
60+
linkedSources.forEach { group.addTask(operation: $0.cancel) }
61+
await group.waitForAll()
62+
}
63+
}
64+
5565
// MARK: Public
5666

5767
/// Creates a new cancellation source object.
5868
///
5969
/// - Returns: The newly created cancellation source.
6070
public init() { }
6171

72+
#if swift(>=5.7)
73+
/// Creates a new cancellation source object linking to all the provided cancellation sources.
74+
///
75+
/// Initiating cancellation in any of the provided cancellation sources
76+
/// will ensure newly created cancellation source receive cancellation event.
77+
///
78+
/// - Parameter sources: The cancellation sources the newly created object will be linked to.
79+
///
80+
/// - Returns: The newly created cancellation source.
81+
public nonisolated init(linkedWith sources: [CancellationSource]) async {
82+
await withTaskGroup(of: Void.self) { group in
83+
sources.forEach { source in
84+
group.addTask { await source._addSource(self) }
85+
}
86+
await group.waitForAll()
87+
}
88+
}
89+
90+
/// Creates a new cancellation source object linking to all the provided cancellation sources.
91+
///
92+
/// Initiating cancellation in any of the provided cancellation sources
93+
/// will ensure newly created cancellation source receive cancellation event.
94+
///
95+
/// - Parameter sources: The cancellation sources the newly created object will be linked to.
96+
///
97+
/// - Returns: The newly created cancellation source.
98+
public init(linkedWith sources: CancellationSource...) async {
99+
await self.init(linkedWith: sources)
100+
}
101+
102+
/// Creates a new cancellation source object
103+
/// and triggers cancellation event on this object after specified timeout.
104+
///
105+
/// - Parameter nanoseconds: The delay after which cancellation event triggered.
106+
///
107+
/// - Returns: The newly created cancellation source.
108+
public init(cancelAfterNanoseconds nanoseconds: UInt64) {
109+
self.init()
110+
Task { [weak self] in
111+
try await self?.cancel(afterNanoseconds: nanoseconds)
112+
}
113+
}
114+
#else
62115
/// Creates a new cancellation source object linking to all the provided cancellation sources.
63116
///
64117
/// Initiating cancellation in any of the provided cancellation sources
@@ -100,6 +153,7 @@ public actor CancellationSource {
100153
try await self?.cancel(afterNanoseconds: nanoseconds)
101154
}
102155
}
156+
#endif
103157

104158
/// Register task for cooperative cancellation when cancellation event received on cancellation source.
105159
///
@@ -120,10 +174,7 @@ public actor CancellationSource {
120174
public func cancel() async {
121175
registeredTasks.forEach { $1() }
122176
registeredTasks = [:]
123-
await withTaskGroup(of: Void.self) { group in
124-
linkedSources.forEach { group.addTask(operation: $0.cancel) }
125-
await group.waitForAll()
126-
}
177+
await _propagateCancellation()
127178
}
128179

129180
/// Trigger cancellation event after provided delay,
@@ -143,16 +194,17 @@ public extension Task {
143194
/// Runs the given non-throwing operation asynchronously as part of a new task on behalf of the current actor,
144195
/// with the provided cancellation source controlling cooperative cancellation.
145196
///
146-
/// A child task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
147-
/// In the event of cancellation child task is cancelled, while returning the value in the returned task.
148-
/// In case you want to register and track the top-level task for cancellation use the async initializer instead.
197+
/// A top-level task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
198+
/// In the event of cancellation top-level task is cancelled, while returning the value in the returned task.
149199
///
150200
/// - Parameters:
151201
/// - priority: The priority of the task. Pass `nil` to use the priority from `Task.currentPriority`.
152202
/// - cancellationSource: The cancellation source on which new task will be registered for cancellation.
153203
/// - operation: The operation to perform.
154204
///
155205
/// - Returns: The newly created task.
206+
/// - Note: In case you want to register and track the top-level task
207+
/// for cancellation use the async initializer instead.
156208
@discardableResult
157209
init(
158210
priority: TaskPriority? = nil,
@@ -169,16 +221,17 @@ public extension Task {
169221
/// Runs the given throwing operation asynchronously as part of a new task on behalf of the current actor,
170222
/// with the provided cancellation source controlling cooperative cancellation.
171223
///
172-
/// A child task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
173-
/// In the event of cancellation child task is cancelled, while propagating error in the returned task.
174-
/// In case you want to register and track the top-level task for cancellation use the async initializer instead.
224+
/// A top-level task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
225+
/// In the event of cancellation top-level task is cancelled, while propagating error in the returned task.
175226
///
176227
/// - Parameters:
177228
/// - priority: The priority of the task. Pass `nil` to use the priority from `Task.currentPriority`.
178229
/// - cancellationSource: The cancellation source on which new task will be registered for cancellation.
179230
/// - operation: The operation to perform.
180231
///
181232
/// - Returns: The newly created task.
233+
/// - Note: In case you want to register and track the top-level task
234+
/// for cancellation use the async initializer instead.
182235
@discardableResult
183236
init(
184237
priority: TaskPriority? = nil,
@@ -195,16 +248,17 @@ public extension Task {
195248
/// Runs the given non-throwing operation asynchronously as part of a new task,
196249
/// with the provided cancellation source controlling cooperative cancellation.
197250
///
198-
/// A child task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
199-
/// In the event of cancellation child task is cancelled, while returning the value in the returned task.
200-
/// In case you want to register and track the top-level task for cancellation use the async initializer instead.
251+
/// A top-level task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
252+
/// In the event of cancellation top-level task is cancelled, while returning the value in the returned task.
201253
///
202254
/// - Parameters:
203-
/// - priority: The priority of the task. Pass `nil` to use the priority from `Task.currentPriority`.
255+
/// - priority: The priority of the task.
204256
/// - cancellationSource: The cancellation source on which new task will be registered for cancellation.
205257
/// - operation: The operation to perform.
206258
///
207259
/// - Returns: The newly created task.
260+
/// - Note: In case you want to register and track the top-level task
261+
/// for cancellation use the async initializer instead.
208262
@discardableResult
209263
static func detached(
210264
priority: TaskPriority? = nil,
@@ -221,16 +275,17 @@ public extension Task {
221275
/// Runs the given throwing operation asynchronously as part of a new task,
222276
/// with the provided cancellation source controlling cooperative cancellation.
223277
///
224-
/// A child task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
225-
/// In the event of cancellation child task is cancelled, while returning the value in the returned task.
226-
/// In case you want to register and track the top-level task for cancellation use the async initializer instead.
278+
/// A top-level task with the provided operation is created, cancellation of which is controlled by provided cancellation source.
279+
/// In the event of cancellation top-level task is cancelled, while returning the value in the returned task.
227280
///
228281
/// - Parameters:
229-
/// - priority: The priority of the task. Pass `nil` to use the priority from `Task.currentPriority`.
282+
/// - priority: The priority of the task.
230283
/// - cancellationSource: The cancellation source on which new task will be registered for cancellation.
231284
/// - operation: The operation to perform.
232285
///
233286
/// - Returns: The newly created task.
287+
/// - Note: In case you want to register and track the top-level task
288+
/// for cancellation use the async initializer instead.
234289
@discardableResult
235290
static func detached(
236291
priority: TaskPriority? = nil,
@@ -292,7 +347,7 @@ public extension Task {
292347
/// The created task will be cancelled when cancellation event triggered on the provided cancellation source.
293348
///
294349
/// - Parameters:
295-
/// - priority: The priority of the task. Pass `nil` to use the priority from `Task.currentPriority`.
350+
/// - priority: The priority of the task.
296351
/// - cancellationSource: The cancellation source on which new task will be registered for cancellation.
297352
/// - operation: The operation to perform.
298353
///
@@ -314,7 +369,7 @@ public extension Task {
314369
/// The created task will be cancelled when cancellation event triggered on the provided cancellation source.
315370
///
316371
/// - Parameters:
317-
/// - priority: The priority of the task. Pass `nil` to use the priority from `Task.currentPriority`.
372+
/// - priority: The priority of the task.
318373
/// - cancellationSource: The cancellation source on which new task will be registered for cancellation.
319374
/// - operation: The operation to perform.
320375
///

Sources/AsyncObjects/Continuable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// A type that allows to interface between synchronous and asynchronous code,
22
/// by representing task state and allowing task resuming with some value or error.
33
@usableFromInline
4-
protocol Continuable: Sendable {
4+
protocol Continuable {
55
/// The type of value to resume the continuation with in case of success.
66
associatedtype Success
77
/// The type of error to resume the continuation with in case of failure.

0 commit comments

Comments
 (0)