Skip to content

Commit 8fa6219

Browse files
committed
fix(CancellationSource): prevent priority inversion of submitted tasks
1 parent 93aacfb commit 8fa6219

File tree

3 files changed

+62
-11
lines changed

3 files changed

+62
-11
lines changed

Sources/AsyncObjects/CancellationSource/CancellationSource+Linking.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ public extension CancellationSource {
55
/// will ensure newly created cancellation source receive cancellation event.
66
///
77
/// - Parameters:
8+
/// - priority: The minimum priority of task that this source is going to handle.
9+
/// By default, minimum priority of provided `sources` is used or
10+
/// `.background` if no provided `sources`.
811
/// - sources: The cancellation sources the newly created object will be linked to.
912
/// - file: The file link request originates from (there's usually no need to pass it
1013
/// explicitly as it defaults to `#fileID`).
@@ -14,13 +17,19 @@ public extension CancellationSource {
1417
/// explicitly as it defaults to `#line`).
1518
///
1619
/// - Returns: The newly created cancellation source.
20+
///
21+
/// - NOTE: `CancellationSource` uses `Task`'s `result` and `value` APIs
22+
/// to wait for completion which has side effect of increasing `Task`'s priority.
23+
/// Hence, provide the least priority for the submitted tasks to use in cancellation task.
1724
init(
25+
priority: TaskPriority? = nil,
1826
linkedWith sources: [CancellationSource],
1927
file: String = #fileID,
2028
function: String = #function,
2129
line: UInt = #line
2230
) {
23-
self.init()
31+
let priority = priority ?? sources.map(\.priority).min() ?? .background
32+
self.init(priority: priority)
2433
sources.forEach {
2534
$0.register(task: self, file: file, function: function, line: line)
2635
}
@@ -32,6 +41,9 @@ public extension CancellationSource {
3241
/// will ensure newly created cancellation source receive cancellation event.
3342
///
3443
/// - Parameters:
44+
/// - priority: The minimum priority of task that this source is going to handle.
45+
/// By default, minimum priority of provided `sources` is used or
46+
/// `.background` if no provided `sources`.
3547
/// - sources: The cancellation sources the newly created object will be linked to.
3648
/// - file: The file link request originates from (there's usually no need to pass it
3749
/// explicitly as it defaults to `#fileID`).
@@ -41,14 +53,19 @@ public extension CancellationSource {
4153
/// explicitly as it defaults to `#line`).
4254
///
4355
/// - Returns: The newly created cancellation source.
56+
///
57+
/// - NOTE: `CancellationSource` uses `Task`'s `result` and `value` APIs
58+
/// to wait for completion which has side effect of increasing `Task`'s priority.
59+
/// Hence, provide the least priority for the submitted tasks to use in cancellation task.
4460
init(
61+
priority: TaskPriority? = nil,
4562
linkedWith sources: CancellationSource...,
4663
file: String = #fileID,
4764
function: String = #function,
4865
line: UInt = #line
4966
) {
5067
self.init(
51-
linkedWith: sources,
68+
priority: priority, linkedWith: sources,
5269
file: file, function: function, line: line
5370
)
5471
}

Sources/AsyncObjects/CancellationSource/CancellationSource+Timeout.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ public extension CancellationSource {
33
/// and triggers cancellation event on this object after specified timeout.
44
///
55
/// - Parameters:
6+
/// - priority: The minimum priority of task that this source is going to handle.
7+
/// By default, priority is `.background`.
68
/// - nanoseconds: The delay after which cancellation event triggered.
79
/// - file: The file cancel request originates from (there's usually no need to pass it
810
/// explicitly as it defaults to `#fileID`).
@@ -12,13 +14,18 @@ public extension CancellationSource {
1214
/// explicitly as it defaults to `#line`).
1315
///
1416
/// - Returns: The newly created cancellation source.
17+
///
18+
/// - NOTE: `CancellationSource` uses `Task`'s `result` and `value` APIs
19+
/// to wait for completion which has side effect of increasing `Task`'s priority.
20+
/// Hence, provide the least priority for the submitted tasks to use in cancellation task.
1521
init(
22+
priority: TaskPriority = .background,
1623
cancelAfterNanoseconds nanoseconds: UInt64,
1724
file: String = #fileID,
1825
function: String = #function,
1926
line: UInt = #line
2027
) {
21-
self.init()
28+
self.init(priority: priority)
2229
self.cancel(
2330
afterNanoseconds: nanoseconds,
2431
file: file, function: function, line: line
@@ -88,6 +95,8 @@ public extension CancellationSource {
8895
/// and triggers cancellation event on this object at specified deadline.
8996
///
9097
/// - Parameters:
98+
/// - priority: The minimum priority of task that this source is going to handle.
99+
/// By default, priority is `.background`.
91100
/// - deadline: The instant in the provided clock at which cancellation event triggered.
92101
/// - clock: The clock for which cancellation deadline provided.
93102
/// - file: The file cancel request originates from (there's usually no need to pass it
@@ -98,14 +107,19 @@ public extension CancellationSource {
98107
/// explicitly as it defaults to `#line`).
99108
///
100109
/// - Returns: The newly created cancellation source.
110+
///
111+
/// - NOTE: `CancellationSource` uses `Task`'s `result` and `value` APIs
112+
/// to wait for completion which has side effect of increasing `Task`'s priority.
113+
/// Hence, provide the least priority for the submitted tasks to use in cancellation task.
101114
init<C: Clock>(
115+
priority: TaskPriority = .background,
102116
at deadline: C.Instant,
103117
clock: C,
104118
file: String = #fileID,
105119
function: String = #function,
106120
line: UInt = #line
107121
) {
108-
self.init()
122+
self.init(priority: priority)
109123
self.cancel(
110124
at: deadline, clock: clock,
111125
file: file, function: function, line: line

Sources/AsyncObjects/CancellationSource/CancellationSource.swift

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import Foundation
22

33
/// An object that controls cooperative cancellation of multiple registered tasks and linked object registered tasks.
44
///
5-
/// You can register tasks for cancellation using the ``register(task:file:function:line:)`` method
6-
/// and link with additional sources by creating object with ``init(linkedWith:)`` method.
5+
/// You can register tasks for cancellation using the ``register(task:file:function:line:)`` method and link with
6+
/// additional sources by creating object with ``init(priority:linkedWith:file:function:line:)-7b9gf`` method.
77
/// By calling the ``cancel(file:function:line:)`` method all the registered tasks will be cancelled
88
/// and the cancellation event will be propagated to linked cancellation sources,
99
/// which in turn cancels their registered tasks and further propagates cancellation.
@@ -42,10 +42,12 @@ public struct CancellationSource: AsyncObject, Cancellable, Loggable {
4242
/// The lifetime task that is cancelled when
4343
/// `CancellationSource` is cancelled.
4444
@usableFromInline
45-
var lifetime: Task<Void, Error>!
45+
let lifetime: Task<Void, Error>!
4646
/// The stream continuation used to register work items
4747
/// for cooperative cancellation.
48-
var pipe: AsyncStream<WorkItem>.Continuation!
48+
let pipe: AsyncStream<WorkItem>.Continuation!
49+
/// The priority of the detached cancellation task.
50+
let priority: TaskPriority
4951

5052
/// A Boolean value that indicates whether cancellation is already
5153
/// invoked on the source.
@@ -59,10 +61,21 @@ public struct CancellationSource: AsyncObject, Cancellable, Loggable {
5961

6062
/// Creates a new cancellation source object.
6163
///
64+
/// - Parameters:
65+
/// - priority: The minimum priority of task that this source is going to handle.
66+
/// By default, priority is `.background`.
67+
///
6268
/// - Returns: The newly created cancellation source.
63-
public init() {
64-
let stream = AsyncStream<WorkItem> { self.pipe = $0 }
65-
self.lifetime = Task.detached {
69+
///
70+
/// - NOTE: `CancellationSource` uses `Task`'s `result` and `value` APIs
71+
/// to wait for completion which has side effect of increasing `Task`'s priority.
72+
/// Hence, provide the least priority for the submitted tasks to use in cancellation task.
73+
public init(priority: TaskPriority = .background) {
74+
var continuation: AsyncStream<WorkItem>.Continuation!
75+
let stream = AsyncStream<WorkItem> { continuation = $0 }
76+
self.priority = priority
77+
self.pipe = continuation
78+
self.lifetime = Task.detached(priority: priority) {
6679
try await withThrowingTaskGroup(of: Void.self) { group in
6780
for await item in stream {
6881
group.addTask {
@@ -94,6 +107,10 @@ public struct CancellationSource: AsyncObject, Cancellable, Loggable {
94107
/// pass it explicitly as it defaults to `#function`).
95108
/// - line: The line work registration originates from (there's usually no need to pass it
96109
/// explicitly as it defaults to `#line`).
110+
///
111+
/// - Important: Do not use this method to link `CancellationSource` as it might introduce
112+
/// circular linking which will cause all the affected cancellation tasks to leak.
113+
/// Use ``init(priority:linkedWith:file:function:line:)-7b9gf`` instead.
97114
@Sendable
98115
public func register<C: Cancellable>(
99116
task: C,
@@ -158,6 +175,9 @@ public struct CancellationSource: AsyncObject, Cancellable, Loggable {
158175
/// pass it explicitly as it defaults to `#function`).
159176
/// - line: The line wait request originates from (there's usually no need to pass it
160177
/// explicitly as it defaults to `#line`).
178+
///
179+
/// - Important: Using this method might introduce circular linking of`CancellationSource`
180+
/// which will cause all the affected cancellation tasks to leak.
161181
@Sendable
162182
public func wait(
163183
file: String = #fileID,

0 commit comments

Comments
 (0)