Skip to content

Commit d95beaf

Browse files
committed
Fix EventLoopFuture and EventLoopPromise under strict concurrency checking
# Motivation We need to tackle the remaining strict concurrency checking related `Sendable` warnings in NIO. The first place to start is making sure that `EventLoopFuture` and `EventLoopPromise` are properly annotated. # Modification In a previous #2496, @weissi changed the `@unchecked Sendable` conformances of `EventLoopFuture/Promise` to be conditional on the sendability of the generic `Value` type. After having looked at all the APIs on the future and promise types as well as reading the latest Concurrency evolution proposals, specifically the [Region based Isolation](https://github.com/apple/swift-evolution/blob/main/proposals/0414-region-based-isolation.md), I came to the conclusion that the previous `@unchecked Sendable` annotations were correct. The reasoning for this is: 1. An `EventLoopPromise` and `EventLoopFuture` pair are tied to a specific `EventLoop` 2. An `EventLoop` represents an isolation region and values tied to its isolation are not allowed to be shared outside of it unless they are disconnected from the region 3. The `value` used to succeed a promise often come from outside the isolation domain of the `EventLoop` hence they must be transferred into the promise. 4. The isolation region of the event loop is enforced through `@Sendable` annotations on all closures that receive the value in some kind of transformation e.g. `map()` 5. Any method on `EventLoopFuture` that combines itself with another future must require `Sendable` of the other futures `Value` since we cannot statically enforce that futures are bound to the same event loop i.e. to the same isolation domain Due to the above rules, this PR adds back the `@unchecked Sendable` conformances to both types. Furthermore, this PR revisits every single method on `EventLoopPromise/Future` and adds missing `Sendable` and `@Sendable` annotation where necessary to uphold the above rules. A few important things to call out: - Since `transferring` is currently not available this PR requires a `Sendable` conformance for some methods on `EventLoopPromise/Future` that should rather take a `transffering` argument - To enable the common case where a value from the same event loop is used to succeed a promise I added two additional methods that take a `eventLoopBoundResult` and enforce dynamic isolation checking. We might have to do this for more methods once we adopt those changes in other targets/packages. # Result After this PR has landed our lowest level building block should be inline with what the rest of the language enforces in Concurrency. The `EventLoopFuture.swift` produces no more warnings under strict concurrency checking on the latest 5.10 snapshots.
1 parent f4c61cf commit d95beaf

7 files changed

+446
-275
lines changed

Sources/NIOCore/AsyncAwaitSupport.swift

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ extension EventLoopFuture {
1919
/// function and want to get the result of this future.
2020
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
2121
@inlinable
22-
public func get() async throws -> Value {
22+
public func get() async throws -> Value where Value: Sendable {
2323
return try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation<UnsafeTransfer<Value>, Error>) in
2424
self.whenComplete { result in
2525
switch result {
@@ -61,7 +61,9 @@ extension EventLoopPromise {
6161
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
6262
@discardableResult
6363
@inlinable
64-
public func completeWithTask(_ body: @escaping @Sendable () async throws -> Value) -> Task<Void, Never> {
64+
public func completeWithTask(
65+
_ body: @escaping @Sendable () async throws -> Value
66+
) -> Task<Void, Never> where Value: Sendable {
6567
Task {
6668
do {
6769
let value = try await body()
@@ -334,7 +336,9 @@ struct AsyncSequenceFromIterator<AsyncIterator: AsyncIteratorProtocol>: AsyncSeq
334336
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
335337
extension EventLoop {
336338
@inlinable
337-
public func makeFutureWithTask<Return>(_ body: @Sendable @escaping () async throws -> Return) -> EventLoopFuture<Return> {
339+
public func makeFutureWithTask<Return: Sendable>(
340+
_ body: @Sendable @escaping () async throws -> Return
341+
) -> EventLoopFuture<Return> {
338342
let promise = self.makePromise(of: Return.self)
339343
promise.completeWithTask(body)
340344
return promise.futureResult

Sources/NIOCore/DispatchQueue+WithFuture.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ extension DispatchQueue {
2828
/// - callbackMayBlock: The scheduled callback for the IO / task.
2929
/// - returns a new `EventLoopFuture<ReturnType>` with value returned by the `block` parameter.
3030
@inlinable
31-
public func asyncWithFuture<NewValue>(
31+
public func asyncWithFuture<NewValue: Sendable>(
3232
eventLoop: EventLoop,
33-
_ callbackMayBlock: @escaping () throws -> NewValue
33+
_ callbackMayBlock: @escaping @Sendable () throws -> NewValue
3434
) -> EventLoopFuture<NewValue> {
3535
let promise = eventLoop.makePromise(of: NewValue.self)
3636

Sources/NIOCore/EventLoop.swift

+11-41
Original file line numberDiff line numberDiff line change
@@ -712,13 +712,7 @@ extension EventLoop {
712712
/// - returns: An `EventLoopFuture` containing the result of `task`'s execution.
713713
@inlinable
714714
@preconcurrency
715-
public func submit<T>(_ task: @escaping @Sendable () throws -> T) -> EventLoopFuture<T> {
716-
_submit(task)
717-
}
718-
@usableFromInline typealias SubmitCallback<T> = @Sendable () throws -> T
719-
720-
@inlinable
721-
func _submit<T>(_ task: @escaping SubmitCallback<T>) -> EventLoopFuture<T> {
715+
public func submit<T: Sendable>(_ task: @escaping @Sendable () throws -> T) -> EventLoopFuture<T> { // TODO: This should take a closure that returns fresh
722716
let promise: EventLoopPromise<T> = makePromise(file: #fileID, line: #line)
723717

724718
self.execute {
@@ -742,18 +736,15 @@ extension EventLoop {
742736
/// - returns: An `EventLoopFuture` identical to the `EventLoopFuture` returned from `task`.
743737
@inlinable
744738
@preconcurrency
745-
public func flatSubmit<T>(_ task: @escaping @Sendable () -> EventLoopFuture<T>) -> EventLoopFuture<T> {
746-
self._flatSubmit(task)
747-
}
748-
@usableFromInline typealias FlatSubmitCallback<T> = @Sendable () -> EventLoopFuture<T>
749-
750-
@inlinable
751-
func _flatSubmit<T>(_ task: @escaping FlatSubmitCallback<T>) -> EventLoopFuture<T> {
739+
public func flatSubmit<T: Sendable>(_ task: @escaping @Sendable () -> EventLoopFuture<T>) -> EventLoopFuture<T> { // TODO: This should take a closure that returns fresh
752740
self.submit(task).flatMap { $0 }
753741
}
754742

755743
/// Schedule a `task` that is executed by this `EventLoop` at the given time.
756744
///
745+
/// - Note: The `T` must be `Sendable` since the isolation domains of the event loop future returned from `task` and
746+
/// this event loop might differ.
747+
///
757748
/// - parameters:
758749
/// - task: The asynchronous task to run. As with everything that runs on the `EventLoop`, it must not block.
759750
/// - returns: A `Scheduled` object which may be used to cancel the task if it has not yet run, or to wait
@@ -763,23 +754,11 @@ extension EventLoop {
763754
@discardableResult
764755
@inlinable
765756
@preconcurrency
766-
public func flatScheduleTask<T>(
757+
public func flatScheduleTask<T: Sendable>(
767758
deadline: NIODeadline,
768759
file: StaticString = #fileID,
769760
line: UInt = #line,
770761
_ task: @escaping @Sendable () throws -> EventLoopFuture<T>
771-
) -> Scheduled<T> {
772-
self._flatScheduleTask(deadline: deadline, file: file, line: line, task)
773-
}
774-
@usableFromInline typealias FlatScheduleTaskDeadlineCallback<T> = () throws -> EventLoopFuture<T>
775-
776-
@discardableResult
777-
@inlinable
778-
func _flatScheduleTask<T>(
779-
deadline: NIODeadline,
780-
file: StaticString,
781-
line: UInt,
782-
_ task: @escaping FlatScheduleTaskDelayCallback<T>
783762
) -> Scheduled<T> {
784763
let promise: EventLoopPromise<T> = self.makePromise(file: file, line: line)
785764
let scheduled = self.scheduleTask(deadline: deadline, task)
@@ -790,6 +769,9 @@ extension EventLoop {
790769

791770
/// Schedule a `task` that is executed by this `EventLoop` after the given amount of time.
792771
///
772+
/// - Note: The `T` must be `Sendable` since the isolation domains of the event loop future returned from `task` and
773+
/// this event loop might differ.
774+
///
793775
/// - parameters:
794776
/// - task: The asynchronous task to run. As everything that runs on the `EventLoop`, it must not block.
795777
/// - returns: A `Scheduled` object which may be used to cancel the task if it has not yet run, or to wait
@@ -799,23 +781,11 @@ extension EventLoop {
799781
@discardableResult
800782
@inlinable
801783
@preconcurrency
802-
public func flatScheduleTask<T>(
784+
public func flatScheduleTask<T: Sendable>(
803785
in delay: TimeAmount,
804786
file: StaticString = #fileID,
805787
line: UInt = #line,
806788
_ task: @escaping @Sendable () throws -> EventLoopFuture<T>
807-
) -> Scheduled<T> {
808-
self._flatScheduleTask(in: delay, file: file, line: line, task)
809-
}
810-
811-
@usableFromInline typealias FlatScheduleTaskDelayCallback<T> = @Sendable () throws -> EventLoopFuture<T>
812-
813-
@inlinable
814-
func _flatScheduleTask<T>(
815-
in delay: TimeAmount,
816-
file: StaticString,
817-
line: UInt,
818-
_ task: @escaping FlatScheduleTaskDelayCallback<T>
819789
) -> Scheduled<T> {
820790
let promise: EventLoopPromise<T> = self.makePromise(file: file, line: line)
821791
let scheduled = self.scheduleTask(in: delay, task)
@@ -951,7 +921,7 @@ extension EventLoop {
951921
notifying promise: EventLoopPromise<Void>?,
952922
_ task: @escaping ScheduleRepeatedTaskCallback
953923
) -> RepeatedTask {
954-
let futureTask: (RepeatedTask) -> EventLoopFuture<Void> = { repeatedTask in
924+
let futureTask: @Sendable (RepeatedTask) -> EventLoopFuture<Void> = { repeatedTask in
955925
do {
956926
try task(repeatedTask)
957927
return self.makeSucceededFuture(())

Sources/NIOCore/EventLoopFuture+Deprecated.swift

+45-17
Original file line numberDiff line numberDiff line change
@@ -15,63 +15,91 @@
1515
extension EventLoopFuture {
1616
@inlinable
1717
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
18-
public func flatMap<NewValue>(file: StaticString = #fileID, line: UInt = #line, _ callback: @escaping (Value) -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
18+
public func flatMap<NewValue: Sendable>(
19+
file: StaticString = #fileID,
20+
line: UInt = #line,
21+
_ callback: @escaping @Sendable (Value) -> EventLoopFuture<NewValue>
22+
) -> EventLoopFuture<NewValue> {
1923
return self.flatMap(callback)
2024
}
2125

2226
@inlinable
2327
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
24-
public func flatMapThrowing<NewValue>(file: StaticString = #fileID,
25-
line: UInt = #line,
26-
_ callback: @escaping (Value) throws -> NewValue) -> EventLoopFuture<NewValue> {
28+
public func flatMapThrowing<NewValue: Sendable>(
29+
file: StaticString = #fileID,
30+
line: UInt = #line,
31+
_ callback: @escaping @Sendable (Value) throws -> NewValue
32+
) -> EventLoopFuture<NewValue> {
2733
return self.flatMapThrowing(callback)
2834
}
2935

3036
@inlinable
3137
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
32-
public func flatMapErrorThrowing(file: StaticString = #fileID, line: UInt = #line, _ callback: @escaping (Error) throws -> Value) -> EventLoopFuture<Value> {
38+
public func flatMapErrorThrowing(
39+
file: StaticString = #fileID,
40+
line: UInt = #line,
41+
_ callback: @escaping @Sendable (Error) throws -> Value
42+
) -> EventLoopFuture<Value> {
3343
return self.flatMapErrorThrowing(callback)
3444
}
3545

3646
@inlinable
3747
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
38-
public func map<NewValue>(file: StaticString = #fileID, line: UInt = #line, _ callback: @escaping (Value) -> (NewValue)) -> EventLoopFuture<NewValue> {
48+
public func map<NewValue>(
49+
file: StaticString = #fileID,
50+
line: UInt = #line,
51+
_ callback: @escaping @Sendable (Value) -> (NewValue)
52+
) -> EventLoopFuture<NewValue> {
3953
return self.map(callback)
4054
}
4155

4256
@inlinable
4357
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
44-
public func flatMapError(file: StaticString = #fileID, line: UInt = #line, _ callback: @escaping (Error) -> EventLoopFuture<Value>) -> EventLoopFuture<Value> {
58+
public func flatMapError(
59+
file: StaticString = #fileID,
60+
line: UInt = #line,
61+
_ callback: @escaping @Sendable (Error) -> EventLoopFuture<Value>
62+
) -> EventLoopFuture<Value> where Value: Sendable {
4563
return self.flatMapError(callback)
4664
}
4765

4866
@inlinable
4967
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
50-
public func flatMapResult<NewValue, SomeError: Error>(file: StaticString = #fileID,
51-
line: UInt = #line,
52-
_ body: @escaping (Value) -> Result<NewValue, SomeError>) -> EventLoopFuture<NewValue> {
68+
public func flatMapResult<NewValue, SomeError: Error>(
69+
file: StaticString = #fileID,
70+
line: UInt = #line,
71+
_ body: @escaping @Sendable (Value) -> Result<NewValue, SomeError>
72+
) -> EventLoopFuture<NewValue> {
5373
return self.flatMapResult(body)
5474
}
5575

5676
@inlinable
5777
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
58-
public func recover(file: StaticString = #fileID, line: UInt = #line, _ callback: @escaping (Error) -> Value) -> EventLoopFuture<Value> {
78+
public func recover(
79+
file: StaticString = #fileID,
80+
line: UInt = #line,
81+
_ callback: @escaping @Sendable (Error) -> Value
82+
) -> EventLoopFuture<Value> {
5983
return self.recover(callback)
6084
}
6185

6286
@inlinable
6387
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
64-
public func and<OtherValue>(_ other: EventLoopFuture<OtherValue>,
65-
file: StaticString = #fileID,
66-
line: UInt = #line) -> EventLoopFuture<(Value, OtherValue)> {
88+
public func and<OtherValue: Sendable>(
89+
_ other: EventLoopFuture<OtherValue>,
90+
file: StaticString = #fileID,
91+
line: UInt = #line
92+
) -> EventLoopFuture<(Value, OtherValue)> {
6793
return self.and(other)
6894
}
6995

7096
@inlinable
7197
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
72-
public func and<OtherValue>(value: OtherValue,
73-
file: StaticString = #fileID,
74-
line: UInt = #line) -> EventLoopFuture<(Value, OtherValue)> {
98+
public func and<OtherValue: Sendable>(
99+
value: OtherValue,
100+
file: StaticString = #fileID,
101+
line: UInt = #line
102+
) -> EventLoopFuture<(Value, OtherValue)> {
75103
return self.and(value: value)
76104
}
77105
}

Sources/NIOCore/EventLoopFuture+WithEventLoop.swift

+9-4
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ extension EventLoopFuture {
4141
/// - returns: A future that will receive the eventual value.
4242
@inlinable
4343
@preconcurrency
44-
public func flatMapWithEventLoop<NewValue>(_ callback: @escaping @Sendable (Value, EventLoop) -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
44+
public func flatMapWithEventLoop<NewValue: Sendable>(
45+
_ callback: @escaping @Sendable (Value, EventLoop) -> EventLoopFuture<NewValue>
46+
) -> EventLoopFuture<NewValue> {
4547
let next = EventLoopPromise<NewValue>.makeUnleakablePromise(eventLoop: self.eventLoop)
4648
self._whenComplete { [eventLoop = self.eventLoop] in
4749
switch self._value! {
@@ -75,7 +77,9 @@ extension EventLoopFuture {
7577
/// - returns: A future that will receive the recovered value.
7678
@inlinable
7779
@preconcurrency
78-
public func flatMapErrorWithEventLoop(_ callback: @escaping @Sendable (Error, EventLoop) -> EventLoopFuture<Value>) -> EventLoopFuture<Value> {
80+
public func flatMapErrorWithEventLoop(
81+
_ callback: @escaping @Sendable (Error, EventLoop) -> EventLoopFuture<Value>
82+
) -> EventLoopFuture<Value> where Value: Sendable {
7983
let next = EventLoopPromise<Value>.makeUnleakablePromise(eventLoop: self.eventLoop)
8084
self._whenComplete { [eventLoop = self.eventLoop] in
8185
switch self._value! {
@@ -114,10 +118,11 @@ extension EventLoopFuture {
114118
/// - returns: A new `EventLoopFuture` with the folded value whose callbacks run on `self.eventLoop`.
115119
@inlinable
116120
@preconcurrency
117-
public func foldWithEventLoop<OtherValue>(
121+
public func foldWithEventLoop<OtherValue: Sendable>(
118122
_ futures: [EventLoopFuture<OtherValue>],
119123
with combiningFunction: @escaping @Sendable (Value, OtherValue, EventLoop) -> EventLoopFuture<Value>
120-
) -> EventLoopFuture<Value> {
124+
) -> EventLoopFuture<Value> where Value: Sendable {
125+
@Sendable
121126
func fold0(eventLoop: EventLoop) -> EventLoopFuture<Value> {
122127
let body = futures.reduce(self) { (f1: EventLoopFuture<Value>, f2: EventLoopFuture<OtherValue>) -> EventLoopFuture<Value> in
123128
let newFuture = f1.and(f2).flatMap { (args: (Value, OtherValue)) -> EventLoopFuture<Value> in

0 commit comments

Comments
 (0)