Skip to content

Commit 05ec0a9

Browse files
committed
Enforce Sendability in EventLoopFuture and EventLoopPromise methods
# Motivation With #2496 we fixed a Sendability checking hole by removing the unconditional conformance of `EventLoopFuture/Promise` to `Sendable`. This was the correct thing to do; however, it has the fallout that a couple of methods are now rightly complaining that their values are send across isolation domains. # Modification This PR requires values on some `ELF/P` methods to be `Sendable` when we might potentially transfer the values across isolation domains/ELs. We have to be overly aggressive here because we do not know that some `ELF` method are staying on the same EL. For example `flatMap` gets a new `ELF` from the closure provided to it. If the `ELF` is on the same EL we do not need to hop; however, we can not guarantee this right now from a type level so we have to stay on the safe side and actually require the `NewValue` to be `Sendable`. # Result This PR makes us more correct from a Sendability perspective but produces warnings for some safe patterns that are currently in use.
1 parent 8c2654c commit 05ec0a9

File tree

3 files changed

+168
-125
lines changed

3 files changed

+168
-125
lines changed

Sources/NIOCore/EventLoopFuture+Deprecated.swift

+9-9
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,61 @@
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>(file: StaticString = #fileID, line: UInt = #line, _ callback: @Sendable @escaping (Value) -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
1919
return self.flatMap(callback)
2020
}
2121

2222
@inlinable
2323
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
2424
public func flatMapThrowing<NewValue>(file: StaticString = #fileID,
2525
line: UInt = #line,
26-
_ callback: @escaping (Value) throws -> NewValue) -> EventLoopFuture<NewValue> {
26+
_ callback: @Sendable @escaping (Value) throws -> NewValue) -> EventLoopFuture<NewValue> {
2727
return self.flatMapThrowing(callback)
2828
}
2929

3030
@inlinable
3131
@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> {
32+
public func flatMapErrorThrowing(file: StaticString = #fileID, line: UInt = #line, _ callback: @Sendable @escaping (Error) throws -> Value) -> EventLoopFuture<Value> where Value: Sendable {
3333
return self.flatMapErrorThrowing(callback)
3434
}
3535

3636
@inlinable
3737
@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> {
38+
public func map<NewValue>(file: StaticString = #fileID, line: UInt = #line, _ callback: @Sendable @escaping (Value) -> (NewValue)) -> EventLoopFuture<NewValue> {
3939
return self.map(callback)
4040
}
4141

4242
@inlinable
4343
@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> {
44+
public func flatMapError(file: StaticString = #fileID, line: UInt = #line, _ callback: @Sendable @escaping (Error) -> EventLoopFuture<Value>) -> EventLoopFuture<Value> where Value: Sendable {
4545
return self.flatMapError(callback)
4646
}
4747

4848
@inlinable
4949
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
5050
public func flatMapResult<NewValue, SomeError: Error>(file: StaticString = #fileID,
5151
line: UInt = #line,
52-
_ body: @escaping (Value) -> Result<NewValue, SomeError>) -> EventLoopFuture<NewValue> {
52+
_ body: @Sendable @escaping (Value) -> Result<NewValue, SomeError>) -> EventLoopFuture<NewValue> {
5353
return self.flatMapResult(body)
5454
}
5555

5656
@inlinable
5757
@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> {
58+
public func recover(file: StaticString = #fileID, line: UInt = #line, _ callback: @Sendable @escaping (Error) -> Value) -> EventLoopFuture<Value> {
5959
return self.recover(callback)
6060
}
6161

6262
@inlinable
6363
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
64-
public func and<OtherValue>(_ other: EventLoopFuture<OtherValue>,
64+
public func and<OtherValue: Sendable>(_ other: EventLoopFuture<OtherValue>,
6565
file: StaticString = #fileID,
6666
line: UInt = #line) -> EventLoopFuture<(Value, OtherValue)> {
6767
return self.and(other)
6868
}
6969

7070
@inlinable
7171
@available(*, deprecated, message: "Please don't pass file:line:, there's no point.")
72-
public func and<OtherValue>(value: OtherValue,
72+
public func and<OtherValue: Sendable>(value: OtherValue,
7373
file: StaticString = #fileID,
7474
line: UInt = #line) -> EventLoopFuture<(Value, OtherValue)> {
7575
return self.and(value: value)

Sources/NIOCore/EventLoopFuture+WithEventLoop.swift

+18-30
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,12 @@ 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> {
45-
let next = EventLoopPromise<NewValue>.makeUnleakablePromise(eventLoop: self.eventLoop)
46-
self._whenComplete { [eventLoop = self.eventLoop] in
47-
switch self._value! {
48-
case .success(let t):
49-
let futureU = callback(t, eventLoop)
50-
if futureU.eventLoop.inEventLoop {
51-
return futureU._addCallback {
52-
next._setValue(value: futureU._value!)
53-
}
54-
} else {
55-
futureU.cascade(to: next)
56-
return CallbackList()
57-
}
58-
case .failure(let error):
59-
return next._setValue(value: .failure(error))
60-
}
61-
}
62-
return next.futureResult
44+
public func flatMapWithEventLoop<NewValue: Sendable>(_ callback: @escaping @Sendable (Value, EventLoop) -> EventLoopFuture<NewValue>) -> EventLoopFuture<NewValue> {
45+
// Is this the same thing and still fast?
46+
let eventLoop = self.eventLoop
47+
return self.flatMap {
48+
callback($0, eventLoop)
49+
}.hop(to: self.eventLoop)
6350
}
6451

6552
/// When the current `EventLoopFuture<Value>` is in an error state, run the provided callback, which
@@ -75,20 +62,22 @@ extension EventLoopFuture {
7562
/// - returns: A future that will receive the recovered value.
7663
@inlinable
7764
@preconcurrency
78-
public func flatMapErrorWithEventLoop(_ callback: @escaping @Sendable (Error, EventLoop) -> EventLoopFuture<Value>) -> EventLoopFuture<Value> {
65+
public func flatMapErrorWithEventLoop(_ callback: @escaping @Sendable (Error, EventLoop) -> EventLoopFuture<Value>) -> EventLoopFuture<Value> where Value: Sendable {
7966
let next = EventLoopPromise<Value>.makeUnleakablePromise(eventLoop: self.eventLoop)
67+
let unsafeSelf = UnsafeTransfer(self)
68+
let unsafeNext = UnsafeTransfer(next)
8069
self._whenComplete { [eventLoop = self.eventLoop] in
81-
switch self._value! {
70+
switch unsafeSelf.wrappedValue._value! {
8271
case .success(let t):
83-
return next._setValue(value: .success(t))
72+
return unsafeNext.wrappedValue._setValue(value: .success(t))
8473
case .failure(let e):
8574
let t = callback(e, eventLoop)
8675
if t.eventLoop.inEventLoop {
8776
return t._addCallback {
88-
next._setValue(value: t._value!)
77+
unsafeNext.wrappedValue._setValue(value: t._value!)
8978
}
9079
} else {
91-
t.cascade(to: next)
80+
t.cascade(to: unsafeNext.wrappedValue)
9281
return CallbackList()
9382
}
9483
}
@@ -113,16 +102,15 @@ extension EventLoopFuture {
113102
/// - with: A function that will be used to fold the values of two `EventLoopFuture`s and return a new value wrapped in an `EventLoopFuture`.
114103
/// - returns: A new `EventLoopFuture` with the folded value whose callbacks run on `self.eventLoop`.
115104
@inlinable
116-
@preconcurrency
117105
public func foldWithEventLoop<OtherValue>(
118106
_ futures: [EventLoopFuture<OtherValue>],
119107
with combiningFunction: @escaping @Sendable (Value, OtherValue, EventLoop) -> EventLoopFuture<Value>
120-
) -> EventLoopFuture<Value> {
121-
func fold0(eventLoop: EventLoop) -> EventLoopFuture<Value> {
108+
) -> EventLoopFuture<Value> where Value: Sendable, OtherValue: Sendable { // This is a breaking change
109+
let fold0: @Sendable (EventLoop) -> EventLoopFuture<Value> = { (eventLoop: EventLoop) in
122110
let body = futures.reduce(self) { (f1: EventLoopFuture<Value>, f2: EventLoopFuture<OtherValue>) -> EventLoopFuture<Value> in
123111
let newFuture = f1.and(f2).flatMap { (args: (Value, OtherValue)) -> EventLoopFuture<Value> in
124112
let (f1Value, f2Value) = args
125-
self.eventLoop.assertInEventLoop()
113+
eventLoop.assertInEventLoop()
126114
return combiningFunction(f1Value, f2Value, eventLoop)
127115
}
128116
assert(newFuture.eventLoop === self.eventLoop)
@@ -132,11 +120,11 @@ extension EventLoopFuture {
132120
}
133121

134122
if self.eventLoop.inEventLoop {
135-
return fold0(eventLoop: self.eventLoop)
123+
return fold0(self.eventLoop)
136124
} else {
137125
let promise = self.eventLoop.makePromise(of: Value.self)
138126
self.eventLoop.execute { [eventLoop = self.eventLoop] in
139-
fold0(eventLoop: eventLoop).cascade(to: promise)
127+
fold0(eventLoop).cascade(to: promise)
140128
}
141129
return promise.futureResult
142130
}

0 commit comments

Comments
 (0)