-
-
Notifications
You must be signed in to change notification settings - Fork 12
Make FIFOQueue, ActorQueue, and tests pass strict concurrency checking #13
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
Conversation
@@ -22,6 +22,11 @@ let package = Package( | |||
dependencies: []), |
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.
Ideally we'd force the vended target to conform to include strict-concurrency=complete
, but doing so would mark this repo as having unsafe build flags. Once we can use enableUpcomingFeature
(see below), this will get 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.
OK I see that we use unsafe build flags in the test target and that's OK since it's not being consumed by other packages.
/// - Precondition: The lifecycle of an `ActorQueue` must not exceed that of the adopted actor. | ||
public final class ActorQueue<ActorType: Actor> { | ||
public final class ActorQueue<ActorType: Actor>: @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.
If we don't make ActorQueue
be Sendable
, then we get the following error in consuming code whenever the ActorQueue
instance is accessed from a nonisolated
method:
Non-sendable type 'ActorQueue<ActorType>' in asynchronous access to actor-isolated property 'actorQueue' cannot cross actor boundary
This error makes sense: we're accessing a non-sendable let
constant outside of the isolated context. I'm looking forward to Swift 6 when I don't need to manually turn on this checking to see these kinds of issues in my packages.
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 also can't make this type Sendable
because weakExecutionContext
is mutable and therefore the type is not Sendable. But given the API contract of:
adoptExecutionContext(of:)
must be called frominit
adoptExecutionContext(of:)
must not be called more than once- An
ActorQueue
should be aprivate let
constant on the adopted actor instance
The ActorQueue
is effectively Sendable, and therefore we can mark it as such.
fd7ba52
to
eddf9b8
Compare
Codecov Report
@@ Coverage Diff @@
## main #13 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 7 7
Lines 752 752
=========================================
Hits 752 752
|
@@ -85,7 +85,7 @@ public final class FIFOQueue: Sendable { | |||
/// - isolatedActor: The actor within which the task is isolated. | |||
/// - task: The task to enqueue. | |||
/// - Returns: The value returned from the enqueued task. | |||
public func enqueueAndWait<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { | |||
public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { |
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 T: Sendable
conformance was only required on methods that execute on an isolated actor on the FIFOQueue
. The warning we saw by not having this conformance was:
Non-sendable type 'T' returned by implicitly asynchronous call to actor-isolated function cannot cross actor boundary
This warning makes sense: T
is not Sendable
, and therefore how could we be passing it across contexts. However, the fact that this warning only surfaced when we explicitly utilizing an isolated parameter seems like a bug – in all cases we are sending T
from an actor's isolated context to the current context. So I made all return types T
conform to Sendable
, including on the ActorQueue
.
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.
To make sure I follow, we got a warning in all cases except the ones where the task
closure doesn't have an isolated
parameter? Making the return type Sendable
seems reasonable but I want to try to understand better which methods didn't have a warning so I can try to understand why some methods are warning differently than others.
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.
If I'm following correctly I think that these two methods on FIFOQueue
didn't have warnings.
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async -> T) async -> T
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async throws -> T) async throws -> T
Is that right?
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.
To make sure I follow, we got a warning in all cases except the ones where the task closure doesn't have an
isolated
parameter?
Correct!
Making the return type
Sendable
seems reasonable but I want to try to understand better which methods didn't have a warning so I can try to understand why some methods are warning differently than others.
My best guess is that this is a compiler bug. In all the methods I marked we're crossing a task boundary: from the task that created the async for loop to the call-site. But only in the methods where we have an isolated
parameter does the compiler notice that we're crossing a task boundary and therefore only then does it require Sendable
conformance.
Is that right?
That is absolutely right.
eddf9b8
to
d38ab11
Compare
@@ -1,6 +1,6 @@ | |||
Pod::Spec.new do |s| | |||
s.name = 'AsyncQueue' | |||
s.version = '0.2.0' | |||
s.version = '0.3.0' |
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.
Unfortunately this PR represents a (reasonably small) breaking API change.
@@ -65,17 +65,17 @@ final class ActorQueueTests: XCTestCase { | |||
|
|||
func test_enqueue_taskParameterIsAdoptedActor() async { | |||
let semaphore = Semaphore() | |||
systemUnderTest.enqueue { counter in | |||
XCTAssertTrue(counter === self.counter) |
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.
capturing non-sendable self
in the sendable closure is not good! So instead we now capture the counter
directly.
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.
One question but overall LGTM. Thanks for addressing feedback from previous PRs.
@@ -22,6 +22,11 @@ let package = Package( | |||
dependencies: []), |
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.
OK I see that we use unsafe build flags in the test target and that's OK since it's not being consumed by other packages.
@@ -85,7 +85,7 @@ public final class FIFOQueue: Sendable { | |||
/// - isolatedActor: The actor within which the task is isolated. | |||
/// - task: The task to enqueue. | |||
/// - Returns: The value returned from the enqueued task. | |||
public func enqueueAndWait<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { | |||
public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { |
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.
To make sure I follow, we got a warning in all cases except the ones where the task
closure doesn't have an isolated
parameter? Making the return type Sendable
seems reasonable but I want to try to understand better which methods didn't have a warning so I can try to understand why some methods are warning differently than others.
@@ -85,7 +85,7 @@ public final class FIFOQueue: Sendable { | |||
/// - isolatedActor: The actor within which the task is isolated. | |||
/// - task: The task to enqueue. | |||
/// - Returns: The value returned from the enqueued task. | |||
public func enqueueAndWait<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { | |||
public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { |
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.
If I'm following correctly I think that these two methods on FIFOQueue
didn't have warnings.
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async -> T) async -> T
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async throws -> T) async throws -> T
Is that right?
I made this PR by turning on strict concurrency checking, and then squashing the warnings/errors.