Skip to content

Commit de116b7

Browse files
authored
Clean up Sendability for ChannelInvoker (#2955)
Motivation: The ChannelInvoker protocols are an awkward beast. They aren't really something that people can do generic programming against. Instead, they were designed to do API sharing. Of course, they didn't do that very well, and the strict concurrency checking world has revealed this. Much of the API surface on ChannelInvoker is confused. There are NIOAnys, which aren't Sendable. We allow sending user events without requiring Sendable. And our two main conforming types are ChannelPipeline and ChannelHandlerContext, two types with wildly differing thread-safety semantics. This PR aims to clean that up. Modifications: - Deprecated all API surface on ChannelInvoker protocols that uses NIOAny. ChannelInvoker has to be assumed to be a cross-thread protocol, and that requires that it only use Sendable types. NIOAny isn't, so these methods are no longer sound. - Re-add non-deprecated versions on ChannelHandlerContext. While it's not safe to use the NIOAny methods on Channel or ChannelPipeline, it's totally safe to use them on ChannelHandlerContext. So we keep those available and undeprecated. - Provide typed generic replacements on ChannelPipeline and on Channel To replace the NIOAny methods on ChannelPipeline and Channel we can use some typed generic ones instead. These are not defined on ChannelInvoker, as the methods are useless on ChannelHandlerContext. This begins the acknowledgement that ChannelHandlerContext should not have conformed to these protocols at all. - Add Sendable constraints to the user event witnesses on ChannelInvoker Again, these were missing, but must be there for Channel and ChannelPipeline. - Provide non-Sendable overloads on ChannelHandlerContext ChannelHandlerContext is thread-bound, and so may safely pass non-Sendable user events. Result: One step closer to strict concurrency cleanliness for NIOCore.
1 parent 9356598 commit de116b7

29 files changed

+719
-270
lines changed

Sources/NIOCore/AsyncAwaitSupport.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ extension Channel {
8888
/// or `nil` if not interested in the outcome of the operation.
8989
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
9090
@inlinable
91-
public func writeAndFlush<T>(_ any: T) async throws {
91+
@preconcurrency
92+
public func writeAndFlush<T: Sendable>(_ any: T) async throws {
9293
try await self.writeAndFlush(any).get()
9394
}
9495

@@ -140,6 +141,11 @@ extension ChannelOutboundInvoker {
140141
/// - data: the data to write
141142
/// - returns: the future which will be notified once the `write` operation completes.
142143
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
144+
@available(
145+
*,
146+
deprecated,
147+
message: "NIOAny is not Sendable: avoid wrapping the value in NIOAny to silence this warning."
148+
)
143149
public func writeAndFlush(_ data: NIOAny, file: StaticString = #fileID, line: UInt = #line) async throws {
144150
try await self.writeAndFlush(data, file: file, line: line).get()
145151
}
@@ -159,8 +165,13 @@ extension ChannelOutboundInvoker {
159165
/// - parameters:
160166
/// - event: the event itself.
161167
/// - returns: the future which will be notified once the operation completes.
168+
@preconcurrency
162169
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
163-
public func triggerUserOutboundEvent(_ event: Any, file: StaticString = #fileID, line: UInt = #line) async throws {
170+
public func triggerUserOutboundEvent(
171+
_ event: Any & Sendable,
172+
file: StaticString = #fileID,
173+
line: UInt = #line
174+
) async throws {
164175
try await self.triggerUserOutboundEvent(event, file: file, line: line).get()
165176
}
166177
}

Sources/NIOCore/Channel.swift

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,30 @@ public protocol Channel: AnyObject, ChannelOutboundInvoker, _NIOPreconcurrencySe
145145
/// The default implementation returns `nil`, and `Channel` implementations must opt in to
146146
/// support this behavior.
147147
var syncOptions: NIOSynchronousChannelOptions? { get }
148+
149+
/// Write data into the `Channel`, automatically wrapping with `NIOAny`.
150+
///
151+
/// - seealso: `ChannelOutboundInvoker.write`.
152+
@preconcurrency
153+
func write<T: Sendable>(_ any: T) -> EventLoopFuture<Void>
154+
155+
/// Write data into the `Channel`, automatically wrapping with `NIOAny`.
156+
///
157+
/// - seealso: `ChannelOutboundInvoker.write`.
158+
@preconcurrency
159+
func write<T: Sendable>(_ any: T, promise: EventLoopPromise<Void>?)
160+
161+
/// Write and flush data into the `Channel`, automatically wrapping with `NIOAny`.
162+
///
163+
/// - seealso: `ChannelOutboundInvoker.writeAndFlush`.
164+
@preconcurrency
165+
func writeAndFlush<T: Sendable>(_ any: T) -> EventLoopFuture<Void>
166+
167+
/// Write and flush data into the `Channel`, automatically wrapping with `NIOAny`.
168+
///
169+
/// - seealso: `ChannelOutboundInvoker.writeAndFlush`.
170+
@preconcurrency
171+
func writeAndFlush<T: Sendable>(_ any: T, promise: EventLoopPromise<Void>?)
148172
}
149173

150174
extension Channel {
@@ -177,18 +201,36 @@ extension Channel {
177201
pipeline.connect(to: address, promise: promise)
178202
}
179203

204+
@available(
205+
*,
206+
deprecated,
207+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
208+
)
180209
public func write(_ data: NIOAny, promise: EventLoopPromise<Void>?) {
181210
pipeline.write(data, promise: promise)
182211
}
183212

213+
public func write<T: Sendable>(_ data: T, promise: EventLoopPromise<Void>?) {
214+
pipeline.write(data, promise: promise)
215+
}
216+
184217
public func flush() {
185218
pipeline.flush()
186219
}
187220

221+
@available(
222+
*,
223+
deprecated,
224+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
225+
)
188226
public func writeAndFlush(_ data: NIOAny, promise: EventLoopPromise<Void>?) {
189227
pipeline.writeAndFlush(data, promise: promise)
190228
}
191229

230+
public func writeAndFlush<T: Sendable>(_ data: T, promise: EventLoopPromise<Void>?) {
231+
pipeline.writeAndFlush(data, promise: promise)
232+
}
233+
192234
public func read() {
193235
pipeline.read()
194236
}
@@ -205,40 +247,33 @@ extension Channel {
205247
promise?.fail(ChannelError._operationUnsupported)
206248
}
207249

208-
public func triggerUserOutboundEvent(_ event: Any, promise: EventLoopPromise<Void>?) {
250+
@preconcurrency
251+
public func triggerUserOutboundEvent(_ event: Any & Sendable, promise: EventLoopPromise<Void>?) {
209252
pipeline.triggerUserOutboundEvent(event, promise: promise)
210253
}
211254
}
212255

213256
/// Provides special extension to make writing data to the `Channel` easier by removing the need to wrap data in `NIOAny` manually.
214257
extension Channel {
215258

216-
/// Write data into the `Channel`, automatically wrapping with `NIOAny`.
259+
/// Write data into the `Channel`.
217260
///
218261
/// - seealso: `ChannelOutboundInvoker.write`.
219-
public func write<T>(_ any: T) -> EventLoopFuture<Void> {
220-
self.write(NIOAny(any))
262+
@preconcurrency
263+
public func write<T: Sendable>(_ any: T) -> EventLoopFuture<Void> {
264+
let promise = self.eventLoop.makePromise(of: Void.self)
265+
self.write(any, promise: promise)
266+
return promise.futureResult
221267
}
222268

223-
/// Write data into the `Channel`, automatically wrapping with `NIOAny`.
224-
///
225-
/// - seealso: `ChannelOutboundInvoker.write`.
226-
public func write<T>(_ any: T, promise: EventLoopPromise<Void>?) {
227-
self.write(NIOAny(any), promise: promise)
228-
}
229-
230-
/// Write and flush data into the `Channel`, automatically wrapping with `NIOAny`.
231-
///
232-
/// - seealso: `ChannelOutboundInvoker.writeAndFlush`.
233-
public func writeAndFlush<T>(_ any: T) -> EventLoopFuture<Void> {
234-
self.writeAndFlush(NIOAny(any))
235-
}
236-
237-
/// Write and flush data into the `Channel`, automatically wrapping with `NIOAny`.
269+
/// Write and flush data into the `Channel`.
238270
///
239271
/// - seealso: `ChannelOutboundInvoker.writeAndFlush`.
240-
public func writeAndFlush<T>(_ any: T, promise: EventLoopPromise<Void>?) {
241-
self.writeAndFlush(NIOAny(any), promise: promise)
272+
@preconcurrency
273+
public func writeAndFlush<T: Sendable>(_ any: T) -> EventLoopFuture<Void> {
274+
let promise = self.eventLoop.makePromise(of: Void.self)
275+
self.writeAndFlush(any, promise: promise)
276+
return promise.futureResult
242277
}
243278
}
244279

Sources/NIOCore/ChannelInvoker.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public protocol ChannelOutboundInvoker {
4545
/// - data: the data to write
4646
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
4747
/// or `nil` if not interested in the outcome of the operation.
48+
@available(
49+
*,
50+
deprecated,
51+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
52+
)
4853
func write(_ data: NIOAny, promise: EventLoopPromise<Void>?)
4954

5055
/// Flush data that was previously written via `write` to the remote peer.
@@ -56,6 +61,11 @@ public protocol ChannelOutboundInvoker {
5661
/// - data: the data to write
5762
/// - promise: the `EventLoopPromise` that will be notified once the `write` operation completes,
5863
/// or `nil` if not interested in the outcome of the operation.
64+
@available(
65+
*,
66+
deprecated,
67+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
68+
)
5969
func writeAndFlush(_ data: NIOAny, promise: EventLoopPromise<Void>?)
6070

6171
/// Signal that we want to read from the `Channel` once there is data ready.
@@ -78,7 +88,8 @@ public protocol ChannelOutboundInvoker {
7888
/// - parameters:
7989
/// - promise: the `EventLoopPromise` that will be notified once the operation completes,
8090
/// or `nil` if not interested in the outcome of the operation.
81-
func triggerUserOutboundEvent(_ event: Any, promise: EventLoopPromise<Void>?)
91+
@preconcurrency
92+
func triggerUserOutboundEvent(_ event: Any & Sendable, promise: EventLoopPromise<Void>?)
8293

8394
/// The `EventLoop` which is used by this `ChannelOutboundInvoker` for execution.
8495
var eventLoop: EventLoop { get }
@@ -135,6 +146,11 @@ extension ChannelOutboundInvoker {
135146
/// - parameters:
136147
/// - data: the data to write
137148
/// - returns: the future which will be notified once the operation completes.
149+
@available(
150+
*,
151+
deprecated,
152+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
153+
)
138154
public func write(_ data: NIOAny, file: StaticString = #fileID, line: UInt = #line) -> EventLoopFuture<Void> {
139155
let promise = makePromise(file: file, line: line)
140156
write(data, promise: promise)
@@ -146,6 +162,11 @@ extension ChannelOutboundInvoker {
146162
/// - parameters:
147163
/// - data: the data to write
148164
/// - returns: the future which will be notified once the `write` operation completes.
165+
@available(
166+
*,
167+
deprecated,
168+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
169+
)
149170
public func writeAndFlush(_ data: NIOAny, file: StaticString = #fileID, line: UInt = #line) -> EventLoopFuture<Void>
150171
{
151172
let promise = makePromise(file: file, line: line)
@@ -170,8 +191,9 @@ extension ChannelOutboundInvoker {
170191
/// - parameters:
171192
/// - event: the event itself.
172193
/// - returns: the future which will be notified once the operation completes.
194+
@preconcurrency
173195
public func triggerUserOutboundEvent(
174-
_ event: Any,
196+
_ event: Any & Sendable,
175197
file: StaticString = #fileID,
176198
line: UInt = #line
177199
) -> EventLoopFuture<Void> {
@@ -210,6 +232,11 @@ public protocol ChannelInboundInvoker {
210232
///
211233
/// - parameters:
212234
/// - data: the data that was read and is ready to be processed.
235+
@available(
236+
*,
237+
deprecated,
238+
message: "NIOAny is not Sendable. Avoid wrapping the value in NIOAny to silence this warning."
239+
)
213240
func fireChannelRead(_ data: NIOAny)
214241

215242
/// Called once there is no more data to read immediately on a `Channel`. Any new data received will be handled later.
@@ -238,7 +265,8 @@ public protocol ChannelInboundInvoker {
238265
///
239266
/// - parameters:
240267
/// - event: the event itself.
241-
func fireUserInboundEventTriggered(_ event: Any)
268+
@preconcurrency
269+
func fireUserInboundEventTriggered(_ event: Any & Sendable)
242270
}
243271

244272
/// A protocol that signals that outbound and inbound events are triggered by this invoker.

0 commit comments

Comments
 (0)