Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
**Unreleased:**

- SwitchToLatest: finish cancelled collection while the latest channel or outer sequence remains open, and discard late producer results (https://github.com/sideeffect-io/AsyncExtensions/issues/53).
- Subjects: fix a deadlock when sending values or termination concurrently with consumer cancellation (https://github.com/sideeffect-io/AsyncExtensions/issues/52).
- Multicast: preserve upstream element order and prevent termination from overtaking values when consumers advance concurrently.

Expand Down
61 changes: 54 additions & 7 deletions Sources/Operators/AsyncSwitchToLatestSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
case processingChildIterator(Result<Base.Element.AsyncIterator, Error>)
case finished(Result<Base.Element.AsyncIterator, Error>?)
case failed(Error)
case cancelled

var isCancelled: Bool {
if case .cancelled = self {
return true
}
return false
}

var isFinished: Bool {
if case .finished = self {
Expand Down Expand Up @@ -94,10 +102,11 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
enum BaseDecision {
case resumeNext(UnsafeContinuation<Task<ChildValue?, Never>?, Never>, Task<ChildValue?, Never>?)
case cancelPreviousChildTask(Task<ChildValue?, Never>?)
case stop
}

enum NextDecision {
case immediatelyResume(Task<ChildValue?, Never>)
case immediatelyResume(Task<ChildValue?, Never>?)
case suspend
}

Expand Down Expand Up @@ -138,8 +147,10 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
self.baseTask = Task { [base, state] in
do {
for try await child in base {
guard !Task.isCancelled else { return }
let childIterator = child.makeAsyncIterator()
let decision = state.withCriticalRegion { state -> BaseDecision in
guard !state.base.isCancelled else { return .stop }
switch state.base {
case .waitingForChildIterator(let continuation):
state.base = .processingChildIterator(.success(childIterator))
Expand All @@ -153,6 +164,8 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
}

switch decision {
case .stop:
return
case .cancelPreviousChildTask(let task):
task?.cancel()
case .resumeNext(let continuation, let childTask):
Expand All @@ -161,6 +174,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
}

let decision = state.withCriticalRegion { state -> BaseDecision in
guard !state.base.isCancelled else { return .stop }
switch state.base {
case .waitingForChildIterator(let continuation):
state.base = .finished(nil)
Expand All @@ -172,13 +186,16 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
}

switch decision {
case .stop:
return
case .cancelPreviousChildTask:
break
case .resumeNext(let continuation, let childTask):
continuation.resume(returning: childTask)
}
} catch {
let decision = state.withCriticalRegion { state -> BaseDecision in
guard !state.base.isCancelled else { return .stop }
switch state.base {
case .waitingForChildIterator(let continuation):
state.base = .failed(error)
Expand All @@ -192,6 +209,8 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
}

switch decision {
case .stop:
return
case .cancelPreviousChildTask(let task):
task?.cancel()
case .resumeNext(let continuation, let childTask):
Expand All @@ -217,15 +236,43 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
}
}

static func cancel(baseTask: Task<Void, Never>?, state: ManagedCriticalState<State>) {
let cancellation = state.withCriticalRegion { state -> (
continuation: UnsafeContinuation<Task<ChildValue?, Never>?, Never>?,
childTask: Task<ChildValue?, Never>?
) in
let continuation: UnsafeContinuation<Task<ChildValue?, Never>?, Never>?
if case .waitingForChildIterator(let waiting) = state.base {
continuation = waiting
} else {
continuation = nil
}
let childTask = state.childTask
// Claim the waiter and discard retained iterators before invoking any cancellation handlers.
state.base = .cancelled
state.childTask = nil
return (continuation, childTask)
}

cancellation.continuation?.resume(returning: nil)
baseTask?.cancel()
cancellation.childTask?.cancel()
}

public mutating func next() async rethrows -> Element? {
guard !Task.isCancelled else { return nil }
guard !Task.isCancelled else {
Self.cancel(baseTask: self.baseTask, state: self.state)
return nil
}
self.startBase()

return try await withTaskCancellationHandler {
while true {
let childTask = await withUnsafeContinuation { [state] (continuation: UnsafeContinuation<Task<ChildValue?, Never>?, Never>) in
let decision = state.withCriticalRegion { state -> NextDecision in
switch state.base {
case .cancelled:
return .immediatelyResume(nil)
case .newChildIteratorAvailable(let childIterator):
state.base = .processingChildIterator(childIterator)
let childTask = Self.makeChildTask(childIterator: childIterator)
Expand Down Expand Up @@ -260,7 +307,10 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
let value = await childTask?.value

let decision = state.withCriticalRegion { state -> PostElementDecision in
if state.base.isNewAvailableChildIterator {
state.childTask = nil
if state.base.isCancelled {
return .returnFinish
} else if state.base.isNewAvailableChildIterator {
return .pass
} else {
switch value {
Expand Down Expand Up @@ -299,10 +349,7 @@ where Base.Element: AsyncSequence, Base: Sendable, Base.Element.Element: Sendabl
}
}
} onCancel: { [baseTask, state] in
baseTask?.cancel()
state.withCriticalRegion {
$0.childTask?.cancel()
}
Self.cancel(baseTask: baseTask, state: state)
}
}
}
Expand Down
Loading
Loading