Skip to content

Warn if send is called after .run completes #1900

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

Merged
merged 13 commits into from
Feb 17, 2023

Conversation

kabiroberai
Copy link
Contributor

@kabiroberai kabiroberai commented Feb 11, 2023

Calling send after .run returns can result in broken assumptions and discarded actions.

  • If the bad .run is directly sent into a Store, the send calls will work but they won't be tracked by [ViewStoreTask|TestStoreTask|TestStore].finish()
  • If the EffectTask.run is converted into a Publisher by applying a Combine operator, the resulting Effect will entirely ignore actions sent after the async closure returns, since EffectTask.publisher marks the effect as completed at that point.

This PR adds a runtime warning that's emitted if send is called after the .run closure returns, formally marking this as undefined behavior.

.run { send in
  Task {
    try await Task.sleep(for: 1)
    await send(.action) // now emits a warning
  }
}

When an Effect.run is converted into a publisher, we already end up discarding `send`s sent after returning. Similarly, `*Task.finish()` will fail to track send()s after other in-flight effects complete. This is effectively UB so let's warn about it.
@@ -417,9 +417,36 @@ public final class Store<State, Action> {
}
case let .run(priority, operation):
tasks.wrappedValue.append(
Task(priority: priority) {
Task(priority: priority) { @MainActor in
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really want to mark it as @MainActor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need the reads and writes to isCompleted to be serialized, and since the Send closure is @MainActor it's sufficient to make this one @MainActor too for that purpose. This doesn't mean that operation will run on the MainActor, but rather just the synchronous parts of the task (i.e. the isCompleted = true). We're already doing a MainActor hop in the Publisher case so this isn't straying from established practice.

@pyrtsa
Copy link
Contributor

pyrtsa commented Feb 11, 2023

For clarity, so the problem is in user code not taking part in structured concurrency of .run { send in ... } but instead wrapping their logic in a non-structured Task { ... }. Nice idea to warn about that!

@kabiroberai
Copy link
Contributor Author

thanks, yeah I wanted to mention "structured concurrency" in the runtime warning but figured it's better to avoid jargon. FWIW there are a number of other ways to trigger this such as calling a completion/GCD-based API (eg DispatchQueue.main.asyncAfter) so "switch from unstructured to structured concurrency" isn't a blanket solution. Users may need to use other synchronization primitives (continuations, AsyncSequence, task groups) instead.

@mbrandonw
Copy link
Member

Hi @kabiroberai, thanks for the PR! Overall I think this looks good! I took a pass at playing with the messaging, but Stephen and I will discuss more. I also split the test to focus just on the publisher API versus the pure async/await API.

We should be able to merge this soon.

@kabiroberai
Copy link
Contributor Author

@mbrandonw btw I think CI is failing because the check is guarded by #if DEBUG, so the release tests don't work. We can conditionally compile the tests too, or maybe make the check run in release builds too — up to you folks.

@mbrandonw
Copy link
Member

🤦‍♂️ thanks for pointing that out. Was confusing me. Yeah I think it's OK to do this check in release. We already have other runtime warnings in release. Want to fix that up if you have the time?

@kabiroberai
Copy link
Contributor Author

@mbrandonw so it looks like the runtimeWarn impl is guarded by #if DEBUG anyway; think we'll have to make the tests debug only to make them pass

@mbrandonw
Copy link
Member

@mbrandonw so it looks like the runtimeWarn impl is guarded by #if DEBUG anyway; think we'll have to make the tests debug only to make them pass

Sorry again, I should have looked into this more closely. You are right, runtimeWarn only causes test failures in DEBUG because we can't ship the dynamic XCTFail stuff in release code due to App Store rejections.

@mbrandonw
Copy link
Member

Finally got tests to pass 😅. Custom executors can't come soon enough.

@stephencelis and I will chat about this today and hopefully get it merged!

Copy link
Member

@stephencelis stephencelis left a comment

Choose a reason for hiding this comment

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

Looks good to me! Thanks for tackling this!

@mbrandonw mbrandonw merged commit cd22f6a into pointfreeco:main Feb 17, 2023
@kabiroberai kabiroberai deleted the send-escape-warning-rebased branch February 17, 2023 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants