-
Notifications
You must be signed in to change notification settings - Fork 651
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
Add isolated views to EventLoop, Promise, and Future #2969
base: main
Are you sure you want to change the base?
Conversation
Motivation: Users writing NIO code in a strict concurrency world often need to interact with futures, promises, and event loops. The main interface to these has strict sendability requirements, as it is possible the user is doing so from outside the EventLoop that provides the isolation domain for these types. However, in many cases the user knows that they are on the isolation domain in question. In that case, they need more capabilities. While they can achieve their goals with NIOLoopBound today, it'd be nice if they had a better option. Modifications: - Make EventLoop.Isolated public. - Make EventLoopFuture.Isolated public. - Make EventLoopPromise.Isolated public. - Make all their relevant methods public. - Move the runtime isolation check from the point of use to the point of construction. - Make the types non-Sendable to ensure that isolation check is sufficient. - Add unsafeUnchecked options to create these types when performance matters and correctness is clear. - Add tests for their behaviour. - Update the documentation. Result: Writing safe code with promises, futures, and event loops is easier.
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 looks great, we're missing a NIO prefix though.
/// | ||
/// Using this type relaxes the need to have the closures for ``EventLoop/execute(_:)``, | ||
/// ``EventLoop/submit(_:)``, and ``EventLoop/scheduleTask(in:_:)`` to be `@Sendable`. | ||
public struct IsolatedEventLoop { |
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 need a NIO
prefix here
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) | ||
defer { | ||
try! group.syncShutdownGracefully() | ||
} |
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.
Not that it matters much, but should we prefer singletons in tests as they save a little boilerplate.
|
||
// This block is the main happy path. | ||
let newFuture = future.flatMap { result in | ||
XCTAssertTrue(originalValue === result) |
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.
FWIW: XCTAssertIdentical
exists for this
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.
LGTM, thanks Cory!
@inlinable | ||
func execute(_ task: @escaping () -> Void) { | ||
self._wrapped.assertInEventLoop() |
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.
Meta question: I understand why you removed those but should we keep them in in-case somebody really mishandles this type? In release builds these all should go away anyways.
/// | ||
/// This type is explicitly not `Sendable`. It may only be constructed on an event loop, | ||
/// using ``EventLoop/assumeIsolated()``, and may not subsequently be passed to other isolation | ||
/// domains. |
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 other isolation domains is actually too weak of a statement. This type must not be held inside any asynchronous context.
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's not appropriate either: non-asynchronous contexts are themselves dangerous. As task executors aren't supported today, I think the statement as written is correct. I would now reject any plan to add task executors until these issues are resolved, so I'm inclined to keep the text.
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.
What non-asynchronous context are you referring to? I consider inside an actor an asynchronous context since you need to be inside a Task
.
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 agree, but I think that framing is going to be prone to misunderstanding, I think: people will see the noasync
directive and believe that the word async
signals the issue, which it doesn't.
@@ -119,26 +130,54 @@ struct IsolatedEventLoop { | |||
|
|||
/// Returns the wrapped event loop. | |||
@inlinable | |||
func nonisolated() -> any EventLoop { | |||
public func nonisolated() -> any 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.
Can we make this method also noasync
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 we need to: this is the only method that is always safe to call on this type.
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 understand your argument my reason is that if a user ever ends up holding this type in an async
method I really want them to see an error. Even if that place is not where the error happened but they should question how they got the type there in the first place.
/// isolation check in release builds. It retains it in debug mode to | ||
/// ensure correctness. | ||
@inlinable | ||
public func assumeIsolatedUnsafeUnchecked() -> NIOIsolatedEventLoop { |
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.
Same here. Can we make this noasync
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 deliberately didn't do that. This is already marked unsafe-unchecked. I have therefore left it for the possible use-case that someone is actually trying to do the thing we're trying to prevent them doing, and is willing to take on the risk of doing it correctly themselves.
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.
Okay fair enough. Can we add that to the documentation then.
/// versions for the functions that do require `Sendable` types. If you have an | ||
/// ``EventLoopPromise/Isolated`` but need a regular ``EventLoopPromise``, use | ||
/// ``EventLoopPromise/Isolated/nonisolated()`` to unwrap the value. | ||
public struct Isolated { |
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 all methods in this type or in general in all those new types should be marked noasync
. None of them should ever be held in an async context.
/// omits the runtime check in release builds. This improves performance, but | ||
/// should only be used sparingly. | ||
@inlinable | ||
public func assumeIsolatedUnsafeUnchecked() -> Isolated { |
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.
public func assumeIsolatedUnsafeUnchecked() -> Isolated { | |
@available(*, noasync) | |
public func assumeIsolatedUnsafeUnchecked() -> Isolated { |
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.
Same note here, I want to leave the unsafe unchecked methods without the annotation.
Motivation:
Users writing NIO code in a strict concurrency world often need to interact with futures, promises, and event loops. The main interface to these has strict sendability requirements, as it is possible the user is doing so from outside the EventLoop that provides the isolation domain for these types.
However, in many cases the user knows that they are on the isolation domain in question. In that case, they need more capabilities. While they can achieve their goals with NIOLoopBound today, it'd be nice if they had a better option.
Modifications:
Result:
Writing safe code with promises, futures, and event loops is easier.