-
Notifications
You must be signed in to change notification settings - Fork 155
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
Merge provides all elements from the subsequences on cancellation #276
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -201,6 +201,38 @@ final class TestMerge2: XCTestCase { | |
} | ||
t.cancel() | ||
} | ||
|
||
func testAsyncStreamElementsThatAreInjectedOnCancellationAreDelivered() async { | ||
let (stream1, continuation1) = AsyncStream.makeStream(of: Int.self) | ||
let (stream2, continuation2) = AsyncStream.makeStream(of: Int.self) | ||
continuation1.onTermination = { reason in | ||
XCTAssertEqual(reason, .cancelled) | ||
continuation1.yield(1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems a bit off; it is yielding a value AFTER it has transitioned to a terminal state? It would be perfectly reasonable for the stream to no longer produce values once it has moved to a terminal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually surprised by this behaviour as well and I don't know if this is intentional in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess that is perhaps the cooperative nature of cancellation - it eventually happens but may not happen immediately There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do agree that cancellation can be cooperative but having users rely on this behaviour seems rather brittle. Nothing in the API nor in the proposal specifies this behaviour. I will include this in the new back pressured interface proposal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is reasonable, that users have the option to yield something before cancellation. If it fits with current naming 🤷, but I think the feature in itself is very important. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree on the premise that user's should be able to Overall, not important to this PR but a general discussion for the forums. |
||
} | ||
continuation2.onTermination = { reason in | ||
XCTAssertEqual(reason, .cancelled) | ||
continuation2.yield(2) | ||
} | ||
continuation1.yield(0) // initial | ||
let merge = merge(stream1, stream2) | ||
let finished = expectation(description: "finished") | ||
let iterated = expectation(description: "iterated") | ||
let task = Task { | ||
var count = 0 | ||
for await _ in merge { | ||
if count == 0 { iterated.fulfill() } | ||
count += 1 | ||
} | ||
finished.fulfill() | ||
XCTAssertEqual(count, 3) | ||
} | ||
// ensure the other task actually starts | ||
await fulfillment(of: [iterated], timeout: 1.0) | ||
// cancellation should ensure the loop finishes | ||
// without regards to the remaining underlying sequence | ||
task.cancel() | ||
await fulfillment(of: [finished], timeout: 1.0) | ||
} | ||
} | ||
|
||
final class TestMerge3: XCTestCase { | ||
|
@@ -555,4 +587,41 @@ final class TestMerge3: XCTestCase { | |
|
||
iterator = nil | ||
} | ||
|
||
func testAsyncStreamElementsThatAreInjectedOnCancellationAreDelivered() async { | ||
let (stream1, continuation1) = AsyncStream.makeStream(of: Int.self) | ||
let (stream2, continuation2) = AsyncStream.makeStream(of: Int.self) | ||
let (stream3, continuation3) = AsyncStream.makeStream(of: Int.self) | ||
continuation1.onTermination = { reason in | ||
XCTAssertEqual(reason, .cancelled) | ||
continuation1.yield(1) | ||
} | ||
continuation2.onTermination = { reason in | ||
XCTAssertEqual(reason, .cancelled) | ||
continuation2.yield(2) | ||
} | ||
continuation3.onTermination = { reason in | ||
XCTAssertEqual(reason, .cancelled) | ||
continuation3.yield(3) | ||
} | ||
continuation1.yield(0) // initial | ||
let merge = merge(stream1, stream2, stream3) | ||
let finished = expectation(description: "finished") | ||
let iterated = expectation(description: "iterated") | ||
let task = Task { | ||
var count = 0 | ||
for await _ in merge { | ||
if count == 0 { iterated.fulfill() } | ||
count += 1 | ||
} | ||
finished.fulfill() | ||
XCTAssertEqual(count, 4) | ||
} | ||
// ensure the other task actually starts | ||
await fulfillment(of: [iterated], timeout: 1.0) | ||
// cancellation should ensure the loop finishes | ||
// without regards to the remaining underlying sequence | ||
task.cancel() | ||
await fulfillment(of: [finished], timeout: 1.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.
should this just be another state case?
if so it wouldn't need to hold onto the
Task
which means that the closure (which captures the bases) would no longer be held on to.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 make this another state case, we will have a lot of duplicated code that is shared between
merging
andcancelled
. In the interest in keeping code duplication minimal, IMO we should not create a separate case.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.
Agreed. I think we should just leave it with the current case