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

Changed NIOAsyncChannel.executeThenClose to accept actor-isolated par… #3121

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

rafaelcepeda
Copy link

Improve usability of NIOAsyncChannel.executeThenClose function from an Actor

Motivation:

When calling executeThenClose from inside an actor, the closure which we pass is not isolated to the same actor
This makes it hard to use

e.g. the following code won’t compile on Swift 6 because the closure is self-isolated so can’t be passed

await withDiscardingTaskGroup { group in
            do {
                try await serverChannel.executeThenClose { inboundStream, _ in
                    try await self.handleInboundStream(inboundStream: inboundStream, group: &group)
                }
             } catch {}
}

Making the closure as @sendable allows it to be passed, but then we can’t access group anymore

Modifications:

Added actor isolation parameter to the executeThenClose function.

Result:

executeThenClose will be able to be asynchronously called from inside an Actor, as it will now be part of the Actor's isolation domain.

@@ -283,8 +283,11 @@ public struct NIOAsyncChannel<Inbound: Sendable, Outbound: Sendable>: Sendable {
///
/// - Important: After this method returned the underlying ``Channel`` will be closed.
///
/// - Parameter body: A closure that gets scoped access to the inbound and outbound.
/// - Parameter:
/// - isolatedTo: actor where this function should be isolated to
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: the parameter name here is actor - isolatedTo is the label (you can think of it as a part of the method's name).

Copy link
Author

Choose a reason for hiding this comment

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

Fixed the issue.

public func executeThenClose<Result>(
isolatedTo actor: isolated(any Actor)? = #isolation,
Copy link
Contributor

Choose a reason for hiding this comment

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

This change is actually API-breaking: even if the parameter you add is defaulted, the function signature changes.

For instance, imagine you have:

func someFunc(foo: Int) { ... }

func doSomething() {
  let f: (Int) -> Void  = someFunc(foo:)
  // ...
}

Say you add a new defaulted parameter...

func someFunc(foo: Int, bar: String = "baz") { ... }

func doSomething() {
  let f1: (Int) -> Void  = someFunc(foo:)  //  this will fail to build: "no member named someFunc(foo:)"
  // Should be: 
  let f2: (Int, String?) -> Void  = someFunc(foo:bar:)
}

You will have to add a new executeThenClose overload that takes a (non-optional) isolated param.

Copy link
Author

Choose a reason for hiding this comment

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

Great point. I've added the old API back and have implemented it using the new one.

@glbrntt glbrntt added the 🆕 semver/minor Adds new public API. label Feb 25, 2025
Copy link
Contributor

@hamzahrmalik hamzahrmalik left a comment

Choose a reason for hiding this comment

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

Thanks for this change, it will make it much easier to use asyncchannels!

Couple of points:

  1. this will not work on older compilers, so probably needs some #if compiler >= 6.0 sprinkled around
  2. following on from gus's comment, are we sure the overload resolution will pick the right thing here? Can we add a test which uses this from an actor to make sure that compiles fine?

The testcase would be something like having an actor-isolated function call executeThenClose and then attempt to reference self inside the closure

/// - actor: actor where this function should be isolated to
/// - body: A closure that gets scoped access to the inbound and outbound.
public func executeThenClose<Result>(
isolatedTo actor: isolated (any Actor)?,
Copy link
Contributor

Choose a reason for hiding this comment

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

We seem to have dropped the defaulted parameter here which I think we want.

Copy link
Contributor

Choose a reason for hiding this comment

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

The problem is the compiler picking the right func if we default this parameter (i.e. if you call executeThenClose { ... } are we calling the old version, or the one defaulting isolation?).
We could use @_disfavoredOverload, but have we agreed that is the route we generally want to take?

Specifically in this case, if what we want is to enable adopters to use executeThenClose from actors, I don't think it's particularly bad to have to explicitly pass self as isolation, as it makes it clear you want to run this on the actor, even though you're awaiting and would otherwise be hopping off the isolation domain.

Copy link
Author

Choose a reason for hiding this comment

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

As spoken offline, we decided to favour the use of @_disfavoredOverload with this API. I'll change the code.

@rnro
Copy link
Contributor

rnro commented Feb 26, 2025

Looks good in general, unfortunately there are some fiddly considerations here. Here's a link to a similar block of code which includes the compiler check which Hamzah mentions https://github.com/apple/swift-service-context/blob/8946c930cae601452149e45d31d8ddfac973c3c7/Sources/ServiceContextModule/ServiceContext.swift#L232-L266

@@ -284,6 +284,7 @@ public struct NIOAsyncChannel<Inbound: Sendable, Outbound: Sendable>: Sendable {
/// - Important: After this method returned the underlying ``Channel`` will be closed.
///
/// - Parameter body: A closure that gets scoped access to the inbound and outbound.
@_disfavoredOverload
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 we should also deprecate those methods since in theory those are not concurrency safe and we want users to adopt a Swift 6 compiler to make those methods safe again.

rafaelcepeda and others added 2 commits March 3, 2025 16:51
Co-authored-by: Franz Busch <privat@franz-busch.de>
Co-authored-by: Franz Busch <privat@franz-busch.de>
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.

6 participants