Skip to content

Create AsyncQueue #2

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 15 commits into from
Nov 28, 2022
Merged

Create AsyncQueue #2

merged 15 commits into from
Nov 28, 2022

Conversation

dfed
Copy link
Owner

@dfed dfed commented Nov 23, 2022

This PR introduces the AsyncQueue class, which makes it possible to send ordered work to an asynchronous context from a synchronous or non-isolated context.

Any and all API feedback is welcome! Note that the name of the queue changes in #3, which builds upon this work.

@dfed dfed force-pushed the dfed--create-async-queue branch from 02d3b9d to 16e00d3 Compare November 23, 2022 05:39
// MARK: Private

private let streamTask: Task<Void, Never>
private let taskStreamContinuation: AsyncStream<@Sendable () async -> Void>.Continuation!
Copy link
Owner Author

Choose a reason for hiding this comment

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

Not ideal that we're force-unwrapping here. I originally made this type optional, but figured we really do want to fail LOUDLY if somehow we don't have a taskStreamContinuation and are attempting to append tasks to the queue.

Copy link
Collaborator

@Gabardone Gabardone Nov 23, 2022

Choose a reason for hiding this comment

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

Can we do the failing at the init method so we can initialize this with a non-optional?

Copy link
Owner Author

Choose a reason for hiding this comment

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

We can, but that would force every call-site to force unwrap, which seems pretty gnarly. Since taskStreamContinuation should exist (and it not existing would mean that the world is very broken), I think it's reasonable to crash if it somehow does not exist.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I meant failing in a very general case. Crashing is fine for something like this where the logic means we would never expect a nil value.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The point was to remove the exclamation mark from here (together with the changes in init proposed).

Copy link
Owner Author

Choose a reason for hiding this comment

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

How's this look? 6e0519c

Copy link
Owner Author

Choose a reason for hiding this comment

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

This is silly, but I'm going to use a force-unwrap and a comment instead of a fatalError. I want 100% coverage 😅

@codecov
Copy link

codecov bot commented Nov 23, 2022

Codecov Report

Merging #2 (a458835) into main (10aad91) will increase coverage by 97.50%.
The diff coverage is 97.50%.

❗ Current head a458835 differs from pull request most recent head 2d59b5a. Consider uploading reports for the commit 2d59b5a to get more accurate results

Impacted file tree graph

@@            Coverage Diff            @@
##           main       #2       +/-   ##
=========================================
+ Coverage      0   97.50%   +97.50%     
=========================================
  Files         0        1        +1     
  Lines         0       40       +40     
=========================================
+ Hits          0       39       +39     
- Misses        0        1        +1     
Impacted Files Coverage Δ
Sources/AsyncQueue/AsyncQueue.swift 97.50% <97.50%> (ø)

@@ -174,6 +174,9 @@ for rawPlatform in rawPlatforms {
if platform.shouldTest {
xcodeBuildArguments.append("test")
}
xcodeBuildArguments.append("-test-iterations")
xcodeBuildArguments.append("100")
xcodeBuildArguments.append("-run-tests-until-failure")
Copy link
Owner Author

Choose a reason for hiding this comment

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

This flag stops running tests once they fail. It respects the iterations limit above.

@dfed dfed mentioned this pull request Nov 23, 2022
}
}

func test_await_canReturn() async {
Copy link
Owner Author

Choose a reason for hiding this comment

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

This test and test_await_canThrow below are a little silly but I found them helpful for ensuring that the capability exists as I iterated on the queue's API.

@@ -26,8 +26,149 @@ import XCTest

final class AsyncQueueTests: XCTestCase {
Copy link
Owner Author

Choose a reason for hiding this comment

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

As you read through the below tests: please be thinking of cases I haven't thought of!

@dfed dfed requested a review from Gabardone November 23, 2022 06:06
@dfed dfed marked this pull request as ready for review November 23, 2022 06:06

A queue that enables ordered sending of events from synchronous to asynchronous code.

## Usage
Copy link
Owner Author

Choose a reason for hiding this comment

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

This is pretty freaking bare-bones but gotta start somewhere.

Co-authored-by: Óscar Morales Vivó <38818781+Gabardone@users.noreply.github.com>
Co-authored-by: Óscar Morales Vivó <38818781+Gabardone@users.noreply.github.com>
Copy link
Collaborator

@Gabardone Gabardone left a comment

Choose a reason for hiding this comment

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

See comments but overall looks like a nice thing to have and hopefully we don't have to complicate it much beyond this (at least until such time as Swift or its libraries adds better support for these use cases).

Copy link
Collaborator

@Gabardone Gabardone left a comment

Choose a reason for hiding this comment

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

See last comment but it's good to merge either way.

dfed and others added 2 commits November 23, 2022 06:56
Co-authored-by: Óscar Morales Vivó <38818781+Gabardone@users.noreply.github.com>
@dfed dfed force-pushed the dfed--create-async-queue branch 3 times, most recently from 07e704a to 0eb5698 Compare November 23, 2022 18:48
@dfed dfed force-pushed the dfed--create-async-queue branch from 0eb5698 to c6f0b50 Compare November 23, 2022 18:50
@dfed dfed force-pushed the dfed--create-async-queue branch from c6f0b50 to 02cf4e6 Compare November 23, 2022 18:50
@dfed dfed requested a review from bachand November 23, 2022 22:45
@dfed dfed force-pushed the dfed--create-async-queue branch from a458835 to ec8838a Compare November 24, 2022 15:08
Copy link
Collaborator

@Gabardone Gabardone left a comment

Choose a reason for hiding this comment

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

I think we ought to merge this already and then keep working on the other PR?

@dfed
Copy link
Owner Author

dfed commented Nov 28, 2022

I think we ought to merge this already and then keep working on the other PR?

Yup. Was giving more folk a chance to weigh in, but this repo is very beta (first published version will be 0.0.1!) and I'm happy to make breaking API changes for a bit.

Folk who come across this PR: please feel free to leave a post-merge review. I will respond, and will happily open up new PRs to address feedback. Given how early we are in the versioning, post-merge naming feedback is still appreciated!

@dfed dfed merged commit d2487e2 into main Nov 28, 2022
@dfed dfed deleted the dfed--create-async-queue branch November 28, 2022 01:12
Copy link
Collaborator

@bachand bachand left a comment

Choose a reason for hiding this comment

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

Nice!

@@ -159,7 +159,8 @@ for rawPlatform in rawPlatforms {
"-scheme", "swift-async-queue",
"-sdk", platform.sdk,
"-derivedDataPath", platform.derivedDataPath,
"-PBXBuildsContinueAfterErrors=0"
"-PBXBuildsContinueAfterErrors=0",
"OTHER_SWIFT_FLAGS=-warnings-as-errors",
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

@@ -1,5 +1,31 @@
# swift-async-queue
A queue that enables ordered sending of events from synchronous to asynchronous code
[![Swift Package Manager compatible](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://github.com/apple/swift-package-manager)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Screenshot 2022-11-28 at 9 45 55 AM

@dfed we seem to have an issue with some of the later badges. The screenshot is from the main page of the repo.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yup. I need to publish the pod before those badges will work. I'll remove them for now: 51e98a3

Comment on lines +35 to +36
// Continuation will be captured during stream creation, so it is safe to force unwrap here.
// If this force-unwrap fails, something is fundamentally broken in the Swift runtime.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👌

// If this force-unwrap fails, something is fundamentally broken in the Swift runtime.
taskStreamContinuation = capturedTaskStreamContinuation!

streamTask = Task.detached(priority: priority) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The standard Task initializer inherits the configuration from the calling context.
Screenshot 2022-11-28 at 9 50 40 AM

In this case I don't think the calling context has any configuration that we need/want to be inherited. So using Task.detached(…) seems reasonable 👍

Copy link
Owner Author

Choose a reason for hiding this comment

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

Exactly!

// MARK: Public

/// Schedules an asynchronous task for execution and immediately returns.
/// The schedueled task will not execute until all prior tasks have completed.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"schedueled" -> "scheduled"

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comment for other documentation comments in this file.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Good catch! 2bd88a1

await systemUnderTest.await { /* Drain the queue */ }
}

func test_async_retainsReceiverUntilFlushed() async {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the receiver in this context?

Copy link
Owner Author

Choose a reason for hiding this comment

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

The receiver is the systemUnderTest, i.e. the async queue.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah got it. I understand now 👍

await waitForExpectations(timeout: 1.0)
}

func test_async_doesNotRetainTaskAfterExecution() async {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice test!

Copy link
Owner Author

Choose a reason for hiding this comment

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

This was a fun one to write 😄

Comment on lines +140 to +143
guard iteration % 25 == 0 else {
// Keep sending async events to the queue.
continue
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't follow why this code exists in the test. Can you help me understand?

Copy link
Owner Author

Choose a reason for hiding this comment

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

I wanted some way to interleave multiple async events with each await event that was deterministic. We could await after every async, but because the await method requires waiting for the work to finish I felt like the test wouldn't necessarily catch an issue where await wouldn't wait for every enqueued event but would just wait for the first enqueued event...

Short version: this guard could be removed and it'd probably be fine. But I kinda like emitting multiple async events for every await check.

private actor Counter {
func incrementAndExpectCount(equals expectedCount: Int) {
increment()
XCTAssertEqual(expectedCount, count)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: would be nice to pass through the file and line number so any XCTest failure is easier to debug.

Copy link
Owner Author

Choose a reason for hiding this comment

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

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.

3 participants