Skip to content
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

Merged
merged 13 commits into from
Dec 16, 2024

Conversation

Lukasa
Copy link
Contributor

@Lukasa Lukasa commented Nov 7, 2024

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.

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.
@Lukasa Lukasa added the 🆕 semver/minor Adds new public API. label Nov 7, 2024
Copy link
Contributor

@glbrntt glbrntt left a 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 {
Copy link
Contributor

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

Comment on lines 28 to 31
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try! group.syncShutdownGracefully()
}
Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Contributor

@glbrntt glbrntt left a 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()
Copy link
Member

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.
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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 {
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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 {
Copy link
Member

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public func assumeIsolatedUnsafeUnchecked() -> Isolated {
@available(*, noasync)
public func assumeIsolatedUnsafeUnchecked() -> Isolated {

Copy link
Contributor Author

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.

@Lukasa Lukasa merged commit 1e4fde1 into apple:main Dec 16, 2024
33 of 35 checks passed
@Lukasa Lukasa deleted the cb-isolated branch December 16, 2024 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants