-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sendability warnings #417
Sendability warnings #417
Conversation
@@ -227,20 +239,31 @@ extension NIOHTTP2Handler { | |||
/// `OutboundStreamOutput`. This type may be `HTTP2Frame` or changed to any other type. | |||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | |||
@_spi(AsyncChannel) | |||
public struct AsyncStreamMultiplexer<InboundStreamOutput> { | |||
private let inlineStreamMultiplexer: InlineStreamMultiplexer | |||
public struct AsyncStreamMultiplexer<InboundStreamOutput>: @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't all the properties of this type Sendable
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, marked as Sendable
Sources/NIOHTTP2/HTTP2ChannelHandler+InlineStreamMultiplexer.swift
Outdated
Show resolved
Hide resolved
@@ -1078,8 +1082,9 @@ extension NIOHTTP2Handler { | |||
return try self.syncMultiplexer() | |||
} | |||
} else { | |||
let loopBoundSelf = NIOLoopBound(self, eventLoop: self.eventLoop!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't do this: NIOLoopBound
preconditions that it's on the right EL on init
and we know we're not on the EL here as we just checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is the canonical use of UnsafeTransfer
.
let loopBoundMultiplexer = NIOLoopBound(multiplexer, eventLoop: self.eventLoop) | ||
let loopBoundHandlers = NIOLoopBound(handlers, eventLoop: self.eventLoop) | ||
let loopBoundPosition = NIOLoopBound(position, eventLoop: self.eventLoop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We know we're not on the EL here so these will fail preconditions on init.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than moving the handlers onto the EL we can just create them on the EL. The pipeline position is super awkward though because it isn't Sendable
. I'm not sure what the solution there is (this is a problem NIO needs to solve).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved the handler creation but left the warning for this and other use of position
rather than trying to hide it because it truly is not sendable.
let loopBoundStreamDelegate = NIOLoopBound(streamDelegate, eventLoop: self.eventLoop) | ||
let loopBoundPosition = NIOLoopBound(position, eventLoop: self.eventLoop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We know we're not on the EL here: these will precondition failure on init.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made it a requirement that the streamDelegate
be Sendable
and left the position warning there.
@@ -460,7 +483,7 @@ extension Channel { | |||
} | |||
} else { | |||
return self.eventLoop.submit { | |||
return try self.pipeline.syncOperations.configureAsyncHTTP2Pipeline( | |||
return try self.pipeline.syncOperations .configureAsyncHTTP2Pipeline( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return try self.pipeline.syncOperations .configureAsyncHTTP2Pipeline( | |
return try self.pipeline.syncOperations.configureAsyncHTTP2Pipeline( |
@@ -167,11 +167,15 @@ extension NIOHTTP2Handler { | |||
public struct StreamMultiplexer: @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point do we need @unchecked
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do not.
@@ -167,11 +167,15 @@ extension NIOHTTP2Handler { | |||
public struct StreamMultiplexer: @unchecked Sendable { | |||
// '@unchecked Sendable' because this state is not intrinsically `Sendable` | |||
// but it is only accessed in `createStreamChannel` which executes the work on the right event loop | |||
private let inlineStreamMultiplexer: InlineStreamMultiplexer | |||
|
|||
private let inlineStreamMultiplexer: NIOLoopBound<InlineStreamMultiplexer> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think NIOLoopBound
is the right approach here. There isn't a particular requirement to force the event loop dance out this far, I don't think. In practice we're calling an API that is thread-safe inside InlineStreamMultiplexer
, so I wonder if using a lens type might be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made InlineStreamMultiplexerSendableView
for this purpose
Sources/NIOHTTP2/HTTP2ChannelHandler+InlineStreamMultiplexer.swift
Outdated
Show resolved
Hide resolved
@@ -103,7 +103,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { | |||
|
|||
/// The delegate for (de)multiplexing inbound streams. | |||
private var inboundStreamMultiplexerState: InboundStreamMultiplexerState | |||
internal var inboundStreamMultiplexer: InboundStreamMultiplexer? { | |||
internal var inboundStreamMultiplexer: NIOLoopBound<InboundStreamMultiplexer>? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this need to be a NIOLoopBound
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't, removed.
@@ -1078,8 +1082,9 @@ extension NIOHTTP2Handler { | |||
return try self.syncMultiplexer() | |||
} | |||
} else { | |||
let loopBoundSelf = NIOLoopBound(self, eventLoop: self.eventLoop!) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is the canonical use of UnsafeTransfer
.
Sources/NIOHTTP2/HTTP2Error.swift
Outdated
@@ -467,7 +467,9 @@ public enum NIOHTTP2Errors { | |||
} | |||
} | |||
|
|||
private final class Storage: Equatable { | |||
private final class Storage: Equatable, @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, this type is very trivially not Sendable
. Can it be made to be? Same for the other changes here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd put the annotation in the wrong place, I've moved it to the broader error type.
f3faf6b
to
da4f322
Compare
Sources/NIOHPACK/HPACKErrors.swift
Outdated
@@ -124,3 +123,5 @@ public enum NIOHPACKErrors { | |||
public init() {} | |||
} | |||
} | |||
|
|||
public protocol Foo: Collection, Sendable {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops!
@@ -546,3 +561,35 @@ extension AsyncThrowingStream { | |||
} | |||
} | |||
#endif | |||
|
|||
struct HTTP2CommonInboundStreamMultiplexerSendableView: @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually nest views inside the type they are viewing, e.g. HTTP2CommonInboundStreamMultiplexer.SendableView
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that actually I didn't end up using this type, so I'll remove it. I did make a different view to which I'll apply this tip though.
Sources/NIOHTTP2/HTTP2Error.swift
Outdated
@@ -1572,7 +1574,9 @@ public enum NIOHTTP2Errors { | |||
/// meaningfully be `Equatable`, so they aren't. There's also no additional location information: that's | |||
/// provided by the base error. | |||
public struct StreamError: Error { | |||
private final class Storage { | |||
private final class Storage: @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The storage isn't sendable, but StreamError
is @unchecked Sendable
because it does the CoW dance when modifying storage.
Sources/NIOHTTP2/HTTP2Error.swift
Outdated
@@ -1690,7 +1694,7 @@ private func _location(file: String, line: UInt) -> String { | |||
return "\(file):\(line)" | |||
} | |||
|
|||
private final class StringAndLocationStorage: Equatable { | |||
private final class StringAndLocationStorage: Equatable, @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be Sendable
, it's a class with mutable properties. It can be used in a Sendable
type if the caller does the appropriate CoW dance though.
} catch { | ||
return self.eventLoop.makeFailedFuture(error) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aim to keep do-catch blocks as small as possible. Here we should scope it to the if self.eventLoop.inEventLoop
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I missed that self.eventLoop.submit
handles the thrown error so I thought both branches could throw and wanted to avoid duplication.
var handlers = [ChannelHandler]() | ||
handlers.reserveCapacity(2) // Update this if we need to add more handlers, to avoid unnecessary reallocation. | ||
|
||
handlers.append(NIOHTTP2Handler(mode: mode, initialSettings: initialLocalSettings)) | ||
let multiplexer = HTTP2StreamMultiplexer(mode: mode, channel: channel, targetWindowSize: targetWindowSize, inboundStreamInitializer: inboundStreamInitializer) | ||
handlers.append(multiplexer) | ||
|
||
try self.addHandlers(handlers, position: position) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given we're doing synchronous operations here we can actually save an allocation by avoiding the handlers
array and just calling addHandler
multiple times.
Motivation: To address warnings revealed with `.enableExperimentalFeature("StrictConcurrency=complete"),` in Swift 5.8 Modifications: * Numerous protections of state in tests * Make the multiplexer instances explicitly loop bound * Update channel initializers to be marked as Sendable * Mark error backing storage types as unchecked sendable Result: Better thread safety
3730e9a
to
823875d
Compare
I squashed all of the reviewed changes into one commit and merged the changes from main into this branch to try and highlight any new changes. |
f8ff28b
to
2df3fd3
Compare
|
||
return self.pipeline.addHandlers(handlers, position: position).map { multiplexer } | ||
if self.eventLoop.inEventLoop { | ||
do { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI eventLoop
has a makeCompletedFuture
function which takes a closure which returns a Result
type. It's effectively the Result
-based counterpart to makeSucceededFuture
and makeFailedFuture
and allows you to avoid doing the do-catch block here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat
configurator: @escaping NIOChannelInitializer | ||
) -> EventLoopFuture<Void> { | ||
let loopBoundStreamDelegate = NIOLoopBound(streamDelegate, eventLoop: self.eventLoop) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct? This pipeline helpers isn't synchronous so we can't make the guarantee that we'll be in the EL at this point. Also I think stream delegate is Sendable
now anyway isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is indeed sendable, good spot. I don't know if this came up in the rebase or if it was just an outstanding issue.
…2 into sendability_warnings
} | ||
|
||
internal func createStreamChannel(promise: EventLoopPromise<Channel>?, _ streamStateInitializer: @escaping NIOHTTP2Handler.StreamInitializer) { | ||
self.inlineStreamMultiplexer.createStreamChannel(promise: promise, streamStateInitializer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused: is this method thread-safe, or is the usage point protecting it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good spot. It's intended that the method is thread-safe. I've moved the event loop dance inside this method to make that the case.
if self.eventLoop.inEventLoop { | ||
self.inlineStreamMultiplexer.createStreamChannel(promise: promise, streamStateInitializer) | ||
} else { | ||
self.eventLoop.execute { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment on the SendableView
.
extension UnsafeTransfer: @unchecked Sendable {} | ||
|
||
extension UnsafeTransfer: Equatable where Wrapped: Equatable {} | ||
extension UnsafeTransfer: Hashable where Wrapped: Hashable {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we put this in its own file rather than smuggling it into here?
inboundStreamStateInitializer: .includesStreamID(nil) | ||
) | ||
self.streams[streamID] = channel | ||
loopBounds.value.0.streams[streamID] = channel |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Repeatedly unpacking the loop bounds here kinda sucks: can we unwrap them at the top of the execute { }
block and then use their names?
public struct AsyncStreamMultiplexer<InboundStreamOutput> { | ||
private let inlineStreamMultiplexer: InlineStreamMultiplexer | ||
public struct AsyncStreamMultiplexer<InboundStreamOutput: Sendable>: Sendable { | ||
private let inlineStreamMultiplexer: NIOLoopBound<InlineStreamMultiplexer> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we don't use the sendable view here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, an oversight. I have used it here and fleshed out its API a bit more to mirror the underlying multiplexer.
* HTTP2StreamMultiplexer stream creation previously called down to underlying code which assumed it was always on the correct event loop however that wasn't the case via this public API. I have introduced the event loop dance. * Remove the use of NIOLoopBound in cases where we know we are on the correct event loop and unsafe transfer is more applicable.
Calls to `createStreamChannel` now always go via a `SendableView` lens type which serializes access on an event loop with an unconditional hop. This combines the serialization with the unconditional hop which was previously performed at the `HTTP2CommonInboundStreamMultiplexer` level, in some cases saving a hop. The `SendableView` is performed at the higher multiplexer levels rather than at the 'common' level because the higher levels pass in a `self` to lower calls which would otherwise need to be transferred in to the event loop hop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general this looks good to me. I would leave the final review to @Lukasa
@@ -146,7 +146,9 @@ private enum HTTP2StreamData { | |||
} | |||
|
|||
@usableFromInline | |||
final class HTTP2StreamChannel: Channel, ChannelCore { | |||
final class HTTP2StreamChannel: Channel, ChannelCore, @unchecked Sendable { | |||
// @unchecked Sendable because the only mutable state is `_pipeline` which is only modified in init |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not fully correct there are a bunch of methods on the ChannelCore
that must be called on the EL such as write0
. However, nobody should really call those methods.
I believe that the discovered API breakages are okay, just to expand on that.
Add sendable requirement:
Change from explicit closure type to equivalent type alias:
Change from explicit closure type to equivalent type alias:
|
Motivation:
To address warnings revealed with
.enableExperimentalFeature("StrictConcurrency=complete"),
in Swift 5.8Modifications:
Result:
Better thread safety