From d4e659debb1dd4afaddad043dbaa64a1c19d4149 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 19 Dec 2023 11:59:17 +0100 Subject: [PATCH 01/16] Add `AsyncBackpressuredStream` proposal and implementation # Motivation The pitch to add external backpressure support to the standard libraries `AsyncStream` got returned for revision since there are larger open questions around `AsyncSequence`. However, having external backpressure in a source asynchronous sequence is becoming more and more important. # Modification This PR adds a modified proposal and implementation that brings the Swift Evolution proposal over to Swift Async Algorithms. --- .../{NNNN-channel.md => 0012-channel.md} | 0 Evolution/{NNNN-chunk.md => 0013-chunk.md} | 0 ...NNN-rate-limits.md => 0014-rate-limits.md} | 0 ...{NNNN-reductions.md => 0015-reductions.md} | 0 ...-mutli-producer-single-consumer-channel.md | 652 ++++++++ .../AsyncAlgorithms/Internal/_TinyArray.swift | 329 ++++ ...oducerSingleConsumerChannel+Internal.swift | 1409 +++++++++++++++++ .../MultiProducerSingleConsumerChannel.swift | 489 ++++++ ...tiProducerSingleConsumerChannelTests.swift | 1041 ++++++++++++ 9 files changed, 3920 insertions(+) rename Evolution/{NNNN-channel.md => 0012-channel.md} (100%) rename Evolution/{NNNN-chunk.md => 0013-chunk.md} (100%) rename Evolution/{NNNN-rate-limits.md => 0014-rate-limits.md} (100%) rename Evolution/{NNNN-reductions.md => 0015-reductions.md} (100%) create mode 100644 Evolution/0016-mutli-producer-single-consumer-channel.md create mode 100644 Sources/AsyncAlgorithms/Internal/_TinyArray.swift create mode 100644 Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift create mode 100644 Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift create mode 100644 Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift diff --git a/Evolution/NNNN-channel.md b/Evolution/0012-channel.md similarity index 100% rename from Evolution/NNNN-channel.md rename to Evolution/0012-channel.md diff --git a/Evolution/NNNN-chunk.md b/Evolution/0013-chunk.md similarity index 100% rename from Evolution/NNNN-chunk.md rename to Evolution/0013-chunk.md diff --git a/Evolution/NNNN-rate-limits.md b/Evolution/0014-rate-limits.md similarity index 100% rename from Evolution/NNNN-rate-limits.md rename to Evolution/0014-rate-limits.md diff --git a/Evolution/NNNN-reductions.md b/Evolution/0015-reductions.md similarity index 100% rename from Evolution/NNNN-reductions.md rename to Evolution/0015-reductions.md diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md new file mode 100644 index 00000000..596a1ccc --- /dev/null +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -0,0 +1,652 @@ +# MutliProducerSingleConsumerChannel + +* Proposal: [SAA-0016](0016-multi-producer-single-consumer-channel.md) +* Authors: [Franz Busch](https://github.com/FranzBusch) +* Review Manager: TBD +* Status: **Implemented** + +## Revision +- 2023/12/18: Migrate proposal from Swift Evolution to Swift Async Algorithms. +- 2023/12/19: Add element size dependent strategy +- 2024/05/19: Rename to multi producer single consumer channel +- 2024/05/28: Add unbounded strategy + +## Introduction + +[SE-0314](https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md) +introduced new `Async[Throwing]Stream` types which act as root asynchronous +sequences. These two types allow bridging from synchronous callbacks such as +delegates to an asynchronous sequence. This proposal adds a new root +asynchronous sequence with the goal to bridge multi producer systems +into an asynchronous sequence. + +## Motivation + +After using the `AsyncSequence` protocol and the `Async[Throwing]Stream` types +extensively over the past years, we learned that there are a few important +behavioral details that any `AsyncSequence` implementation needs to support. +These behaviors are: + +1. Backpressure +2. Multi/single consumer support +3. Downstream consumer termination +4. Upstream producer termination + +In general, `AsyncSequence` implementations can be divided into two kinds: Root +asynchronous sequences that are the source of values such as +`Async[Throwing]Stream` and transformational asynchronous sequences such as +`AsyncMapSequence`. Most transformational asynchronous sequences implicitly +fulfill the above behaviors since they forward any demand to a base asynchronous +sequence that should implement the behaviors. On the other hand, root +asynchronous sequences need to make sure that all of the above behaviors are +correctly implemented. Let's look at the current behavior of +`Async[Throwing]Stream` to see if and how it achieves these behaviors. + +### Backpressure + +Root asynchronous sequences need to relay the backpressure to the producing +system. `Async[Throwing]Stream` aims to support backpressure by providing a +configurable buffer and returning +`Async[Throwing]Stream.Continuation.YieldResult` which contains the current +buffer depth from the `yield()` method. However, only providing the current +buffer depth on `yield()` is not enough to bridge a backpressured system into +an asynchronous sequence since this can only be used as a "stop" signal but we +are missing a signal to indicate resuming the production. The only viable +backpressure strategy that can be implemented with the current API is a timed +backoff where we stop producing for some period of time and then speculatively +produce again. This is a very inefficient pattern that produces high latencies +and inefficient use of resources. + +### Multi/single consumer support + +The `AsyncSequence` protocol itself makes no assumptions about whether the +implementation supports multiple consumers or not. This allows the creation of +unicast and multicast asynchronous sequences. The difference between a unicast +and multicast asynchronous sequence is if they allow multiple iterators to be +created. `AsyncStream` does support the creation of multiple iterators and it +does handle multiple consumers correctly. On the other hand, +`AsyncThrowingStream` also supports multiple iterators but does `fatalError` +when more than one iterator has to suspend. The original proposal states: + +> As with any sequence, iterating over an AsyncStream multiple times, or +creating multiple iterators and iterating over them separately, may produce an +unexpected series of values. + +While that statement leaves room for any behavior we learned that a clear distinction +of behavior for root asynchronous sequences is beneficial; especially, when it comes to +how transformation algorithms are applied on top. + +### Downstream consumer termination + +Downstream consumer termination allows the producer to notify the consumer that +no more values are going to be produced. `Async[Throwing]Stream` does support +this by calling the `finish()` or `finish(throwing:)` methods of the +`Async[Throwing]Stream.Continuation`. However, `Async[Throwing]Stream` does not +handle the case that the `Continuation` may be `deinit`ed before one of the +finish methods is called. This currently leads to async streams that never +terminate. The behavior could be changed but it could result in semantically +breaking code. + +### Upstream producer termination + +Upstream producer termination is the inverse of downstream consumer termination +where the producer is notified once the consumption has terminated. Currently, +`Async[Throwing]Stream` does expose the `onTermination` property on the +`Continuation`. The `onTermination` closure is invoked once the consumer has +terminated. The consumer can terminate in four separate cases: + +1. The asynchronous sequence was `deinit`ed and no iterator was created +2. The iterator was `deinit`ed and the asynchronous sequence is unicast +3. The consuming task is canceled +4. The asynchronous sequence returned `nil` or threw + +`Async[Throwing]Stream` currently invokes `onTermination` in all cases; however, +since `Async[Throwing]Stream` supports multiple consumers (as discussed in the +`Multi/single consumer support` section), a single consumer task being canceled +leads to the termination of all consumers. This is not expected from multicast +asynchronous sequences in general. + +## Proposed solution + +The above motivation lays out the expected behaviors from a root asynchronous +sequence and compares them to the behaviors of `Async[Throwing]Stream`. These +are the behaviors where `Async[Throwing]Stream` diverges from the expectations. + +- Backpressure: Doesn't expose a "resumption" signal to the producer +- Multi/single consumer: + - Divergent implementation between throwing and non-throwing variant + - Supports multiple consumers even though proposal positions it as a unicast + asynchronous sequence +- Consumer termination: Doesn't handle the `Continuation` being `deinit`ed +- Producer termination: Happens on first consumer termination + +This section proposes a new type called `MutliProducerSingleConsumerChannel` that implement all of +the above-mentioned behaviors. + +### Creating an MutliProducerSingleConsumerChannel + +You can create an `MutliProducerSingleConsumerChannel` instance using the new +`makeChannel(of: backpressureStrategy:)` method. This method returns you the +channel and the source. The source can be used to send new values to the +asynchronous channel. The new API specifically provides a +multi-producer/single-consumer pattern. + +```swift +let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +``` + +The new proposed APIs offer three different ways to bridge a backpressured +system. The foundation is the multi-step synchronous interface. Below is an +example of how it can be used: + +```swift +do { + let sendResult = try source.send(contentsOf: sequence) + + switch sendResult { + case .produceMore: + // Trigger more production + + case .enqueueCallback(let callbackToken): + source.enqueueCallback(token: callbackToken, onProduceMore: { result in + switch result { + case .success: + // Trigger more production + case .failure(let error): + // Terminate the underlying producer + } + }) + } +} catch { + // `send(contentsOf:)` throws if the asynchronous stream already terminated +} +``` + +The above API offers the most control and highest performance when bridging a +synchronous producer to an asynchronous sequence. First, you have to send +values using the `send(contentsOf:)` which returns a `SendResult`. The result +either indicates that more values should be produced or that a callback should +be enqueued by calling the `enqueueCallback(callbackToken: onProduceMore:)` +method. This callback is invoked once the backpressure strategy decided that +more values should be produced. This API aims to offer the most flexibility with +the greatest performance. The callback only has to be allocated in the case +where the producer needs to be suspended. + +Additionally, the above API is the building block for some higher-level and +easier-to-use APIs to send values to the channel. Below is an +example of the two higher-level APIs. + +```swift +// Writing new values and providing a callback when to produce more +try source.send(contentsOf: sequence, onProduceMore: { result in + switch result { + case .success: + // Trigger more production + case .failure(let error): + // Terminate the underlying producer + } +}) + +// This method suspends until more values should be produced +try await source.send(contentsOf: sequence) +``` + +With the above APIs, we should be able to effectively bridge any system into an +asynchronous sequence regardless if the system is callback-based, blocking or +asynchronous. + +### Downstream consumer termination + +> When reading the next two examples around termination behaviour keep in mind +that the newly proposed APIs are providing a strict unicast asynchronous sequence. + +Calling `finish()` terminates the downstream consumer. Below is an example of +this: + +```swift +// Termination through calling finish +let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) + +_ = try await source.send(1) +source.finish() + +for try await element in channel { + print(element) +} +print("Finished") + +// Prints +// 1 +// Finished +``` + +The other way to terminate the consumer is by deiniting the source. This has the +same effect as calling `finish()` and makes sure that no consumer is stuck +indefinitely. + +```swift +// Termination through deiniting the source +let (channel, _) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) + +for await element in channel { + print(element) +} +print("Finished") + +// Prints +// Finished +``` + +Trying to send more elements after the source has been finish will result in an +error thrown from the send methods. + +### Upstream producer termination + +The producer will get notified about termination through the `onTerminate` +callback. Termination of the producer happens in the following scenarios: + +```swift +// Termination through task cancellation +let (channel source) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) + +let task = Task { + for await element in channel { + + } +} +task.cancel() +``` + +```swift +// Termination through deiniting the sequence +let (_, source) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +``` + +```swift +// Termination through deiniting the iterator +let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +_ = channel.makeAsyncIterator() +``` + +```swift +// Termination through calling finish +let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) + +_ = try source.send(1) +source.finish() + +for await element in channel {} + +// onTerminate will be called after all elements have been consumed +``` + +Similar to the downstream consumer termination, trying to send more elements after the +producer has been terminated will result in an error thrown from the send methods. + +## Detailed design + +```swift +/// An error that is thrown from the various `send` methods of the +/// ``MultiProducerSingleConsumerChannel/Source``. +/// +/// This error is thrown when the channel is already finished when +/// trying to send new elements to the source. +public struct MultiProducerSingleConsumerChannelAlreadyFinishedError : Error { + + @usableFromInline + internal init() +} + +/// A multi producer single consumer channel. +/// +/// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to +/// send values to the channel. The source exposes the internal backpressure of the asynchronous sequence to the +/// producer. Additionally, the source can be used from synchronous and asynchronous contexts. +/// +/// +/// ## Using a MultiProducerSingleConsumerChannel +/// +/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with it's source first by calling +/// the ``MultiProducerSingleConsumerChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. +/// Afterwards, you can pass the source to the producer and the channel to the consumer. +/// +/// ``` +/// let (channel, source) = MultiProducerSingleConsumerChannel.makeChannel( +/// backpressureStrategy: .watermark(low: 2, high: 4) +/// ) +/// ``` +/// +/// ### Asynchronous producers +/// +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-9b5do`` +/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)-4myrz``. Backpressure results in calls +/// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. +/// +/// ``` +/// try await withThrowingTaskGroup(of: Void.self) { group in +/// group.addTask { +/// try await source.send(1) +/// try await source.send(2) +/// try await source.send(3) +/// } +/// +/// for await element in channel { +/// print(element) +/// } +/// } +/// ``` +/// +/// ### Synchronous producers +/// +/// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, +/// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. +/// +/// ## Finishing the source +/// +/// To properly notify the consumer if the production of values has been finished the source's ``MultiProducerSingleConsumerChannel/Source/finish(throwing:)`` **must** be called. +public struct MultiProducerSingleConsumerChannel: AsyncSequence { + /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. + /// + /// - Parameters: + /// - elementType: The element type of the channel. + /// - failureType: The failure type of the channel. + /// - BackpressureStrategy: The backpressure strategy that the channel should use. + /// - Returns: A tuple containing the channel and its source. The source should be passed to the + /// producer while the channel should be passed to the consumer. + public static func makeChannel(of elementType: Element.Type = Element.self, throwing failureType: Failure.Type = Never.self, backpressureStrategy: Source.BackpressureStrategy) -> (`Self`, Source) +} + +extension MultiProducerSingleConsumerChannel { + /// A struct to send values to the channel. + /// + /// Use this source to provide elements to the channel by calling one of the `send` methods. + /// + /// - Important: You must terminate the source by calling ``finish(throwing:)``. + public struct Source: Sendable { + /// A strategy that handles the backpressure of the channel. + public struct BackpressureStrategy: Sendable { + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + public static func watermark(low: Int, high: Int) -> BackpressureStrategy + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. + /// + /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when + /// it is consumed from the channel, so it is recommended to provide an function that runs in constant time. + public static func watermark(low: Int, high: Int, waterLevelForElement: @escaping @Sendable (Element) -> Int) -> BackpressureStrategy + } + + /// A type that indicates the result of sending elements to the source. + public enum SendResult: Sendable { + /// A token that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. + public struct CallbackToken: Sendable { } + + /// Indicates that more elements should be produced and written to the source. + case produceMore + + /// Indicates that a callback should be enqueued. + /// + /// The associated token should be passed to the ``enqueueCallback(_:)`` method. + case enqueueCallback(CallbackToken) + } + + /// A callback to invoke when the channel finished. + /// + /// The channel finishes and calls this closure in the following cases: + /// - No iterator was created and the sequence was deinited + /// - An iterator was created and deinited + /// - After ``finish(throwing:)`` was called and all elements have been consumed + public var onTermination: (@Sendable () -> Void)? { get set } + + /// Sends new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter sequence: The elements to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + public func send(contentsOf sequence: S) throws -> SendResult where Element == S.Element, S : Sequence + + /// Send the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter element: The element to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + public func send(_ element: Element) throws -> SendResult + + /// Enqueues a callback that will be invoked once more elements should be produced. + /// + /// Call this method after ``send(contentsOf:)-5honm`` or ``send(_:)-3jxzb`` returned ``SendResult/enqueueCallback(_:)``. + /// + /// - Important: Enqueueing the same token multiple times is not allowed. + /// + /// - Parameters: + /// - callbackToken: The callback token. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. + public func enqueueCallback(callbackToken: consuming SendResult.CallbackToken, onProduceMore: @escaping @Sendable (Result) -> Void) + + /// Cancel an enqueued callback. + /// + /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and + /// will mark the passed `callbackToken` as cancelled. + /// + /// - Parameter callbackToken: The callback token. + public func cancelCallback(callbackToken: consuming SendResult.CallbackToken) + + /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(contentsOf:onProduceMore:)``. + public func send(contentsOf sequence: S, onProduceMore: @escaping @Sendable (Result) -> Void) where Element == S.Element, S : Sequence + + /// Sends the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - element: The element to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(_:onProduceMore:)``. + public func send(_ element: Element, onProduceMore: @escaping @Sendable (Result) -> Void) + + /// Send new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + public func send(contentsOf sequence: S) async throws where Element == S.Element, S : Sequence + + /// Send new element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - element: The element to send to the channel. + public func send(_ element: Element) async throws + + /// Send the elements of the asynchronous sequence to the channel. + /// + /// This method returns once the provided asynchronous sequence or the channel finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + public func send(contentsOf sequence: S) async throws where Element == S.Element, S : AsyncSequence + + /// Indicates that the production terminated. + /// + /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. + /// + /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept + /// new elements. + /// + /// - Parameters: + /// - error: The error to throw, or `nil`, to finish normally. + public func finish(throwing error: Failure? = nil) + } +} + +extension MultiProducerSingleConsumerChannel { + /// The asynchronous iterator for iterating the channel. + /// + /// This type is not `Sendable`. Don't use it from multiple + /// concurrent contexts. It is a programmer error to invoke `next()` from a + /// concurrent context that contends with another such call, which + /// results in a call to `fatalError()`. + public struct Iterator: AsyncIteratorProtocol {} + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + public func makeAsyncIterator() -> Iterator +} + +extension MultiProducerSingleConsumerChannel: Sendable where Element : Sendable {} +``` + +## Comparison to other root asynchronous sequences + +### swift-async-algorithm: AsyncChannel + +The `AsyncChannel` is a multi-consumer/multi-producer root asynchronous sequence +which can be used to communicate between two tasks. It only offers asynchronous +production APIs and has no internal buffer. This means that any producer will be +suspended until its value has been consumed. `AsyncChannel` can handle multiple +consumers and resumes them in FIFO order. + +### swift-nio: NIOAsyncSequenceProducer + +The NIO team have created their own root asynchronous sequence with the goal to +provide a high performance sequence that can be used to bridge a NIO `Channel` +inbound stream into Concurrency. The `NIOAsyncSequenceProducer` is a highly +generic and fully inlinable type and quite unwiedly to use. This proposal is +heavily inspired by the learnings from this type but tries to create a more +flexible and easier to use API that fits into the standard library. + +## Future directions + +### Adaptive backpressure strategy + +The high/low watermark strategy is common in networking code; however, there are +other strategies such as an adaptive strategy that we could offer in the future. +An adaptive strategy regulates the backpressure based on the rate of +consumption and production. With the proposed new APIs we can easily add further +strategies. + +## Alternatives considered + +### Provide the `onTermination` callback to the factory method + +During development of the new APIs, I first tried to provide the `onTermination` +callback in the `makeStream` method. However, that showed significant usability +problems in scenarios where one wants to store the source in a type and +reference `self` in the `onTermination` closure at the same time; hence, I kept +the current pattern of setting the `onTermination` closure on the source. + +### Provide a `onConsumerCancellation` callback + +During the pitch phase, it was raised that we should provide a +`onConsumerCancellation` callback which gets invoked once the asynchronous +stream notices that the consuming task got cancelled. This callback could be +used to customize how cancellation is handled by the stream e.g. one could +imagine writing a few more elements to the stream before finishing it. Right now +the stream immediately returns `nil` or throws a `CancellationError` when it +notices cancellation. This proposal decided to not provide this customization +because it opens up the possiblity that asynchronous streams are not terminating +when implemented incorrectly. Additionally, asynchronous sequences are not the +only place where task cancellation leads to an immediate error being thrown i.e. +`Task.sleep()` does the same. Hence, the value of the asynchronous not +terminating immediately brings little value when the next call in the iterating +task might throw. However, the implementation is flexible enough to add this in +the future and we can just default it to the current behaviour. + +### Create a custom type for the `Result` of the `onProduceMore` callback + +The `onProducerMore` callback takes a `Result` which is used to +indicate if the producer should produce more or if the asynchronous stream +finished. We could introduce a new type for this but the proposal decided +against it since it effectively is a result type. + +### Use an initializer instead of factory methods + +Instead of providing a `makeStream` factory method we could use an initializer +approach that takes a closure which gets the `Source` passed into. A similar API +has been offered with the `Continuation` based approach and +[SE-0388](https://github.com/apple/swift-evolution/blob/main/proposals/0388-async-stream-factory.md) +introduced new factory methods to solve some of the usability ergonomics with +the initializer based APIs. + +### Follow the `AsyncStream` & `AsyncThrowingStream` naming + +All other types that offer throwing and non-throwing variants are currently +following the naming scheme where the throwing variant gets an extra `Throwing` +in its name. Now that Swift is gaining typed throws support this would make the +type with the `Failure` parameter capable to express both throwing and +non-throwing variants. However, the less flexible type has the better name. +Hence, this proposal uses the good name for the throwing variant with the +potential in the future to deprecate the `AsyncNonThrowingBackpressuredStream` +in favour of adopting typed throws. + +## Acknowledgements + +- [Johannes Weiss](https://github.com/weissi) - For making me aware how +important this problem is and providing great ideas on how to shape the API. +- [Philippe Hausler](https://github.com/phausler) - For helping me designing the +APIs and continuously providing feedback +- [George Barnett](https://github.com/glbrntt) - For providing extensive code +reviews and testing the implementation. +- [Si Beaumont](https://github.com/simonjbeaumont) - For implementing the element size dependent strategy diff --git a/Sources/AsyncAlgorithms/Internal/_TinyArray.swift b/Sources/AsyncAlgorithms/Internal/_TinyArray.swift new file mode 100644 index 00000000..07357ccb --- /dev/null +++ b/Sources/AsyncAlgorithms/Internal/_TinyArray.swift @@ -0,0 +1,329 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCertificates open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftCertificates project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCertificates project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// ``_TinyArray`` is a ``RandomAccessCollection`` optimised to store zero or one ``Element``. +/// It supports arbitrary many elements but if only up to one ``Element`` is stored it does **not** allocate separate storage on the heap +/// and instead stores the ``Element`` inline. +@usableFromInline +struct _TinyArray { + @usableFromInline + enum Storage { + case one(Element) + case arbitrary([Element]) + } + + @usableFromInline + var storage: Storage +} + +// MARK: - TinyArray "public" interface + +extension _TinyArray: Equatable where Element: Equatable {} +extension _TinyArray: Hashable where Element: Hashable {} +extension _TinyArray: Sendable where Element: Sendable {} + +extension _TinyArray: RandomAccessCollection { + @usableFromInline + typealias Element = Element + + @usableFromInline + typealias Index = Int + + @inlinable + subscript(position: Int) -> Element { + get { + self.storage[position] + } + set { + self.storage[position] = newValue + } + } + + @inlinable + var startIndex: Int { + self.storage.startIndex + } + + @inlinable + var endIndex: Int { + self.storage.endIndex + } +} + +extension _TinyArray { + @inlinable + init(_ elements: some Sequence) { + self.storage = .init(elements) + } + + @inlinable + init() { + self.storage = .init() + } + + @inlinable + mutating func append(_ newElement: Element) { + self.storage.append(newElement) + } + + @inlinable + mutating func append(contentsOf newElements: some Sequence) { + self.storage.append(contentsOf: newElements) + } + + @discardableResult + @inlinable + mutating func remove(at index: Int) -> Element { + self.storage.remove(at: index) + } + + @inlinable + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + try self.storage.removeAll(where: shouldBeRemoved) + } + + @inlinable + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + try self.storage.sort(by: areInIncreasingOrder) + } +} + +// MARK: - TinyArray.Storage "private" implementation + +extension _TinyArray.Storage: Equatable where Element: Equatable { + @inlinable + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.one(let lhs), .one(let rhs)): + return lhs == rhs + case (.arbitrary(let lhs), .arbitrary(let rhs)): + // we don't use lhs.elementsEqual(rhs) so we can hit the fast path from Array + // if both arrays share the same underlying storage: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1775 + return lhs == rhs + + case (.one(let element), .arbitrary(let array)), + (.arbitrary(let array), .one(let element)): + guard array.count == 1 else { + return false + } + return element == array[0] + + } + } +} +extension _TinyArray.Storage: Hashable where Element: Hashable { + @inlinable + func hash(into hasher: inout Hasher) { + // same strategy as Array: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1801 + hasher.combine(count) + for element in self { + hasher.combine(element) + } + } +} +extension _TinyArray.Storage: Sendable where Element: Sendable {} + +extension _TinyArray.Storage: RandomAccessCollection { + @inlinable + subscript(position: Int) -> Element { + get { + switch self { + case .one(let element): + guard position == 0 else { + fatalError("index \(position) out of bounds") + } + return element + case .arbitrary(let elements): + return elements[position] + } + } + set { + switch self { + case .one: + guard position == 0 else { + fatalError("index \(position) out of bounds") + } + self = .one(newValue) + case .arbitrary(var elements): + elements[position] = newValue + self = .arbitrary(elements) + } + } + } + + @inlinable + var startIndex: Int { + 0 + } + + @inlinable + var endIndex: Int { + switch self { + case .one: return 1 + case .arbitrary(let elements): return elements.endIndex + } + } +} + +extension _TinyArray.Storage { + @inlinable + init(_ elements: some Sequence) { + var iterator = elements.makeIterator() + guard let firstElement = iterator.next() else { + self = .arbitrary([]) + return + } + guard let secondElement = iterator.next() else { + // newElements just contains a single element + // and we hit the fast path + self = .one(firstElement) + return + } + + var elements: [Element] = [] + elements.reserveCapacity(elements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + while let nextElement = iterator.next() { + elements.append(nextElement) + } + self = .arbitrary(elements) + } + + @inlinable + init() { + self = .arbitrary([]) + } + + @inlinable + mutating func append(_ newElement: Element) { + self.append(contentsOf: CollectionOfOne(newElement)) + } + + @inlinable + mutating func append(contentsOf newElements: some Sequence) { + switch self { + case .one(let firstElement): + var iterator = newElements.makeIterator() + guard let secondElement = iterator.next() else { + // newElements is empty, nothing to do + return + } + var elements: [Element] = [] + elements.reserveCapacity(1 + newElements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + elements.appendRemainingElements(from: &iterator) + self = .arbitrary(elements) + + case .arbitrary(var elements): + if elements.isEmpty { + // if `self` is currently empty and `newElements` just contains a single + // element, we skip allocating an array and set `self` to `.one(firstElement)` + var iterator = newElements.makeIterator() + guard let firstElement = iterator.next() else { + // newElements is empty, nothing to do + return + } + guard let secondElement = iterator.next() else { + // newElements just contains a single element + // and we hit the fast path + self = .one(firstElement) + return + } + elements.reserveCapacity(elements.count + newElements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + elements.appendRemainingElements(from: &iterator) + self = .arbitrary(elements) + + } else { + elements.append(contentsOf: newElements) + self = .arbitrary(elements) + } + + } + } + + @discardableResult + @inlinable + mutating func remove(at index: Int) -> Element { + switch self { + case .one(let oldElement): + guard index == 0 else { + fatalError("index \(index) out of bounds") + } + self = .arbitrary([]) + return oldElement + + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + return elements.remove(at: index) + + } + } + + @inlinable + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + switch self { + case .one(let oldElement): + if try shouldBeRemoved(oldElement) { + self = .arbitrary([]) + } + + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + return try elements.removeAll(where: shouldBeRemoved) + + } + } + + @inlinable + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + switch self { + case .one: + // a collection of just one element is always sorted, nothing to do + break + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + + try elements.sort(by: areInIncreasingOrder) + } + } +} + +extension Array { + @inlinable + mutating func appendRemainingElements(from iterator: inout some IteratorProtocol) { + while let nextElement = iterator.next() { + append(nextElement) + } + } +} diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift new file mode 100644 index 00000000..58b41ae1 --- /dev/null +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift @@ -0,0 +1,1409 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.0) +import DequeModule + +extension MultiProducerSingleConsumerChannel { + @usableFromInline + enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { + @usableFromInline + struct _Watermark: Sendable, CustomStringConvertible { + /// The low watermark where demand should start. + @usableFromInline + let _low: Int + + /// The high watermark where demand should be stopped. + @usableFromInline + let _high: Int + + /// The current watermark level. + @usableFromInline + var _currentWatermark: Int = 0 + + /// A closure that can be used to calculate the watermark impact of a single element + @usableFromInline + let _waterLevelForElement: (@Sendable (Element) -> Int)? + + @usableFromInline + var description: String { + "watermark(\(self._currentWatermark))" + } + + init(low: Int, high: Int, waterLevelForElement: (@Sendable (Element) -> Int)?) { + precondition(low <= high) + self._low = low + self._high = high + self._waterLevelForElement = waterLevelForElement + } + + @inlinable + mutating func didSend(elements: Deque.SubSequence) -> Bool { + if let waterLevelForElement = self._waterLevelForElement { + self._currentWatermark += elements.reduce(0) { $0 + waterLevelForElement($1) } + } else { + self._currentWatermark += elements.count + } + precondition(self._currentWatermark >= 0) + // We are demanding more until we reach the high watermark + return self._currentWatermark < self._high + } + + @inlinable + mutating func didConsume(element: Element) -> Bool { + if let waterLevelForElement = self._waterLevelForElement { + self._currentWatermark -= waterLevelForElement(element) + } else { + self._currentWatermark -= 1 + } + precondition(self._currentWatermark >= 0) + // We start demanding again once we are below the low watermark + return self._currentWatermark < self._low + } + } + + @usableFromInline + struct _Unbounded: Sendable, CustomStringConvertible { + @usableFromInline + var description: String { + return "unbounded" + } + + init() { } + + @inlinable + mutating func didSend(elements: Deque.SubSequence) -> Bool { + return true + } + + @inlinable + mutating func didConsume(element: Element) -> Bool { + return true + } + } + + /// A watermark based strategy. + case watermark(_Watermark) + /// An unbounded based strategy. + case unbounded(_Unbounded) + + @usableFromInline + var description: String { + switch consume self { + case .watermark(let strategy): + return strategy.description + case .unbounded(let unbounded): + return unbounded.description + } + } + + @inlinable + mutating func didSend(elements: Deque.SubSequence) -> Bool { + switch consume self { + case .watermark(var strategy): + let result = strategy.didSend(elements: elements) + self = .watermark(strategy) + return result + case .unbounded(var strategy): + let result = strategy.didSend(elements: elements) + self = .unbounded(strategy) + return result + } + } + + @inlinable + mutating func didConsume(element: Element) -> Bool { + switch consume self { + case .watermark(var strategy): + let result = strategy.didConsume(element: element) + self = .watermark(strategy) + return result + case .unbounded(var strategy): + let result = strategy.didConsume(element: element) + self = .unbounded(strategy) + return result + } + } + } +} + +extension MultiProducerSingleConsumerChannel { + @usableFromInline + final class _Storage { + @usableFromInline + let _lock = Lock.allocate() + /// The state machine + @usableFromInline + var _stateMachine: _StateMachine + + var onTermination: (@Sendable () -> Void)? { + set { + self._lock.withLockVoid { + self._stateMachine._onTermination = newValue + } + } + get { + self._lock.withLock { + self._stateMachine._onTermination + } + } + } + + init( + backpressureStrategy: _InternalBackpressureStrategy + ) { + self._stateMachine = .init(backpressureStrategy: backpressureStrategy) + } + + func sequenceDeinitialized() { + let action = self._lock.withLock { + self._stateMachine.sequenceDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + onTermination?() + + case .none: + break + } + } + + func iteratorInitialized() { + self._lock.withLockVoid { + self._stateMachine.iteratorInitialized() + } + } + + func iteratorDeinitialized() { + let action = self._lock.withLock { + self._stateMachine.iteratorDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + onTermination?() + + case .none: + break + } + } + + func sourceDeinitialized() { + let action = self._lock.withLock { + self._stateMachine.sourceDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + onTermination?() + + case .none: + break + } + } + + @inlinable + func send( + contentsOf sequence: some Sequence + ) throws -> MultiProducerSingleConsumerChannel.Source.SendResult { + let action = self._lock.withLock { + return self._stateMachine.send(sequence) + } + + switch action { + case .returnProduceMore: + return .produceMore + + case .returnEnqueue(let callbackToken): + return .enqueueCallback(.init(id: callbackToken)) + + case .resumeConsumerAndReturnProduceMore(let continuation, let element): + continuation.resume(returning: element) + return .produceMore + + case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): + continuation.resume(returning: element) + return .enqueueCallback(.init(id: callbackToken)) + + case .throwFinishedError: + throw MultiProducerSingleConsumerChannelAlreadyFinishedError() + } + } + + @inlinable + func enqueueProducer( + callbackToken: UInt64, + continuation: UnsafeContinuation + ) { + let action = self._lock.withLock { + self._stateMachine.enqueueContinuation(callbackToken: callbackToken, continuation: continuation) + } + + switch action { + case .resumeProducer(let continuation): + continuation.resume() + + case .resumeProducerWithError(let continuation, let error): + continuation.resume(throwing: error) + + case .none: + break + } + } + + @inlinable + func enqueueProducer( + callbackToken: UInt64, + onProduceMore: sending @escaping (Result) -> Void + ) { + let action = self._lock.withLock { + self._stateMachine.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + + switch action { + case .resumeProducer(let onProduceMore): + onProduceMore(Result.success(())) + + case .resumeProducerWithError(let onProduceMore, let error): + onProduceMore(Result.failure(error)) + + case .none: + break + } + } + + @inlinable + func cancelProducer( + callbackToken: UInt64 + ) { + let action = self._lock.withLock { + self._stateMachine.cancelProducer(callbackToken: callbackToken) + } + + switch action { + case .resumeProducerWithCancellationError(let onProduceMore): + switch onProduceMore { + case .closure(let onProduceMore): + onProduceMore(.failure(CancellationError())) + case .continuation(let continuation): + continuation.resume(throwing: CancellationError()) + } + + case .none: + break + } + } + + @inlinable + func finish(_ failure: Failure?) { + let action = self._lock.withLock { + self._stateMachine.finish(failure) + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .resumeConsumerAndCallOnTermination(let consumerContinuation, let failure, let onTermination): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) + } + + onTermination?() + + case .resumeProducers(let producerContinuations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + + case .none: + break + } + } + + @inlinable + func next(isolation actor: isolated (any Actor)?) async throws -> Element? { + let action = self._lock.withLock { + self._stateMachine.next() + } + + switch action { + case .returnElement(let element): + return element + + case .returnElementAndResumeProducers(let element, let producerContinuations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.success(())) + case .continuation(let continuation): + continuation.resume() + } + } + + return element + + case .returnFailureAndCallOnTermination(let failure, let onTermination): + onTermination?() + switch failure { + case .some(let error): + throw error + + case .none: + return nil + } + + case .returnNil: + return nil + + case .suspendTask: + return try await self.suspendNext(isolation: actor) + } + } + + @inlinable + func suspendNext(isolation actor: isolated (any Actor)?) async throws -> Element? { + return try await withTaskCancellationHandler { + return try await withUnsafeThrowingContinuation { continuation in + let action = self._lock.withLock { + self._stateMachine.suspendNext(continuation: continuation) + } + + switch action { + case .resumeConsumerWithElement(let continuation, let element): + continuation.resume(returning: element) + + case .resumeConsumerWithElementAndProducers(let continuation, let element, let producerContinuations): + continuation.resume(returning: element) + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(CancellationError())) + case .continuation(let continuation): + continuation.resume() + } + } + + case .resumeConsumerWithFailureAndCallOnTermination(let continuation, let failure, let onTermination): + switch failure { + case .some(let error): + continuation.resume(throwing: error) + + case .none: + continuation.resume(returning: nil) + } + onTermination?() + + case .resumeConsumerWithNil(let continuation): + continuation.resume(returning: nil) + + case .none: + break + } + } + } onCancel: { + let action = self._lock.withLock { + self._stateMachine.cancelNext() + } + + switch action { + case .resumeConsumerWithNilAndCallOnTermination(let continuation, let onTermination): + continuation.resume(returning: nil) + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + onTermination?() + + case .none: + break + } + } + } + } +} + +extension MultiProducerSingleConsumerChannel._Storage { + /// The state machine of the channel. + @usableFromInline + struct _StateMachine: ~Copyable { + /// The state machine's current state. + @usableFromInline + var _state: _State + + @inlinable + var _onTermination: (@Sendable () -> Void)? { + set { + switch consume self._state { + case .channeling(var channeling): + channeling.onTermination = newValue + self = .init(state: .channeling(channeling)) + + case .sourceFinished(var sourceFinished): + sourceFinished.onTermination = newValue + self = .init(state: .sourceFinished(sourceFinished)) + + case .finished(let finished): + self = .init(state: .finished(finished)) + } + } + get { + switch self._state { + case .channeling(let channeling): + return channeling.onTermination + + case .sourceFinished(let sourceFinished): + return sourceFinished.onTermination + + case .finished: + return nil + } + } + } + + init( + backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy + ) { + self._state = .channeling( + .init( + backpressureStrategy: backpressureStrategy, + iteratorInitialized: false, + buffer: .init(), + producerContinuations: .init(), + cancelledAsyncProducers: .init(), + hasOutstandingDemand: true, + activeProducers: 1, + nextCallbackTokenID: 0 + ) + ) + } + + @inlinable + init(state: consuming _State) { + self._state = state + } + + /// Actions returned by `sourceDeinitialized()`. + @usableFromInline + enum SourceDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (@Sendable () -> Void)? + ) + } + + @inlinable + mutating func sourceDeinitialized() -> SourceDeinitializedAction? { + switch consume self._state { + case .channeling(var channeling): + channeling.activeProducers -= 1 + + if channeling.activeProducers == 0 { + // This was the last producer so we can transition to source finished now + + self = .init(state: .sourceFinished(.init( + iteratorInitialized: channeling.iteratorInitialized, + buffer: channeling.buffer + ))) + + if channeling.suspendedProducers.isEmpty { + return .callOnTermination(channeling.onTermination) + } else { + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) + } + } else { + // We still have more producers + self = .init(state: .channeling(channeling)) + + return nil + } + case .sourceFinished(let sourceFinished): + // This can happen if one producer calls finish and another deinits afterwards + self = .init(state: .sourceFinished(sourceFinished)) + + return nil + case .finished(let finished): + // This can happen if the consumer finishes and the producers deinit + self = .init(state: .finished(finished)) + + return nil + } + } + + /// Actions returned by `sequenceDeinitialized()`. + @usableFromInline + enum SequenceDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (@Sendable () -> Void)? + ) + } + + @inlinable + mutating func sequenceDeinitialized() -> SequenceDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + guard channeling.iteratorInitialized else { + // No iterator was created so we can transition to finished right away. + self = .init(state: .finished(.init(iteratorInitialized: false, sourceFinished: false))) + + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) + } + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + self = .init(state: .channeling(channeling)) + + return .none + + case .sourceFinished(let sourceFinished): + guard sourceFinished.iteratorInitialized else { + // No iterator was created so we can transition to finished right away. + self = .init(state: .finished(.init(iteratorInitialized: false, sourceFinished: true))) + + return .callOnTermination(sourceFinished.onTermination) + } + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } + + @inlinable + mutating func iteratorInitialized() { + switch consume self._state { + case .channeling(var channeling): + if channeling.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + channeling.iteratorInitialized = true + self = .init(state: .channeling(channeling)) + } + + case .sourceFinished(var sourceFinished): + if sourceFinished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + sourceFinished.iteratorInitialized = true + self = .init(state: .sourceFinished(sourceFinished)) + } + + case .finished(let finished): + if finished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + self = .init(state: .finished(.init(iteratorInitialized: true, sourceFinished: finished.sourceFinished))) + } + } + } + + /// Actions returned by `iteratorDeinitialized()`. + @usableFromInline + enum IteratorDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (@Sendable () -> Void)? + ) + } + + @inlinable + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + if channeling.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self = .init(state: .finished(.init(iteratorInitialized: true, sourceFinished: false))) + + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + } + + case .sourceFinished(let sourceFinished): + if sourceFinished.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self = .init(state: .finished(.init(iteratorInitialized: true, sourceFinished: true))) + + return .callOnTermination(sourceFinished.onTermination) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + } + + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } + + /// Actions returned by `send()`. + @usableFromInline + enum SendAction { + /// Indicates that the producer should be notified to produce more. + case returnProduceMore + /// Indicates that the producer should be suspended to stop producing. + case returnEnqueue( + callbackToken: UInt64 + ) + /// Indicates that the consumer should be resumed and the producer should be notified to produce more. + case resumeConsumerAndReturnProduceMore( + continuation: UnsafeContinuation, + element: Element + ) + /// Indicates that the consumer should be resumed and the producer should be suspended. + case resumeConsumerAndReturnEnqueue( + continuation: UnsafeContinuation, + element: Element, + callbackToken: UInt64 + ) + /// Indicates that the producer has been finished. + case throwFinishedError + + @inlinable + init( + callbackToken: UInt64?, + continuationAndElement: (UnsafeContinuation, Element)? = nil + ) { + switch (callbackToken, continuationAndElement) { + case (.none, .none): + self = .returnProduceMore + + case (.some(let callbackToken), .none): + self = .returnEnqueue(callbackToken: callbackToken) + + case (.none, .some((let continuation, let element))): + self = .resumeConsumerAndReturnProduceMore( + continuation: continuation, + element: element + ) + + case (.some(let callbackToken), .some((let continuation, let element))): + self = .resumeConsumerAndReturnEnqueue( + continuation: continuation, + element: element, + callbackToken: callbackToken + ) + } + } + } + + @inlinable + mutating func send(_ sequence: some Sequence) -> SendAction { + switch consume self._state { + case .channeling(var channeling): + // We have an element and can resume the continuation + let bufferEndIndexBeforeAppend = channeling.buffer.endIndex + channeling.buffer.append(contentsOf: sequence) + var shouldProduceMore = channeling.backpressureStrategy.didSend( + elements: channeling.buffer[bufferEndIndexBeforeAppend...] + ) + channeling.hasOutstandingDemand = shouldProduceMore + + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we just buffer the elements + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + return .init( + callbackToken: callbackToken + ) + } + guard let element = channeling.buffer.popFirst() else { + // We got a send of an empty sequence. We just tolerate this. + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + return .init(callbackToken: callbackToken) + } + // We need to tell the back pressure strategy that we consumed + shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore + + // We got a consumer continuation and an element. We can resume the consumer now + channeling.consumerContinuation = nil + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + return .init( + callbackToken: callbackToken, + continuationAndElement: (consumerContinuation, element) + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished we are dropping the elements. + self = .init(state: .sourceFinished(sourceFinished)) + + return .throwFinishedError + + case .finished(let finished): + // If the source has finished we are dropping the elements. + self = .init(state: .finished(finished)) + + return .throwFinishedError + } + } + + /// Actions returned by `enqueueProducer()`. + @usableFromInline + enum EnqueueProducerAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer((Result) -> Void) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError((Result) -> Void, Error) + } + + @inlinable + mutating func enqueueProducer( + callbackToken: UInt64, + onProduceMore: sending @escaping (Result) -> Void + ) -> EnqueueProducerAction? { + switch consume self._state { + case .channeling(var channeling): + if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { + // Our producer got marked as cancelled. + channeling.cancelledAsyncProducers.remove(at: index) + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithError(onProduceMore, CancellationError()) + } else if channeling.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + self = .init(state: .channeling(channeling)) + + return .resumeProducer(onProduceMore) + } else { + channeling.suspendedProducers.append((callbackToken, .closure(onProduceMore))) + self = .init(state: .channeling(channeling)) + + return .none + } + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + + /// Actions returned by `enqueueContinuation()`. + @usableFromInline + enum EnqueueContinuationAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer(UnsafeContinuation) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError(UnsafeContinuation, Error) + } + + @inlinable + mutating func enqueueContinuation( + callbackToken: UInt64, + continuation: UnsafeContinuation + ) -> EnqueueContinuationAction? { + switch consume self._state { + case .channeling(var channeling): + if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { + // Our producer got marked as cancelled. + channeling.cancelledAsyncProducers.remove(at: index) + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithError(continuation, CancellationError()) + } else if channeling.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + self = .init(state: .channeling(channeling)) + + return .resumeProducer(continuation) + } else { + channeling.suspendedProducers.append((callbackToken, .continuation(continuation))) + self = .init(state: .channeling(channeling)) + + return .none + } + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + + /// Actions returned by `cancelProducer()`. + @usableFromInline + enum CancelProducerAction { + /// Indicates that the producer should be notified about cancellation. + case resumeProducerWithCancellationError(_MultiProducerSingleConsumerSuspendedProducer) + } + + @inlinable + mutating func cancelProducer( + callbackToken: UInt64 + ) -> CancelProducerAction? { + switch consume self._state { + case .channeling(var channeling): + guard let index = channeling.suspendedProducers.firstIndex(where: { $0.0 == callbackToken }) else { + // The task that sends was cancelled before sending elements so the cancellation handler + // got invoked right away + channeling.cancelledAsyncProducers.append(callbackToken) + self = .init(state: .channeling(channeling)) + + return .none + } + // We have an enqueued producer that we need to resume now + let continuation = channeling.suspendedProducers.remove(at: index).1 + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithCancellationError(continuation) + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .none + } + } + + /// Actions returned by `finish()`. + @usableFromInline + enum FinishAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((() -> Void)?) + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination` should be called. + case resumeConsumerAndCallOnTermination( + consumerContinuation: UnsafeContinuation, + failure: Failure?, + onTermination: (() -> Void)? + ) + /// Indicates that the producers should be resumed with an error. + case resumeProducers( + producerContinuations: _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + } + + @inlinable + mutating func finish(_ failure: Failure?) -> FinishAction? { + switch consume self._state { + case .channeling(let channeling): + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we are just going to mark + // the source as finished and terminate the current suspended producers. + self = .init(state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + buffer: channeling.buffer, + failure: failure, + onTermination: channeling.onTermination + )) + ) + + return .resumeProducers(producerContinuations: .init(channeling.suspendedProducers.lazy.map { $0.1 })) + } + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(channeling.buffer.isEmpty, "Expected an empty buffer") + + self = .init(state: .finished(.init(iteratorInitialized: channeling.iteratorInitialized, sourceFinished: true))) + + return .resumeConsumerAndCallOnTermination( + consumerContinuation: consumerContinuation, + failure: failure, + onTermination: channeling.onTermination + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished, finishing again has no effect. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(var finished): + finished.sourceFinished = true + self = .init(state: .finished(finished)) + return .none + } + } + + /// Actions returned by `next()`. + @usableFromInline + enum NextAction { + /// Indicates that the element should be returned to the caller. + case returnElement(Element) + /// Indicates that the element should be returned to the caller and that all producers should be called. + case returnElementAndResumeProducers(Element, _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>) + /// Indicates that the `Failure` should be returned to the caller and that `onTermination` should be called. + case returnFailureAndCallOnTermination(Failure?, (() -> Void)?) + /// Indicates that the `nil` should be returned to the caller. + case returnNil + /// Indicates that the `Task` of the caller should be suspended. + case suspendTask + } + + @inlinable + mutating func next() -> NextAction { + switch consume self._state { + case .channeling(var channeling): + guard channeling.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + } + + guard let element = channeling.buffer.popFirst() else { + // There is nothing in the buffer to fulfil the demand so we need to suspend. + // We are not interacting with the backpressure strategy here because + // we are doing this inside `suspendNext` + self = .init(state: .channeling(channeling)) + + return .suspendTask + } + // We have an element to fulfil the demand right away. + let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore + + guard shouldProduceMore else { + // We don't have any new demand, so we can just return the element. + self = .init(state: .channeling(channeling)) + + return .returnElement(element) + } + // There is demand and we have to resume our producers + let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) + channeling.suspendedProducers.removeAll(keepingCapacity: true) + self = .init(state: .channeling(channeling)) + + return .returnElementAndResumeProducers(element, producers) + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + guard let element = sourceFinished.buffer.popFirst() else { + // We are returning the queued failure now and can transition to finished + self = .init(state: .finished(.init(iteratorInitialized: sourceFinished.iteratorInitialized, sourceFinished: true))) + + return .returnFailureAndCallOnTermination(sourceFinished.failure, sourceFinished.onTermination) + } + self = .init(state: .sourceFinished(sourceFinished)) + + return .returnElement(element) + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .returnNil + } + } + + /// Actions returned by `suspendNext()`. + @usableFromInline + enum SuspendNextAction { + /// Indicates that the consumer should be resumed. + case resumeConsumerWithElement(UnsafeContinuation, Element) + /// Indicates that the consumer and all producers should be resumed. + case resumeConsumerWithElementAndProducers( + UnsafeContinuation, + Element, + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. + case resumeConsumerWithFailureAndCallOnTermination( + UnsafeContinuation, + Failure?, + (() -> Void)? + ) + /// Indicates that the consumer should be resumed with `nil`. + case resumeConsumerWithNil(UnsafeContinuation) + } + + @inlinable + mutating func suspendNext(continuation: UnsafeContinuation) -> SuspendNextAction? { + switch consume self._state { + case .channeling(var channeling): + guard channeling.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + } + + // We have to check here again since we might have a producer interleave next and suspendNext + guard let element = channeling.buffer.popFirst() else { + // There is nothing in the buffer to fulfil the demand so we to store the continuation. + channeling.consumerContinuation = continuation + self = .init(state: .channeling(channeling)) + + return .none + } + // We have an element to fulfil the demand right away. + + let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore + + guard shouldProduceMore else { + // We don't have any new demand, so we can just return the element. + self = .init(state: .channeling(channeling)) + + return .resumeConsumerWithElement(continuation, element) + } + // There is demand and we have to resume our producers + let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) + channeling.suspendedProducers.removeAll(keepingCapacity: true) + self = .init(state: .channeling(channeling)) + + return .resumeConsumerWithElementAndProducers(continuation, element, producers) + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + guard let element = sourceFinished.buffer.popFirst() else { + // We are returning the queued failure now and can transition to finished + self = .init(state: .finished(.init(iteratorInitialized: sourceFinished.iteratorInitialized, sourceFinished: true))) + + return .resumeConsumerWithFailureAndCallOnTermination( + continuation, + sourceFinished.failure, + sourceFinished.onTermination + ) + } + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeConsumerWithElement(continuation, element) + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .resumeConsumerWithNil(continuation) + } + } + + /// Actions returned by `cancelNext()`. + @usableFromInline + enum CancelNextAction { + /// Indicates that the continuation should be resumed with nil, the producers should be finished and call onTermination. + case resumeConsumerWithNilAndCallOnTermination(UnsafeContinuation, (() -> Void)?) + /// Indicates that the producers should be finished and call onTermination. + case failProducersAndCallOnTermination(_TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, (() -> Void)?) + } + + @inlinable + mutating func cancelNext() -> CancelNextAction? { + switch consume self._state { + case .channeling(let channeling): + self = .init(state: .finished(.init(iteratorInitialized: channeling.iteratorInitialized, sourceFinished: false))) + + guard let consumerContinuation = channeling.consumerContinuation else { + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) + } + precondition( + channeling.suspendedProducers.isEmpty, + "Internal inconsistency. Unexpected producer continuations." + ) + return .resumeConsumerWithNilAndCallOnTermination( + consumerContinuation, + channeling.onTermination + ) + + case .sourceFinished(let sourceFinished): + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .none + } + } + } +} + +extension MultiProducerSingleConsumerChannel._Storage._StateMachine { + @usableFromInline + enum _State: ~Copyable { + @usableFromInline + struct Channeling: ~Copyable { + /// The backpressure strategy. + @usableFromInline + var backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy + + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + + /// The buffer of elements. + @usableFromInline + var buffer: Deque + + /// The optional consumer continuation. + @usableFromInline + var consumerContinuation: UnsafeContinuation? + + /// The producer continuations. + @usableFromInline + var suspendedProducers: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)> + + /// The producers that have been cancelled. + @usableFromInline + var cancelledAsyncProducers: Deque + + /// Indicates if we currently have outstanding demand. + @usableFromInline + var hasOutstandingDemand: Bool + + /// The number of active producers. + @usableFromInline + var activeProducers: UInt64 + + /// The next callback token. + @usableFromInline + var nextCallbackTokenID: UInt64 + + var description: String { + "backpressure:\(self.backpressureStrategy.description) iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) consumerContinuation:\(self.consumerContinuation == nil) producerContinuations:\(self.suspendedProducers.count) cancelledProducers:\(self.cancelledAsyncProducers.count) hasOutstandingDemand:\(self.hasOutstandingDemand)" + } + + @inlinable + init( + backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy, + iteratorInitialized: Bool, + onTermination: (@Sendable () -> Void)? = nil, + buffer: Deque, + consumerContinuation: UnsafeContinuation? = nil, + producerContinuations: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)>, + cancelledAsyncProducers: Deque, + hasOutstandingDemand: Bool, + activeProducers: UInt64, + nextCallbackTokenID: UInt64 + ) { + self.backpressureStrategy = backpressureStrategy + self.iteratorInitialized = iteratorInitialized + self.onTermination = onTermination + self.buffer = buffer + self.consumerContinuation = consumerContinuation + self.suspendedProducers = producerContinuations + self.cancelledAsyncProducers = cancelledAsyncProducers + self.hasOutstandingDemand = hasOutstandingDemand + self.activeProducers = activeProducers + self.nextCallbackTokenID = nextCallbackTokenID + } + + /// Generates the next callback token. + @inlinable + mutating func nextCallbackToken() -> UInt64 { + let id = self.nextCallbackTokenID + self.nextCallbackTokenID += 1 + return id + } + } + + @usableFromInline + struct SourceFinished: ~Copyable { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// The buffer of elements. + @usableFromInline + var buffer: Deque + + /// The failure that should be thrown after the last element has been consumed. + @usableFromInline + var failure: Failure? + + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + + var description: String { + "iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) failure:\(self.failure == nil)" + } + + @inlinable + init( + iteratorInitialized: Bool, + buffer: Deque, + failure: Failure? = nil, + onTermination: (@Sendable () -> Void)? = nil + ) { + self.iteratorInitialized = iteratorInitialized + self.buffer = buffer + self.failure = failure + self.onTermination = onTermination + } + } + + @usableFromInline + struct Finished: ~Copyable { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if the source was finished. + @usableFromInline + var sourceFinished: Bool + + var description: String { + "iteratorInitialized:\(self.iteratorInitialized) sourceFinished:\(self.sourceFinished)" + } + + @inlinable + init( + iteratorInitialized: Bool, + sourceFinished: Bool + ) { + self.iteratorInitialized = iteratorInitialized + self.sourceFinished = sourceFinished + } + } + + /// The state once either any element was sent or `next()` was called. + case channeling(Channeling) + + /// The state once the underlying source signalled that it is finished. + case sourceFinished(SourceFinished) + + /// The state once there can be no outstanding demand. This can happen if: + /// 1. The iterator was deinited + /// 2. The underlying source finished and all buffered elements have been consumed + case finished(Finished) + + @usableFromInline + var description: String { + switch self { + case .channeling(let channeling): + return "channeling \(channeling.description)" + case .sourceFinished(let sourceFinished): + return "sourceFinished \(sourceFinished.description)" + case .finished(let finished): + return "finished \(finished.description)" + } + } + } +} + +@usableFromInline +enum _MultiProducerSingleConsumerSuspendedProducer { + case closure((Result) -> Void) + case continuation(UnsafeContinuation) +} +#endif diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift new file mode 100644 index 00000000..5e860a89 --- /dev/null +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -0,0 +1,489 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.0) +/// An error that is thrown from the various `send` methods of the +/// ``MultiProducerSingleConsumerChannel/Source``. +/// +/// This error is thrown when the channel is already finished when +/// trying to send new elements to the source. +public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { + @usableFromInline + init() {} +} + +/// A multi producer single consumer channel. +/// +/// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to +/// send values to the channel. The source exposes the internal backpressure of the asynchronous sequence to the +/// producer. Additionally, the source can be used from synchronous and asynchronous contexts. +/// +/// +/// ## Using a MultiProducerSingleConsumerChannel +/// +/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with it's source first by calling +/// the ``MultiProducerSingleConsumerChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. +/// Afterwards, you can pass the source to the producer and the channel to the consumer. +/// +/// ``` +/// let (channel, source) = MultiProducerSingleConsumerChannel.makeChannel( +/// backpressureStrategy: .watermark(low: 2, high: 4) +/// ) +/// ``` +/// +/// ### Asynchronous producers +/// +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-9b5do`` +/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)-4myrz``. Backpressure results in calls +/// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. +/// +/// ``` +/// try await withThrowingTaskGroup(of: Void.self) { group in +/// group.addTask { +/// try await source.send(1) +/// try await source.send(2) +/// try await source.send(3) +/// } +/// +/// for await element in channel { +/// print(element) +/// } +/// } +/// ``` +/// +/// ### Synchronous producers +/// +/// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, +/// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. +/// +/// ## Finishing the source +/// +/// To properly notify the consumer if the production of values has been finished the source's ``MultiProducerSingleConsumerChannel/Source/finish(throwing:)`` **must** be called. +public struct MultiProducerSingleConsumerChannel: AsyncSequence { + /// A private class to give the ``MultiProducerSingleConsumerChannel`` a deinit so we + /// can tell the producer when any potential consumer went away. + private final class _Backing: Sendable { + /// The underlying storage. + fileprivate let storage: _Storage + + init(storage: _Storage) { + self.storage = storage + } + + deinit { + storage.sequenceDeinitialized() + } + } + + /// The backing storage. + private let backing: _Backing + + @frozen + public struct ChannelAndStream: ~Copyable { + public var channel: MultiProducerSingleConsumerChannel + public var source: Source + + public init( + channel: MultiProducerSingleConsumerChannel, + source: consuming Source + ) { + self.channel = channel + self.source = source + } + } + + /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. + /// + /// - Parameters: + /// - elementType: The element type of the channel. + /// - failureType: The failure type of the channel. + /// - BackpressureStrategy: The backpressure strategy that the channel should use. + /// - Returns: A tuple containing the channel and its source. The source should be passed to the + /// producer while the channel should be passed to the consumer. + public static func makeChannel( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Never.self, + backpressureStrategy: Source.BackpressureStrategy + ) -> ChannelAndStream { + let storage = _Storage( + backpressureStrategy: backpressureStrategy.internalBackpressureStrategy + ) + let source = Source(storage: storage) + + return .init(channel: .init(storage: storage), source: source) + } + + init(storage: _Storage) { + self.backing = .init(storage: storage) + } +} + +extension MultiProducerSingleConsumerChannel { + /// A struct to send values to the channel. + /// + /// Use this source to provide elements to the channel by calling one of the `send` methods. + /// + /// - Important: You must terminate the source by calling ``finish(throwing:)``. + public struct Source: ~Copyable, Sendable { + /// A strategy that handles the backpressure of the channel. + public struct BackpressureStrategy: Sendable { + var internalBackpressureStrategy: _InternalBackpressureStrategy + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + public static func watermark(low: Int, high: Int) -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .watermark( + .init(low: low, high: high, waterLevelForElement: nil) + ) + ) + } + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. + /// + /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when + /// it is consumed from the channel, so it is recommended to provide an function that runs in constant time. + public static func watermark( + low: Int, + high: Int, + waterLevelForElement: @escaping @Sendable (Element) -> Int // TODO: In the future this should become sending + ) -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .watermark( + .init(low: low, high: high, waterLevelForElement: waterLevelForElement) + ) + ) + } + + /// An unbounded backpressure strategy. + /// + /// - Important: Only use this strategy if the production of elements is limited through some other mean. Otherwise + /// an unbounded backpressure strategy can result in infinite memory usage and open your application to denial of service + /// attacks. + public static func unbounded() -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .unbounded(.init()) + ) + } + } + + /// A type that indicates the result of sending elements to the source. + public enum SendResult: ~Copyable, Sendable { + /// A token that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. + public struct CallbackToken: Sendable { + @usableFromInline + let _id: UInt64 + + @usableFromInline + init(id: UInt64) { + self._id = id + } + } + + /// Indicates that more elements should be produced and written to the source. + case produceMore + + /// Indicates that a callback should be enqueued. + /// + /// The associated token should be passed to the ``enqueueCallback(_:)`` method. + case enqueueCallback(CallbackToken) + } + + + /// A callback to invoke when the channel finished. + /// + /// The channel finishes and calls this closure in the following cases: + /// - No iterator was created and the sequence was deinited + /// - An iterator was created and deinited + /// - After ``finish(throwing:)`` was called and all elements have been consumed + public var onTermination: (@Sendable () -> Void)? { + set { + self._storage.onTermination = newValue + } + get { + self._storage.onTermination + } + } + + @usableFromInline + let _storage: _Storage + + internal init(storage: _Storage) { + self._storage = storage + } + + deinit { + self._storage.sourceDeinitialized() + } + + + /// Creates a new source which can be used to send elements to the channel concurrently. + /// + /// The channel will only automatically be finished if all existing sources have been deinited. + /// + /// - Returns: A new source for sending elements to the channel. + public mutating func copy() -> Self { + .init(storage: self._storage) + } + + /// Sends new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter sequence: The elements to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + public mutating func send(contentsOf sequence: sending S) throws -> SendResult where Element == S.Element, S: Sequence { + try self._storage.send(contentsOf: sequence) + } + + /// Send the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter element: The element to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + public mutating func send(_ element: sending Element) throws -> SendResult { + try self._storage.send(contentsOf: CollectionOfOne(element)) + } + + /// Enqueues a callback that will be invoked once more elements should be produced. + /// + /// Call this method after ``send(contentsOf:)-5honm`` or ``send(_:)-3jxzb`` returned ``SendResult/enqueueCallback(_:)``. + /// + /// - Important: Enqueueing the same token multiple times is not allowed. + /// + /// - Parameters: + /// - callbackToken: The callback token. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. + @inlinable + public mutating func enqueueCallback( + callbackToken: consuming SendResult.CallbackToken, + onProduceMore: sending @escaping (Result) -> Void + ) { + self._storage.enqueueProducer(callbackToken: callbackToken._id, onProduceMore: onProduceMore) + } + + /// Cancel an enqueued callback. + /// + /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and + /// will mark the passed `callbackToken` as cancelled. + /// + /// - Parameter callbackToken: The callback token. + @inlinable + public mutating func cancelCallback(callbackToken: consuming SendResult.CallbackToken) { + self._storage.cancelProducer(callbackToken: callbackToken._id) + } + + /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(contentsOf:onProduceMore:)``. + @inlinable + public mutating func send( + contentsOf sequence: sending S, + onProduceMore: @escaping @Sendable (Result) -> Void + ) where Element == S.Element, S: Sequence { + do { + let sendResult = try self.send(contentsOf: sequence) + + switch consume sendResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(let callbackToken): + self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + } catch { + onProduceMore(.failure(error)) + } + } + + /// Sends the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - element: The element to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(_:onProduceMore:)``. + @inlinable + public mutating func send( + _ element: sending Element, + onProduceMore: @escaping @Sendable (Result) -> Void + ) { + self.send(contentsOf: CollectionOfOne(element), onProduceMore: onProduceMore) + } + + /// Send new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating func send(contentsOf sequence: sending S) async throws where Element == S.Element, S: Sequence { + let sendResult = try { try self.send(contentsOf: sequence) }() + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackToken): + let id = callbackToken._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) + } + } + } + + /// Send new element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - element: The element to send to the channel. + @inlinable + public mutating func send(_ element: sending Element) async throws { + try await self.send(contentsOf: CollectionOfOne(element)) + } + + /// Send the elements of the asynchronous sequence to the channel. + /// + /// This method returns once the provided asynchronous sequence or the channel finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating func send(contentsOf sequence: sending S) async throws where Element == S.Element, S: AsyncSequence { + for try await element in sequence { + try await self.send(contentsOf: CollectionOfOne(element)) + } + } + + /// Indicates that the production terminated. + /// + /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. + /// + /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept + /// new elements. + /// + /// - Parameters: + /// - error: The error to throw, or `nil`, to finish normally. + @inlinable + public consuming func finish(throwing error: Failure? = nil) { + self._storage.finish(error) + } + } +} + +extension MultiProducerSingleConsumerChannel { + /// The asynchronous iterator for iterating the channel. + /// + /// This type is not `Sendable`. Don't use it from multiple + /// concurrent contexts. It is a programmer error to invoke `next()` from a + /// concurrent context that contends with another such call, which + /// results in a call to `fatalError()`. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + final class _Backing { + @usableFromInline + let storage: _Storage + + init(storage: _Storage) { + self.storage = storage + self.storage.iteratorInitialized() + } + + deinit { + self.storage.iteratorDeinitialized() + } + } + + @usableFromInline + let _backing: _Backing + + init(storage: _Storage) { + self._backing = .init(storage: storage) + } + + @_disfavoredOverload + @inlinable + public mutating func next() async throws -> Element? { + try await self._backing.storage.next(isolation: nil) + } + + @inlinable + public mutating func next( + isolation actor: isolated (any Actor)? = #isolation + ) async throws(Failure) -> Element? { + do { + return try await self._backing.storage.next(isolation: actor) + } catch { + throw error as! Failure + } + } + } + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + public func makeAsyncIterator() -> Iterator { + Iterator(storage: self.backing.storage) + } +} + +extension MultiProducerSingleConsumerChannel: Sendable where Element: Sendable {} + +@available(*, unavailable) +extension MultiProducerSingleConsumerChannel.Iterator: Sendable {} +#endif diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift new file mode 100644 index 00000000..e15fccf7 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift @@ -0,0 +1,1041 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import AsyncAlgorithms +import XCTest + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class MultiProducerSingleConsumerChannelTests: XCTestCase { + // MARK: - sequenceDeinitialized + + // Following tests are disabled since the channel is not getting deinited due to a known bug + +// func testSequenceDeinitialized_whenNoIterator() async throws { +// var channelAndStream: MultiProducerSingleConsumerChannel.ChannelAndStream! = MultiProducerSingleConsumerChannel.makeChannel( +// of: Int.self, +// backpressureStrategy: .watermark(low: 5, high: 10) +// ) +// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel +// var source = channelAndStream.source +// channelAndStream = nil +// +// let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() +// source.onTermination = { +// onTerminationContinuation.finish() +// } +// +// await withThrowingTaskGroup(of: Void.self) { group in +// group.addTask { +// onTerminationContinuation.yield() +// try await Task.sleep(for: .seconds(10)) +// } +// +// var onTerminationIterator = onTerminationStream.makeAsyncIterator() +// _ = await onTerminationIterator.next() +// +// withExtendedLifetime(channel) {} +// channel = nil +// +// let terminationResult: Void? = await onTerminationIterator.next() +// XCTAssertNil(terminationResult) +// +// do { +// _ = try { try source.send(2) }() +// XCTFail("Expected an error to be thrown") +// } catch { +// XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) +// } +// +// group.cancelAll() +// } +// } +// +// func testSequenceDeinitialized_whenIterator() async throws { +// let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( +// of: Int.self, +// backpressureStrategy: .watermark(low: 5, high: 10) +// ) +// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel +// var source = consume channelAndStream.source +// +// var iterator = channel?.makeAsyncIterator() +// +// let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() +// source.onTermination = { +// onTerminationContinuation.finish() +// } +// +// try await withThrowingTaskGroup(of: Void.self) { group in +// group.addTask { +// while !Task.isCancelled { +// onTerminationContinuation.yield() +// try await Task.sleep(for: .seconds(0.2)) +// } +// } +// +// var onTerminationIterator = onTerminationStream.makeAsyncIterator() +// _ = await onTerminationIterator.next() +// +// try withExtendedLifetime(channel) { +// let writeResult = try source.send(1) +// writeResult.assertIsProducerMore() +// } +// +// channel = nil +// +// do { +// let writeResult = try { try source.send(2) }() +// writeResult.assertIsProducerMore() +// } catch { +// XCTFail("Expected no error to be thrown") +// } +// +// let element1 = await iterator?.next() +// XCTAssertEqual(element1, 1) +// let element2 = await iterator?.next() +// XCTAssertEqual(element2, 2) +// +// group.cancelAll() +// } +// } +// +// func testSequenceDeinitialized_whenFinished() async throws { +// let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( +// of: Int.self, +// backpressureStrategy: .watermark(low: 5, high: 10) +// ) +// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel +// var source = consume channelAndStream.source +// +// let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() +// source.onTermination = { +// onTerminationContinuation.finish() +// } +// +// await withThrowingTaskGroup(of: Void.self) { group in +// group.addTask { +// while !Task.isCancelled { +// onTerminationContinuation.yield() +// try await Task.sleep(for: .seconds(0.2)) +// } +// } +// +// var onTerminationIterator = onTerminationStream.makeAsyncIterator() +// _ = await onTerminationIterator.next() +// +// channel = nil +// +// let terminationResult: Void? = await onTerminationIterator.next() +// XCTAssertNil(terminationResult) +// XCTAssertNil(channel) +// +// group.cancelAll() +// } +// } +// +// func testSequenceDeinitialized_whenChanneling_andSuspendedProducer() async throws { +// let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( +// of: Int.self, +// backpressureStrategy: .watermark(low: 1, high: 2) +// ) +// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel +// var source = consume channelAndStream.source +// +// _ = try { try source.send(1) }() +// +// do { +// try await withCheckedThrowingContinuation { continuation in +// source.send(1) { result in +// continuation.resume(with: result) +// } +// +// channel = nil +// _ = channel?.makeAsyncIterator() +// } +// } catch { +// XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) +// } +// } + + // MARK: - iteratorInitialized + + func testIteratorInitialized_whenInitial() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + let source = consume channelAndStream.source + + _ = channel.makeAsyncIterator() + } + + func testIteratorInitialized_whenChanneling() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await source.send(1) + + var iterator = channel.makeAsyncIterator() + let element = await iterator.next() + XCTAssertEqual(element, 1) + } + + func testIteratorInitialized_whenSourceFinished() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await source.send(1) + source.finish(throwing: nil) + + var iterator = channel.makeAsyncIterator() + let element1 = await iterator.next() + XCTAssertEqual(element1, 1) + let element2 = await iterator.next() + XCTAssertNil(element2) + } + + func testIteratorInitialized_whenFinished() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + let source = consume channelAndStream.source + + source.finish(throwing: nil) + + var iterator = channel.makeAsyncIterator() + let element = await iterator.next() + XCTAssertNil(element) + } + + // MARK: - iteratorDeinitialized + + func testIteratorDeinitialized_whenInitial() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + iterator = nil + _ = await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenChanneling() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + try await source.send(1) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + iterator = nil + _ = await iterator?.next(isolation: nil) + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenSourceFinished() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + try await source.send(1) + source.finish(throwing: nil) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + iterator = nil + _ = await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenFinished() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + source.finish(throwing: nil) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + iterator = nil + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenChanneling_andSuspendedProducer() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel + var source = consume channelAndStream.source + + var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel?.makeAsyncIterator() + channel = nil + + _ = try { try source.send(1) }() + + do { + try await withCheckedThrowingContinuation { continuation in + source.send(1) { result in + continuation.resume(with: result) + } + + iterator = nil + } + } catch { + XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) + } + + _ = try await iterator?.next() + } + + // MARK: - sourceDeinitialized + + func testSourceDeinitialized_whenSourceFinished() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndStream.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + try await source?.send(1) + try await source?.send(2) + source?.finish(throwing: nil) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + _ = try await iterator?.next() + + _ = await onTerminationIterator.next() + + _ = try await iterator?.next() + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenFinished() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndStream.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndStream.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + source?.finish(throwing: nil) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + _ = channel.makeAsyncIterator() + + _ = await onTerminationIterator.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + // MARK: - write + + func testWrite_whenInitial() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await source.send(1) + + var iterator = channel.makeAsyncIterator() + let element = await iterator.next() + XCTAssertEqual(element, 1) + } + + func testWrite_whenChanneling_andNoConsumer() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await source.send(1) + try await source.send(2) + + var iterator = channel.makeAsyncIterator() + let element1 = await iterator.next() + XCTAssertEqual(element1, 1) + let element2 = await iterator.next() + XCTAssertEqual(element2, 2) + } + + func testWrite_whenChanneling_andSuspendedConsumer() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await withThrowingTaskGroup(of: Int?.self) { group in + group.addTask { + return await channel.first { _ in true } + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + try await source.send(1) + let element = try await group.next() + XCTAssertEqual(element, 1) + } + } + + func testWrite_whenChanneling_andSuspendedConsumer_andEmptySequence() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await withThrowingTaskGroup(of: Int?.self) { group in + group.addTask { + return await channel.first { _ in true } + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + try await source.send(contentsOf: []) + try await source.send(contentsOf: [1]) + let element = try await group.next() + XCTAssertEqual(element, 1) + } + } + + // MARK: - enqueueProducer + + func testEnqueueProducer_whenChanneling_andAndCancelled() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.send(1) + + let writeResult = try { try source.send(2) }() + + switch consume writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.cancelCallback(callbackToken: callbackToken) + + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = await channel.first { _ in true } + XCTAssertEqual(element, 1) + } + + func testEnqueueProducer_whenChanneling_andAndCancelled_andAsync() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await source.send(1) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await source.send(2) + } + + group.cancelAll() + do { + try await group.next() + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + } + + let element = await channel.first { _ in true } + XCTAssertEqual(element, 1) + } + + func testEnqueueProducer_whenChanneling_andInterleaving() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + var iterator = channel.makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.send(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + let element = await iterator.next() + XCTAssertEqual(element, 1) + + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + func testEnqueueProducer_whenChanneling_andSuspending() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + var iterator = channel.makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.send(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + let element = await iterator.next() + XCTAssertEqual(element, 1) + + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + // MARK: - cancelProducer + + func testCancelProducer_whenChanneling() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.send(1) + + let writeResult = try { try source.send(2) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + + source.cancelCallback(callbackToken: callbackToken) + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = await channel.first { _ in true } + XCTAssertEqual(element, 1) + } + + // MARK: - finish + + func testFinish_whenChanneling_andConsumerSuspended() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndStream.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndStream.source + + try await withThrowingTaskGroup(of: Int?.self) { group in + group.addTask { + return await channel.first { $0 == 2 } + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + source?.finish(throwing: nil) + source = nil + + let element = try await group.next() + XCTAssertEqual(element, .some(nil)) + } + } + + func testFinish_whenInitial() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + source.finish(throwing: CancellationError()) + + do { + for try await _ in channel {} + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + } + + // MARK: - Backpressure + + func testBackpressure() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await source.send(contentsOf: [1]) + } + } + + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.makeAsyncIterator() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + _ = await iterator.next() + _ = await iterator.next() + _ = await iterator.next() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + group.cancelAll() + } + } + + func testBackpressureSync() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await withCheckedThrowingContinuation { continuation in + source.send(contentsOf: [1]) { result in + continuation.resume(with: result) + } + } + } + } + + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.makeAsyncIterator() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + _ = await iterator.next() + _ = await iterator.next() + _ = await iterator.next() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + group.cancelAll() + } + } + + func testWatermarkWithCustomCoount() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: [Int].self, + backpressureStrategy: .watermark(low: 2, high: 4, waterLevelForElement: { $0.count }) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + var iterator = channel.makeAsyncIterator() + + try await source.send([1, 1, 1]) + + _ = await iterator.next() + + try await source.send([1, 1, 1]) + + _ = await iterator.next() + } + + func testWatermarWithLotsOfElements() async throws { + // This test should in the future use a custom task executor to schedule to avoid sending + // 1000 elements. + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndStream.channel + var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndStream.source + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + var source = source.take()! + for i in 0...10000 { + try await source.send(i) + } + source.finish() + } + + group.addTask { + var sum = 0 + for try await element in channel { + sum += element + } + } + } + } + + func testThrowsError() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + try await source.send(1) + try await source.send(2) + source.finish(throwing: CancellationError()) + + var elements = [Int]() + var iterator = channel.makeAsyncIterator() + + do { + while let element = try await iterator.next() { + elements.append(element) + } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + XCTAssertEqual(elements, [1, 2]) + } + + let element = try await iterator.next() + XCTAssertNil(element) + } + + func testAsyncSequenceWrite() async throws { + let (stream, continuation) = AsyncStream.makeStream() + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + continuation.yield(1) + continuation.yield(2) + continuation.finish() + + try await source.send(contentsOf: stream) + source.finish(throwing: nil) + + let elements = await channel.collect() + XCTAssertEqual(elements, [1, 2]) + } + + // MARK: NonThrowing + + func testNonThrowing() async throws { + let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndStream.channel + var source = consume channelAndStream.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await source.send(contentsOf: [1]) + } + } + + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.makeAsyncIterator() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + _ = await iterator.next() + _ = await iterator.next() + _ = await iterator.next() + + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + + group.cancelAll() + } + } +} + +extension AsyncSequence { + /// Collect all elements in the sequence into an array. + fileprivate func collect() async rethrows -> [Element] { + try await self.reduce(into: []) { accumulated, next in + accumulated.append(next) + } + } +} + +extension MultiProducerSingleConsumerChannel.Source.SendResult { + func assertIsProducerMore() { + switch self { + case .produceMore: + return () + + case .enqueueCallback: + XCTFail("Expected produceMore") + } + } + + func assertIsEnqueueCallback() { + switch self { + case .produceMore: + XCTFail("Expected enqueueCallback") + + case .enqueueCallback: + return () + } + } +} + +extension Optional where Wrapped: ~Copyable { + fileprivate mutating func take() -> Self { + let result = consume self + self = nil + return result + } +} From 4d7b2716a59f29bdafefa19c263bc1cf17278a9a Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 25 Mar 2025 14:52:39 +0100 Subject: [PATCH 02/16] Update proposal and implementation --- ...-mutli-producer-single-consumer-channel.md | 477 ++++++--- ...oducerSingleConsumerChannel+Internal.swift | 373 +++++-- .../MultiProducerSingleConsumerChannel.swift | 278 +++-- ...tiProducerSingleConsumerChannelTests.swift | 979 ++++++++++-------- .../Support/ManualExecutor.swift | 17 + 5 files changed, 1317 insertions(+), 807 deletions(-) create mode 100644 Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index 596a1ccc..f5f9cefa 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -1,4 +1,4 @@ -# MutliProducerSingleConsumerChannel +# MultiProducerSingleConsumerChannel * Proposal: [SAA-0016](0016-multi-producer-single-consumer-channel.md) * Authors: [Franz Busch](https://github.com/FranzBusch) @@ -6,6 +6,7 @@ * Status: **Implemented** ## Revision +- 2025/03/24: Adopt `~Copyable` for better performance. - 2023/12/18: Migrate proposal from Swift Evolution to Swift Async Algorithms. - 2023/12/19: Add element size dependent strategy - 2024/05/19: Rename to multi producer single consumer channel @@ -16,46 +17,64 @@ [SE-0314](https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md) introduced new `Async[Throwing]Stream` types which act as root asynchronous sequences. These two types allow bridging from synchronous callbacks such as -delegates to an asynchronous sequence. This proposal adds a new root -asynchronous sequence with the goal to bridge multi producer systems -into an asynchronous sequence. +delegates to an asynchronous sequence. This proposal adds a new root primitive +with the goal to model asynchronous multi-producer-single-consumer systems. ## Motivation -After using the `AsyncSequence` protocol and the `Async[Throwing]Stream` types -extensively over the past years, we learned that there are a few important -behavioral details that any `AsyncSequence` implementation needs to support. -These behaviors are: +After using the `AsyncSequence` protocol, the `Async[Throwing]Stream` types, and +the `Async[Throwing]Channel` types extensively over the past years, we learned +that there is a gap in the ecosystem for a type that provides strict +multi-producer-single-consumer guarantees with backpressure support. +Additionally, any type stream/channel like type needs to have a clear definition +about the following behaviors: 1. Backpressure 2. Multi/single consumer support 3. Downstream consumer termination 4. Upstream producer termination -In general, `AsyncSequence` implementations can be divided into two kinds: Root -asynchronous sequences that are the source of values such as -`Async[Throwing]Stream` and transformational asynchronous sequences such as -`AsyncMapSequence`. Most transformational asynchronous sequences implicitly -fulfill the above behaviors since they forward any demand to a base asynchronous -sequence that should implement the behaviors. On the other hand, root -asynchronous sequences need to make sure that all of the above behaviors are -correctly implemented. Let's look at the current behavior of -`Async[Throwing]Stream` to see if and how it achieves these behaviors. +The below sections are providing a detailed explanation of each of those. ### Backpressure -Root asynchronous sequences need to relay the backpressure to the producing -system. `Async[Throwing]Stream` aims to support backpressure by providing a -configurable buffer and returning -`Async[Throwing]Stream.Continuation.YieldResult` which contains the current -buffer depth from the `yield()` method. However, only providing the current -buffer depth on `yield()` is not enough to bridge a backpressured system into -an asynchronous sequence since this can only be used as a "stop" signal but we -are missing a signal to indicate resuming the production. The only viable -backpressure strategy that can be implemented with the current API is a timed -backoff where we stop producing for some period of time and then speculatively -produce again. This is a very inefficient pattern that produces high latencies -and inefficient use of resources. +In general, backpressure is the mechanism that prevents a fast producer from +overwhelming a slow consumer. It helps stability of the overall system by +regulating the flow of data between different components. Additionally, it +allows to put an upper bound on resource consumption of a system. In reality, +backpressure is used in almost all networked applications. + +In Swift, asynchronous sequence also have the concept of internal backpressure. +This modeled by the pull-based implementation where a consumer has to call +`next` on the `AsyncIterator`. In this model, there is no way for a consumer to +overwhelm a producer since the producer controls the rate of pulling elements. + +However, the internal backpressure of an asynchronous isn't the only +backpressure in play. There is also the source backpressure that is producing +the actual elements. For a backpressured system it is important that every +component of such a system is aware of the backpressure of its consumer and its +producer. + +Let's take a quick look how our current root asynchronous sequences are handling +this. + +`Async[Throwing]Stream` aims to support backpressure by providing a configurable +buffer and returning `Async[Throwing]Stream.Continuation.YieldResult` which +contains the current buffer depth from the `yield()` method. However, only +providing the current buffer depth on `yield()` is not enough to bridge a +backpressured system into an asynchronous sequence since this can only be used +as a "stop" signal but we are missing a signal to indicate resuming the +production. The only viable backpressure strategy that can be implemented with +the current API is a timed backoff where we stop producing for some period of +time and then speculatively produce again. This is a very inefficient pattern +that produces high latencies and inefficient use of resources. + +`Async[Throwing]Channel` is a multi-producer-multi-consumer channel that only +supports asynchronous producers. Additionally, the backpressure strategy is +fixed by a buffer size of 1 element per producer. + +We are currently lacking a type that supports a configurable backpressure +strategy and both asynchronous and synchronous producers. ### Multi/single consumer support @@ -84,8 +103,7 @@ this by calling the `finish()` or `finish(throwing:)` methods of the `Async[Throwing]Stream.Continuation`. However, `Async[Throwing]Stream` does not handle the case that the `Continuation` may be `deinit`ed before one of the finish methods is called. This currently leads to async streams that never -terminate. The behavior could be changed but it could result in semantically -breaking code. +terminate. ### Upstream producer termination @@ -108,39 +126,46 @@ asynchronous sequences in general. ## Proposed solution -The above motivation lays out the expected behaviors from a root asynchronous -sequence and compares them to the behaviors of `Async[Throwing]Stream`. These -are the behaviors where `Async[Throwing]Stream` diverges from the expectations. +The above motivation lays out the expected behaviors for any consumer/producer +system and compares them to the behaviors of `Async[Throwing]Stream` and +`Async[Throwing]Channel`. -- Backpressure: Doesn't expose a "resumption" signal to the producer -- Multi/single consumer: - - Divergent implementation between throwing and non-throwing variant - - Supports multiple consumers even though proposal positions it as a unicast - asynchronous sequence -- Consumer termination: Doesn't handle the `Continuation` being `deinit`ed -- Producer termination: Happens on first consumer termination +This section proposes a new type called `MultiProducerSingleConsumerChannel` +that implement all of the above-mentioned behaviors. Importantly, this proposed +solution is taking advantage of `~Copyable` types to model the +multi-producer-single-consumer behavior. While the current `AsyncSequence` +protocols are not supporting `~Copyable` types we provide a way to convert the +proposed channel to an asynchronous sequence. This leaves us room to support any +potential future asynchronous streaming protocol that supports `~Copyable`. -This section proposes a new type called `MutliProducerSingleConsumerChannel` that implement all of -the above-mentioned behaviors. +### Creating an MultiProducerSingleConsumerChannel -### Creating an MutliProducerSingleConsumerChannel - -You can create an `MutliProducerSingleConsumerChannel` instance using the new +You can create an `MultiProducerSingleConsumerChannel` instance using the new `makeChannel(of: backpressureStrategy:)` method. This method returns you the channel and the source. The source can be used to send new values to the asynchronous channel. The new API specifically provides a multi-producer/single-consumer pattern. ```swift -let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) + +// The channel and source can be extracted from the returned type +let channel = consume channelAndSource.channel +let source = consume channelAndSource.source ``` -The new proposed APIs offer three different ways to bridge a backpressured -system. The foundation is the multi-step synchronous interface. Below is an -example of how it can be used: +The new proposed APIs offer two different backpressure strategies: +- Watermark: Using a low and high watermark +- Unbounded: Unbounded buffering of the channel. **Only** use this if the + production is limited through some other mean. + +The source is used to send values to the channel. It provides different APIs for +synchronous and asynchronous producers. All of the APIs are relaying the +backpressure of the channel. The synchronous multi-step APIs are the foundation +for all other APIs. Below is an example of how it can be used: ```swift do { @@ -148,32 +173,34 @@ do { switch sendResult { case .produceMore: - // Trigger more production + // Trigger more production in the underlying system case .enqueueCallback(let callbackToken): + // There are enough values in the channel already. We need to enqueue + // a callback to get notified when we should produce more. source.enqueueCallback(token: callbackToken, onProduceMore: { result in switch result { case .success: - // Trigger more production + // Trigger more production in the underlying system case .failure(let error): // Terminate the underlying producer } }) } } catch { - // `send(contentsOf:)` throws if the asynchronous stream already terminated + // `send(contentsOf:)` throws if the channel already terminated } ``` The above API offers the most control and highest performance when bridging a -synchronous producer to an asynchronous sequence. First, you have to send -values using the `send(contentsOf:)` which returns a `SendResult`. The result -either indicates that more values should be produced or that a callback should -be enqueued by calling the `enqueueCallback(callbackToken: onProduceMore:)` -method. This callback is invoked once the backpressure strategy decided that -more values should be produced. This API aims to offer the most flexibility with -the greatest performance. The callback only has to be allocated in the case -where the producer needs to be suspended. +synchronous producer to a `MultiProducerSingleConsumerChannel`. First, you have +to send values using the `send(contentsOf:)` which returns a `SendResult`. The +result either indicates that more values should be produced or that a callback +should be enqueued by calling the `enqueueCallback(callbackToken: +onProduceMore:)` method. This callback is invoked once the backpressure strategy +decided that more values should be produced. This API aims to offer the most +flexibility with the greatest performance. The callback only has to be allocated +in the case where the producer needs to pause production. Additionally, the above API is the building block for some higher-level and easier-to-use APIs to send values to the channel. Below is an @@ -194,61 +221,101 @@ try source.send(contentsOf: sequence, onProduceMore: { result in try await source.send(contentsOf: sequence) ``` -With the above APIs, we should be able to effectively bridge any system into an -asynchronous sequence regardless if the system is callback-based, blocking or -asynchronous. +With the above APIs, we should be able to effectively bridge any system into a +`MultiProducerSingleConsumerChannel` regardless if the system is callback-based, +blocking, or asynchronous. + +### Multi producer + +To support multiple producers the source offers a `copy` method to produce a new +source. The source is returned `sending` so it is in a disconnected isolation +region than the original source allowing to pass it into a different isolation +region to concurrently produce elements. + +```swift +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) +) +var channel = consume channelAndSource.channel +var source1 = consume channelAndSource.source +var source2 = source1.copy() + +group.addTask { + try await source1.send(1) +} + +group.addTask() { + try await source2.send(2) +} + +print(await channel.next()) // Prints either 1 or 2 depending on which child task runs first +print(await channel.next()) // Prints either 1 or 2 depending on which child task runs first +``` ### Downstream consumer termination > When reading the next two examples around termination behaviour keep in mind -that the newly proposed APIs are providing a strict unicast asynchronous sequence. +that the newly proposed APIs are providing a strict a single consumer channel. Calling `finish()` terminates the downstream consumer. Below is an example of this: ```swift // Termination through calling finish -let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source -_ = try await source.send(1) +try await source.send(1) source.finish() -for try await element in channel { - print(element) -} -print("Finished") +print(await channel.next()) // Prints Optional(1) +print(await channel.next()) // Prints nil +``` -// Prints -// 1 -// Finished +If the channel has a failure type it can also be finished with an error. + +```swift +// Termination through calling finish +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: SomeError.self, + backpressureStrategy: .watermark(low: 2, high: 4) +) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source + +try await source.send(1) +source.finish(throwing: SomeError) + +print(try await channel.next()) // Prints Optional(1) +print(try await channel.next()) // Throws SomeError ``` The other way to terminate the consumer is by deiniting the source. This has the -same effect as calling `finish()` and makes sure that no consumer is stuck -indefinitely. +same effect as calling `finish()`. Since the source is a `~Copyable` type this +will happen automatically when the source is last used or explicitly consumed. ```swift // Termination through deiniting the source -let (channel, _) = MutliProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source -for await element in channel { - print(element) -} -print("Finished") +try await source.send(1) +_ = consume source // Explicitly consume the source -// Prints -// Finished +print(await channel.next()) // Prints Optional(1) +print(await channel.next()) // Prints nil ``` -Trying to send more elements after the source has been finish will result in an -error thrown from the send methods. - ### Upstream producer termination The producer will get notified about termination through the `onTerminate` @@ -256,49 +323,68 @@ callback. Termination of the producer happens in the following scenarios: ```swift // Termination through task cancellation -let (channel source) = MutliProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source +source.onTermination = { print("Terminated") } let task = Task { - for await element in channel { - - } + await channel.next() } -task.cancel() +task.cancel() // Prints Terminated ``` ```swift -// Termination through deiniting the sequence -let (_, source) = MutliProducerSingleConsumerChannel.makeChannel( +// Termination through deiniting the channel +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source +source.onTermination = { print("Terminated") } +_ = consume channel // Prints Terminated ``` ```swift -// Termination through deiniting the iterator -let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( +// Termination through finishing the source and consuming the last element +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) -_ = channel.makeAsyncIterator() +var channel = consume channelAndSource.channel +var source = consume channelAndSource.source +source.onTermination = { print("Terminated") } + +_ = try await source.send(1) +source.finish() + +print(await channel.next()) // Prints Optional(1) +await channel.next() // Prints Terminated ``` ```swift -// Termination through calling finish -let (channel, source) = MutliProducerSingleConsumerChannel.makeChannel( +// Termination through deiniting the last source and consuming the last element +let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) - -_ = try source.send(1) -source.finish() - -for await element in channel {} - -// onTerminate will be called after all elements have been consumed +var channel = consume channelAndSource.channel +var source1 = consume channelAndSource.source +var source2 = source1.copy() +source1.onTermination = { print("Terminated") } + +_ = try await source1.send(1) +_ = consume source1 +_ = try await source2.send(2) + +print(await channel.next()) // Prints Optional(1) +print(await channel.next()) // Prints Optional(2) +_ = consume source2 +await channel.next() // Prints Terminated ``` Similar to the downstream consumer termination, trying to send more elements after the @@ -307,22 +393,20 @@ producer has been terminated will result in an error thrown from the send method ## Detailed design ```swift +#if compiler(>=6.0) /// An error that is thrown from the various `send` methods of the /// ``MultiProducerSingleConsumerChannel/Source``. /// /// This error is thrown when the channel is already finished when /// trying to send new elements to the source. -public struct MultiProducerSingleConsumerChannelAlreadyFinishedError : Error { - - @usableFromInline - internal init() -} +public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { } /// A multi producer single consumer channel. /// /// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to -/// send values to the channel. The source exposes the internal backpressure of the asynchronous sequence to the -/// producer. Additionally, the source can be used from synchronous and asynchronous contexts. +/// send values to the channel. The channel supports different back pressure strategies to control the +/// buffering and demand. The channel will buffer values until its backpressure strategy decides that the +/// producer have to wait. /// /// /// ## Using a MultiProducerSingleConsumerChannel @@ -361,32 +445,66 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError : Error { /// /// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, /// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. -/// -/// ## Finishing the source -/// -/// To properly notify the consumer if the production of values has been finished the source's ``MultiProducerSingleConsumerChannel/Source/finish(throwing:)`` **must** be called. -public struct MultiProducerSingleConsumerChannel: AsyncSequence { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MultiProducerSingleConsumerChannel: ~Copyable { + /// A struct containing the initialized channel and source. + /// + /// This struct can be deconstructed by consuming the individual + /// components from it. + /// + /// ```swift + /// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + /// of: Int.self, + /// backpressureStrategy: .watermark(low: 5, high: 10) + /// ) + /// var channel = consume channelAndSource.channel + /// var source = consume channelAndSource.source + /// ``` + @frozen + public struct ChannelAndStream : ~Copyable { + /// The channel. + public var channel: MultiProducerSingleConsumerChannel + /// The source. + public var source: Source + } + /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. /// /// - Parameters: /// - elementType: The element type of the channel. /// - failureType: The failure type of the channel. - /// - BackpressureStrategy: The backpressure strategy that the channel should use. + /// - backpressureStrategy: The backpressure strategy that the channel should use. /// - Returns: A tuple containing the channel and its source. The source should be passed to the /// producer while the channel should be passed to the consumer. - public static func makeChannel(of elementType: Element.Type = Element.self, throwing failureType: Failure.Type = Never.self, backpressureStrategy: Source.BackpressureStrategy) -> (`Self`, Source) + public static func makeChannel( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Never.self, + backpressureStrategy: Source.BackpressureStrategy + ) -> ChannelAndStream + + /// Returns the next element. + /// + /// If this method returns `nil` it indicates that no further values can ever + /// be returned. The channel automatically closes when all sources have been deinited. + /// + /// If there are no elements and the channel has not been finished yet, this method will + /// suspend until an element is send to the channel. + /// + /// If the task calling this method is cancelled this method will return `nil`. + /// + /// - Parameter isolation: The callers isolation. + /// - Returns: The next buffered element. + public func next(isolation: isolated (any Actor)? = #isolation) async throws(Failure) -> Element? } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { /// A struct to send values to the channel. /// /// Use this source to provide elements to the channel by calling one of the `send` methods. - /// - /// - Important: You must terminate the source by calling ``finish(throwing:)``. - public struct Source: Sendable { - /// A strategy that handles the backpressure of the channel. + public struct Source: ~Copyable, Sendable { + /// A struct representing the backpressure of the channel. public struct BackpressureStrategy: Sendable { - /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. /// /// - Parameters: @@ -402,32 +520,48 @@ extension MultiProducerSingleConsumerChannel { /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. /// /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when - /// it is consumed from the channel, so it is recommended to provide an function that runs in constant time. - public static func watermark(low: Int, high: Int, waterLevelForElement: @escaping @Sendable (Element) -> Int) -> BackpressureStrategy + /// it is consumed from the channel, so it is recommended to provide a function that runs in constant time. + public static func watermark(low: Int, high: Int, waterLevelForElement: @escaping @Sendable (borrowing Element) -> Int) -> BackpressureStrategy + + /// An unbounded backpressure strategy. + /// + /// - Important: Only use this strategy if the production of elements is limited through some other mean. Otherwise + /// an unbounded backpressure strategy can result in infinite memory usage and cause + /// your process to run out of memory. + public static func unbounded() -> BackpressureStrategy } /// A type that indicates the result of sending elements to the source. - public enum SendResult: Sendable { - /// A token that is returned when the channel's backpressure strategy indicated that production should - /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. - public struct CallbackToken: Sendable { } + public enum SendResult: ~Copyable, Sendable { + /// An opaque token that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` + /// and ``MultiProducerSingleConsumerChannel/Source/cancelCallback(callbackToken:)``. + public struct CallbackToken: Sendable, Hashable { } - /// Indicates that more elements should be produced and written to the source. + /// Indicates that more elements should be produced and send to the source. case produceMore /// Indicates that a callback should be enqueued. /// - /// The associated token should be passed to the ``enqueueCallback(_:)`` method. + /// The associated token should be passed to the ````MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. case enqueueCallback(CallbackToken) } /// A callback to invoke when the channel finished. /// - /// The channel finishes and calls this closure in the following cases: - /// - No iterator was created and the sequence was deinited - /// - An iterator was created and deinited - /// - After ``finish(throwing:)`` was called and all elements have been consumed - public var onTermination: (@Sendable () -> Void)? { get set } + /// This is called after the last element has been consumed by the channel. + public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) { + self._storage.onTermination = callback + } + + /// Creates a new source which can be used to send elements to the channel concurrently. + /// + /// The channel will only automatically be finished if all existing sources have been deinited. + /// + /// - Returns: A new source for sending elements to the channel. + public mutating func copy() -> Source /// Sends new elements to the channel. /// @@ -437,7 +571,9 @@ extension MultiProducerSingleConsumerChannel { /// /// - Parameter sequence: The elements to send to the channel. /// - Returns: The result that indicates if more elements should be produced at this time. - public func send(contentsOf sequence: S) throws -> SendResult where Element == S.Element, S : Sequence + public mutating func send( + contentsOf sequence: consuming sending S + ) throws -> SendResult where Element == S.Element, S: Sequence /// Send the element to the channel. /// @@ -447,18 +583,21 @@ extension MultiProducerSingleConsumerChannel { /// /// - Parameter element: The element to send to the channel. /// - Returns: The result that indicates if more elements should be produced at this time. - public func send(_ element: Element) throws -> SendResult + public mutating func send(_ element: sending consuming Element) throws -> SendResult /// Enqueues a callback that will be invoked once more elements should be produced. /// /// Call this method after ``send(contentsOf:)-5honm`` or ``send(_:)-3jxzb`` returned ``SendResult/enqueueCallback(_:)``. /// - /// - Important: Enqueueing the same token multiple times is not allowed. + /// - Important: Enqueueing the same token multiple times is **not allowed**. /// /// - Parameters: /// - callbackToken: The callback token. /// - onProduceMore: The callback which gets invoked once more elements should be produced. - public func enqueueCallback(callbackToken: consuming SendResult.CallbackToken, onProduceMore: @escaping @Sendable (Result) -> Void) + public mutating func enqueueCallback( + callbackToken: consuming SendResult.CallbackToken, + onProduceMore: sending @escaping (Result + ) -> Void) /// Cancel an enqueued callback. /// @@ -468,7 +607,9 @@ extension MultiProducerSingleConsumerChannel { /// will mark the passed `callbackToken` as cancelled. /// /// - Parameter callbackToken: The callback token. - public func cancelCallback(callbackToken: consuming SendResult.CallbackToken) + public mutating func cancelCallback( + callbackToken: consuming SendResult.CallbackToken + ) /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. /// @@ -480,7 +621,10 @@ extension MultiProducerSingleConsumerChannel { /// - sequence: The elements to send to the channel. /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be /// invoked during the call to ``send(contentsOf:onProduceMore:)``. - public func send(contentsOf sequence: S, onProduceMore: @escaping @Sendable (Result) -> Void) where Element == S.Element, S : Sequence + public mutating func send( + contentsOf sequence: consuming sending S, + onProduceMore: @escaping @Sendable (Result) -> Void + ) where Element == S.Element, S: Sequence /// Sends the element to the channel. /// @@ -492,7 +636,10 @@ extension MultiProducerSingleConsumerChannel { /// - element: The element to send to the channel. /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be /// invoked during the call to ``send(_:onProduceMore:)``. - public func send(_ element: Element, onProduceMore: @escaping @Sendable (Result) -> Void) + public mutating func send( + _ element: consuming sending Element, + onProduceMore: @escaping @Sendable (Result + ) -> Void) /// Send new elements to the channel. /// @@ -504,7 +651,9 @@ extension MultiProducerSingleConsumerChannel { /// /// - Parameters: /// - sequence: The elements to send to the channel. - public func send(contentsOf sequence: S) async throws where Element == S.Element, S : Sequence + public mutating func send( + contentsOf sequence: consuming sending S + ) async throws where Element == S.Element, S: Sequence /// Send new element to the channel. /// @@ -516,57 +665,53 @@ extension MultiProducerSingleConsumerChannel { /// /// - Parameters: /// - element: The element to send to the channel. - public func send(_ element: Element) async throws + public mutating func send(_ element: consuming sending Element) async throws /// Send the elements of the asynchronous sequence to the channel. /// - /// This method returns once the provided asynchronous sequence or the channel finished. + /// This method returns once the provided asynchronous sequence or the channel finished. /// /// - Important: This method does not finish the source if consuming the upstream sequence terminated. /// /// - Parameters: /// - sequence: The elements to send to the channel. - public func send(contentsOf sequence: S) async throws where Element == S.Element, S : AsyncSequence + public mutating func send( + contentsOf sequence: consuming sending S + ) async throws where Element: Sendable, Element == S.Element, S: Sendable, S: AsyncSequence /// Indicates that the production terminated. /// - /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerChannel/next(isolation:)`` will return + /// `nil` or throw an error. /// /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept /// new elements. /// /// - Parameters: /// - error: The error to throw, or `nil`, to finish normally. - public func finish(throwing error: Failure? = nil) + public consuming func finish(throwing error: Failure? = nil) } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { - /// The asynchronous iterator for iterating the channel. + /// Converts the channel to an asynchronous sequence for consumption. /// - /// This type is not `Sendable`. Don't use it from multiple - /// concurrent contexts. It is a programmer error to invoke `next()` from a - /// concurrent context that contends with another such call, which - /// results in a call to `fatalError()`. - public struct Iterator: AsyncIteratorProtocol {} - - /// Creates the asynchronous iterator that produces elements of this - /// asynchronous sequence. - public func makeAsyncIterator() -> Iterator + /// - Important: The returned asynchronous sequence only supports a single iterator to be created and + /// will fatal error at runtime on subsequent calls to `makeAsyncIterator`. + public consuming func asyncSequence() -> some (AsyncSequence & Sendable) } - -extension MultiProducerSingleConsumerChannel: Sendable where Element : Sendable {} ``` -## Comparison to other root asynchronous sequences +## Comparison to other root asynchronous primitives ### swift-async-algorithm: AsyncChannel The `AsyncChannel` is a multi-consumer/multi-producer root asynchronous sequence which can be used to communicate between two tasks. It only offers asynchronous -production APIs and has no internal buffer. This means that any producer will be -suspended until its value has been consumed. `AsyncChannel` can handle multiple -consumers and resumes them in FIFO order. +production APIs and has an effective buffer of one per producer. This means that +any producer will be suspended until its value has been consumed. `AsyncChannel` +can handle multiple consumers and resumes them in FIFO order. ### swift-nio: NIOAsyncSequenceProducer diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift index 58b41ae1..9542e762 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift @@ -11,8 +11,10 @@ #if compiler(>=6.0) import DequeModule +import Synchronization -extension MultiProducerSingleConsumerChannel { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel { @usableFromInline enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { @usableFromInline @@ -31,14 +33,14 @@ extension MultiProducerSingleConsumerChannel { /// A closure that can be used to calculate the watermark impact of a single element @usableFromInline - let _waterLevelForElement: (@Sendable (Element) -> Int)? + let _waterLevelForElement: (@Sendable (borrowing Element) -> Int)? @usableFromInline var description: String { "watermark(\(self._currentWatermark))" } - init(low: Int, high: Int, waterLevelForElement: (@Sendable (Element) -> Int)?) { + init(low: Int, high: Int, waterLevelForElement: (@Sendable (borrowing Element) -> Int)?) { precondition(low <= high) self._low = low self._high = high @@ -48,7 +50,9 @@ extension MultiProducerSingleConsumerChannel { @inlinable mutating func didSend(elements: Deque.SubSequence) -> Bool { if let waterLevelForElement = self._waterLevelForElement { - self._currentWatermark += elements.reduce(0) { $0 + waterLevelForElement($1) } + for element in elements { + self._currentWatermark += waterLevelForElement(element) + } } else { self._currentWatermark += elements.count } @@ -135,24 +139,22 @@ extension MultiProducerSingleConsumerChannel { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { @usableFromInline - final class _Storage { + final class _Storage: Sendable { @usableFromInline - let _lock = Lock.allocate() - /// The state machine - @usableFromInline - var _stateMachine: _StateMachine + let _stateMachine: Mutex<_StateMachine> var onTermination: (@Sendable () -> Void)? { set { - self._lock.withLockVoid { - self._stateMachine._onTermination = newValue + self._stateMachine.withLock { + $0._onTermination = newValue } } get { - self._lock.withLock { - self._stateMachine._onTermination + self._stateMachine.withLock { + $0._onTermination } } } @@ -160,12 +162,12 @@ extension MultiProducerSingleConsumerChannel { init( backpressureStrategy: _InternalBackpressureStrategy ) { - self._stateMachine = .init(backpressureStrategy: backpressureStrategy) + self._stateMachine = .init(.init(backpressureStrategy: backpressureStrategy)) } - func sequenceDeinitialized() { - let action = self._lock.withLock { - self._stateMachine.sequenceDeinitialized() + func channelDeinitialized() { + let action = self._stateMachine.withLock { + $0.channelDeinitialized() } switch action { @@ -187,16 +189,17 @@ extension MultiProducerSingleConsumerChannel { break } } - - func iteratorInitialized() { - self._lock.withLockVoid { - self._stateMachine.iteratorInitialized() + + func sequenceInitialized() { + self._stateMachine.withLock { + $0.sequenceInitialized() } } + - func iteratorDeinitialized() { - let action = self._lock.withLock { - self._stateMachine.iteratorDeinitialized() + func sequenceDeinitialized() { + let action = self._stateMachine.withLock { + $0.sequenceDeinitialized() } switch action { @@ -219,9 +222,15 @@ extension MultiProducerSingleConsumerChannel { } } - func sourceDeinitialized() { - let action = self._lock.withLock { - self._stateMachine.sourceDeinitialized() + func iteratorInitialized() { + self._stateMachine.withLock { + $0.iteratorInitialized() + } + } + + func iteratorDeinitialized() { + let action = self._stateMachine.withLock { + $0.iteratorDeinitialized() } switch action { @@ -243,13 +252,40 @@ extension MultiProducerSingleConsumerChannel { break } } + + func sourceInitialized() { + self._stateMachine.withLock { + $0.sourceInitialized() + } + } + + func sourceDeinitialized() { + let action = self._stateMachine.withLock { + $0.sourceDeinitialized() + } + + switch action { + case .resumeConsumerAndCallOnTermination(let consumerContinuation, let failure, let onTermination): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) + } + + onTermination?() + + case .none: + break + } + } @inlinable func send( - contentsOf sequence: some Sequence + contentsOf sequence: sending some Sequence ) throws -> MultiProducerSingleConsumerChannel.Source.SendResult { - let action = self._lock.withLock { - return self._stateMachine.send(sequence) + let action = self._stateMachine.withLock { + $0.send(sequence) } switch action { @@ -277,8 +313,8 @@ extension MultiProducerSingleConsumerChannel { callbackToken: UInt64, continuation: UnsafeContinuation ) { - let action = self._lock.withLock { - self._stateMachine.enqueueContinuation(callbackToken: callbackToken, continuation: continuation) + let action = self._stateMachine.withLock { + $0.enqueueContinuation(callbackToken: callbackToken, continuation: continuation) } switch action { @@ -298,8 +334,8 @@ extension MultiProducerSingleConsumerChannel { callbackToken: UInt64, onProduceMore: sending @escaping (Result) -> Void ) { - let action = self._lock.withLock { - self._stateMachine.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) + let action = self._stateMachine.withLock { + $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) } switch action { @@ -318,8 +354,8 @@ extension MultiProducerSingleConsumerChannel { func cancelProducer( callbackToken: UInt64 ) { - let action = self._lock.withLock { - self._stateMachine.cancelProducer(callbackToken: callbackToken) + let action = self._stateMachine.withLock { + $0.cancelProducer(callbackToken: callbackToken) } switch action { @@ -338,8 +374,8 @@ extension MultiProducerSingleConsumerChannel { @inlinable func finish(_ failure: Failure?) { - let action = self._lock.withLock { - self._stateMachine.finish(failure) + let action = self._stateMachine.withLock { + $0.finish(failure) } switch action { @@ -372,9 +408,9 @@ extension MultiProducerSingleConsumerChannel { } @inlinable - func next(isolation actor: isolated (any Actor)?) async throws -> Element? { - let action = self._lock.withLock { - self._stateMachine.next() + func next(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { + let action = self._stateMachine.withLock { + $0.next() } switch action { @@ -407,16 +443,16 @@ extension MultiProducerSingleConsumerChannel { return nil case .suspendTask: - return try await self.suspendNext(isolation: actor) + return try await self.suspendNext() } } @inlinable - func suspendNext(isolation actor: isolated (any Actor)?) async throws -> Element? { + func suspendNext(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { return try await withTaskCancellationHandler { - return try await withUnsafeThrowingContinuation { continuation in - let action = self._lock.withLock { - self._stateMachine.suspendNext(continuation: continuation) + return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + let action = self._stateMachine.withLock { + $0.suspendNext(continuation: continuation) } switch action { @@ -428,7 +464,7 @@ extension MultiProducerSingleConsumerChannel { for producerContinuation in producerContinuations { switch producerContinuation { case .closure(let onProduceMore): - onProduceMore(.failure(CancellationError())) + onProduceMore(.success(())) case .continuation(let continuation): continuation.resume() } @@ -452,8 +488,8 @@ extension MultiProducerSingleConsumerChannel { } } } onCancel: { - let action = self._lock.withLock { - self._stateMachine.cancelNext() + let action = self._stateMachine.withLock { + $0.cancelNext() } switch action { @@ -480,7 +516,8 @@ extension MultiProducerSingleConsumerChannel { } } -extension MultiProducerSingleConsumerChannel._Storage { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel._Storage { /// The state machine of the channel. @usableFromInline struct _StateMachine: ~Copyable { @@ -525,11 +562,12 @@ extension MultiProducerSingleConsumerChannel._Storage { .init( backpressureStrategy: backpressureStrategy, iteratorInitialized: false, + sequenceInitialized: false, buffer: .init(), producerContinuations: .init(), cancelledAsyncProducers: .init(), hasOutstandingDemand: true, - activeProducers: 1, + activeProducers: 0, nextCallbackTokenID: 0 ) ) @@ -539,21 +577,36 @@ extension MultiProducerSingleConsumerChannel._Storage { init(state: consuming _State) { self._state = state } - + + @inlinable + mutating func sourceInitialized() { + switch consume self._state { + case .channeling(var channeling): + channeling.activeProducers += 1 + self = .init(state: .channeling(channeling)) + + case .sourceFinished(let sourceFinished): + self = .init(state: .sourceFinished(sourceFinished)) + + case .finished(let finished): + self = .init(state: .finished(finished)) + } + } + /// Actions returned by `sourceDeinitialized()`. @usableFromInline - enum SourceDeinitializedAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((@Sendable () -> Void)?) - /// Indicates that all producers should be failed and `onTermination` should be called. - case failProducersAndCallOnTermination( - _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, - (@Sendable () -> Void)? + enum SourceDeinitialized { + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination` should be called. + case resumeConsumerAndCallOnTermination( + consumerContinuation: UnsafeContinuation, + failure: Failure?, + onTermination: (() -> Void)? ) } @inlinable - mutating func sourceDeinitialized() -> SourceDeinitializedAction? { + mutating func sourceDeinitialized() -> SourceDeinitialized? { switch consume self._state { case .channeling(var channeling): channeling.activeProducers -= 1 @@ -561,41 +614,77 @@ extension MultiProducerSingleConsumerChannel._Storage { if channeling.activeProducers == 0 { // This was the last producer so we can transition to source finished now - self = .init(state: .sourceFinished(.init( + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we are just going to mark + // the source as finished. + self = .init(state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: nil, + onTermination: channeling.onTermination + )) + ) + + return nil + } + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(channeling.buffer.isEmpty, "Expected an empty buffer") + + self = .init(state: .finished(.init( iteratorInitialized: channeling.iteratorInitialized, - buffer: channeling.buffer + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true ))) - if channeling.suspendedProducers.isEmpty { - return .callOnTermination(channeling.onTermination) - } else { - return .failProducersAndCallOnTermination( - .init(channeling.suspendedProducers.lazy.map { $0.1 }), - channeling.onTermination - ) - } + return .resumeConsumerAndCallOnTermination( + consumerContinuation: consumerContinuation, + failure: nil, + onTermination: channeling.onTermination + ) } else { // We still have more producers self = .init(state: .channeling(channeling)) return nil } + case .sourceFinished(let sourceFinished): - // This can happen if one producer calls finish and another deinits afterwards + // If the source has finished, finishing again has no effect. self = .init(state: .sourceFinished(sourceFinished)) - return nil - case .finished(let finished): - // This can happen if the consumer finishes and the producers deinit - self = .init(state: .finished(finished)) + return .none - return nil + case .finished(var finished): + finished.sourceFinished = true + self = .init(state: .finished(finished)) + return .none + } + } + + @inlinable + mutating func sequenceInitialized() { + switch consume self._state { + case .channeling(var channeling): + channeling.sequenceInitialized = true + self = .init(state: .channeling(channeling)) + + case .sourceFinished(var sourceFinished): + sourceFinished.sequenceInitialized = true + self = .init(state: .sourceFinished(sourceFinished)) + + case .finished(var finished): + finished.sequenceInitialized = true + self = .init(state: .finished(finished)) } } /// Actions returned by `sequenceDeinitialized()`. @usableFromInline - enum SequenceDeinitializedAction { + enum ChannelOrSequenceDeinitializedAction { /// Indicates that `onTermination` should be called. case callOnTermination((@Sendable () -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. @@ -606,12 +695,17 @@ extension MultiProducerSingleConsumerChannel._Storage { } @inlinable - mutating func sequenceDeinitialized() -> SequenceDeinitializedAction? { + mutating func sequenceDeinitialized() -> ChannelOrSequenceDeinitializedAction? { switch consume self._state { case .channeling(let channeling): guard channeling.iteratorInitialized else { + precondition(channeling.sequenceInitialized, "Sequence was not initialized") // No iterator was created so we can transition to finished right away. - self = .init(state: .finished(.init(iteratorInitialized: false, sourceFinished: false))) + self = .init(state: .finished(.init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: false + ))) return .failProducersAndCallOnTermination( .init(channeling.suspendedProducers.lazy.map { $0.1 }), @@ -626,8 +720,14 @@ extension MultiProducerSingleConsumerChannel._Storage { case .sourceFinished(let sourceFinished): guard sourceFinished.iteratorInitialized else { + precondition(sourceFinished.sequenceInitialized, "Sequence was not initialized") // No iterator was created so we can transition to finished right away. - self = .init(state: .finished(.init(iteratorInitialized: false, sourceFinished: true))) + self = .init(state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: true + ))) return .callOnTermination(sourceFinished.onTermination) } @@ -645,6 +745,55 @@ extension MultiProducerSingleConsumerChannel._Storage { return .none } } + + @inlinable + mutating func channelDeinitialized() -> ChannelOrSequenceDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + if channeling.sequenceInitialized { + // An async sequence was created so we need to ignore this deinit + self = .init(state: .channeling(channeling)) + return nil + } else { + // No async sequence was created so we can transition to finished + self = .init(state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ))) + + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) + } + + case .sourceFinished(let sourceFinished): + if sourceFinished.sequenceInitialized { + // An async sequence was created so we need to ignore this deinit + self = .init(state: .sourceFinished(sourceFinished)) + return nil + } else { + // No async sequence was created so we can transition to finished + self = .init(state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ))) + + return .callOnTermination(sourceFinished.onTermination) + } + + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } @inlinable mutating func iteratorInitialized() { @@ -674,7 +823,11 @@ extension MultiProducerSingleConsumerChannel._Storage { // Our sequence is a unicast sequence and does not support multiple AsyncIterator's fatalError("Only a single AsyncIterator can be created") } else { - self = .init(state: .finished(.init(iteratorInitialized: true, sourceFinished: finished.sourceFinished))) + self = .init(state: .finished(.init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: finished.sourceFinished + ))) } } } @@ -698,7 +851,11 @@ extension MultiProducerSingleConsumerChannel._Storage { if channeling.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. - self = .init(state: .finished(.init(iteratorInitialized: true, sourceFinished: false))) + self = .init(state: .finished(.init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: false + ))) return .failProducersAndCallOnTermination( .init(channeling.suspendedProducers.lazy.map { $0.1 }), @@ -713,7 +870,11 @@ extension MultiProducerSingleConsumerChannel._Storage { if sourceFinished.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. - self = .init(state: .finished(.init(iteratorInitialized: true, sourceFinished: true))) + self = .init(state: .finished(.init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: true + ))) return .callOnTermination(sourceFinished.onTermination) } else { @@ -782,7 +943,7 @@ extension MultiProducerSingleConsumerChannel._Storage { } @inlinable - mutating func send(_ sequence: some Sequence) -> SendAction { + mutating func send(_ sequence: sending some Sequence) -> SendAction { switch consume self._state { case .channeling(var channeling): // We have an element and can resume the continuation @@ -1008,6 +1169,7 @@ extension MultiProducerSingleConsumerChannel._Storage { self = .init(state: .sourceFinished( .init( iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, buffer: channeling.buffer, failure: failure, onTermination: channeling.onTermination @@ -1021,7 +1183,11 @@ extension MultiProducerSingleConsumerChannel._Storage { // and resume the continuation with the failure precondition(channeling.buffer.isEmpty, "Expected an empty buffer") - self = .init(state: .finished(.init(iteratorInitialized: channeling.iteratorInitialized, sourceFinished: true))) + self = .init(state: .finished(.init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ))) return .resumeConsumerAndCallOnTermination( consumerContinuation: consumerContinuation, @@ -1095,7 +1261,11 @@ extension MultiProducerSingleConsumerChannel._Storage { // Check if we have an element left in the buffer and return it guard let element = sourceFinished.buffer.popFirst() else { // We are returning the queued failure now and can transition to finished - self = .init(state: .finished(.init(iteratorInitialized: sourceFinished.iteratorInitialized, sourceFinished: true))) + self = .init(state: .finished(.init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ))) return .returnFailureAndCallOnTermination(sourceFinished.failure, sourceFinished.onTermination) } @@ -1170,7 +1340,11 @@ extension MultiProducerSingleConsumerChannel._Storage { // Check if we have an element left in the buffer and return it guard let element = sourceFinished.buffer.popFirst() else { // We are returning the queued failure now and can transition to finished - self = .init(state: .finished(.init(iteratorInitialized: sourceFinished.iteratorInitialized, sourceFinished: true))) + self = .init(state: .finished(.init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ))) return .resumeConsumerWithFailureAndCallOnTermination( continuation, @@ -1202,7 +1376,11 @@ extension MultiProducerSingleConsumerChannel._Storage { mutating func cancelNext() -> CancelNextAction? { switch consume self._state { case .channeling(let channeling): - self = .init(state: .finished(.init(iteratorInitialized: channeling.iteratorInitialized, sourceFinished: false))) + self = .init(state: .finished(.init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: false + ))) guard let consumerContinuation = channeling.consumerContinuation else { return .failProducersAndCallOnTermination( @@ -1233,6 +1411,7 @@ extension MultiProducerSingleConsumerChannel._Storage { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel._Storage._StateMachine { @usableFromInline enum _State: ~Copyable { @@ -1245,6 +1424,10 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool /// The onTermination callback. @usableFromInline @@ -1286,6 +1469,7 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { init( backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy, iteratorInitialized: Bool, + sequenceInitialized: Bool, onTermination: (@Sendable () -> Void)? = nil, buffer: Deque, consumerContinuation: UnsafeContinuation? = nil, @@ -1297,6 +1481,7 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { ) { self.backpressureStrategy = backpressureStrategy self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized self.onTermination = onTermination self.buffer = buffer self.consumerContinuation = consumerContinuation @@ -1321,6 +1506,10 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool /// The buffer of elements. @usableFromInline @@ -1341,11 +1530,13 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { @inlinable init( iteratorInitialized: Bool, + sequenceInitialized: Bool, buffer: Deque, failure: Failure? = nil, onTermination: (@Sendable () -> Void)? = nil ) { self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized self.buffer = buffer self.failure = failure self.onTermination = onTermination @@ -1357,6 +1548,10 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool /// Indicates if the source was finished. @usableFromInline @@ -1369,9 +1564,11 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { @inlinable init( iteratorInitialized: Bool, + sequenceInitialized: Bool, sourceFinished: Bool ) { self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized self.sourceFinished = sourceFinished } } diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index 5e860a89..e30eaae2 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -23,8 +23,9 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { /// A multi producer single consumer channel. /// /// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to -/// send values to the channel. The source exposes the internal backpressure of the asynchronous sequence to the -/// producer. Additionally, the source can be used from synchronous and asynchronous contexts. +/// send values to the channel. The channel supports different back pressure strategies to control the +/// buffering and demand. The channel will buffer values until its backpressure strategy decides that the +/// producer have to wait. /// /// /// ## Using a MultiProducerSingleConsumerChannel @@ -63,36 +64,34 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { /// /// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, /// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. -/// -/// ## Finishing the source -/// -/// To properly notify the consumer if the production of values has been finished the source's ``MultiProducerSingleConsumerChannel/Source/finish(throwing:)`` **must** be called. -public struct MultiProducerSingleConsumerChannel: AsyncSequence { - /// A private class to give the ``MultiProducerSingleConsumerChannel`` a deinit so we - /// can tell the producer when any potential consumer went away. - private final class _Backing: Sendable { - /// The underlying storage. - fileprivate let storage: _Storage - - init(storage: _Storage) { - self.storage = storage - } - - deinit { - storage.sequenceDeinitialized() - } - } - +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct MultiProducerSingleConsumerChannel: ~Copyable { /// The backing storage. - private let backing: _Backing + @usableFromInline + let storage: _Storage + /// A struct containing the initialized channel and source. + /// + /// This struct can be deconstructed by consuming the individual + /// components from it. + /// + /// ```swift + /// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + /// of: Int.self, + /// backpressureStrategy: .watermark(low: 5, high: 10) + /// ) + /// var channel = consume channelAndSource.channel + /// var source = consume channelAndSource.source + /// ``` @frozen public struct ChannelAndStream: ~Copyable { + /// The channel. public var channel: MultiProducerSingleConsumerChannel + /// The source. public var source: Source - public init( - channel: MultiProducerSingleConsumerChannel, + init( + channel: consuming MultiProducerSingleConsumerChannel, source: consuming Source ) { self.channel = channel @@ -105,8 +104,8 @@ public struct MultiProducerSingleConsumerChannel: Async /// - Parameters: /// - elementType: The element type of the channel. /// - failureType: The failure type of the channel. - /// - BackpressureStrategy: The backpressure strategy that the channel should use. - /// - Returns: A tuple containing the channel and its source. The source should be passed to the + /// - backpressureStrategy: The backpressure strategy that the channel should use. + /// - Returns: A struct containing the channel and its source. The source should be passed to the /// producer while the channel should be passed to the consumer. public static func makeChannel( of elementType: Element.Type = Element.self, @@ -122,18 +121,46 @@ public struct MultiProducerSingleConsumerChannel: Async } init(storage: _Storage) { - self.backing = .init(storage: storage) + self.storage = storage + } + + deinit { + self.storage.channelDeinitialized() + } + + /// Returns the next element. + /// + /// If this method returns `nil` it indicates that no further values can ever + /// be returned. The channel automatically closes when all sources have been deinited. + /// + /// If there are no elements and the channel has not been finished yet, this method will + /// suspend until an element is send to the channel. + /// + /// If the task calling this method is cancelled this method will return `nil`. + /// + /// - Parameter isolation: The callers isolation. + /// - Returns: The next buffered element. + @inlinable + public mutating func next( + isolation: isolated (any Actor)? = #isolation + ) async throws(Failure) -> Element? { + do { + return try await self.storage.next() + } catch { + // This force-cast is safe since we only allow closing the source with this failure + // We only need this force cast since continuations don't support typed throws yet. + throw error as! Failure + } } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { /// A struct to send values to the channel. /// /// Use this source to provide elements to the channel by calling one of the `send` methods. - /// - /// - Important: You must terminate the source by calling ``finish(throwing:)``. public struct Source: ~Copyable, Sendable { - /// A strategy that handles the backpressure of the channel. + /// A struct representing the backpressure of the channel. public struct BackpressureStrategy: Sendable { var internalBackpressureStrategy: _InternalBackpressureStrategy @@ -158,11 +185,11 @@ extension MultiProducerSingleConsumerChannel { /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. /// /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when - /// it is consumed from the channel, so it is recommended to provide an function that runs in constant time. + /// it is consumed from the channel, so it is recommended to provide a function that runs in constant time. public static func watermark( low: Int, high: Int, - waterLevelForElement: @escaping @Sendable (Element) -> Int // TODO: In the future this should become sending + waterLevelForElement: @escaping @Sendable (borrowing Element) -> Int ) -> BackpressureStrategy { .init( internalBackpressureStrategy: .watermark( @@ -174,8 +201,8 @@ extension MultiProducerSingleConsumerChannel { /// An unbounded backpressure strategy. /// /// - Important: Only use this strategy if the production of elements is limited through some other mean. Otherwise - /// an unbounded backpressure strategy can result in infinite memory usage and open your application to denial of service - /// attacks. + /// an unbounded backpressure strategy can result in infinite memory usage and cause + /// your process to run out of memory. public static func unbounded() -> BackpressureStrategy { .init( internalBackpressureStrategy: .unbounded(.init()) @@ -185,9 +212,12 @@ extension MultiProducerSingleConsumerChannel { /// A type that indicates the result of sending elements to the source. public enum SendResult: ~Copyable, Sendable { - /// A token that is returned when the channel's backpressure strategy indicated that production should - /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. - public struct CallbackToken: Sendable { + /// An opaque token that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` + /// and ``MultiProducerSingleConsumerChannel/Source/cancelCallback(callbackToken:)``. + public struct CallbackToken: Sendable, Hashable { @usableFromInline let _id: UInt64 @@ -197,49 +227,40 @@ extension MultiProducerSingleConsumerChannel { } } - /// Indicates that more elements should be produced and written to the source. + /// Indicates that more elements should be produced and send to the source. case produceMore /// Indicates that a callback should be enqueued. /// - /// The associated token should be passed to the ``enqueueCallback(_:)`` method. + /// The associated token should be passed to the ````MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. case enqueueCallback(CallbackToken) } - - /// A callback to invoke when the channel finished. - /// - /// The channel finishes and calls this closure in the following cases: - /// - No iterator was created and the sequence was deinited - /// - An iterator was created and deinited - /// - After ``finish(throwing:)`` was called and all elements have been consumed - public var onTermination: (@Sendable () -> Void)? { - set { - self._storage.onTermination = newValue - } - get { - self._storage.onTermination - } - } - @usableFromInline let _storage: _Storage internal init(storage: _Storage) { self._storage = storage + self._storage.sourceInitialized() } deinit { self._storage.sourceDeinitialized() } + /// Sets a callback to invoke when the channel terminated. + /// + /// This is called after the last element has been consumed by the channel. + public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) { + self._storage.onTermination = callback + } /// Creates a new source which can be used to send elements to the channel concurrently. /// /// The channel will only automatically be finished if all existing sources have been deinited. /// /// - Returns: A new source for sending elements to the channel. - public mutating func copy() -> Self { + public mutating func copy() -> sending Self { .init(storage: self._storage) } @@ -252,7 +273,9 @@ extension MultiProducerSingleConsumerChannel { /// - Parameter sequence: The elements to send to the channel. /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable - public mutating func send(contentsOf sequence: sending S) throws -> SendResult where Element == S.Element, S: Sequence { + public mutating func send( + contentsOf sequence: consuming sending S + ) throws -> SendResult where Element == S.Element, S: Sequence, Element: Copyable { try self._storage.send(contentsOf: sequence) } @@ -265,15 +288,15 @@ extension MultiProducerSingleConsumerChannel { /// - Parameter element: The element to send to the channel. /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable - public mutating func send(_ element: sending Element) throws -> SendResult { + public mutating func send(_ element: consuming sending Element) throws -> SendResult { try self._storage.send(contentsOf: CollectionOfOne(element)) } /// Enqueues a callback that will be invoked once more elements should be produced. /// - /// Call this method after ``send(contentsOf:)-5honm`` or ``send(_:)-3jxzb`` returned ``SendResult/enqueueCallback(_:)``. + /// Call this method after ``send(contentsOf:)-65yju`` or ``send(_:)`` returned ``SendResult/enqueueCallback(_:)``. /// - /// - Important: Enqueueing the same token multiple times is not allowed. + /// - Important: Enqueueing the same token multiple times is **not allowed**. /// /// - Parameters: /// - callbackToken: The callback token. @@ -311,9 +334,9 @@ extension MultiProducerSingleConsumerChannel { /// invoked during the call to ``send(contentsOf:onProduceMore:)``. @inlinable public mutating func send( - contentsOf sequence: sending S, + contentsOf sequence: consuming sending S, onProduceMore: @escaping @Sendable (Result) -> Void - ) where Element == S.Element, S: Sequence { + ) where Element == S.Element, S: Sequence, Element: Copyable { do { let sendResult = try self.send(contentsOf: sequence) @@ -341,10 +364,22 @@ extension MultiProducerSingleConsumerChannel { /// invoked during the call to ``send(_:onProduceMore:)``. @inlinable public mutating func send( - _ element: sending Element, + _ element: consuming sending Element, onProduceMore: @escaping @Sendable (Result) -> Void ) { - self.send(contentsOf: CollectionOfOne(element), onProduceMore: onProduceMore) + do { + let sendResult = try self.send(element) + + switch consume sendResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(let callbackToken): + self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + } catch { + onProduceMore(.failure(error)) + } } /// Send new elements to the channel. @@ -358,8 +393,11 @@ extension MultiProducerSingleConsumerChannel { /// - Parameters: /// - sequence: The elements to send to the channel. @inlinable - public mutating func send(contentsOf sequence: sending S) async throws where Element == S.Element, S: Sequence { - let sendResult = try { try self.send(contentsOf: sequence) }() + public mutating func send( + contentsOf sequence: consuming sending S + ) async throws where Element == S.Element, S: Sequence, Element: Copyable { + let syncSend: (sending S, inout sending Self) throws -> SendResult = { try $1.send(contentsOf: $0) } + let sendResult = try syncSend(sequence, &self) switch consume sendResult { case .produceMore: @@ -392,28 +430,49 @@ extension MultiProducerSingleConsumerChannel { /// - Parameters: /// - element: The element to send to the channel. @inlinable - public mutating func send(_ element: sending Element) async throws { - try await self.send(contentsOf: CollectionOfOne(element)) + public mutating func send(_ element: consuming sending Element) async throws { + let syncSend: (consuming sending Element, inout sending Self) throws -> SendResult = { try $1.send($0) } + let sendResult = try syncSend(element, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackToken): + let id = callbackToken._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) + } + } } /// Send the elements of the asynchronous sequence to the channel. /// - /// This method returns once the provided asynchronous sequence or the channel finished. + /// This method returns once the provided asynchronous sequence or the channel finished. /// /// - Important: This method does not finish the source if consuming the upstream sequence terminated. /// /// - Parameters: /// - sequence: The elements to send to the channel. @inlinable - public mutating func send(contentsOf sequence: sending S) async throws where Element == S.Element, S: AsyncSequence { - for try await element in sequence { + public mutating func send(contentsOf sequence: consuming sending S) async throws where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { + for try await element in sequence { try await self.send(contentsOf: CollectionOfOne(element)) } } /// Indicates that the production terminated. /// - /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerChannel/next(isolation:)`` will return + /// `nil` or throw an error. /// /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept /// new elements. @@ -427,20 +486,51 @@ extension MultiProducerSingleConsumerChannel { } } -extension MultiProducerSingleConsumerChannel { - /// The asynchronous iterator for iterating the channel. + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel where Element: Copyable { + struct ChannelAsyncSequence: AsyncSequence { + @usableFromInline + final class _Backing: Sendable { + @usableFromInline + let storage: MultiProducerSingleConsumerChannel._Storage + + init(storage: MultiProducerSingleConsumerChannel._Storage) { + self.storage = storage + self.storage.sequenceInitialized() + } + + deinit { + self.storage.sequenceDeinitialized() + } + } + + @usableFromInline + let _backing: _Backing + + public func makeAsyncIterator() -> Self.Iterator { + .init(storage: self._backing.storage) + } + } + + /// Converts the channel to an asynchronous sequence for consumption. /// - /// This type is not `Sendable`. Don't use it from multiple - /// concurrent contexts. It is a programmer error to invoke `next()` from a - /// concurrent context that contends with another such call, which - /// results in a call to `fatalError()`. - public struct Iterator: AsyncIteratorProtocol { + /// - Important: The returned asynchronous sequence only supports a single iterator to be created and + /// will fatal error at runtime on subsequent calls to `makeAsyncIterator`. + public consuming func asyncSequence() -> some (AsyncSequence & Sendable) { + ChannelAsyncSequence(_backing: .init(storage: self.storage)) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence where Element: Copyable { + struct Iterator: AsyncIteratorProtocol { @usableFromInline final class _Backing { @usableFromInline - let storage: _Storage + let storage: MultiProducerSingleConsumerChannel._Storage - init(storage: _Storage) { + init(storage: MultiProducerSingleConsumerChannel._Storage) { self.storage = storage self.storage.iteratorInitialized() } @@ -453,18 +543,12 @@ extension MultiProducerSingleConsumerChannel { @usableFromInline let _backing: _Backing - init(storage: _Storage) { + init(storage: MultiProducerSingleConsumerChannel._Storage) { self._backing = .init(storage: storage) } - @_disfavoredOverload - @inlinable - public mutating func next() async throws -> Element? { - try await self._backing.storage.next(isolation: nil) - } - @inlinable - public mutating func next( + mutating func next( isolation actor: isolated (any Actor)? = #isolation ) async throws(Failure) -> Element? { do { @@ -474,16 +558,8 @@ extension MultiProducerSingleConsumerChannel { } } } - - /// Creates the asynchronous iterator that produces elements of this - /// asynchronous sequence. - public func makeAsyncIterator() -> Iterator { - Iterator(storage: self.backing.storage) - } } - -extension MultiProducerSingleConsumerChannel: Sendable where Element: Sendable {} - -@available(*, unavailable) -extension MultiProducerSingleConsumerChannel.Iterator: Sendable {} +// +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence: Sendable {} #endif diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift index e15fccf7..a8d60fdc 100644 --- a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift @@ -14,234 +14,324 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class MultiProducerSingleConsumerChannelTests: XCTestCase { - // MARK: - sequenceDeinitialized + // MARK: - sourceDeinitialized + + func testSourceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel = consume channelAndSource.channel + let source = consume channelAndSource.source + + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { + didTerminate = true + } + + group.addTask(executorPreference: manualExecutor) { + await channel.next() + } + + withExtendedLifetime(source) { } + _ = consume source + XCTAssertFalse(didTerminate) + manualExecutor.run() + _ = try await group.next() + XCTAssertTrue(didTerminate) + } + } + + func testSourceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel = channelAndSource.channel + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { + didTerminate = true + } + + group.addTask(executorPreference: manualExecutor) { + await channel.next() + } + manualExecutor.run() + XCTAssertFalse(didTerminate) + + withExtendedLifetime(source) { } + _ = consume source + XCTAssertTrue(didTerminate) + manualExecutor.run() + _ = try await group.next() + } + } + + func testSourceDeinitialized_whenMultipleSources() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + var channel = consume channelAndSource.channel + var source1 = consume channelAndSource.source + var source2 = source1.copy() + nonisolated(unsafe) var didTerminate = false + source1.setOnTerminationCallback { + didTerminate = true + } - // Following tests are disabled since the channel is not getting deinited due to a known bug + _ = try await source1.send(1) + XCTAssertFalse(didTerminate) + _ = consume source1 + XCTAssertFalse(didTerminate) + _ = try await source2.send(2) + XCTAssertFalse(didTerminate) + + _ = await channel.next() + XCTAssertFalse(didTerminate) + _ = await channel.next() + XCTAssertFalse(didTerminate) + _ = consume source2 + _ = await channel.next() + XCTAssertTrue(didTerminate) + } + + func testSourceDeinitialized_whenSourceFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.setOnTerminationCallback { + onTerminationContinuation.finish() + } -// func testSequenceDeinitialized_whenNoIterator() async throws { -// var channelAndStream: MultiProducerSingleConsumerChannel.ChannelAndStream! = MultiProducerSingleConsumerChannel.makeChannel( -// of: Int.self, -// backpressureStrategy: .watermark(low: 5, high: 10) -// ) -// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel -// var source = channelAndStream.source -// channelAndStream = nil -// -// let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() -// source.onTermination = { -// onTerminationContinuation.finish() -// } -// -// await withThrowingTaskGroup(of: Void.self) { group in -// group.addTask { -// onTerminationContinuation.yield() -// try await Task.sleep(for: .seconds(10)) -// } -// -// var onTerminationIterator = onTerminationStream.makeAsyncIterator() -// _ = await onTerminationIterator.next() -// -// withExtendedLifetime(channel) {} -// channel = nil -// -// let terminationResult: Void? = await onTerminationIterator.next() -// XCTAssertNil(terminationResult) -// -// do { -// _ = try { try source.send(2) }() -// XCTFail("Expected an error to be thrown") -// } catch { -// XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) -// } -// -// group.cancelAll() -// } -// } -// -// func testSequenceDeinitialized_whenIterator() async throws { -// let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( -// of: Int.self, -// backpressureStrategy: .watermark(low: 5, high: 10) -// ) -// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel -// var source = consume channelAndStream.source -// -// var iterator = channel?.makeAsyncIterator() -// -// let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() -// source.onTermination = { -// onTerminationContinuation.finish() -// } -// -// try await withThrowingTaskGroup(of: Void.self) { group in -// group.addTask { -// while !Task.isCancelled { -// onTerminationContinuation.yield() -// try await Task.sleep(for: .seconds(0.2)) -// } -// } -// -// var onTerminationIterator = onTerminationStream.makeAsyncIterator() -// _ = await onTerminationIterator.next() -// -// try withExtendedLifetime(channel) { -// let writeResult = try source.send(1) -// writeResult.assertIsProducerMore() -// } -// -// channel = nil -// -// do { -// let writeResult = try { try source.send(2) }() -// writeResult.assertIsProducerMore() -// } catch { -// XCTFail("Expected no error to be thrown") -// } -// -// let element1 = await iterator?.next() -// XCTAssertEqual(element1, 1) -// let element2 = await iterator?.next() -// XCTAssertEqual(element2, 2) -// -// group.cancelAll() -// } -// } -// -// func testSequenceDeinitialized_whenFinished() async throws { -// let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( -// of: Int.self, -// backpressureStrategy: .watermark(low: 5, high: 10) -// ) -// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel -// var source = consume channelAndStream.source -// -// let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() -// source.onTermination = { -// onTerminationContinuation.finish() -// } -// -// await withThrowingTaskGroup(of: Void.self) { group in -// group.addTask { -// while !Task.isCancelled { -// onTerminationContinuation.yield() -// try await Task.sleep(for: .seconds(0.2)) -// } -// } -// -// var onTerminationIterator = onTerminationStream.makeAsyncIterator() -// _ = await onTerminationIterator.next() -// -// channel = nil -// -// let terminationResult: Void? = await onTerminationIterator.next() -// XCTAssertNil(terminationResult) -// XCTAssertNil(channel) -// -// group.cancelAll() -// } -// } -// -// func testSequenceDeinitialized_whenChanneling_andSuspendedProducer() async throws { -// let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( -// of: Int.self, -// backpressureStrategy: .watermark(low: 1, high: 2) -// ) -// var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel -// var source = consume channelAndStream.source -// -// _ = try { try source.send(1) }() -// -// do { -// try await withCheckedThrowingContinuation { continuation in -// source.send(1) { result in -// continuation.resume(with: result) -// } -// -// channel = nil -// _ = channel?.makeAsyncIterator() -// } -// } catch { -// XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) -// } -// } + try await source?.send(1) + try await source?.send(2) + source?.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) + _ = try await iterator?.next() + + _ = await onTerminationIterator.next() + + _ = try await iterator?.next() + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenFinished() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + source?.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + _ = channel.asyncSequence().makeAsyncIterator() + + _ = await onTerminationIterator.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + // MARK: Channel deinitialized + + func testChannelDeinitialized() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + XCTAssertFalse(didTerminate) + _ = consume channel + XCTAssertTrue(didTerminate) + } + + // MARK: - sequenceDeinitialized + + func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let asyncSequence = channel.asyncSequence() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + group.addTask(executorPreference: manualExecutor) { + await asyncSequence.first { _ in true } + } + + withExtendedLifetime(source) { } + _ = consume source + XCTAssertFalse(didTerminate) + manualExecutor.run() + _ = try await group.next() + XCTAssertTrue(didTerminate) + } + } + + func testSequenceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let asyncSequence = channel.asyncSequence() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + group.addTask(executorPreference: manualExecutor) { + _ = await asyncSequence.first { _ in true } + } + manualExecutor.run() + XCTAssertFalse(didTerminate) + + withExtendedLifetime(source) { } + _ = consume source + XCTAssertTrue(didTerminate) + manualExecutor.run() + _ = try await group.next() + } + } // MARK: - iteratorInitialized func testIteratorInitialized_whenInitial() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) - let channel = channelAndStream.channel - let source = consume channelAndStream.source + let channel = channelAndSource.channel + _ = consume channelAndSource.source - _ = channel.makeAsyncIterator() + _ = channel.asyncSequence().makeAsyncIterator() } func testIteratorInitialized_whenChanneling() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + let channel = channelAndSource.channel + var source = consume channelAndSource.source try await source.send(1) - var iterator = channel.makeAsyncIterator() - let element = await iterator.next() + var iterator = channel.asyncSequence().makeAsyncIterator() + let element = await iterator.next(isolation: nil) XCTAssertEqual(element, 1) } func testIteratorInitialized_whenSourceFinished() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + let channel = channelAndSource.channel + var source = consume channelAndSource.source try await source.send(1) source.finish(throwing: nil) - var iterator = channel.makeAsyncIterator() - let element1 = await iterator.next() + var iterator = channel.asyncSequence().makeAsyncIterator() + let element1 = await iterator.next(isolation: nil) XCTAssertEqual(element1, 1) - let element2 = await iterator.next() + let element2 = await iterator.next(isolation: nil) XCTAssertNil(element2) } func testIteratorInitialized_whenFinished() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) - let channel = channelAndStream.channel - let source = consume channelAndStream.source + let channel = channelAndSource.channel + let source = consume channelAndSource.source source.finish(throwing: nil) - var iterator = channel.makeAsyncIterator() - let element = await iterator.next() + var iterator = channel.asyncSequence().makeAsyncIterator() + let element = await iterator.next(isolation: nil) XCTAssertNil(element) } // MARK: - iteratorDeinitialized func testIteratorDeinitialized_whenInitial() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -252,9 +342,9 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() - var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) iterator = nil - _ = await iterator?.next() + _ = await iterator?.next(isolation: nil) let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) @@ -264,21 +354,21 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorDeinitialized_whenChanneling() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - try await source.send(1) + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } - await withThrowingTaskGroup(of: Void.self) { group in + try await source.send(1) + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -289,7 +379,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() - var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) iterator = nil _ = await iterator?.next(isolation: nil) @@ -301,22 +391,22 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorDeinitialized_whenSourceFinished() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - try await source.send(1) - source.finish(throwing: nil) + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } - await withThrowingTaskGroup(of: Void.self) { group in + try await source.send(1) + source.finish(throwing: nil) + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -327,9 +417,9 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() - var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) iterator = nil - _ = await iterator?.next() + _ = await iterator?.next(isolation: nil) let terminationResult: Void? = await onTerminationIterator.next() XCTAssertNil(terminationResult) @@ -339,22 +429,22 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorDeinitialized_whenFinished() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - source.finish(throwing: nil) - try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + source.finish(throwing: nil) + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -365,7 +455,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var onTerminationIterator = onTerminationStream.makeAsyncIterator() _ = await onTerminationIterator.next() - var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) iterator = nil _ = try await iterator?.next() @@ -377,15 +467,15 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorDeinitialized_whenChanneling_andSuspendedProducer() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 5, high: 10) ) - var channel: MultiProducerSingleConsumerChannel? = channelAndStream.channel - var source = consume channelAndStream.source + var channel: MultiProducerSingleConsumerChannel? = channelAndSource.channel + var source = consume channelAndSource.source - var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel?.makeAsyncIterator() + var iterator = channel?.asyncSequence().makeAsyncIterator() channel = nil _ = try { try source.send(1) }() @@ -405,136 +495,52 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { _ = try await iterator?.next() } - // MARK: - sourceDeinitialized - - func testSourceDeinitialized_whenSourceFinished() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndStream.channel - var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndStream.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - try await source?.send(1) - try await source?.send(2) - source?.finish(throwing: nil) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: MultiProducerSingleConsumerChannel.AsyncIterator? = channel.makeAsyncIterator() - _ = try await iterator?.next() - - _ = await onTerminationIterator.next() - - _ = try await iterator?.next() - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testSourceDeinitialized_whenFinished() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndStream.channel - var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndStream.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - source?.finish(throwing: nil) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - _ = channel.makeAsyncIterator() - - _ = await onTerminationIterator.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - // MARK: - write func testWrite_whenInitial() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + let channel = channelAndSource.channel + var source = consume channelAndSource.source try await source.send(1) - var iterator = channel.makeAsyncIterator() - let element = await iterator.next() + var iterator = channel.asyncSequence().makeAsyncIterator() + let element = await iterator.next(isolation: nil) XCTAssertEqual(element, 1) } func testWrite_whenChanneling_andNoConsumer() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + let channel = channelAndSource.channel + var source = consume channelAndSource.source try await source.send(1) try await source.send(2) - var iterator = channel.makeAsyncIterator() - let element1 = await iterator.next() + var iterator = channel.asyncSequence().makeAsyncIterator() + let element1 = await iterator.next(isolation: nil) XCTAssertEqual(element1, 1) - let element2 = await iterator.next() + let element2 = await iterator.next(isolation: nil) XCTAssertEqual(element2, 2) } func testWrite_whenChanneling_andSuspendedConsumer() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - try await withThrowingTaskGroup(of: Int?.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source + group.addTask { - return await channel.first { _ in true } + return await channel.next() } // This is always going to be a bit racy since we need the call to next() suspend @@ -547,16 +553,15 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testWrite_whenChanneling_andSuspendedConsumer_andEmptySequence() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - try await withThrowingTaskGroup(of: Int?.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source group.addTask { - return await channel.first { _ in true } + return await channel.next() } // This is always going to be a bit racy since we need the call to next() suspend @@ -568,16 +573,74 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { XCTAssertEqual(element, 1) } } + + func testWrite_whenSourceFinished() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = consume channelAndSource.channel + var source1 = consume channelAndSource.source + var source2 = source1.copy() + + try await source1.send(1) + source1.finish() + do { + try await source2.send(1) + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) + } + let element1 = await channel.next() + XCTAssertEqual(element1, 1) + let element2 = await channel.next() + XCTAssertNil(element2) + } + + func testWrite_whenConcurrentProduction() async throws { + await withThrowingTaskGroup { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = consume channelAndSource.channel + var source1 = consume channelAndSource.source + var source2 = Optional.some(source1.copy()) + + let manualExecutor1 = ManualTaskExecutor() + group.addTask(executorPreference: manualExecutor1) { + try await source1.send(1) + } + + let manualExecutor2 = ManualTaskExecutor() + group.addTask(executorPreference: manualExecutor2) { + var source2 = source2.take()! + try await source2.send(2) + source2.finish() + } + + manualExecutor1.run() + let element1 = await channel.next() + XCTAssertEqual(element1, 1) + + manualExecutor2.run() + let element2 = await channel.next() + XCTAssertEqual(element2, 2) + + let element3 = await channel.next() + XCTAssertNil(element3) + } + } // MARK: - enqueueProducer func testEnqueueProducer_whenChanneling_andAndCancelled() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 2) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + var channel = channelAndSource.channel + var source = consume channelAndSource.source let (producerStream, producerSource) = AsyncThrowingStream.makeStream() @@ -603,21 +666,21 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { XCTAssertTrue(error is CancellationError) } - let element = await channel.first { _ in true } + let element = await channel.next() XCTAssertEqual(element, 1) } func testEnqueueProducer_whenChanneling_andAndCancelled_andAsync() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 2) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - try await source.send(1) + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source - await withThrowingTaskGroup(of: Void.self) { group in + try await source.send(1) + group.addTask { try await source.send(2) } @@ -629,20 +692,20 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } catch { XCTAssertTrue(error is CancellationError) } + + let element = await channel.next() + XCTAssertEqual(element, 1) } - - let element = await channel.first { _ in true } - XCTAssertEqual(element, 1) } func testEnqueueProducer_whenChanneling_andInterleaving() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 1) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - var iterator = channel.makeAsyncIterator() + let channel = channelAndSource.channel + var source = consume channelAndSource.source + var iterator = channel.asyncSequence().makeAsyncIterator() let (producerStream, producerSource) = AsyncThrowingStream.makeStream() @@ -652,7 +715,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { case .produceMore: preconditionFailure() case .enqueueCallback(let callbackToken): - let element = await iterator.next() + let element = await iterator.next(isolation: nil) XCTAssertEqual(element, 1) source.enqueueCallback(callbackToken: callbackToken) { result in @@ -668,13 +731,13 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testEnqueueProducer_whenChanneling_andSuspending() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 1) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - var iterator = channel.makeAsyncIterator() + let channel = channelAndSource.channel + var source = consume channelAndSource.source + var iterator = channel.asyncSequence().makeAsyncIterator() let (producerStream, producerSource) = AsyncThrowingStream.makeStream() @@ -689,7 +752,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } } - let element = await iterator.next() + let element = await iterator.next(isolation: nil) XCTAssertEqual(element, 1) do { @@ -702,12 +765,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - cancelProducer func testCancelProducer_whenChanneling() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 2) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + var channel = channelAndSource.channel + var source = consume channelAndSource.source let (producerStream, producerSource) = AsyncThrowingStream.makeStream() @@ -733,23 +796,28 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { XCTAssertTrue(error is CancellationError) } - let element = await channel.first { _ in true } + let element = await channel.next() XCTAssertEqual(element, 1) } // MARK: - finish func testFinish_whenChanneling_andConsumerSuspended() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 1) - ) - let channel = channelAndStream.channel - var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndStream.source - try await withThrowingTaskGroup(of: Int?.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + var channel = channelAndSource.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + group.addTask { - return await channel.first { $0 == 2 } + while let element = await channel.next() { + if element == 2 { + return element + } + } + return nil } // This is always going to be a bit racy since we need the call to next() suspend @@ -764,18 +832,18 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testFinish_whenInitial() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 1, high: 1) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + let channel = channelAndSource.channel + let source = consume channelAndSource.source source.finish(throwing: CancellationError()) do { - for try await _ in channel {} + for try await _ in channel.asyncSequence() {} XCTFail("Expected an error to be thrown") } catch { XCTAssertTrue(error is CancellationError) @@ -786,16 +854,16 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - Backpressure func testBackpressure() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + group.addTask { while true { backpressureEventContinuation.yield(()) @@ -804,16 +872,16 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() - var iterator = channel.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() await backpressureEventIterator.next() await backpressureEventIterator.next() await backpressureEventIterator.next() await backpressureEventIterator.next() - _ = await iterator.next() - _ = await iterator.next() - _ = await iterator.next() + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) await backpressureEventIterator.next() await backpressureEventIterator.next() @@ -824,16 +892,16 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testBackpressureSync() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + group.addTask { while true { backpressureEventContinuation.yield(()) @@ -846,16 +914,16 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() - var iterator = channel.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() await backpressureEventIterator.next() await backpressureEventIterator.next() await backpressureEventIterator.next() await backpressureEventIterator.next() - _ = await iterator.next() - _ = await iterator.next() - _ = await iterator.next() + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) await backpressureEventIterator.next() await backpressureEventIterator.next() @@ -866,33 +934,34 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testWatermarkWithCustomCoount() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: [Int].self, backpressureStrategy: .watermark(low: 2, high: 4, waterLevelForElement: { $0.count }) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - var iterator = channel.makeAsyncIterator() + let channel = channelAndSource.channel + var source = consume channelAndSource.source + var iterator = channel.asyncSequence().makeAsyncIterator() try await source.send([1, 1, 1]) - _ = await iterator.next() + _ = await iterator.next(isolation: nil) try await source.send([1, 1, 1]) - _ = await iterator.next() + _ = await iterator.next(isolation: nil) } func testWatermarWithLotsOfElements() async throws { - // This test should in the future use a custom task executor to schedule to avoid sending - // 1000 elements. - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndStream.channel - var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndStream.source await withThrowingTaskGroup(of: Void.self) { group in + // This test should in the future use a custom task executor to schedule to avoid sending + // 1000 elements. + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndSource.source + group.addTask { var source = source.take()! for i in 0...10000 { @@ -900,10 +969,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } source.finish() } + + let asyncSequence = channel.asyncSequence() group.addTask { var sum = 0 - for try await element in channel { + for try await element in asyncSequence { sum += element } } @@ -911,20 +982,20 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testThrowsError() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 2, high: 4) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + let channel = channelAndSource.channel + var source = consume channelAndSource.source try await source.send(1) try await source.send(2) source.finish(throwing: CancellationError()) var elements = [Int]() - var iterator = channel.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() do { while let element = try await iterator.next() { @@ -942,12 +1013,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testAsyncSequenceWrite() async throws { let (stream, continuation) = AsyncStream.makeStream() - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source + var channel = channelAndSource.channel + var source = consume channelAndSource.source continuation.yield(1) continuation.yield(2) @@ -963,16 +1034,16 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: NonThrowing func testNonThrowing() async throws { - let channelAndStream = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndStream.channel - var source = consume channelAndStream.source - - let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + group.addTask { while true { backpressureEventContinuation.yield(()) @@ -981,16 +1052,16 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() - var iterator = channel.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() await backpressureEventIterator.next() await backpressureEventIterator.next() await backpressureEventIterator.next() await backpressureEventIterator.next() - _ = await iterator.next() - _ = await iterator.next() - _ = await iterator.next() + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) await backpressureEventIterator.next() await backpressureEventIterator.next() @@ -1001,15 +1072,19 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } } -extension AsyncSequence { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel { /// Collect all elements in the sequence into an array. - fileprivate func collect() async rethrows -> [Element] { - try await self.reduce(into: []) { accumulated, next in - accumulated.append(next) + fileprivate mutating func collect() async throws(Failure) -> [Element] { + var elements = [Element]() + while let element = try await self.next() { + elements.append(element) } + return elements } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel.Source.SendResult { func assertIsProducerMore() { switch self { diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift new file mode 100644 index 00000000..79956991 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift @@ -0,0 +1,17 @@ +import DequeModule +import Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class ManualTaskExecutor: TaskExecutor { + private let jobs = Mutex>(.init()) + + func enqueue(_ job: UnownedJob) { + self.jobs.withLock { $0.append(job) } + } + + func run() { + while let job = self.jobs.withLock({ $0.popFirst() }) { + job.runSynchronously(on: self.asUnownedTaskExecutor()) + } + } +} From d0eef05726ec3e3e55ab3efd7e1a48b4e4e99af2 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 25 Mar 2025 15:03:20 +0100 Subject: [PATCH 03/16] Update proposal --- ...-mutli-producer-single-consumer-channel.md | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index f5f9cefa..cbb26b38 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -734,10 +734,10 @@ strategies. ## Alternatives considered -### Provide the `onTermination` callback to the factory method +### Provide an `onTermination` callback to the factory method During development of the new APIs, I first tried to provide the `onTermination` -callback in the `makeStream` method. However, that showed significant usability +callback in the `makeChannel` method. However, that showed significant usability problems in scenarios where one wants to store the source in a type and reference `self` in the `onTermination` closure at the same time; hence, I kept the current pattern of setting the `onTermination` closure on the source. @@ -746,12 +746,12 @@ the current pattern of setting the `onTermination` closure on the source. During the pitch phase, it was raised that we should provide a `onConsumerCancellation` callback which gets invoked once the asynchronous -stream notices that the consuming task got cancelled. This callback could be -used to customize how cancellation is handled by the stream e.g. one could -imagine writing a few more elements to the stream before finishing it. Right now -the stream immediately returns `nil` or throws a `CancellationError` when it +channel notices that the consuming task got cancelled. This callback could be +used to customize how cancellation is handled by the channel e.g. one could +imagine writing a few more elements to the channel before finishing it. Right now +the channel immediately returns `nil` or throws a `CancellationError` when it notices cancellation. This proposal decided to not provide this customization -because it opens up the possiblity that asynchronous streams are not terminating +because it opens up the possiblity that asynchronous channels are not terminating when implemented incorrectly. Additionally, asynchronous sequences are not the only place where task cancellation leads to an immediate error being thrown i.e. `Task.sleep()` does the same. Hence, the value of the asynchronous not @@ -762,30 +762,19 @@ the future and we can just default it to the current behaviour. ### Create a custom type for the `Result` of the `onProduceMore` callback The `onProducerMore` callback takes a `Result` which is used to -indicate if the producer should produce more or if the asynchronous stream +indicate if the producer should produce more or if the asynchronous channel finished. We could introduce a new type for this but the proposal decided against it since it effectively is a result type. ### Use an initializer instead of factory methods -Instead of providing a `makeStream` factory method we could use an initializer +Instead of providing a `makeChannel` factory method we could use an initializer approach that takes a closure which gets the `Source` passed into. A similar API has been offered with the `Continuation` based approach and [SE-0388](https://github.com/apple/swift-evolution/blob/main/proposals/0388-async-stream-factory.md) introduced new factory methods to solve some of the usability ergonomics with the initializer based APIs. -### Follow the `AsyncStream` & `AsyncThrowingStream` naming - -All other types that offer throwing and non-throwing variants are currently -following the naming scheme where the throwing variant gets an extra `Throwing` -in its name. Now that Swift is gaining typed throws support this would make the -type with the `Failure` parameter capable to express both throwing and -non-throwing variants. However, the less flexible type has the better name. -Hence, this proposal uses the good name for the throwing variant with the -potential in the future to deprecate the `AsyncNonThrowingBackpressuredStream` -in favour of adopting typed throws. - ## Acknowledgements - [Johannes Weiss](https://github.com/weissi) - For making me aware how From 7b7c35979488aa6e52a4a52230bdc4e3eca4123c Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 25 Mar 2025 15:24:26 +0100 Subject: [PATCH 04/16] Add example project --- Package.swift | 7 ++++ Sources/Example/Example.swift | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 Sources/Example/Example.swift diff --git a/Package.swift b/Package.swift index 1177d22d..09ad1330 100644 --- a/Package.swift +++ b/Package.swift @@ -39,6 +39,13 @@ let package = Package( .enableExperimentalFeature("StrictConcurrency=complete") ] ), + .executableTarget( + name: "Example", + dependencies: ["AsyncAlgorithms"], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + ] + ), .testTarget( name: "AsyncAlgorithmsTests", dependencies: ["AsyncAlgorithms", "AsyncSequenceValidation", "AsyncAlgorithms_XCTest"], diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift new file mode 100644 index 00000000..9cffe0c1 --- /dev/null +++ b/Sources/Example/Example.swift @@ -0,0 +1,67 @@ +import AsyncAlgorithms + +@available(macOS 15.0, *) +@main +struct Example { + static func main() async throws { + let durationUnboundedMPSC = await ContinuousClock().measure { + await testMPSCChannel(count: 1000000, backpressureStrategy: .unbounded()) + } + print("Unbounded MPSC:", durationUnboundedMPSC) + let durationHighLowMPSC = await ContinuousClock().measure { + await testMPSCChannel(count: 1000000, backpressureStrategy: .watermark(low: 100, high: 500)) + } + print("HighLow MPSC:", durationHighLowMPSC) + let durationAsyncStream = await ContinuousClock().measure { + await testAsyncStream(count: 1000000) + } + print("AsyncStream:", durationAsyncStream) + } + + static func testMPSCChannel( + count: Int, + backpressureStrategy: MultiProducerSingleConsumerChannel.Source.BackpressureStrategy + ) async { + await withTaskGroup { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: backpressureStrategy + ) + var channel = channelAndSource.channel + var source = Optional.some(consume channelAndSource.source) + + group.addTask { + var source = source.take()! + for i in 0.. Date: Tue, 25 Mar 2025 15:26:16 +0100 Subject: [PATCH 05/16] Formatting --- ...oducerSingleConsumerChannel+Internal.swift | 273 +++++++++++------- .../MultiProducerSingleConsumerChannel.swift | 16 +- ...tiProducerSingleConsumerChannelTests.swift | 94 +++--- 3 files changed, 222 insertions(+), 161 deletions(-) diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift index 9542e762..19c85f16 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift @@ -14,7 +14,7 @@ import DequeModule import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerChannel { @usableFromInline enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { @usableFromInline @@ -78,19 +78,19 @@ extension MultiProducerSingleConsumerChannel { struct _Unbounded: Sendable, CustomStringConvertible { @usableFromInline var description: String { - return "unbounded" + "unbounded" } - init() { } + init() {} @inlinable mutating func didSend(elements: Deque.SubSequence) -> Bool { - return true + true } @inlinable mutating func didConsume(element: Element) -> Bool { - return true + true } } @@ -189,13 +189,12 @@ extension MultiProducerSingleConsumerChannel { break } } - + func sequenceInitialized() { self._stateMachine.withLock { $0.sequenceInitialized() } } - func sequenceDeinitialized() { let action = self._stateMachine.withLock { @@ -252,7 +251,7 @@ extension MultiProducerSingleConsumerChannel { break } } - + func sourceInitialized() { self._stateMachine.withLock { $0.sourceInitialized() @@ -449,8 +448,8 @@ extension MultiProducerSingleConsumerChannel { @inlinable func suspendNext(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { - return try await withTaskCancellationHandler { - return try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in let action = self._stateMachine.withLock { $0.suspendNext(continuation: continuation) } @@ -459,7 +458,11 @@ extension MultiProducerSingleConsumerChannel { case .resumeConsumerWithElement(let continuation, let element): continuation.resume(returning: element) - case .resumeConsumerWithElementAndProducers(let continuation, let element, let producerContinuations): + case .resumeConsumerWithElementAndProducers( + let continuation, + let element, + let producerContinuations + ): continuation.resume(returning: element) for producerContinuation in producerContinuations { switch producerContinuation { @@ -470,7 +473,11 @@ extension MultiProducerSingleConsumerChannel { } } - case .resumeConsumerWithFailureAndCallOnTermination(let continuation, let failure, let onTermination): + case .resumeConsumerWithFailureAndCallOnTermination( + let continuation, + let failure, + let onTermination + ): switch failure { case .some(let error): continuation.resume(throwing: error) @@ -517,7 +524,7 @@ extension MultiProducerSingleConsumerChannel { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel._Storage { +extension MultiProducerSingleConsumerChannel._Storage { /// The state machine of the channel. @usableFromInline struct _StateMachine: ~Copyable { @@ -575,24 +582,24 @@ extension MultiProducerSingleConsumerChannel._Storage { @inlinable init(state: consuming _State) { - self._state = state + self._state = state } - + @inlinable mutating func sourceInitialized() { switch consume self._state { case .channeling(var channeling): channeling.activeProducers += 1 self = .init(state: .channeling(channeling)) - + case .sourceFinished(let sourceFinished): self = .init(state: .sourceFinished(sourceFinished)) - + case .finished(let finished): self = .init(state: .finished(finished)) } } - + /// Actions returned by `sourceDeinitialized()`. @usableFromInline enum SourceDeinitialized { @@ -617,14 +624,16 @@ extension MultiProducerSingleConsumerChannel._Storage { guard let consumerContinuation = channeling.consumerContinuation else { // We don't have a suspended consumer so we are just going to mark // the source as finished. - self = .init(state: .sourceFinished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - buffer: channeling.buffer, - failure: nil, - onTermination: channeling.onTermination - )) + self = .init( + state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: nil, + onTermination: channeling.onTermination + ) + ) ) return nil @@ -634,11 +643,15 @@ extension MultiProducerSingleConsumerChannel._Storage { // and resume the continuation with the failure precondition(channeling.buffer.isEmpty, "Expected an empty buffer") - self = .init(state: .finished(.init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) return .resumeConsumerAndCallOnTermination( consumerContinuation: consumerContinuation, @@ -664,18 +677,18 @@ extension MultiProducerSingleConsumerChannel._Storage { return .none } } - + @inlinable mutating func sequenceInitialized() { switch consume self._state { case .channeling(var channeling): channeling.sequenceInitialized = true self = .init(state: .channeling(channeling)) - + case .sourceFinished(var sourceFinished): sourceFinished.sequenceInitialized = true self = .init(state: .sourceFinished(sourceFinished)) - + case .finished(var finished): finished.sequenceInitialized = true self = .init(state: .finished(finished)) @@ -701,11 +714,15 @@ extension MultiProducerSingleConsumerChannel._Storage { guard channeling.iteratorInitialized else { precondition(channeling.sequenceInitialized, "Sequence was not initialized") // No iterator was created so we can transition to finished right away. - self = .init(state: .finished(.init( - iteratorInitialized: false, - sequenceInitialized: true, - sourceFinished: false - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: false + ) + ) + ) return .failProducersAndCallOnTermination( .init(channeling.suspendedProducers.lazy.map { $0.1 }), @@ -722,12 +739,15 @@ extension MultiProducerSingleConsumerChannel._Storage { guard sourceFinished.iteratorInitialized else { precondition(sourceFinished.sequenceInitialized, "Sequence was not initialized") // No iterator was created so we can transition to finished right away. - self = .init(state: .finished( - .init( - iteratorInitialized: false, - sequenceInitialized: true, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: true + ) + ) + ) return .callOnTermination(sourceFinished.onTermination) } @@ -745,7 +765,7 @@ extension MultiProducerSingleConsumerChannel._Storage { return .none } } - + @inlinable mutating func channelDeinitialized() -> ChannelOrSequenceDeinitializedAction? { switch consume self._state { @@ -756,12 +776,15 @@ extension MultiProducerSingleConsumerChannel._Storage { return nil } else { // No async sequence was created so we can transition to finished - self = .init(state: .finished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) return .failProducersAndCallOnTermination( .init(channeling.suspendedProducers.lazy.map { $0.1 }), @@ -776,12 +799,15 @@ extension MultiProducerSingleConsumerChannel._Storage { return nil } else { // No async sequence was created so we can transition to finished - self = .init(state: .finished( - .init( - iteratorInitialized: sourceFinished.iteratorInitialized, - sequenceInitialized: sourceFinished.sequenceInitialized, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) return .callOnTermination(sourceFinished.onTermination) } @@ -823,11 +849,15 @@ extension MultiProducerSingleConsumerChannel._Storage { // Our sequence is a unicast sequence and does not support multiple AsyncIterator's fatalError("Only a single AsyncIterator can be created") } else { - self = .init(state: .finished(.init( - iteratorInitialized: true, - sequenceInitialized: true, - sourceFinished: finished.sourceFinished - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: finished.sourceFinished + ) + ) + ) } } } @@ -851,11 +881,15 @@ extension MultiProducerSingleConsumerChannel._Storage { if channeling.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. - self = .init(state: .finished(.init( - iteratorInitialized: true, - sequenceInitialized: true, - sourceFinished: false - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: false + ) + ) + ) return .failProducersAndCallOnTermination( .init(channeling.suspendedProducers.lazy.map { $0.1 }), @@ -870,11 +904,15 @@ extension MultiProducerSingleConsumerChannel._Storage { if sourceFinished.iteratorInitialized { // An iterator was created and deinited. Since we only support // a single iterator we can now transition to finish. - self = .init(state: .finished(.init( - iteratorInitialized: true, - sequenceInitialized: true, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: true + ) + ) + ) return .callOnTermination(sourceFinished.onTermination) } else { @@ -1166,28 +1204,36 @@ extension MultiProducerSingleConsumerChannel._Storage { guard let consumerContinuation = channeling.consumerContinuation else { // We don't have a suspended consumer so we are just going to mark // the source as finished and terminate the current suspended producers. - self = .init(state: .sourceFinished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - buffer: channeling.buffer, - failure: failure, - onTermination: channeling.onTermination - )) + self = .init( + state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: failure, + onTermination: channeling.onTermination + ) + ) ) - return .resumeProducers(producerContinuations: .init(channeling.suspendedProducers.lazy.map { $0.1 })) + return .resumeProducers( + producerContinuations: .init(channeling.suspendedProducers.lazy.map { $0.1 }) + ) } // We have a continuation, this means our buffer must be empty // Furthermore, we can now transition to finished // and resume the continuation with the failure precondition(channeling.buffer.isEmpty, "Expected an empty buffer") - self = .init(state: .finished(.init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) return .resumeConsumerAndCallOnTermination( consumerContinuation: consumerContinuation, @@ -1261,11 +1307,15 @@ extension MultiProducerSingleConsumerChannel._Storage { // Check if we have an element left in the buffer and return it guard let element = sourceFinished.buffer.popFirst() else { // We are returning the queued failure now and can transition to finished - self = .init(state: .finished(.init( - iteratorInitialized: sourceFinished.iteratorInitialized, - sequenceInitialized: sourceFinished.sequenceInitialized, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) return .returnFailureAndCallOnTermination(sourceFinished.failure, sourceFinished.onTermination) } @@ -1340,11 +1390,15 @@ extension MultiProducerSingleConsumerChannel._Storage { // Check if we have an element left in the buffer and return it guard let element = sourceFinished.buffer.popFirst() else { // We are returning the queued failure now and can transition to finished - self = .init(state: .finished(.init( - iteratorInitialized: sourceFinished.iteratorInitialized, - sequenceInitialized: sourceFinished.sequenceInitialized, - sourceFinished: true - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) return .resumeConsumerWithFailureAndCallOnTermination( continuation, @@ -1369,18 +1423,25 @@ extension MultiProducerSingleConsumerChannel._Storage { /// Indicates that the continuation should be resumed with nil, the producers should be finished and call onTermination. case resumeConsumerWithNilAndCallOnTermination(UnsafeContinuation, (() -> Void)?) /// Indicates that the producers should be finished and call onTermination. - case failProducersAndCallOnTermination(_TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, (() -> Void)?) + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (() -> Void)? + ) } @inlinable mutating func cancelNext() -> CancelNextAction? { switch consume self._state { case .channeling(let channeling): - self = .init(state: .finished(.init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: false - ))) + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: false + ) + ) + ) guard let consumerContinuation = channeling.consumerContinuation else { return .failProducersAndCallOnTermination( @@ -1424,7 +1485,7 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool - + /// Indicates if an async sequence was initialized. @usableFromInline var sequenceInitialized: Bool @@ -1506,7 +1567,7 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool - + /// Indicates if an async sequence was initialized. @usableFromInline var sequenceInitialized: Bool @@ -1548,7 +1609,7 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { /// Indicates if the iterator was initialized. @usableFromInline var iteratorInitialized: Bool - + /// Indicates if an async sequence was initialized. @usableFromInline var sequenceInitialized: Bool diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index e30eaae2..5af1169a 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -123,11 +123,11 @@ public struct MultiProducerSingleConsumerChannel: ~Copy init(storage: _Storage) { self.storage = storage } - + deinit { self.storage.channelDeinitialized() } - + /// Returns the next element. /// /// If this method returns `nil` it indicates that no further values can ever @@ -463,8 +463,9 @@ extension MultiProducerSingleConsumerChannel { /// - Parameters: /// - sequence: The elements to send to the channel. @inlinable - public mutating func send(contentsOf sequence: consuming sending S) async throws where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { - for try await element in sequence { + public mutating func send(contentsOf sequence: consuming sending S) async throws + where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { + for try await element in sequence { try await self.send(contentsOf: CollectionOfOne(element)) } } @@ -486,7 +487,6 @@ extension MultiProducerSingleConsumerChannel { } } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel where Element: Copyable { struct ChannelAsyncSequence: AsyncSequence { @@ -504,15 +504,15 @@ extension MultiProducerSingleConsumerChannel where Element: Copyable { self.storage.sequenceDeinitialized() } } - + @usableFromInline let _backing: _Backing - + public func makeAsyncIterator() -> Self.Iterator { .init(storage: self._backing.storage) } } - + /// Converts the channel to an asynchronous sequence for consumption. /// /// - Important: The returned asynchronous sequence only supports a single iterator to be created and diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift index a8d60fdc..deb05bd4 100644 --- a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift @@ -15,7 +15,7 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - sourceDeinitialized - + func testSourceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup { group in @@ -25,17 +25,17 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { ) var channel = consume channelAndSource.channel let source = consume channelAndSource.source - + nonisolated(unsafe) var didTerminate = false source.setOnTerminationCallback { didTerminate = true } - + group.addTask(executorPreference: manualExecutor) { await channel.next() } - - withExtendedLifetime(source) { } + + withExtendedLifetime(source) {} _ = consume source XCTAssertFalse(didTerminate) manualExecutor.run() @@ -43,7 +43,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { XCTAssertTrue(didTerminate) } } - + func testSourceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup { group in @@ -57,21 +57,21 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { source.setOnTerminationCallback { didTerminate = true } - + group.addTask(executorPreference: manualExecutor) { await channel.next() } manualExecutor.run() XCTAssertFalse(didTerminate) - - withExtendedLifetime(source) { } + + withExtendedLifetime(source) {} _ = consume source XCTAssertTrue(didTerminate) manualExecutor.run() _ = try await group.next() } } - + func testSourceDeinitialized_whenMultipleSources() async throws { let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, @@ -100,7 +100,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { _ = await channel.next() XCTAssertTrue(didTerminate) } - + func testSourceDeinitialized_whenSourceFinished() async throws { try await withThrowingTaskGroup(of: Void.self) { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( @@ -119,7 +119,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { try await source?.send(1) try await source?.send(2) source?.finish(throwing: nil) - + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -161,7 +161,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } source?.finish(throwing: nil) - + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -182,9 +182,9 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { group.cancelAll() } } - + // MARK: Channel deinitialized - + func testChannelDeinitialized() async throws { let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, @@ -194,14 +194,14 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { let source = consume channelAndSource.source nonisolated(unsafe) var didTerminate = false source.setOnTerminationCallback { didTerminate = true } - + XCTAssertFalse(didTerminate) _ = consume channel XCTAssertTrue(didTerminate) } - + // MARK: - sequenceDeinitialized - + func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup { group in @@ -214,12 +214,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { let source = consume channelAndSource.source nonisolated(unsafe) var didTerminate = false source.setOnTerminationCallback { didTerminate = true } - + group.addTask(executorPreference: manualExecutor) { await asyncSequence.first { _ in true } } - - withExtendedLifetime(source) { } + + withExtendedLifetime(source) {} _ = consume source XCTAssertFalse(didTerminate) manualExecutor.run() @@ -227,7 +227,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { XCTAssertTrue(didTerminate) } } - + func testSequenceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup(of: Void.self) { group in @@ -240,14 +240,14 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { let source = consume channelAndSource.source nonisolated(unsafe) var didTerminate = false source.setOnTerminationCallback { didTerminate = true } - + group.addTask(executorPreference: manualExecutor) { _ = await asyncSequence.first { _ in true } } manualExecutor.run() XCTAssertFalse(didTerminate) - - withExtendedLifetime(source) { } + + withExtendedLifetime(source) {} _ = consume source XCTAssertTrue(didTerminate) manualExecutor.run() @@ -331,7 +331,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { source.setOnTerminationCallback { onTerminationContinuation.finish() } - + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -368,7 +368,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } try await source.send(1) - + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -406,7 +406,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { try await source.send(1) source.finish(throwing: nil) - + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -444,7 +444,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } source.finish(throwing: nil) - + group.addTask { while !Task.isCancelled { onTerminationContinuation.yield() @@ -538,9 +538,9 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { ) var channel = channelAndSource.channel var source = consume channelAndSource.source - + group.addTask { - return await channel.next() + await channel.next() } // This is always going to be a bit racy since we need the call to next() suspend @@ -561,7 +561,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var channel = channelAndSource.channel var source = consume channelAndSource.source group.addTask { - return await channel.next() + await channel.next() } // This is always going to be a bit racy since we need the call to next() suspend @@ -573,7 +573,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { XCTAssertEqual(element, 1) } } - + func testWrite_whenSourceFinished() async throws { let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, @@ -582,7 +582,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var channel = consume channelAndSource.channel var source1 = consume channelAndSource.source var source2 = source1.copy() - + try await source1.send(1) source1.finish() do { @@ -596,7 +596,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { let element2 = await channel.next() XCTAssertNil(element2) } - + func testWrite_whenConcurrentProduction() async throws { await withThrowingTaskGroup { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( @@ -606,27 +606,27 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var channel = consume channelAndSource.channel var source1 = consume channelAndSource.source var source2 = Optional.some(source1.copy()) - + let manualExecutor1 = ManualTaskExecutor() group.addTask(executorPreference: manualExecutor1) { try await source1.send(1) } - + let manualExecutor2 = ManualTaskExecutor() group.addTask(executorPreference: manualExecutor2) { var source2 = source2.take()! try await source2.send(2) source2.finish() } - + manualExecutor1.run() let element1 = await channel.next() XCTAssertEqual(element1, 1) - + manualExecutor2.run() let element2 = await channel.next() XCTAssertEqual(element2, 2) - + let element3 = await channel.next() XCTAssertNil(element3) } @@ -680,7 +680,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var source = consume channelAndSource.source try await source.send(1) - + group.addTask { try await source.send(2) } @@ -692,7 +692,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } catch { XCTAssertTrue(error is CancellationError) } - + let element = await channel.next() XCTAssertEqual(element, 1) } @@ -863,7 +863,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var source = consume channelAndSource.source let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - + group.addTask { while true { backpressureEventContinuation.yield(()) @@ -901,7 +901,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var source = consume channelAndSource.source let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - + group.addTask { while true { backpressureEventContinuation.yield(()) @@ -961,7 +961,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { ) let channel = channelAndSource.channel var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndSource.source - + group.addTask { var source = source.take()! for i in 0...10000 { @@ -969,7 +969,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } source.finish() } - + let asyncSequence = channel.asyncSequence() group.addTask { @@ -1043,7 +1043,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { var source = consume channelAndSource.source let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - + group.addTask { while true { backpressureEventContinuation.yield(()) From baf72ef92ab7a7e6a59f7002a1cef935cd9ced75 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 25 Mar 2025 15:30:14 +0100 Subject: [PATCH 06/16] Fix Swift 6.0 build --- .../MultiProducerSingleConsumerChannel.swift | 9 +++++++++ Sources/Example/Example.swift | 4 ++-- .../MultiProducerSingleConsumerChannelTests.swift | 14 +++++++------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index 5af1169a..3c3b64a9 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -546,6 +546,15 @@ extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence where Element: init(storage: MultiProducerSingleConsumerChannel._Storage) { self._backing = .init(storage: storage) } + + @inlinable + mutating func next() async throws -> Element? { + do { + return try await self._backing.storage.next(isolation: nil) + } catch { + throw error as! Failure + } + } @inlinable mutating func next( diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index 9cffe0c1..c121e3b1 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -22,7 +22,7 @@ struct Example { count: Int, backpressureStrategy: MultiProducerSingleConsumerChannel.Source.BackpressureStrategy ) async { - await withTaskGroup { group in + await withTaskGroup(of: Void.self) { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: backpressureStrategy @@ -47,7 +47,7 @@ struct Example { } static func testAsyncStream(count: Int) async { - await withTaskGroup { group in + await withTaskGroup(of: Void.self) { group in let (stream, continuation) = AsyncStream.makeStream(of: Int.self, bufferingPolicy: .unbounded) group.addTask { diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift index deb05bd4..a9687d5c 100644 --- a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift @@ -18,7 +18,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSourceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() - try await withThrowingTaskGroup { group in + try await withThrowingTaskGroup(of: Void.self) { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) @@ -32,7 +32,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } group.addTask(executorPreference: manualExecutor) { - await channel.next() + _ = await channel.next() } withExtendedLifetime(source) {} @@ -46,7 +46,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSourceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() - try await withThrowingTaskGroup { group in + try await withThrowingTaskGroup(of: Void.self) { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) @@ -59,7 +59,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } group.addTask(executorPreference: manualExecutor) { - await channel.next() + _ = await channel.next() } manualExecutor.run() XCTAssertFalse(didTerminate) @@ -204,7 +204,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() - try await withThrowingTaskGroup { group in + try await withThrowingTaskGroup(of: Void.self) { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) @@ -216,7 +216,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { source.setOnTerminationCallback { didTerminate = true } group.addTask(executorPreference: manualExecutor) { - await asyncSequence.first { _ in true } + _ = await asyncSequence.first { _ in true } } withExtendedLifetime(source) {} @@ -598,7 +598,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testWrite_whenConcurrentProduction() async throws { - await withThrowingTaskGroup { group in + await withThrowingTaskGroup(of: Void.self) { group in let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) From c9b9c810a4f0ed26a41fa219ec5dbbeed47b426b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 25 Mar 2025 15:35:47 +0100 Subject: [PATCH 07/16] Future direction for ~Copyable elements --- Evolution/0016-mutli-producer-single-consumer-channel.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index cbb26b38..d52c7149 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -732,6 +732,14 @@ An adaptive strategy regulates the backpressure based on the rate of consumption and production. With the proposed new APIs we can easily add further strategies. +### Support `~Copyable` elements + +In the future, we can extend the channel to support `~Copyable` elements. We +only need an underlying buffer primitive that can hold `~Copyable` types and the +continuations need to support `~Copyable` elements as well. By making the +channel not directly conform to `AsyncSequence` we can support this down the +road. + ## Alternatives considered ### Provide an `onTermination` callback to the factory method From 0ae60923d6c4286eedb904386ee5854edbbcb921 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 13:02:40 +0100 Subject: [PATCH 08/16] Apply formatting --- Package.swift | 2 +- .../AsyncAlgorithms/Internal/_TinyArray.swift | 478 +-- ...oducerSingleConsumerChannel+Internal.swift | 2981 ++++++++--------- .../MultiProducerSingleConsumerChannel.swift | 908 ++--- Sources/Example/Example.swift | 112 +- ...tiProducerSingleConsumerChannelTests.swift | 1924 +++++------ .../Support/ManualExecutor.swift | 20 +- 7 files changed, 3211 insertions(+), 3214 deletions(-) diff --git a/Package.swift b/Package.swift index 09ad1330..8f7e804d 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,7 @@ let package = Package( name: "Example", dependencies: ["AsyncAlgorithms"], swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency=complete"), + .enableExperimentalFeature("StrictConcurrency=complete") ] ), .testTarget( diff --git a/Sources/AsyncAlgorithms/Internal/_TinyArray.swift b/Sources/AsyncAlgorithms/Internal/_TinyArray.swift index 07357ccb..4d3e64a2 100644 --- a/Sources/AsyncAlgorithms/Internal/_TinyArray.swift +++ b/Sources/AsyncAlgorithms/Internal/_TinyArray.swift @@ -27,14 +27,14 @@ /// and instead stores the ``Element`` inline. @usableFromInline struct _TinyArray { - @usableFromInline - enum Storage { - case one(Element) - case arbitrary([Element]) - } - - @usableFromInline - var storage: Storage + @usableFromInline + enum Storage { + case one(Element) + case arbitrary([Element]) + } + + @usableFromInline + var storage: Storage } // MARK: - TinyArray "public" interface @@ -44,286 +44,286 @@ extension _TinyArray: Hashable where Element: Hashable {} extension _TinyArray: Sendable where Element: Sendable {} extension _TinyArray: RandomAccessCollection { - @usableFromInline - typealias Element = Element + @usableFromInline + typealias Element = Element - @usableFromInline - typealias Index = Int + @usableFromInline + typealias Index = Int - @inlinable - subscript(position: Int) -> Element { - get { - self.storage[position] - } - set { - self.storage[position] = newValue - } + @inlinable + subscript(position: Int) -> Element { + get { + self.storage[position] } - - @inlinable - var startIndex: Int { - self.storage.startIndex + set { + self.storage[position] = newValue } + } - @inlinable - var endIndex: Int { - self.storage.endIndex - } + @inlinable + var startIndex: Int { + self.storage.startIndex + } + + @inlinable + var endIndex: Int { + self.storage.endIndex + } } extension _TinyArray { - @inlinable - init(_ elements: some Sequence) { - self.storage = .init(elements) - } - - @inlinable - init() { - self.storage = .init() - } - - @inlinable - mutating func append(_ newElement: Element) { - self.storage.append(newElement) - } - - @inlinable - mutating func append(contentsOf newElements: some Sequence) { - self.storage.append(contentsOf: newElements) - } - - @discardableResult - @inlinable - mutating func remove(at index: Int) -> Element { - self.storage.remove(at: index) - } - - @inlinable - mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { - try self.storage.removeAll(where: shouldBeRemoved) - } - - @inlinable - mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { - try self.storage.sort(by: areInIncreasingOrder) - } + @inlinable + init(_ elements: some Sequence) { + self.storage = .init(elements) + } + + @inlinable + init() { + self.storage = .init() + } + + @inlinable + mutating func append(_ newElement: Element) { + self.storage.append(newElement) + } + + @inlinable + mutating func append(contentsOf newElements: some Sequence) { + self.storage.append(contentsOf: newElements) + } + + @discardableResult + @inlinable + mutating func remove(at index: Int) -> Element { + self.storage.remove(at: index) + } + + @inlinable + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + try self.storage.removeAll(where: shouldBeRemoved) + } + + @inlinable + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + try self.storage.sort(by: areInIncreasingOrder) + } } // MARK: - TinyArray.Storage "private" implementation extension _TinyArray.Storage: Equatable where Element: Equatable { - @inlinable - static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.one(let lhs), .one(let rhs)): - return lhs == rhs - case (.arbitrary(let lhs), .arbitrary(let rhs)): - // we don't use lhs.elementsEqual(rhs) so we can hit the fast path from Array - // if both arrays share the same underlying storage: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1775 - return lhs == rhs - - case (.one(let element), .arbitrary(let array)), - (.arbitrary(let array), .one(let element)): - guard array.count == 1 else { - return false - } - return element == array[0] + @inlinable + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.one(let lhs), .one(let rhs)): + return lhs == rhs + case (.arbitrary(let lhs), .arbitrary(let rhs)): + // we don't use lhs.elementsEqual(rhs) so we can hit the fast path from Array + // if both arrays share the same underlying storage: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1775 + return lhs == rhs + + case (.one(let element), .arbitrary(let array)), + (.arbitrary(let array), .one(let element)): + guard array.count == 1 else { + return false + } + return element == array[0] - } } + } } extension _TinyArray.Storage: Hashable where Element: Hashable { - @inlinable - func hash(into hasher: inout Hasher) { - // same strategy as Array: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1801 - hasher.combine(count) - for element in self { - hasher.combine(element) - } + @inlinable + func hash(into hasher: inout Hasher) { + // same strategy as Array: https://github.com/apple/swift/blob/b42019005988b2d13398025883e285a81d323efa/stdlib/public/core/Array.swift#L1801 + hasher.combine(count) + for element in self { + hasher.combine(element) } + } } extension _TinyArray.Storage: Sendable where Element: Sendable {} extension _TinyArray.Storage: RandomAccessCollection { - @inlinable - subscript(position: Int) -> Element { - get { - switch self { - case .one(let element): - guard position == 0 else { - fatalError("index \(position) out of bounds") - } - return element - case .arbitrary(let elements): - return elements[position] - } - } - set { - switch self { - case .one: - guard position == 0 else { - fatalError("index \(position) out of bounds") - } - self = .one(newValue) - case .arbitrary(var elements): - elements[position] = newValue - self = .arbitrary(elements) - } + @inlinable + subscript(position: Int) -> Element { + get { + switch self { + case .one(let element): + guard position == 0 else { + fatalError("index \(position) out of bounds") } + return element + case .arbitrary(let elements): + return elements[position] + } } - - @inlinable - var startIndex: Int { - 0 - } - - @inlinable - var endIndex: Int { - switch self { - case .one: return 1 - case .arbitrary(let elements): return elements.endIndex + set { + switch self { + case .one: + guard position == 0 else { + fatalError("index \(position) out of bounds") } + self = .one(newValue) + case .arbitrary(var elements): + elements[position] = newValue + self = .arbitrary(elements) + } + } + } + + @inlinable + var startIndex: Int { + 0 + } + + @inlinable + var endIndex: Int { + switch self { + case .one: return 1 + case .arbitrary(let elements): return elements.endIndex } + } } extension _TinyArray.Storage { - @inlinable - init(_ elements: some Sequence) { - var iterator = elements.makeIterator() + @inlinable + init(_ elements: some Sequence) { + var iterator = elements.makeIterator() + guard let firstElement = iterator.next() else { + self = .arbitrary([]) + return + } + guard let secondElement = iterator.next() else { + // newElements just contains a single element + // and we hit the fast path + self = .one(firstElement) + return + } + + var elements: [Element] = [] + elements.reserveCapacity(elements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + while let nextElement = iterator.next() { + elements.append(nextElement) + } + self = .arbitrary(elements) + } + + @inlinable + init() { + self = .arbitrary([]) + } + + @inlinable + mutating func append(_ newElement: Element) { + self.append(contentsOf: CollectionOfOne(newElement)) + } + + @inlinable + mutating func append(contentsOf newElements: some Sequence) { + switch self { + case .one(let firstElement): + var iterator = newElements.makeIterator() + guard let secondElement = iterator.next() else { + // newElements is empty, nothing to do + return + } + var elements: [Element] = [] + elements.reserveCapacity(1 + newElements.underestimatedCount) + elements.append(firstElement) + elements.append(secondElement) + elements.appendRemainingElements(from: &iterator) + self = .arbitrary(elements) + + case .arbitrary(var elements): + if elements.isEmpty { + // if `self` is currently empty and `newElements` just contains a single + // element, we skip allocating an array and set `self` to `.one(firstElement)` + var iterator = newElements.makeIterator() guard let firstElement = iterator.next() else { - self = .arbitrary([]) - return + // newElements is empty, nothing to do + return } guard let secondElement = iterator.next() else { - // newElements just contains a single element - // and we hit the fast path - self = .one(firstElement) - return + // newElements just contains a single element + // and we hit the fast path + self = .one(firstElement) + return } - - var elements: [Element] = [] - elements.reserveCapacity(elements.underestimatedCount) + elements.reserveCapacity(elements.count + newElements.underestimatedCount) elements.append(firstElement) elements.append(secondElement) - while let nextElement = iterator.next() { - elements.append(nextElement) - } + elements.appendRemainingElements(from: &iterator) self = .arbitrary(elements) - } - - @inlinable - init() { - self = .arbitrary([]) - } - @inlinable - mutating func append(_ newElement: Element) { - self.append(contentsOf: CollectionOfOne(newElement)) - } - - @inlinable - mutating func append(contentsOf newElements: some Sequence) { - switch self { - case .one(let firstElement): - var iterator = newElements.makeIterator() - guard let secondElement = iterator.next() else { - // newElements is empty, nothing to do - return - } - var elements: [Element] = [] - elements.reserveCapacity(1 + newElements.underestimatedCount) - elements.append(firstElement) - elements.append(secondElement) - elements.appendRemainingElements(from: &iterator) - self = .arbitrary(elements) - - case .arbitrary(var elements): - if elements.isEmpty { - // if `self` is currently empty and `newElements` just contains a single - // element, we skip allocating an array and set `self` to `.one(firstElement)` - var iterator = newElements.makeIterator() - guard let firstElement = iterator.next() else { - // newElements is empty, nothing to do - return - } - guard let secondElement = iterator.next() else { - // newElements just contains a single element - // and we hit the fast path - self = .one(firstElement) - return - } - elements.reserveCapacity(elements.count + newElements.underestimatedCount) - elements.append(firstElement) - elements.append(secondElement) - elements.appendRemainingElements(from: &iterator) - self = .arbitrary(elements) - - } else { - elements.append(contentsOf: newElements) - self = .arbitrary(elements) - } + } else { + elements.append(contentsOf: newElements) + self = .arbitrary(elements) + } - } } + } + + @discardableResult + @inlinable + mutating func remove(at index: Int) -> Element { + switch self { + case .one(let oldElement): + guard index == 0 else { + fatalError("index \(index) out of bounds") + } + self = .arbitrary([]) + return oldElement + + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + return elements.remove(at: index) - @discardableResult - @inlinable - mutating func remove(at index: Int) -> Element { - switch self { - case .one(let oldElement): - guard index == 0 else { - fatalError("index \(index) out of bounds") - } - self = .arbitrary([]) - return oldElement - - case .arbitrary(var elements): - defer { - self = .arbitrary(elements) - } - return elements.remove(at: index) - - } } + } - @inlinable - mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { - switch self { - case .one(let oldElement): - if try shouldBeRemoved(oldElement) { - self = .arbitrary([]) - } + @inlinable + mutating func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrows { + switch self { + case .one(let oldElement): + if try shouldBeRemoved(oldElement) { + self = .arbitrary([]) + } - case .arbitrary(var elements): - defer { - self = .arbitrary(elements) - } - return try elements.removeAll(where: shouldBeRemoved) + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } + return try elements.removeAll(where: shouldBeRemoved) - } } + } + + @inlinable + mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { + switch self { + case .one: + // a collection of just one element is always sorted, nothing to do + break + case .arbitrary(var elements): + defer { + self = .arbitrary(elements) + } - @inlinable - mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows { - switch self { - case .one: - // a collection of just one element is always sorted, nothing to do - break - case .arbitrary(var elements): - defer { - self = .arbitrary(elements) - } - - try elements.sort(by: areInIncreasingOrder) - } + try elements.sort(by: areInIncreasingOrder) } + } } extension Array { - @inlinable - mutating func appendRemainingElements(from iterator: inout some IteratorProtocol) { - while let nextElement = iterator.next() { - append(nextElement) - } + @inlinable + mutating func appendRemainingElements(from iterator: inout some IteratorProtocol) { + while let nextElement = iterator.next() { + append(nextElement) } + } } diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift index 19c85f16..74253c75 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift @@ -15,1653 +15,1650 @@ import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { + @usableFromInline + enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { @usableFromInline - enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { - @usableFromInline - struct _Watermark: Sendable, CustomStringConvertible { - /// The low watermark where demand should start. - @usableFromInline - let _low: Int - - /// The high watermark where demand should be stopped. - @usableFromInline - let _high: Int - - /// The current watermark level. - @usableFromInline - var _currentWatermark: Int = 0 - - /// A closure that can be used to calculate the watermark impact of a single element - @usableFromInline - let _waterLevelForElement: (@Sendable (borrowing Element) -> Int)? - - @usableFromInline - var description: String { - "watermark(\(self._currentWatermark))" - } - - init(low: Int, high: Int, waterLevelForElement: (@Sendable (borrowing Element) -> Int)?) { - precondition(low <= high) - self._low = low - self._high = high - self._waterLevelForElement = waterLevelForElement - } - - @inlinable - mutating func didSend(elements: Deque.SubSequence) -> Bool { - if let waterLevelForElement = self._waterLevelForElement { - for element in elements { - self._currentWatermark += waterLevelForElement(element) - } - } else { - self._currentWatermark += elements.count - } - precondition(self._currentWatermark >= 0) - // We are demanding more until we reach the high watermark - return self._currentWatermark < self._high - } - - @inlinable - mutating func didConsume(element: Element) -> Bool { - if let waterLevelForElement = self._waterLevelForElement { - self._currentWatermark -= waterLevelForElement(element) - } else { - self._currentWatermark -= 1 - } - precondition(self._currentWatermark >= 0) - // We start demanding again once we are below the low watermark - return self._currentWatermark < self._low - } + struct _Watermark: Sendable, CustomStringConvertible { + /// The low watermark where demand should start. + @usableFromInline + let _low: Int + + /// The high watermark where demand should be stopped. + @usableFromInline + let _high: Int + + /// The current watermark level. + @usableFromInline + var _currentWatermark: Int = 0 + + /// A closure that can be used to calculate the watermark impact of a single element + @usableFromInline + let _waterLevelForElement: (@Sendable (borrowing Element) -> Int)? + + @usableFromInline + var description: String { + "watermark(\(self._currentWatermark))" + } + + init(low: Int, high: Int, waterLevelForElement: (@Sendable (borrowing Element) -> Int)?) { + precondition(low <= high) + self._low = low + self._high = high + self._waterLevelForElement = waterLevelForElement + } + + @inlinable + mutating func didSend(elements: Deque.SubSequence) -> Bool { + if let waterLevelForElement = self._waterLevelForElement { + for element in elements { + self._currentWatermark += waterLevelForElement(element) + } + } else { + self._currentWatermark += elements.count } + precondition(self._currentWatermark >= 0) + // We are demanding more until we reach the high watermark + return self._currentWatermark < self._high + } + + @inlinable + mutating func didConsume(element: Element) -> Bool { + if let waterLevelForElement = self._waterLevelForElement { + self._currentWatermark -= waterLevelForElement(element) + } else { + self._currentWatermark -= 1 + } + precondition(self._currentWatermark >= 0) + // We start demanding again once we are below the low watermark + return self._currentWatermark < self._low + } + } - @usableFromInline - struct _Unbounded: Sendable, CustomStringConvertible { - @usableFromInline - var description: String { - "unbounded" - } - - init() {} + @usableFromInline + struct _Unbounded: Sendable, CustomStringConvertible { + @usableFromInline + var description: String { + "unbounded" + } + + init() {} + + @inlinable + mutating func didSend(elements: Deque.SubSequence) -> Bool { + true + } + + @inlinable + mutating func didConsume(element: Element) -> Bool { + true + } + } - @inlinable - mutating func didSend(elements: Deque.SubSequence) -> Bool { - true - } + /// A watermark based strategy. + case watermark(_Watermark) + /// An unbounded based strategy. + case unbounded(_Unbounded) - @inlinable - mutating func didConsume(element: Element) -> Bool { - true - } - } - - /// A watermark based strategy. - case watermark(_Watermark) - /// An unbounded based strategy. - case unbounded(_Unbounded) - - @usableFromInline - var description: String { - switch consume self { - case .watermark(let strategy): - return strategy.description - case .unbounded(let unbounded): - return unbounded.description - } - } + @usableFromInline + var description: String { + switch consume self { + case .watermark(let strategy): + return strategy.description + case .unbounded(let unbounded): + return unbounded.description + } + } - @inlinable - mutating func didSend(elements: Deque.SubSequence) -> Bool { - switch consume self { - case .watermark(var strategy): - let result = strategy.didSend(elements: elements) - self = .watermark(strategy) - return result - case .unbounded(var strategy): - let result = strategy.didSend(elements: elements) - self = .unbounded(strategy) - return result - } - } + @inlinable + mutating func didSend(elements: Deque.SubSequence) -> Bool { + switch consume self { + case .watermark(var strategy): + let result = strategy.didSend(elements: elements) + self = .watermark(strategy) + return result + case .unbounded(var strategy): + let result = strategy.didSend(elements: elements) + self = .unbounded(strategy) + return result + } + } - @inlinable - mutating func didConsume(element: Element) -> Bool { - switch consume self { - case .watermark(var strategy): - let result = strategy.didConsume(element: element) - self = .watermark(strategy) - return result - case .unbounded(var strategy): - let result = strategy.didConsume(element: element) - self = .unbounded(strategy) - return result - } - } + @inlinable + mutating func didConsume(element: Element) -> Bool { + switch consume self { + case .watermark(var strategy): + let result = strategy.didConsume(element: element) + self = .watermark(strategy) + return result + case .unbounded(var strategy): + let result = strategy.didConsume(element: element) + self = .unbounded(strategy) + return result + } } + } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { + @usableFromInline + final class _Storage: Sendable { @usableFromInline - final class _Storage: Sendable { - @usableFromInline - let _stateMachine: Mutex<_StateMachine> - - var onTermination: (@Sendable () -> Void)? { - set { - self._stateMachine.withLock { - $0._onTermination = newValue - } - } - get { - self._stateMachine.withLock { - $0._onTermination - } - } - } + let _stateMachine: Mutex<_StateMachine> - init( - backpressureStrategy: _InternalBackpressureStrategy - ) { - self._stateMachine = .init(.init(backpressureStrategy: backpressureStrategy)) + var onTermination: (@Sendable () -> Void)? { + set { + self._stateMachine.withLock { + $0._onTermination = newValue } - - func channelDeinitialized() { - let action = self._stateMachine.withLock { - $0.channelDeinitialized() - } - - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) - case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } - } - onTermination?() - - case .none: - break - } + } + get { + self._stateMachine.withLock { + $0._onTermination } + } + } - func sequenceInitialized() { - self._stateMachine.withLock { - $0.sequenceInitialized() - } + init( + backpressureStrategy: _InternalBackpressureStrategy + ) { + self._stateMachine = .init(.init(backpressureStrategy: backpressureStrategy)) + } + + func channelDeinitialized() { + let action = self._stateMachine.withLock { + $0.channelDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } } + onTermination?() - func sequenceDeinitialized() { - let action = self._stateMachine.withLock { - $0.sequenceDeinitialized() - } + case .none: + break + } + } - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) - case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } - } - onTermination?() + func sequenceInitialized() { + self._stateMachine.withLock { + $0.sequenceInitialized() + } + } - case .none: - break - } + func sequenceDeinitialized() { + let action = self._stateMachine.withLock { + $0.sequenceDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } } + onTermination?() - func iteratorInitialized() { - self._stateMachine.withLock { - $0.iteratorInitialized() - } + case .none: + break + } + } + + func iteratorInitialized() { + self._stateMachine.withLock { + $0.iteratorInitialized() + } + } + + func iteratorDeinitialized() { + let action = self._stateMachine.withLock { + $0.iteratorDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } } + onTermination?() - func iteratorDeinitialized() { - let action = self._stateMachine.withLock { - $0.iteratorDeinitialized() - } + case .none: + break + } + } - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) - case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } - } - onTermination?() + func sourceInitialized() { + self._stateMachine.withLock { + $0.sourceInitialized() + } + } - case .none: - break - } + func sourceDeinitialized() { + let action = self._stateMachine.withLock { + $0.sourceDeinitialized() + } + + switch action { + case .resumeConsumerAndCallOnTermination(let consumerContinuation, let failure, let onTermination): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) } - func sourceInitialized() { - self._stateMachine.withLock { - $0.sourceInitialized() - } - } + onTermination?() - func sourceDeinitialized() { - let action = self._stateMachine.withLock { - $0.sourceDeinitialized() - } + case .none: + break + } + } - switch action { - case .resumeConsumerAndCallOnTermination(let consumerContinuation, let failure, let onTermination): - switch failure { - case .some(let error): - consumerContinuation.resume(throwing: error) - case .none: - consumerContinuation.resume(returning: nil) - } + @inlinable + func send( + contentsOf sequence: sending some Sequence + ) throws -> MultiProducerSingleConsumerChannel.Source.SendResult { + let action = self._stateMachine.withLock { + $0.send(sequence) + } - onTermination?() + switch action { + case .returnProduceMore: + return .produceMore - case .none: - break - } - } + case .returnEnqueue(let callbackToken): + return .enqueueCallback(.init(id: callbackToken)) - @inlinable - func send( - contentsOf sequence: sending some Sequence - ) throws -> MultiProducerSingleConsumerChannel.Source.SendResult { - let action = self._stateMachine.withLock { - $0.send(sequence) - } + case .resumeConsumerAndReturnProduceMore(let continuation, let element): + continuation.resume(returning: element) + return .produceMore - switch action { - case .returnProduceMore: - return .produceMore + case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): + continuation.resume(returning: element) + return .enqueueCallback(.init(id: callbackToken)) - case .returnEnqueue(let callbackToken): - return .enqueueCallback(.init(id: callbackToken)) + case .throwFinishedError: + throw MultiProducerSingleConsumerChannelAlreadyFinishedError() + } + } - case .resumeConsumerAndReturnProduceMore(let continuation, let element): - continuation.resume(returning: element) - return .produceMore + @inlinable + func enqueueProducer( + callbackToken: UInt64, + continuation: UnsafeContinuation + ) { + let action = self._stateMachine.withLock { + $0.enqueueContinuation(callbackToken: callbackToken, continuation: continuation) + } + + switch action { + case .resumeProducer(let continuation): + continuation.resume() + + case .resumeProducerWithError(let continuation, let error): + continuation.resume(throwing: error) + + case .none: + break + } + } - case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): - continuation.resume(returning: element) - return .enqueueCallback(.init(id: callbackToken)) + @inlinable + func enqueueProducer( + callbackToken: UInt64, + onProduceMore: sending @escaping (Result) -> Void + ) { + let action = self._stateMachine.withLock { + $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + + switch action { + case .resumeProducer(let onProduceMore): + onProduceMore(Result.success(())) + + case .resumeProducerWithError(let onProduceMore, let error): + onProduceMore(Result.failure(error)) + + case .none: + break + } + } - case .throwFinishedError: - throw MultiProducerSingleConsumerChannelAlreadyFinishedError() - } + @inlinable + func cancelProducer( + callbackToken: UInt64 + ) { + let action = self._stateMachine.withLock { + $0.cancelProducer(callbackToken: callbackToken) + } + + switch action { + case .resumeProducerWithCancellationError(let onProduceMore): + switch onProduceMore { + case .closure(let onProduceMore): + onProduceMore(.failure(CancellationError())) + case .continuation(let continuation): + continuation.resume(throwing: CancellationError()) } - @inlinable - func enqueueProducer( - callbackToken: UInt64, - continuation: UnsafeContinuation - ) { - let action = self._stateMachine.withLock { - $0.enqueueContinuation(callbackToken: callbackToken, continuation: continuation) - } - - switch action { - case .resumeProducer(let continuation): - continuation.resume() - - case .resumeProducerWithError(let continuation, let error): - continuation.resume(throwing: error) + case .none: + break + } + } - case .none: - break - } + @inlinable + func finish(_ failure: Failure?) { + let action = self._stateMachine.withLock { + $0.finish(failure) + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .resumeConsumerAndCallOnTermination(let consumerContinuation, let failure, let onTermination): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) } - @inlinable - func enqueueProducer( - callbackToken: UInt64, - onProduceMore: sending @escaping (Result) -> Void - ) { - let action = self._stateMachine.withLock { - $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) - } + onTermination?() - switch action { - case .resumeProducer(let onProduceMore): - onProduceMore(Result.success(())) + case .resumeProducers(let producerContinuations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } - case .resumeProducerWithError(let onProduceMore, let error): - onProduceMore(Result.failure(error)) + case .none: + break + } + } - case .none: - break - } + @inlinable + func next(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { + let action = self._stateMachine.withLock { + $0.next() + } + + switch action { + case .returnElement(let element): + return element + + case .returnElementAndResumeProducers(let element, let producerContinuations): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.success(())) + case .continuation(let continuation): + continuation.resume() + } } - @inlinable - func cancelProducer( - callbackToken: UInt64 - ) { - let action = self._stateMachine.withLock { - $0.cancelProducer(callbackToken: callbackToken) - } + return element - switch action { - case .resumeProducerWithCancellationError(let onProduceMore): - switch onProduceMore { - case .closure(let onProduceMore): - onProduceMore(.failure(CancellationError())) - case .continuation(let continuation): - continuation.resume(throwing: CancellationError()) - } + case .returnFailureAndCallOnTermination(let failure, let onTermination): + onTermination?() + switch failure { + case .some(let error): + throw error - case .none: - break - } + case .none: + return nil } - @inlinable - func finish(_ failure: Failure?) { - let action = self._stateMachine.withLock { - $0.finish(failure) + case .returnNil: + return nil + + case .suspendTask: + return try await self.suspendNext() + } + } + + @inlinable + func suspendNext(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in + let action = self._stateMachine.withLock { + $0.suspendNext(continuation: continuation) + } + + switch action { + case .resumeConsumerWithElement(let continuation, let element): + continuation.resume(returning: element) + + case .resumeConsumerWithElementAndProducers( + let continuation, + let element, + let producerContinuations + ): + continuation.resume(returning: element) + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.success(())) + case .continuation(let continuation): + continuation.resume() + } } - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .resumeConsumerAndCallOnTermination(let consumerContinuation, let failure, let onTermination): - switch failure { - case .some(let error): - consumerContinuation.resume(throwing: error) - case .none: - consumerContinuation.resume(returning: nil) - } - - onTermination?() - - case .resumeProducers(let producerContinuations): - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) - case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } - } + case .resumeConsumerWithFailureAndCallOnTermination( + let continuation, + let failure, + let onTermination + ): + switch failure { + case .some(let error): + continuation.resume(throwing: error) case .none: - break + continuation.resume(returning: nil) } - } + onTermination?() - @inlinable - func next(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { - let action = self._stateMachine.withLock { - $0.next() - } + case .resumeConsumerWithNil(let continuation): + continuation.resume(returning: nil) - switch action { - case .returnElement(let element): - return element - - case .returnElementAndResumeProducers(let element, let producerContinuations): - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.success(())) - case .continuation(let continuation): - continuation.resume() - } - } - - return element - - case .returnFailureAndCallOnTermination(let failure, let onTermination): - onTermination?() - switch failure { - case .some(let error): - throw error - - case .none: - return nil - } - - case .returnNil: - return nil - - case .suspendTask: - return try await self.suspendNext() - } + case .none: + break + } + } + } onCancel: { + let action = self._stateMachine.withLock { + $0.cancelNext() } - @inlinable - func suspendNext(isolation: isolated (any Actor)? = #isolation) async throws -> Element? { - try await withTaskCancellationHandler { - try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation) in - let action = self._stateMachine.withLock { - $0.suspendNext(continuation: continuation) - } - - switch action { - case .resumeConsumerWithElement(let continuation, let element): - continuation.resume(returning: element) - - case .resumeConsumerWithElementAndProducers( - let continuation, - let element, - let producerContinuations - ): - continuation.resume(returning: element) - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.success(())) - case .continuation(let continuation): - continuation.resume() - } - } - - case .resumeConsumerWithFailureAndCallOnTermination( - let continuation, - let failure, - let onTermination - ): - switch failure { - case .some(let error): - continuation.resume(throwing: error) - - case .none: - continuation.resume(returning: nil) - } - onTermination?() - - case .resumeConsumerWithNil(let continuation): - continuation.resume(returning: nil) - - case .none: - break - } - } - } onCancel: { - let action = self._stateMachine.withLock { - $0.cancelNext() - } - - switch action { - case .resumeConsumerWithNilAndCallOnTermination(let continuation, let onTermination): - continuation.resume(returning: nil) - onTermination?() - - case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): - for producerContinuation in producerContinuations { - switch producerContinuation { - case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) - case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } - } - onTermination?() - - case .none: - break - } + switch action { + case .resumeConsumerWithNilAndCallOnTermination(let continuation, let onTermination): + continuation.resume(returning: nil) + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + switch producerContinuation { + case .closure(let onProduceMore): + onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + case .continuation(let continuation): + continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) } + } + onTermination?() + + case .none: + break } + } } + } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel._Storage { - /// The state machine of the channel. + /// The state machine of the channel. + @usableFromInline + struct _StateMachine: ~Copyable { + /// The state machine's current state. @usableFromInline - struct _StateMachine: ~Copyable { - /// The state machine's current state. - @usableFromInline - var _state: _State - - @inlinable - var _onTermination: (@Sendable () -> Void)? { - set { - switch consume self._state { - case .channeling(var channeling): - channeling.onTermination = newValue - self = .init(state: .channeling(channeling)) - - case .sourceFinished(var sourceFinished): - sourceFinished.onTermination = newValue - self = .init(state: .sourceFinished(sourceFinished)) - - case .finished(let finished): - self = .init(state: .finished(finished)) - } - } - get { - switch self._state { - case .channeling(let channeling): - return channeling.onTermination + var _state: _State + + @inlinable + var _onTermination: (@Sendable () -> Void)? { + set { + switch consume self._state { + case .channeling(var channeling): + channeling.onTermination = newValue + self = .init(state: .channeling(channeling)) + + case .sourceFinished(var sourceFinished): + sourceFinished.onTermination = newValue + self = .init(state: .sourceFinished(sourceFinished)) + + case .finished(let finished): + self = .init(state: .finished(finished)) + } + } + get { + switch self._state { + case .channeling(let channeling): + return channeling.onTermination - case .sourceFinished(let sourceFinished): - return sourceFinished.onTermination + case .sourceFinished(let sourceFinished): + return sourceFinished.onTermination - case .finished: - return nil - } - } + case .finished: + return nil } + } + } - init( - backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy - ) { - self._state = .channeling( - .init( - backpressureStrategy: backpressureStrategy, - iteratorInitialized: false, - sequenceInitialized: false, - buffer: .init(), - producerContinuations: .init(), - cancelledAsyncProducers: .init(), - hasOutstandingDemand: true, - activeProducers: 0, - nextCallbackTokenID: 0 - ) - ) - } + init( + backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy + ) { + self._state = .channeling( + .init( + backpressureStrategy: backpressureStrategy, + iteratorInitialized: false, + sequenceInitialized: false, + buffer: .init(), + producerContinuations: .init(), + cancelledAsyncProducers: .init(), + hasOutstandingDemand: true, + activeProducers: 0, + nextCallbackTokenID: 0 + ) + ) + } - @inlinable - init(state: consuming _State) { - self._state = state - } + @inlinable + init(state: consuming _State) { + self._state = state + } - @inlinable - mutating func sourceInitialized() { - switch consume self._state { - case .channeling(var channeling): - channeling.activeProducers += 1 - self = .init(state: .channeling(channeling)) + @inlinable + mutating func sourceInitialized() { + switch consume self._state { + case .channeling(var channeling): + channeling.activeProducers += 1 + self = .init(state: .channeling(channeling)) - case .sourceFinished(let sourceFinished): - self = .init(state: .sourceFinished(sourceFinished)) + case .sourceFinished(let sourceFinished): + self = .init(state: .sourceFinished(sourceFinished)) - case .finished(let finished): - self = .init(state: .finished(finished)) - } + case .finished(let finished): + self = .init(state: .finished(finished)) + } + } + + /// Actions returned by `sourceDeinitialized()`. + @usableFromInline + enum SourceDeinitialized { + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination` should be called. + case resumeConsumerAndCallOnTermination( + consumerContinuation: UnsafeContinuation, + failure: Failure?, + onTermination: (() -> Void)? + ) + } + + @inlinable + mutating func sourceDeinitialized() -> SourceDeinitialized? { + switch consume self._state { + case .channeling(var channeling): + channeling.activeProducers -= 1 + + guard channeling.activeProducers == 0 else { + // We still have more producers + self = .init(state: .channeling(channeling)) + + return nil + } + // This was the last producer so we can transition to source finished now + + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we are just going to mark + // the source as finished. + self = .init( + state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: nil, + onTermination: channeling.onTermination + ) + ) + ) + + return nil } + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(channeling.buffer.isEmpty, "Expected an empty buffer") + + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) + ) + ) + + return .resumeConsumerAndCallOnTermination( + consumerContinuation: consumerContinuation, + failure: nil, + onTermination: channeling.onTermination + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished, finishing again has no effect. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(var finished): + finished.sourceFinished = true + self = .init(state: .finished(finished)) + return .none + } + } - /// Actions returned by `sourceDeinitialized()`. - @usableFromInline - enum SourceDeinitialized { - /// Indicates that the consumer should be resumed with the failure, the producers - /// should be resumed with an error and `onTermination` should be called. - case resumeConsumerAndCallOnTermination( - consumerContinuation: UnsafeContinuation, - failure: Failure?, - onTermination: (() -> Void)? + @inlinable + mutating func sequenceInitialized() { + switch consume self._state { + case .channeling(var channeling): + channeling.sequenceInitialized = true + self = .init(state: .channeling(channeling)) + + case .sourceFinished(var sourceFinished): + sourceFinished.sequenceInitialized = true + self = .init(state: .sourceFinished(sourceFinished)) + + case .finished(var finished): + finished.sequenceInitialized = true + self = .init(state: .finished(finished)) + } + } + + /// Actions returned by `sequenceDeinitialized()`. + @usableFromInline + enum ChannelOrSequenceDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (@Sendable () -> Void)? + ) + } + + @inlinable + mutating func sequenceDeinitialized() -> ChannelOrSequenceDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + guard channeling.iteratorInitialized else { + precondition(channeling.sequenceInitialized, "Sequence was not initialized") + // No iterator was created so we can transition to finished right away. + self = .init( + state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: false + ) ) + ) + + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) } + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + self = .init(state: .channeling(channeling)) + + return .none + + case .sourceFinished(let sourceFinished): + guard sourceFinished.iteratorInitialized else { + precondition(sourceFinished.sequenceInitialized, "Sequence was not initialized") + // No iterator was created so we can transition to finished right away. + self = .init( + state: .finished( + .init( + iteratorInitialized: false, + sequenceInitialized: true, + sourceFinished: true + ) + ) + ) - @inlinable - mutating func sourceDeinitialized() -> SourceDeinitialized? { - switch consume self._state { - case .channeling(var channeling): - channeling.activeProducers -= 1 - - if channeling.activeProducers == 0 { - // This was the last producer so we can transition to source finished now - - guard let consumerContinuation = channeling.consumerContinuation else { - // We don't have a suspended consumer so we are just going to mark - // the source as finished. - self = .init( - state: .sourceFinished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - buffer: channeling.buffer, - failure: nil, - onTermination: channeling.onTermination - ) - ) - ) - - return nil - } - // We have a continuation, this means our buffer must be empty - // Furthermore, we can now transition to finished - // and resume the continuation with the failure - precondition(channeling.buffer.isEmpty, "Expected an empty buffer") - - self = .init( - state: .finished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: true - ) - ) - ) - - return .resumeConsumerAndCallOnTermination( - consumerContinuation: consumerContinuation, - failure: nil, - onTermination: channeling.onTermination - ) - } else { - // We still have more producers - self = .init(state: .channeling(channeling)) - - return nil - } - - case .sourceFinished(let sourceFinished): - // If the source has finished, finishing again has no effect. - self = .init(state: .sourceFinished(sourceFinished)) - - return .none - - case .finished(var finished): - finished.sourceFinished = true - self = .init(state: .finished(finished)) - return .none - } + return .callOnTermination(sourceFinished.onTermination) } + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + self = .init(state: .sourceFinished(sourceFinished)) - @inlinable - mutating func sequenceInitialized() { - switch consume self._state { - case .channeling(var channeling): - channeling.sequenceInitialized = true - self = .init(state: .channeling(channeling)) + return .none - case .sourceFinished(var sourceFinished): - sourceFinished.sequenceInitialized = true - self = .init(state: .sourceFinished(sourceFinished)) + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) - case .finished(var finished): - finished.sequenceInitialized = true - self = .init(state: .finished(finished)) - } - } + return .none + } + } - /// Actions returned by `sequenceDeinitialized()`. - @usableFromInline - enum ChannelOrSequenceDeinitializedAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((@Sendable () -> Void)?) - /// Indicates that all producers should be failed and `onTermination` should be called. - case failProducersAndCallOnTermination( - _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, - (@Sendable () -> Void)? + @inlinable + mutating func channelDeinitialized() -> ChannelOrSequenceDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + guard channeling.sequenceInitialized else { + // No async sequence was created so we can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true + ) ) + ) + + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) } + // An async sequence was created so we need to ignore this deinit + self = .init(state: .channeling(channeling)) + return nil + + case .sourceFinished(let sourceFinished): + guard sourceFinished.sequenceInitialized else { + // No async sequence was created so we can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) - @inlinable - mutating func sequenceDeinitialized() -> ChannelOrSequenceDeinitializedAction? { - switch consume self._state { - case .channeling(let channeling): - guard channeling.iteratorInitialized else { - precondition(channeling.sequenceInitialized, "Sequence was not initialized") - // No iterator was created so we can transition to finished right away. - self = .init( - state: .finished( - .init( - iteratorInitialized: false, - sequenceInitialized: true, - sourceFinished: false - ) - ) - ) - - return .failProducersAndCallOnTermination( - .init(channeling.suspendedProducers.lazy.map { $0.1 }), - channeling.onTermination - ) - } - // An iterator was created and we deinited the sequence. - // This is an expected pattern and we just continue on normal. - self = .init(state: .channeling(channeling)) - - return .none - - case .sourceFinished(let sourceFinished): - guard sourceFinished.iteratorInitialized else { - precondition(sourceFinished.sequenceInitialized, "Sequence was not initialized") - // No iterator was created so we can transition to finished right away. - self = .init( - state: .finished( - .init( - iteratorInitialized: false, - sequenceInitialized: true, - sourceFinished: true - ) - ) - ) - - return .callOnTermination(sourceFinished.onTermination) - } - // An iterator was created and we deinited the sequence. - // This is an expected pattern and we just continue on normal. - self = .init(state: .sourceFinished(sourceFinished)) - - return .none - - case .finished(let finished): - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - self = .init(state: .finished(finished)) - - return .none - } + return .callOnTermination(sourceFinished.onTermination) } + // An async sequence was created so we need to ignore this deinit + self = .init(state: .sourceFinished(sourceFinished)) + return nil - @inlinable - mutating func channelDeinitialized() -> ChannelOrSequenceDeinitializedAction? { - switch consume self._state { - case .channeling(let channeling): - if channeling.sequenceInitialized { - // An async sequence was created so we need to ignore this deinit - self = .init(state: .channeling(channeling)) - return nil - } else { - // No async sequence was created so we can transition to finished - self = .init( - state: .finished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: true - ) - ) - ) - - return .failProducersAndCallOnTermination( - .init(channeling.suspendedProducers.lazy.map { $0.1 }), - channeling.onTermination - ) - } - - case .sourceFinished(let sourceFinished): - if sourceFinished.sequenceInitialized { - // An async sequence was created so we need to ignore this deinit - self = .init(state: .sourceFinished(sourceFinished)) - return nil - } else { - // No async sequence was created so we can transition to finished - self = .init( - state: .finished( - .init( - iteratorInitialized: sourceFinished.iteratorInitialized, - sequenceInitialized: sourceFinished.sequenceInitialized, - sourceFinished: true - ) - ) - ) - - return .callOnTermination(sourceFinished.onTermination) - } - - case .finished(let finished): - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - self = .init(state: .finished(finished)) - - return .none - } + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) + + return .none + } + } + + @inlinable + mutating func iteratorInitialized() { + switch consume self._state { + case .channeling(var channeling): + if channeling.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + channeling.iteratorInitialized = true + self = .init(state: .channeling(channeling)) } - @inlinable - mutating func iteratorInitialized() { - switch consume self._state { - case .channeling(var channeling): - if channeling.iteratorInitialized { - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - } else { - // The first and only iterator was initialized. - channeling.iteratorInitialized = true - self = .init(state: .channeling(channeling)) - } - - case .sourceFinished(var sourceFinished): - if sourceFinished.iteratorInitialized { - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - } else { - // The first and only iterator was initialized. - sourceFinished.iteratorInitialized = true - self = .init(state: .sourceFinished(sourceFinished)) - } - - case .finished(let finished): - if finished.iteratorInitialized { - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - } else { - self = .init( - state: .finished( - .init( - iteratorInitialized: true, - sequenceInitialized: true, - sourceFinished: finished.sourceFinished - ) - ) - ) - } - } + case .sourceFinished(var sourceFinished): + if sourceFinished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + sourceFinished.iteratorInitialized = true + self = .init(state: .sourceFinished(sourceFinished)) } - /// Actions returned by `iteratorDeinitialized()`. - @usableFromInline - enum IteratorDeinitializedAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((@Sendable () -> Void)?) - /// Indicates that all producers should be failed and `onTermination` should be called. - case failProducersAndCallOnTermination( - _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, - (@Sendable () -> Void)? + case .finished(let finished): + if finished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: finished.sourceFinished + ) ) + ) } + } + } - @inlinable - mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { - switch consume self._state { - case .channeling(let channeling): - if channeling.iteratorInitialized { - // An iterator was created and deinited. Since we only support - // a single iterator we can now transition to finish. - self = .init( - state: .finished( - .init( - iteratorInitialized: true, - sequenceInitialized: true, - sourceFinished: false - ) - ) - ) - - return .failProducersAndCallOnTermination( - .init(channeling.suspendedProducers.lazy.map { $0.1 }), - channeling.onTermination - ) - } else { - // An iterator needs to be initialized before it can be deinitialized. - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") - } - - case .sourceFinished(let sourceFinished): - if sourceFinished.iteratorInitialized { - // An iterator was created and deinited. Since we only support - // a single iterator we can now transition to finish. - self = .init( - state: .finished( - .init( - iteratorInitialized: true, - sequenceInitialized: true, - sourceFinished: true - ) - ) - ) - - return .callOnTermination(sourceFinished.onTermination) - } else { - // An iterator needs to be initialized before it can be deinitialized. - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") - } - - case .finished(let finished): - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - self = .init(state: .finished(finished)) - - return .none - } - } + /// Actions returned by `iteratorDeinitialized()`. + @usableFromInline + enum IteratorDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (@Sendable () -> Void)? + ) + } - /// Actions returned by `send()`. - @usableFromInline - enum SendAction { - /// Indicates that the producer should be notified to produce more. - case returnProduceMore - /// Indicates that the producer should be suspended to stop producing. - case returnEnqueue( - callbackToken: UInt64 - ) - /// Indicates that the consumer should be resumed and the producer should be notified to produce more. - case resumeConsumerAndReturnProduceMore( - continuation: UnsafeContinuation, - element: Element + @inlinable + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch consume self._state { + case .channeling(let channeling): + if channeling.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: false + ) ) - /// Indicates that the consumer should be resumed and the producer should be suspended. - case resumeConsumerAndReturnEnqueue( - continuation: UnsafeContinuation, - element: Element, - callbackToken: UInt64 - ) - /// Indicates that the producer has been finished. - case throwFinishedError - - @inlinable - init( - callbackToken: UInt64?, - continuationAndElement: (UnsafeContinuation, Element)? = nil - ) { - switch (callbackToken, continuationAndElement) { - case (.none, .none): - self = .returnProduceMore - - case (.some(let callbackToken), .none): - self = .returnEnqueue(callbackToken: callbackToken) - - case (.none, .some((let continuation, let element))): - self = .resumeConsumerAndReturnProduceMore( - continuation: continuation, - element: element - ) - - case (.some(let callbackToken), .some((let continuation, let element))): - self = .resumeConsumerAndReturnEnqueue( - continuation: continuation, - element: element, - callbackToken: callbackToken - ) - } - } + ) + + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") } - @inlinable - mutating func send(_ sequence: sending some Sequence) -> SendAction { - switch consume self._state { - case .channeling(var channeling): - // We have an element and can resume the continuation - let bufferEndIndexBeforeAppend = channeling.buffer.endIndex - channeling.buffer.append(contentsOf: sequence) - var shouldProduceMore = channeling.backpressureStrategy.didSend( - elements: channeling.buffer[bufferEndIndexBeforeAppend...] - ) - channeling.hasOutstandingDemand = shouldProduceMore - - guard let consumerContinuation = channeling.consumerContinuation else { - // We don't have a suspended consumer so we just buffer the elements - let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() - self = .init(state: .channeling(channeling)) - - return .init( - callbackToken: callbackToken - ) - } - guard let element = channeling.buffer.popFirst() else { - // We got a send of an empty sequence. We just tolerate this. - let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() - self = .init(state: .channeling(channeling)) - - return .init(callbackToken: callbackToken) - } - // We need to tell the back pressure strategy that we consumed - shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) - channeling.hasOutstandingDemand = shouldProduceMore - - // We got a consumer continuation and an element. We can resume the consumer now - channeling.consumerContinuation = nil - let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() - self = .init(state: .channeling(channeling)) - - return .init( - callbackToken: callbackToken, - continuationAndElement: (consumerContinuation, element) - ) - - case .sourceFinished(let sourceFinished): - // If the source has finished we are dropping the elements. - self = .init(state: .sourceFinished(sourceFinished)) - - return .throwFinishedError - - case .finished(let finished): - // If the source has finished we are dropping the elements. - self = .init(state: .finished(finished)) - - return .throwFinishedError - } - } + case .sourceFinished(let sourceFinished): + if sourceFinished.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self = .init( + state: .finished( + .init( + iteratorInitialized: true, + sequenceInitialized: true, + sourceFinished: true + ) + ) + ) - /// Actions returned by `enqueueProducer()`. - @usableFromInline - enum EnqueueProducerAction { - /// Indicates that the producer should be notified to produce more. - case resumeProducer((Result) -> Void) - /// Indicates that the producer should be notified about an error. - case resumeProducerWithError((Result) -> Void, Error) + return .callOnTermination(sourceFinished.onTermination) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") } - @inlinable - mutating func enqueueProducer( - callbackToken: UInt64, - onProduceMore: sending @escaping (Result) -> Void - ) -> EnqueueProducerAction? { - switch consume self._state { - case .channeling(var channeling): - if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { - // Our producer got marked as cancelled. - channeling.cancelledAsyncProducers.remove(at: index) - self = .init(state: .channeling(channeling)) - - return .resumeProducerWithError(onProduceMore, CancellationError()) - } else if channeling.hasOutstandingDemand { - // We hit an edge case here where we wrote but the consuming thread got interleaved - self = .init(state: .channeling(channeling)) - - return .resumeProducer(onProduceMore) - } else { - channeling.suspendedProducers.append((callbackToken, .closure(onProduceMore))) - self = .init(state: .channeling(channeling)) - - return .none - } - - case .sourceFinished(let sourceFinished): - // Since we are unlocking between sending elements and suspending the send - // It can happen that the source got finished or the consumption fully finishes. - self = .init(state: .sourceFinished(sourceFinished)) - - return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) - - case .finished(let finished): - // Since we are unlocking between sending elements and suspending the send - // It can happen that the source got finished or the consumption fully finishes. - self = .init(state: .finished(finished)) - - return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } - } + case .finished(let finished): + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + self = .init(state: .finished(finished)) - /// Actions returned by `enqueueContinuation()`. - @usableFromInline - enum EnqueueContinuationAction { - /// Indicates that the producer should be notified to produce more. - case resumeProducer(UnsafeContinuation) - /// Indicates that the producer should be notified about an error. - case resumeProducerWithError(UnsafeContinuation, Error) - } + return .none + } + } - @inlinable - mutating func enqueueContinuation( - callbackToken: UInt64, - continuation: UnsafeContinuation - ) -> EnqueueContinuationAction? { - switch consume self._state { - case .channeling(var channeling): - if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { - // Our producer got marked as cancelled. - channeling.cancelledAsyncProducers.remove(at: index) - self = .init(state: .channeling(channeling)) - - return .resumeProducerWithError(continuation, CancellationError()) - } else if channeling.hasOutstandingDemand { - // We hit an edge case here where we wrote but the consuming thread got interleaved - self = .init(state: .channeling(channeling)) - - return .resumeProducer(continuation) - } else { - channeling.suspendedProducers.append((callbackToken, .continuation(continuation))) - self = .init(state: .channeling(channeling)) - - return .none - } - - case .sourceFinished(let sourceFinished): - // Since we are unlocking between sending elements and suspending the send - // It can happen that the source got finished or the consumption fully finishes. - self = .init(state: .sourceFinished(sourceFinished)) - - return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) - - case .finished(let finished): - // Since we are unlocking between sending elements and suspending the send - // It can happen that the source got finished or the consumption fully finishes. - self = .init(state: .finished(finished)) - - return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) - } + /// Actions returned by `send()`. + @usableFromInline + enum SendAction { + /// Indicates that the producer should be notified to produce more. + case returnProduceMore + /// Indicates that the producer should be suspended to stop producing. + case returnEnqueue( + callbackToken: UInt64 + ) + /// Indicates that the consumer should be resumed and the producer should be notified to produce more. + case resumeConsumerAndReturnProduceMore( + continuation: UnsafeContinuation, + element: Element + ) + /// Indicates that the consumer should be resumed and the producer should be suspended. + case resumeConsumerAndReturnEnqueue( + continuation: UnsafeContinuation, + element: Element, + callbackToken: UInt64 + ) + /// Indicates that the producer has been finished. + case throwFinishedError + + @inlinable + init( + callbackToken: UInt64?, + continuationAndElement: (UnsafeContinuation, Element)? = nil + ) { + switch (callbackToken, continuationAndElement) { + case (.none, .none): + self = .returnProduceMore + + case (.some(let callbackToken), .none): + self = .returnEnqueue(callbackToken: callbackToken) + + case (.none, .some((let continuation, let element))): + self = .resumeConsumerAndReturnProduceMore( + continuation: continuation, + element: element + ) + + case (.some(let callbackToken), .some((let continuation, let element))): + self = .resumeConsumerAndReturnEnqueue( + continuation: continuation, + element: element, + callbackToken: callbackToken + ) } + } + } - /// Actions returned by `cancelProducer()`. - @usableFromInline - enum CancelProducerAction { - /// Indicates that the producer should be notified about cancellation. - case resumeProducerWithCancellationError(_MultiProducerSingleConsumerSuspendedProducer) + @inlinable + mutating func send(_ sequence: sending some Sequence) -> SendAction { + switch consume self._state { + case .channeling(var channeling): + // We have an element and can resume the continuation + let bufferEndIndexBeforeAppend = channeling.buffer.endIndex + channeling.buffer.append(contentsOf: sequence) + var shouldProduceMore = channeling.backpressureStrategy.didSend( + elements: channeling.buffer[bufferEndIndexBeforeAppend...] + ) + channeling.hasOutstandingDemand = shouldProduceMore + + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we just buffer the elements + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) + + return .init( + callbackToken: callbackToken + ) } + guard let element = channeling.buffer.popFirst() else { + // We got a send of an empty sequence. We just tolerate this. + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) - @inlinable - mutating func cancelProducer( - callbackToken: UInt64 - ) -> CancelProducerAction? { - switch consume self._state { - case .channeling(var channeling): - guard let index = channeling.suspendedProducers.firstIndex(where: { $0.0 == callbackToken }) else { - // The task that sends was cancelled before sending elements so the cancellation handler - // got invoked right away - channeling.cancelledAsyncProducers.append(callbackToken) - self = .init(state: .channeling(channeling)) - - return .none - } - // We have an enqueued producer that we need to resume now - let continuation = channeling.suspendedProducers.remove(at: index).1 - self = .init(state: .channeling(channeling)) - - return .resumeProducerWithCancellationError(continuation) - - case .sourceFinished(let sourceFinished): - // Since we are unlocking between sending elements and suspending the send - // It can happen that the source got finished or the consumption fully finishes. - self = .init(state: .sourceFinished(sourceFinished)) - - return .none - - case .finished(let finished): - // Since we are unlocking between sending elements and suspending the send - // It can happen that the source got finished or the consumption fully finishes. - self = .init(state: .finished(finished)) - - return .none - } + return .init(callbackToken: callbackToken) } + // We need to tell the back pressure strategy that we consumed + shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore - /// Actions returned by `finish()`. - @usableFromInline - enum FinishAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((() -> Void)?) - /// Indicates that the consumer should be resumed with the failure, the producers - /// should be resumed with an error and `onTermination` should be called. - case resumeConsumerAndCallOnTermination( - consumerContinuation: UnsafeContinuation, - failure: Failure?, - onTermination: (() -> Void)? - ) - /// Indicates that the producers should be resumed with an error. - case resumeProducers( - producerContinuations: _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> - ) - } + // We got a consumer continuation and an element. We can resume the consumer now + channeling.consumerContinuation = nil + let callbackToken = shouldProduceMore ? nil : channeling.nextCallbackToken() + self = .init(state: .channeling(channeling)) - @inlinable - mutating func finish(_ failure: Failure?) -> FinishAction? { - switch consume self._state { - case .channeling(let channeling): - guard let consumerContinuation = channeling.consumerContinuation else { - // We don't have a suspended consumer so we are just going to mark - // the source as finished and terminate the current suspended producers. - self = .init( - state: .sourceFinished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - buffer: channeling.buffer, - failure: failure, - onTermination: channeling.onTermination - ) - ) - ) - - return .resumeProducers( - producerContinuations: .init(channeling.suspendedProducers.lazy.map { $0.1 }) - ) - } - // We have a continuation, this means our buffer must be empty - // Furthermore, we can now transition to finished - // and resume the continuation with the failure - precondition(channeling.buffer.isEmpty, "Expected an empty buffer") - - self = .init( - state: .finished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: true - ) - ) - ) - - return .resumeConsumerAndCallOnTermination( - consumerContinuation: consumerContinuation, - failure: failure, - onTermination: channeling.onTermination - ) - - case .sourceFinished(let sourceFinished): - // If the source has finished, finishing again has no effect. - self = .init(state: .sourceFinished(sourceFinished)) - - return .none - - case .finished(var finished): - finished.sourceFinished = true - self = .init(state: .finished(finished)) - return .none - } + return .init( + callbackToken: callbackToken, + continuationAndElement: (consumerContinuation, element) + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished we are dropping the elements. + self = .init(state: .sourceFinished(sourceFinished)) + + return .throwFinishedError + + case .finished(let finished): + // If the source has finished we are dropping the elements. + self = .init(state: .finished(finished)) + + return .throwFinishedError + } + } + + /// Actions returned by `enqueueProducer()`. + @usableFromInline + enum EnqueueProducerAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer((Result) -> Void) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError((Result) -> Void, Error) + } + + @inlinable + mutating func enqueueProducer( + callbackToken: UInt64, + onProduceMore: sending @escaping (Result) -> Void + ) -> EnqueueProducerAction? { + switch consume self._state { + case .channeling(var channeling): + if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { + // Our producer got marked as cancelled. + channeling.cancelledAsyncProducers.remove(at: index) + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithError(onProduceMore, CancellationError()) + } else if channeling.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + self = .init(state: .channeling(channeling)) + + return .resumeProducer(onProduceMore) + } else { + channeling.suspendedProducers.append((callbackToken, .closure(onProduceMore))) + self = .init(state: .channeling(channeling)) + + return .none } - /// Actions returned by `next()`. - @usableFromInline - enum NextAction { - /// Indicates that the element should be returned to the caller. - case returnElement(Element) - /// Indicates that the element should be returned to the caller and that all producers should be called. - case returnElementAndResumeProducers(Element, _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>) - /// Indicates that the `Failure` should be returned to the caller and that `onTermination` should be called. - case returnFailureAndCallOnTermination(Failure?, (() -> Void)?) - /// Indicates that the `nil` should be returned to the caller. - case returnNil - /// Indicates that the `Task` of the caller should be suspended. - case suspendTask + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + + /// Actions returned by `enqueueContinuation()`. + @usableFromInline + enum EnqueueContinuationAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer(UnsafeContinuation) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError(UnsafeContinuation, Error) + } + + @inlinable + mutating func enqueueContinuation( + callbackToken: UInt64, + continuation: UnsafeContinuation + ) -> EnqueueContinuationAction? { + switch consume self._state { + case .channeling(var channeling): + if let index = channeling.cancelledAsyncProducers.firstIndex(of: callbackToken) { + // Our producer got marked as cancelled. + channeling.cancelledAsyncProducers.remove(at: index) + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithError(continuation, CancellationError()) + } else if channeling.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + self = .init(state: .channeling(channeling)) + + return .resumeProducer(continuation) + } else { + channeling.suspendedProducers.append((callbackToken, .continuation(continuation))) + self = .init(state: .channeling(channeling)) + + return .none } - @inlinable - mutating func next() -> NextAction { - switch consume self._state { - case .channeling(var channeling): - guard channeling.consumerContinuation == nil else { - // We have multiple AsyncIterators iterating the sequence - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") - } - - guard let element = channeling.buffer.popFirst() else { - // There is nothing in the buffer to fulfil the demand so we need to suspend. - // We are not interacting with the backpressure strategy here because - // we are doing this inside `suspendNext` - self = .init(state: .channeling(channeling)) - - return .suspendTask - } - // We have an element to fulfil the demand right away. - let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) - channeling.hasOutstandingDemand = shouldProduceMore - - guard shouldProduceMore else { - // We don't have any new demand, so we can just return the element. - self = .init(state: .channeling(channeling)) - - return .returnElement(element) - } - // There is demand and we have to resume our producers - let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) - channeling.suspendedProducers.removeAll(keepingCapacity: true) - self = .init(state: .channeling(channeling)) - - return .returnElementAndResumeProducers(element, producers) - - case .sourceFinished(var sourceFinished): - // Check if we have an element left in the buffer and return it - guard let element = sourceFinished.buffer.popFirst() else { - // We are returning the queued failure now and can transition to finished - self = .init( - state: .finished( - .init( - iteratorInitialized: sourceFinished.iteratorInitialized, - sequenceInitialized: sourceFinished.sequenceInitialized, - sourceFinished: true - ) - ) - ) - - return .returnFailureAndCallOnTermination(sourceFinished.failure, sourceFinished.onTermination) - } - self = .init(state: .sourceFinished(sourceFinished)) - - return .returnElement(element) - - case .finished(let finished): - self = .init(state: .finished(finished)) - - return .returnNil - } + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) + + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + } + } + + /// Actions returned by `cancelProducer()`. + @usableFromInline + enum CancelProducerAction { + /// Indicates that the producer should be notified about cancellation. + case resumeProducerWithCancellationError(_MultiProducerSingleConsumerSuspendedProducer) + } + + @inlinable + mutating func cancelProducer( + callbackToken: UInt64 + ) -> CancelProducerAction? { + switch consume self._state { + case .channeling(var channeling): + guard let index = channeling.suspendedProducers.firstIndex(where: { $0.0 == callbackToken }) else { + // The task that sends was cancelled before sending elements so the cancellation handler + // got invoked right away + channeling.cancelledAsyncProducers.append(callbackToken) + self = .init(state: .channeling(channeling)) + + return .none } + // We have an enqueued producer that we need to resume now + let continuation = channeling.suspendedProducers.remove(at: index).1 + self = .init(state: .channeling(channeling)) + + return .resumeProducerWithCancellationError(continuation) + + case .sourceFinished(let sourceFinished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .sourceFinished(sourceFinished)) - /// Actions returned by `suspendNext()`. - @usableFromInline - enum SuspendNextAction { - /// Indicates that the consumer should be resumed. - case resumeConsumerWithElement(UnsafeContinuation, Element) - /// Indicates that the consumer and all producers should be resumed. - case resumeConsumerWithElementAndProducers( - UnsafeContinuation, - Element, - _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + return .none + + case .finished(let finished): + // Since we are unlocking between sending elements and suspending the send + // It can happen that the source got finished or the consumption fully finishes. + self = .init(state: .finished(finished)) + + return .none + } + } + + /// Actions returned by `finish()`. + @usableFromInline + enum FinishAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((() -> Void)?) + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination` should be called. + case resumeConsumerAndCallOnTermination( + consumerContinuation: UnsafeContinuation, + failure: Failure?, + onTermination: (() -> Void)? + ) + /// Indicates that the producers should be resumed with an error. + case resumeProducers( + producerContinuations: _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + } + + @inlinable + mutating func finish(_ failure: Failure?) -> FinishAction? { + switch consume self._state { + case .channeling(let channeling): + guard let consumerContinuation = channeling.consumerContinuation else { + // We don't have a suspended consumer so we are just going to mark + // the source as finished and terminate the current suspended producers. + self = .init( + state: .sourceFinished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + buffer: channeling.buffer, + failure: failure, + onTermination: channeling.onTermination + ) ) - /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. - case resumeConsumerWithFailureAndCallOnTermination( - UnsafeContinuation, - Failure?, - (() -> Void)? + ) + + return .resumeProducers( + producerContinuations: .init(channeling.suspendedProducers.lazy.map { $0.1 }) + ) + } + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(channeling.buffer.isEmpty, "Expected an empty buffer") + + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: true ) - /// Indicates that the consumer should be resumed with `nil`. - case resumeConsumerWithNil(UnsafeContinuation) + ) + ) + + return .resumeConsumerAndCallOnTermination( + consumerContinuation: consumerContinuation, + failure: failure, + onTermination: channeling.onTermination + ) + + case .sourceFinished(let sourceFinished): + // If the source has finished, finishing again has no effect. + self = .init(state: .sourceFinished(sourceFinished)) + + return .none + + case .finished(var finished): + finished.sourceFinished = true + self = .init(state: .finished(finished)) + return .none + } + } + + /// Actions returned by `next()`. + @usableFromInline + enum NextAction { + /// Indicates that the element should be returned to the caller. + case returnElement(Element) + /// Indicates that the element should be returned to the caller and that all producers should be called. + case returnElementAndResumeProducers(Element, _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>) + /// Indicates that the `Failure` should be returned to the caller and that `onTermination` should be called. + case returnFailureAndCallOnTermination(Failure?, (() -> Void)?) + /// Indicates that the `nil` should be returned to the caller. + case returnNil + /// Indicates that the `Task` of the caller should be suspended. + case suspendTask + } + + @inlinable + mutating func next() -> NextAction { + switch consume self._state { + case .channeling(var channeling): + guard channeling.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") } - @inlinable - mutating func suspendNext(continuation: UnsafeContinuation) -> SuspendNextAction? { - switch consume self._state { - case .channeling(var channeling): - guard channeling.consumerContinuation == nil else { - // We have multiple AsyncIterators iterating the sequence - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") - } - - // We have to check here again since we might have a producer interleave next and suspendNext - guard let element = channeling.buffer.popFirst() else { - // There is nothing in the buffer to fulfil the demand so we to store the continuation. - channeling.consumerContinuation = continuation - self = .init(state: .channeling(channeling)) - - return .none - } - // We have an element to fulfil the demand right away. - - let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) - channeling.hasOutstandingDemand = shouldProduceMore - - guard shouldProduceMore else { - // We don't have any new demand, so we can just return the element. - self = .init(state: .channeling(channeling)) - - return .resumeConsumerWithElement(continuation, element) - } - // There is demand and we have to resume our producers - let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) - channeling.suspendedProducers.removeAll(keepingCapacity: true) - self = .init(state: .channeling(channeling)) - - return .resumeConsumerWithElementAndProducers(continuation, element, producers) - - case .sourceFinished(var sourceFinished): - // Check if we have an element left in the buffer and return it - guard let element = sourceFinished.buffer.popFirst() else { - // We are returning the queued failure now and can transition to finished - self = .init( - state: .finished( - .init( - iteratorInitialized: sourceFinished.iteratorInitialized, - sequenceInitialized: sourceFinished.sequenceInitialized, - sourceFinished: true - ) - ) - ) - - return .resumeConsumerWithFailureAndCallOnTermination( - continuation, - sourceFinished.failure, - sourceFinished.onTermination - ) - } - self = .init(state: .sourceFinished(sourceFinished)) - - return .resumeConsumerWithElement(continuation, element) - - case .finished(let finished): - self = .init(state: .finished(finished)) - - return .resumeConsumerWithNil(continuation) - } + guard let element = channeling.buffer.popFirst() else { + // There is nothing in the buffer to fulfil the demand so we need to suspend. + // We are not interacting with the backpressure strategy here because + // we are doing this inside `suspendNext` + self = .init(state: .channeling(channeling)) + + return .suspendTask } + // We have an element to fulfil the demand right away. + let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore - /// Actions returned by `cancelNext()`. - @usableFromInline - enum CancelNextAction { - /// Indicates that the continuation should be resumed with nil, the producers should be finished and call onTermination. - case resumeConsumerWithNilAndCallOnTermination(UnsafeContinuation, (() -> Void)?) - /// Indicates that the producers should be finished and call onTermination. - case failProducersAndCallOnTermination( - _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, - (() -> Void)? - ) + guard shouldProduceMore else { + // We don't have any new demand, so we can just return the element. + self = .init(state: .channeling(channeling)) + + return .returnElement(element) } + // There is demand and we have to resume our producers + let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) + channeling.suspendedProducers.removeAll(keepingCapacity: true) + self = .init(state: .channeling(channeling)) + + return .returnElementAndResumeProducers(element, producers) + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + guard let element = sourceFinished.buffer.popFirst() else { + // We are returning the queued failure now and can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) - @inlinable - mutating func cancelNext() -> CancelNextAction? { - switch consume self._state { - case .channeling(let channeling): - self = .init( - state: .finished( - .init( - iteratorInitialized: channeling.iteratorInitialized, - sequenceInitialized: channeling.sequenceInitialized, - sourceFinished: false - ) - ) - ) - - guard let consumerContinuation = channeling.consumerContinuation else { - return .failProducersAndCallOnTermination( - .init(channeling.suspendedProducers.lazy.map { $0.1 }), - channeling.onTermination - ) - } - precondition( - channeling.suspendedProducers.isEmpty, - "Internal inconsistency. Unexpected producer continuations." - ) - return .resumeConsumerWithNilAndCallOnTermination( - consumerContinuation, - channeling.onTermination - ) - - case .sourceFinished(let sourceFinished): - self = .init(state: .sourceFinished(sourceFinished)) - - return .none - - case .finished(let finished): - self = .init(state: .finished(finished)) - - return .none - } + return .returnFailureAndCallOnTermination(sourceFinished.failure, sourceFinished.onTermination) } + self = .init(state: .sourceFinished(sourceFinished)) + + return .returnElement(element) + + case .finished(let finished): + self = .init(state: .finished(finished)) + + return .returnNil + } } -} -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel._Storage._StateMachine { + /// Actions returned by `suspendNext()`. @usableFromInline - enum _State: ~Copyable { - @usableFromInline - struct Channeling: ~Copyable { - /// The backpressure strategy. - @usableFromInline - var backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy - - /// Indicates if the iterator was initialized. - @usableFromInline - var iteratorInitialized: Bool - - /// Indicates if an async sequence was initialized. - @usableFromInline - var sequenceInitialized: Bool - - /// The onTermination callback. - @usableFromInline - var onTermination: (@Sendable () -> Void)? - - /// The buffer of elements. - @usableFromInline - var buffer: Deque - - /// The optional consumer continuation. - @usableFromInline - var consumerContinuation: UnsafeContinuation? - - /// The producer continuations. - @usableFromInline - var suspendedProducers: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)> - - /// The producers that have been cancelled. - @usableFromInline - var cancelledAsyncProducers: Deque - - /// Indicates if we currently have outstanding demand. - @usableFromInline - var hasOutstandingDemand: Bool - - /// The number of active producers. - @usableFromInline - var activeProducers: UInt64 - - /// The next callback token. - @usableFromInline - var nextCallbackTokenID: UInt64 - - var description: String { - "backpressure:\(self.backpressureStrategy.description) iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) consumerContinuation:\(self.consumerContinuation == nil) producerContinuations:\(self.suspendedProducers.count) cancelledProducers:\(self.cancelledAsyncProducers.count) hasOutstandingDemand:\(self.hasOutstandingDemand)" - } + enum SuspendNextAction { + /// Indicates that the consumer should be resumed. + case resumeConsumerWithElement(UnsafeContinuation, Element) + /// Indicates that the consumer and all producers should be resumed. + case resumeConsumerWithElementAndProducers( + UnsafeContinuation, + Element, + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer> + ) + /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. + case resumeConsumerWithFailureAndCallOnTermination( + UnsafeContinuation, + Failure?, + (() -> Void)? + ) + /// Indicates that the consumer should be resumed with `nil`. + case resumeConsumerWithNil(UnsafeContinuation) + } - @inlinable - init( - backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy, - iteratorInitialized: Bool, - sequenceInitialized: Bool, - onTermination: (@Sendable () -> Void)? = nil, - buffer: Deque, - consumerContinuation: UnsafeContinuation? = nil, - producerContinuations: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)>, - cancelledAsyncProducers: Deque, - hasOutstandingDemand: Bool, - activeProducers: UInt64, - nextCallbackTokenID: UInt64 - ) { - self.backpressureStrategy = backpressureStrategy - self.iteratorInitialized = iteratorInitialized - self.sequenceInitialized = sequenceInitialized - self.onTermination = onTermination - self.buffer = buffer - self.consumerContinuation = consumerContinuation - self.suspendedProducers = producerContinuations - self.cancelledAsyncProducers = cancelledAsyncProducers - self.hasOutstandingDemand = hasOutstandingDemand - self.activeProducers = activeProducers - self.nextCallbackTokenID = nextCallbackTokenID - } + @inlinable + mutating func suspendNext(continuation: UnsafeContinuation) -> SuspendNextAction? { + switch consume self._state { + case .channeling(var channeling): + guard channeling.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + } - /// Generates the next callback token. - @inlinable - mutating func nextCallbackToken() -> UInt64 { - let id = self.nextCallbackTokenID - self.nextCallbackTokenID += 1 - return id - } + // We have to check here again since we might have a producer interleave next and suspendNext + guard let element = channeling.buffer.popFirst() else { + // There is nothing in the buffer to fulfil the demand so we to store the continuation. + channeling.consumerContinuation = continuation + self = .init(state: .channeling(channeling)) + + return .none } + // We have an element to fulfil the demand right away. - @usableFromInline - struct SourceFinished: ~Copyable { - /// Indicates if the iterator was initialized. - @usableFromInline - var iteratorInitialized: Bool + let shouldProduceMore = channeling.backpressureStrategy.didConsume(element: element) + channeling.hasOutstandingDemand = shouldProduceMore - /// Indicates if an async sequence was initialized. - @usableFromInline - var sequenceInitialized: Bool + guard shouldProduceMore else { + // We don't have any new demand, so we can just return the element. + self = .init(state: .channeling(channeling)) - /// The buffer of elements. - @usableFromInline - var buffer: Deque + return .resumeConsumerWithElement(continuation, element) + } + // There is demand and we have to resume our producers + let producers = _TinyArray(channeling.suspendedProducers.lazy.map { $0.1 }) + channeling.suspendedProducers.removeAll(keepingCapacity: true) + self = .init(state: .channeling(channeling)) + + return .resumeConsumerWithElementAndProducers(continuation, element, producers) + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + guard let element = sourceFinished.buffer.popFirst() else { + // We are returning the queued failure now and can transition to finished + self = .init( + state: .finished( + .init( + iteratorInitialized: sourceFinished.iteratorInitialized, + sequenceInitialized: sourceFinished.sequenceInitialized, + sourceFinished: true + ) + ) + ) - /// The failure that should be thrown after the last element has been consumed. - @usableFromInline - var failure: Failure? + return .resumeConsumerWithFailureAndCallOnTermination( + continuation, + sourceFinished.failure, + sourceFinished.onTermination + ) + } + self = .init(state: .sourceFinished(sourceFinished)) - /// The onTermination callback. - @usableFromInline - var onTermination: (@Sendable () -> Void)? + return .resumeConsumerWithElement(continuation, element) - var description: String { - "iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) failure:\(self.failure == nil)" - } + case .finished(let finished): + self = .init(state: .finished(finished)) - @inlinable - init( - iteratorInitialized: Bool, - sequenceInitialized: Bool, - buffer: Deque, - failure: Failure? = nil, - onTermination: (@Sendable () -> Void)? = nil - ) { - self.iteratorInitialized = iteratorInitialized - self.sequenceInitialized = sequenceInitialized - self.buffer = buffer - self.failure = failure - self.onTermination = onTermination - } + return .resumeConsumerWithNil(continuation) + } + } + + /// Actions returned by `cancelNext()`. + @usableFromInline + enum CancelNextAction { + /// Indicates that the continuation should be resumed with nil, the producers should be finished and call onTermination. + case resumeConsumerWithNilAndCallOnTermination(UnsafeContinuation, (() -> Void)?) + /// Indicates that the producers should be finished and call onTermination. + case failProducersAndCallOnTermination( + _TinyArray<_MultiProducerSingleConsumerSuspendedProducer>, + (() -> Void)? + ) + } + + @inlinable + mutating func cancelNext() -> CancelNextAction? { + switch consume self._state { + case .channeling(let channeling): + self = .init( + state: .finished( + .init( + iteratorInitialized: channeling.iteratorInitialized, + sequenceInitialized: channeling.sequenceInitialized, + sourceFinished: false + ) + ) + ) + + guard let consumerContinuation = channeling.consumerContinuation else { + return .failProducersAndCallOnTermination( + .init(channeling.suspendedProducers.lazy.map { $0.1 }), + channeling.onTermination + ) } + precondition( + channeling.suspendedProducers.isEmpty, + "Internal inconsistency. Unexpected producer continuations." + ) + return .resumeConsumerWithNilAndCallOnTermination( + consumerContinuation, + channeling.onTermination + ) - @usableFromInline - struct Finished: ~Copyable { - /// Indicates if the iterator was initialized. - @usableFromInline - var iteratorInitialized: Bool + case .sourceFinished(let sourceFinished): + self = .init(state: .sourceFinished(sourceFinished)) - /// Indicates if an async sequence was initialized. - @usableFromInline - var sequenceInitialized: Bool + return .none - /// Indicates if the source was finished. - @usableFromInline - var sourceFinished: Bool + case .finished(let finished): + self = .init(state: .finished(finished)) - var description: String { - "iteratorInitialized:\(self.iteratorInitialized) sourceFinished:\(self.sourceFinished)" - } + return .none + } + } + } +} - @inlinable - init( - iteratorInitialized: Bool, - sequenceInitialized: Bool, - sourceFinished: Bool - ) { - self.iteratorInitialized = iteratorInitialized - self.sequenceInitialized = sequenceInitialized - self.sourceFinished = sourceFinished - } - } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel._Storage._StateMachine { + @usableFromInline + enum _State: ~Copyable { + @usableFromInline + struct Channeling: ~Copyable { + /// The backpressure strategy. + @usableFromInline + var backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy + + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool + + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + + /// The buffer of elements. + @usableFromInline + var buffer: Deque + + /// The optional consumer continuation. + @usableFromInline + var consumerContinuation: UnsafeContinuation? + + /// The producer continuations. + @usableFromInline + var suspendedProducers: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)> + + /// The producers that have been cancelled. + @usableFromInline + var cancelledAsyncProducers: Deque + + /// Indicates if we currently have outstanding demand. + @usableFromInline + var hasOutstandingDemand: Bool + + /// The number of active producers. + @usableFromInline + var activeProducers: UInt64 + + /// The next callback token. + @usableFromInline + var nextCallbackTokenID: UInt64 + + var description: String { + "backpressure:\(self.backpressureStrategy.description) iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) consumerContinuation:\(self.consumerContinuation == nil) producerContinuations:\(self.suspendedProducers.count) cancelledProducers:\(self.cancelledAsyncProducers.count) hasOutstandingDemand:\(self.hasOutstandingDemand)" + } + + @inlinable + init( + backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy, + iteratorInitialized: Bool, + sequenceInitialized: Bool, + onTermination: (@Sendable () -> Void)? = nil, + buffer: Deque, + consumerContinuation: UnsafeContinuation? = nil, + producerContinuations: Deque<(UInt64, _MultiProducerSingleConsumerSuspendedProducer)>, + cancelledAsyncProducers: Deque, + hasOutstandingDemand: Bool, + activeProducers: UInt64, + nextCallbackTokenID: UInt64 + ) { + self.backpressureStrategy = backpressureStrategy + self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized + self.onTermination = onTermination + self.buffer = buffer + self.consumerContinuation = consumerContinuation + self.suspendedProducers = producerContinuations + self.cancelledAsyncProducers = cancelledAsyncProducers + self.hasOutstandingDemand = hasOutstandingDemand + self.activeProducers = activeProducers + self.nextCallbackTokenID = nextCallbackTokenID + } + + /// Generates the next callback token. + @inlinable + mutating func nextCallbackToken() -> UInt64 { + let id = self.nextCallbackTokenID + self.nextCallbackTokenID += 1 + return id + } + } - /// The state once either any element was sent or `next()` was called. - case channeling(Channeling) - - /// The state once the underlying source signalled that it is finished. - case sourceFinished(SourceFinished) - - /// The state once there can be no outstanding demand. This can happen if: - /// 1. The iterator was deinited - /// 2. The underlying source finished and all buffered elements have been consumed - case finished(Finished) - - @usableFromInline - var description: String { - switch self { - case .channeling(let channeling): - return "channeling \(channeling.description)" - case .sourceFinished(let sourceFinished): - return "sourceFinished \(sourceFinished.description)" - case .finished(let finished): - return "finished \(finished.description)" - } - } + @usableFromInline + struct SourceFinished: ~Copyable { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool + + /// The buffer of elements. + @usableFromInline + var buffer: Deque + + /// The failure that should be thrown after the last element has been consumed. + @usableFromInline + var failure: Failure? + + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + + var description: String { + "iteratorInitialized:\(self.iteratorInitialized) buffer:\(self.buffer.count) failure:\(self.failure == nil)" + } + + @inlinable + init( + iteratorInitialized: Bool, + sequenceInitialized: Bool, + buffer: Deque, + failure: Failure? = nil, + onTermination: (@Sendable () -> Void)? = nil + ) { + self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized + self.buffer = buffer + self.failure = failure + self.onTermination = onTermination + } + } + + @usableFromInline + struct Finished: ~Copyable { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + + /// Indicates if an async sequence was initialized. + @usableFromInline + var sequenceInitialized: Bool + + /// Indicates if the source was finished. + @usableFromInline + var sourceFinished: Bool + + var description: String { + "iteratorInitialized:\(self.iteratorInitialized) sourceFinished:\(self.sourceFinished)" + } + + @inlinable + init( + iteratorInitialized: Bool, + sequenceInitialized: Bool, + sourceFinished: Bool + ) { + self.iteratorInitialized = iteratorInitialized + self.sequenceInitialized = sequenceInitialized + self.sourceFinished = sourceFinished + } + } + + /// The state once either any element was sent or `next()` was called. + case channeling(Channeling) + + /// The state once the underlying source signalled that it is finished. + case sourceFinished(SourceFinished) + + /// The state once there can be no outstanding demand. This can happen if: + /// 1. The iterator was deinited + /// 2. The underlying source finished and all buffered elements have been consumed + case finished(Finished) + + @usableFromInline + var description: String { + switch self { + case .channeling(let channeling): + return "channeling \(channeling.description)" + case .sourceFinished(let sourceFinished): + return "sourceFinished \(sourceFinished.description)" + case .finished(let finished): + return "finished \(finished.description)" + } } + } } @usableFromInline enum _MultiProducerSingleConsumerSuspendedProducer { - case closure((Result) -> Void) - case continuation(UnsafeContinuation) + case closure((Result) -> Void) + case continuation(UnsafeContinuation) } #endif diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index 3c3b64a9..4208c116 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -16,8 +16,8 @@ /// This error is thrown when the channel is already finished when /// trying to send new elements to the source. public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { - @usableFromInline - init() {} + @usableFromInline + init() {} } /// A multi producer single consumer channel. @@ -66,507 +66,507 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { /// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct MultiProducerSingleConsumerChannel: ~Copyable { - /// The backing storage. - @usableFromInline - let storage: _Storage - - /// A struct containing the initialized channel and source. - /// - /// This struct can be deconstructed by consuming the individual - /// components from it. - /// - /// ```swift - /// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - /// of: Int.self, - /// backpressureStrategy: .watermark(low: 5, high: 10) - /// ) - /// var channel = consume channelAndSource.channel - /// var source = consume channelAndSource.source - /// ``` - @frozen - public struct ChannelAndStream: ~Copyable { - /// The channel. - public var channel: MultiProducerSingleConsumerChannel - /// The source. - public var source: Source - - init( - channel: consuming MultiProducerSingleConsumerChannel, - source: consuming Source - ) { - self.channel = channel - self.source = source - } + /// The backing storage. + @usableFromInline + let storage: _Storage + + /// A struct containing the initialized channel and source. + /// + /// This struct can be deconstructed by consuming the individual + /// components from it. + /// + /// ```swift + /// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + /// of: Int.self, + /// backpressureStrategy: .watermark(low: 5, high: 10) + /// ) + /// var channel = consume channelAndSource.channel + /// var source = consume channelAndSource.source + /// ``` + @frozen + public struct ChannelAndStream: ~Copyable { + /// The channel. + public var channel: MultiProducerSingleConsumerChannel + /// The source. + public var source: Source + + init( + channel: consuming MultiProducerSingleConsumerChannel, + source: consuming Source + ) { + self.channel = channel + self.source = source + } + } + + /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. + /// + /// - Parameters: + /// - elementType: The element type of the channel. + /// - failureType: The failure type of the channel. + /// - backpressureStrategy: The backpressure strategy that the channel should use. + /// - Returns: A struct containing the channel and its source. The source should be passed to the + /// producer while the channel should be passed to the consumer. + public static func makeChannel( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Never.self, + backpressureStrategy: Source.BackpressureStrategy + ) -> ChannelAndStream { + let storage = _Storage( + backpressureStrategy: backpressureStrategy.internalBackpressureStrategy + ) + let source = Source(storage: storage) + + return .init(channel: .init(storage: storage), source: source) + } + + init(storage: _Storage) { + self.storage = storage + } + + deinit { + self.storage.channelDeinitialized() + } + + /// Returns the next element. + /// + /// If this method returns `nil` it indicates that no further values can ever + /// be returned. The channel automatically closes when all sources have been deinited. + /// + /// If there are no elements and the channel has not been finished yet, this method will + /// suspend until an element is send to the channel. + /// + /// If the task calling this method is cancelled this method will return `nil`. + /// + /// - Parameter isolation: The callers isolation. + /// - Returns: The next buffered element. + @inlinable + public mutating func next( + isolation: isolated (any Actor)? = #isolation + ) async throws(Failure) -> Element? { + do { + return try await self.storage.next() + } catch { + // This force-cast is safe since we only allow closing the source with this failure + // We only need this force cast since continuations don't support typed throws yet. + throw error as! Failure } + } +} - /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. - /// - /// - Parameters: - /// - elementType: The element type of the channel. - /// - failureType: The failure type of the channel. - /// - backpressureStrategy: The backpressure strategy that the channel should use. - /// - Returns: A struct containing the channel and its source. The source should be passed to the - /// producer while the channel should be passed to the consumer. - public static func makeChannel( - of elementType: Element.Type = Element.self, - throwing failureType: Failure.Type = Never.self, - backpressureStrategy: Source.BackpressureStrategy - ) -> ChannelAndStream { - let storage = _Storage( - backpressureStrategy: backpressureStrategy.internalBackpressureStrategy +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension MultiProducerSingleConsumerChannel { + /// A struct to send values to the channel. + /// + /// Use this source to provide elements to the channel by calling one of the `send` methods. + public struct Source: ~Copyable, Sendable { + /// A struct representing the backpressure of the channel. + public struct BackpressureStrategy: Sendable { + var internalBackpressureStrategy: _InternalBackpressureStrategy + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + public static func watermark(low: Int, high: Int) -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .watermark( + .init(low: low, high: high, waterLevelForElement: nil) + ) + ) + } + + /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. + /// + /// - Parameters: + /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. + /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. + /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. + /// + /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when + /// it is consumed from the channel, so it is recommended to provide a function that runs in constant time. + public static func watermark( + low: Int, + high: Int, + waterLevelForElement: @escaping @Sendable (borrowing Element) -> Int + ) -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .watermark( + .init(low: low, high: high, waterLevelForElement: waterLevelForElement) + ) ) - let source = Source(storage: storage) + } + + /// An unbounded backpressure strategy. + /// + /// - Important: Only use this strategy if the production of elements is limited through some other mean. Otherwise + /// an unbounded backpressure strategy can result in infinite memory usage and cause + /// your process to run out of memory. + public static func unbounded() -> BackpressureStrategy { + .init( + internalBackpressureStrategy: .unbounded(.init()) + ) + } + } + + /// A type that indicates the result of sending elements to the source. + public enum SendResult: ~Copyable, Sendable { + /// An opaque token that is returned when the channel's backpressure strategy indicated that production should + /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` + /// and ``MultiProducerSingleConsumerChannel/Source/cancelCallback(callbackToken:)``. + public struct CallbackToken: Sendable, Hashable { + @usableFromInline + let _id: UInt64 - return .init(channel: .init(storage: storage), source: source) + @usableFromInline + init(id: UInt64) { + self._id = id + } + } + + /// Indicates that more elements should be produced and send to the source. + case produceMore + + /// Indicates that a callback should be enqueued. + /// + /// The associated token should be passed to the ````MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. + case enqueueCallback(CallbackToken) } - init(storage: _Storage) { - self.storage = storage + @usableFromInline + let _storage: _Storage + + internal init(storage: _Storage) { + self._storage = storage + self._storage.sourceInitialized() } deinit { - self.storage.channelDeinitialized() + self._storage.sourceDeinitialized() + } + + /// Sets a callback to invoke when the channel terminated. + /// + /// This is called after the last element has been consumed by the channel. + public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) { + self._storage.onTermination = callback } - /// Returns the next element. + /// Creates a new source which can be used to send elements to the channel concurrently. /// - /// If this method returns `nil` it indicates that no further values can ever - /// be returned. The channel automatically closes when all sources have been deinited. + /// The channel will only automatically be finished if all existing sources have been deinited. /// - /// If there are no elements and the channel has not been finished yet, this method will - /// suspend until an element is send to the channel. + /// - Returns: A new source for sending elements to the channel. + public mutating func copy() -> sending Self { + .init(storage: self._storage) + } + + /// Sends new elements to the channel. /// - /// If the task calling this method is cancelled this method will return `nil`. + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. /// - /// - Parameter isolation: The callers isolation. - /// - Returns: The next buffered element. + /// - Parameter sequence: The elements to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable - public mutating func next( - isolation: isolated (any Actor)? = #isolation - ) async throws(Failure) -> Element? { - do { - return try await self.storage.next() - } catch { - // This force-cast is safe since we only allow closing the source with this failure - // We only need this force cast since continuations don't support typed throws yet. - throw error as! Failure - } + public mutating func send( + contentsOf sequence: consuming sendingS + ) throws -> SendResult where Element == S.Element, S: Sequence, Element: Copyable { + try self._storage.send(contentsOf: sequence) } -} -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { - /// A struct to send values to the channel. + /// Send the element to the channel. /// - /// Use this source to provide elements to the channel by calling one of the `send` methods. - public struct Source: ~Copyable, Sendable { - /// A struct representing the backpressure of the channel. - public struct BackpressureStrategy: Sendable { - var internalBackpressureStrategy: _InternalBackpressureStrategy - - /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. - /// - /// - Parameters: - /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. - /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. - public static func watermark(low: Int, high: Int) -> BackpressureStrategy { - .init( - internalBackpressureStrategy: .watermark( - .init(low: low, high: high, waterLevelForElement: nil) - ) - ) - } - - /// A backpressure strategy using a high and low watermark to suspend and resume production respectively. - /// - /// - Parameters: - /// - low: When the number of buffered elements drops below the low watermark, producers will be resumed. - /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. - /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. - /// - /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when - /// it is consumed from the channel, so it is recommended to provide a function that runs in constant time. - public static func watermark( - low: Int, - high: Int, - waterLevelForElement: @escaping @Sendable (borrowing Element) -> Int - ) -> BackpressureStrategy { - .init( - internalBackpressureStrategy: .watermark( - .init(low: low, high: high, waterLevelForElement: waterLevelForElement) - ) - ) - } - - /// An unbounded backpressure strategy. - /// - /// - Important: Only use this strategy if the production of elements is limited through some other mean. Otherwise - /// an unbounded backpressure strategy can result in infinite memory usage and cause - /// your process to run out of memory. - public static func unbounded() -> BackpressureStrategy { - .init( - internalBackpressureStrategy: .unbounded(.init()) - ) - } - } - - /// A type that indicates the result of sending elements to the source. - public enum SendResult: ~Copyable, Sendable { - /// An opaque token that is returned when the channel's backpressure strategy indicated that production should - /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. - /// - /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` - /// and ``MultiProducerSingleConsumerChannel/Source/cancelCallback(callbackToken:)``. - public struct CallbackToken: Sendable, Hashable { - @usableFromInline - let _id: UInt64 - - @usableFromInline - init(id: UInt64) { - self._id = id - } - } - - /// Indicates that more elements should be produced and send to the source. - case produceMore - - /// Indicates that a callback should be enqueued. - /// - /// The associated token should be passed to the ````MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. - case enqueueCallback(CallbackToken) - } - - @usableFromInline - let _storage: _Storage - - internal init(storage: _Storage) { - self._storage = storage - self._storage.sourceInitialized() - } - - deinit { - self._storage.sourceDeinitialized() - } - - /// Sets a callback to invoke when the channel terminated. - /// - /// This is called after the last element has been consumed by the channel. - public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) { - self._storage.onTermination = callback - } - - /// Creates a new source which can be used to send elements to the channel concurrently. - /// - /// The channel will only automatically be finished if all existing sources have been deinited. - /// - /// - Returns: A new source for sending elements to the channel. - public mutating func copy() -> sending Self { - .init(storage: self._storage) - } - - /// Sends new elements to the channel. - /// - /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the - /// first element of the provided sequence. If the channel already terminated then this method will throw an error - /// indicating the failure. - /// - /// - Parameter sequence: The elements to send to the channel. - /// - Returns: The result that indicates if more elements should be produced at this time. - @inlinable - public mutating func send( - contentsOf sequence: consuming sending S - ) throws -> SendResult where Element == S.Element, S: Sequence, Element: Copyable { - try self._storage.send(contentsOf: sequence) - } - - /// Send the element to the channel. - /// - /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the - /// provided element. If the channel already terminated then this method will throw an error - /// indicating the failure. - /// - /// - Parameter element: The element to send to the channel. - /// - Returns: The result that indicates if more elements should be produced at this time. - @inlinable - public mutating func send(_ element: consuming sending Element) throws -> SendResult { - try self._storage.send(contentsOf: CollectionOfOne(element)) - } + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter element: The element to send to the channel. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + public mutating func send(_ element: consuming sendingElement) throws -> SendResult { + try self._storage.send(contentsOf: CollectionOfOne(element)) + } - /// Enqueues a callback that will be invoked once more elements should be produced. - /// - /// Call this method after ``send(contentsOf:)-65yju`` or ``send(_:)`` returned ``SendResult/enqueueCallback(_:)``. - /// - /// - Important: Enqueueing the same token multiple times is **not allowed**. - /// - /// - Parameters: - /// - callbackToken: The callback token. - /// - onProduceMore: The callback which gets invoked once more elements should be produced. - @inlinable - public mutating func enqueueCallback( - callbackToken: consuming SendResult.CallbackToken, - onProduceMore: sending @escaping (Result) -> Void - ) { - self._storage.enqueueProducer(callbackToken: callbackToken._id, onProduceMore: onProduceMore) - } + /// Enqueues a callback that will be invoked once more elements should be produced. + /// + /// Call this method after ``send(contentsOf:)-65yju`` or ``send(_:)`` returned ``SendResult/enqueueCallback(_:)``. + /// + /// - Important: Enqueueing the same token multiple times is **not allowed**. + /// + /// - Parameters: + /// - callbackToken: The callback token. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. + @inlinable + public mutating func enqueueCallback( + callbackToken: consuming SendResult.CallbackToken, + onProduceMore: sending @escaping (Result) -> Void + ) { + self._storage.enqueueProducer(callbackToken: callbackToken._id, onProduceMore: onProduceMore) + } - /// Cancel an enqueued callback. - /// - /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. - /// - /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and - /// will mark the passed `callbackToken` as cancelled. - /// - /// - Parameter callbackToken: The callback token. - @inlinable - public mutating func cancelCallback(callbackToken: consuming SendResult.CallbackToken) { - self._storage.cancelProducer(callbackToken: callbackToken._id) - } + /// Cancel an enqueued callback. + /// + /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and + /// will mark the passed `callbackToken` as cancelled. + /// + /// - Parameter callbackToken: The callback token. + @inlinable + public mutating func cancelCallback(callbackToken: consuming SendResult.CallbackToken) { + self._storage.cancelProducer(callbackToken: callbackToken._id) + } - /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. - /// - /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the - /// first element of the provided sequence. If the channel already terminated then `onProduceMore` will be invoked with - /// a `Result.failure`. - /// - /// - Parameters: - /// - sequence: The elements to send to the channel. - /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be - /// invoked during the call to ``send(contentsOf:onProduceMore:)``. - @inlinable - public mutating func send( - contentsOf sequence: consuming sending S, - onProduceMore: @escaping @Sendable (Result) -> Void - ) where Element == S.Element, S: Sequence, Element: Copyable { - do { - let sendResult = try self.send(contentsOf: sequence) - - switch consume sendResult { - case .produceMore: - onProduceMore(Result.success(())) - - case .enqueueCallback(let callbackToken): - self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) - } - } catch { - onProduceMore(.failure(error)) - } + /// Send new elements to the channel and provide a callback which will be invoked once more elements should be produced. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(contentsOf:onProduceMore:)``. + @inlinable + public mutating func send( + contentsOf sequence: consuming sendingS, + onProduceMore: @escaping @Sendable (Result) -> Void + ) where Element == S.Element, S: Sequence, Element: Copyable { + do { + let sendResult = try self.send(contentsOf: sequence) + + switch consume sendResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(let callbackToken): + self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) } + } catch { + onProduceMore(.failure(error)) + } + } - /// Sends the element to the channel. - /// - /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the - /// provided element. If the channel already terminated then `onProduceMore` will be invoked with - /// a `Result.failure`. - /// - /// - Parameters: - /// - element: The element to send to the channel. - /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be - /// invoked during the call to ``send(_:onProduceMore:)``. - @inlinable - public mutating func send( - _ element: consuming sending Element, - onProduceMore: @escaping @Sendable (Result) -> Void - ) { - do { - let sendResult = try self.send(element) - - switch consume sendResult { - case .produceMore: - onProduceMore(Result.success(())) - - case .enqueueCallback(let callbackToken): - self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) - } - } catch { - onProduceMore(.failure(error)) - } + /// Sends the element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - element: The element to send to the channel. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``send(_:onProduceMore:)``. + @inlinable + public mutating func send( + _ element: consuming sendingElement, + onProduceMore: @escaping @Sendable (Result) -> Void + ) { + do { + let sendResult = try self.send(element) + + switch consume sendResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(let callbackToken): + self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) } + } catch { + onProduceMore(.failure(error)) + } + } - /// Send new elements to the channel. - /// - /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the - /// first element of the provided sequence. If the channel already terminated then this method will throw an error - /// indicating the failure. - /// - /// This method returns once more elements should be produced. - /// - /// - Parameters: - /// - sequence: The elements to send to the channel. - @inlinable - public mutating func send( - contentsOf sequence: consuming sending S - ) async throws where Element == S.Element, S: Sequence, Element: Copyable { - let syncSend: (sending S, inout sending Self) throws -> SendResult = { try $1.send(contentsOf: $0) } - let sendResult = try syncSend(sequence, &self) - - switch consume sendResult { - case .produceMore: - return () - - case .enqueueCallback(let callbackToken): - let id = callbackToken._id - let storage = self._storage - try await withTaskCancellationHandler { - try await withUnsafeThrowingContinuation { continuation in - self._storage.enqueueProducer( - callbackToken: id, - continuation: continuation - ) - } - } onCancel: { - storage.cancelProducer(callbackToken: id) - } - } + /// Send new elements to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating func send( + contentsOf sequence: consuming sendingS + ) async throws where Element == S.Element, S: Sequence, Element: Copyable { + let syncSend: (sending S, inout sendingSelf) throws -> SendResult = { try $1.send(contentsOf: $0) } + let sendResult = try syncSend(sequence, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackToken): + let id = callbackToken._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) } + } + } - /// Send new element to the channel. - /// - /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the - /// provided element. If the channel already terminated then this method will throw an error - /// indicating the failure. - /// - /// This method returns once more elements should be produced. - /// - /// - Parameters: - /// - element: The element to send to the channel. - @inlinable - public mutating func send(_ element: consuming sending Element) async throws { - let syncSend: (consuming sending Element, inout sending Self) throws -> SendResult = { try $1.send($0) } - let sendResult = try syncSend(element, &self) - - switch consume sendResult { - case .produceMore: - return () - - case .enqueueCallback(let callbackToken): - let id = callbackToken._id - let storage = self._storage - try await withTaskCancellationHandler { - try await withUnsafeThrowingContinuation { continuation in - self._storage.enqueueProducer( - callbackToken: id, - continuation: continuation - ) - } - } onCancel: { - storage.cancelProducer(callbackToken: id) - } - } + /// Send new element to the channel. + /// + /// If there is a task consuming the channel and awaiting the next element then the task will get resumed with the + /// provided element. If the channel already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - element: The element to send to the channel. + @inlinable + public mutating func send(_ element: consuming sendingElement) async throws { + let syncSend: (consuming sendingElement, inout sendingSelf) throws -> SendResult = { try $1.send($0) } + let sendResult = try syncSend(element, &self) + + switch consume sendResult { + case .produceMore: + return () + + case .enqueueCallback(let callbackToken): + let id = callbackToken._id + let storage = self._storage + try await withTaskCancellationHandler { + try await withUnsafeThrowingContinuation { continuation in + self._storage.enqueueProducer( + callbackToken: id, + continuation: continuation + ) + } + } onCancel: { + storage.cancelProducer(callbackToken: id) } + } + } - /// Send the elements of the asynchronous sequence to the channel. - /// - /// This method returns once the provided asynchronous sequence or the channel finished. - /// - /// - Important: This method does not finish the source if consuming the upstream sequence terminated. - /// - /// - Parameters: - /// - sequence: The elements to send to the channel. - @inlinable - public mutating func send(contentsOf sequence: consuming sending S) async throws - where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { - for try await element in sequence { - try await self.send(contentsOf: CollectionOfOne(element)) - } - } + /// Send the elements of the asynchronous sequence to the channel. + /// + /// This method returns once the provided asynchronous sequence or the channel finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to send to the channel. + @inlinable + public mutating func send(contentsOf sequence: consuming sendingS) async throws + where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { + for try await element in sequence { + try await self.send(contentsOf: CollectionOfOne(element)) + } + } - /// Indicates that the production terminated. - /// - /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerChannel/next(isolation:)`` will return - /// `nil` or throw an error. - /// - /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept - /// new elements. - /// - /// - Parameters: - /// - error: The error to throw, or `nil`, to finish normally. - @inlinable - public consuming func finish(throwing error: Failure? = nil) { - self._storage.finish(error) - } + /// Indicates that the production terminated. + /// + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerChannel/next(isolation:)`` will return + /// `nil` or throw an error. + /// + /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept + /// new elements. + /// + /// - Parameters: + /// - error: The error to throw, or `nil`, to finish normally. + @inlinable + public consuming func finish(throwing error: Failure? = nil) { + self._storage.finish(error) } + } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel where Element: Copyable { - struct ChannelAsyncSequence: AsyncSequence { - @usableFromInline - final class _Backing: Sendable { - @usableFromInline - let storage: MultiProducerSingleConsumerChannel._Storage - - init(storage: MultiProducerSingleConsumerChannel._Storage) { - self.storage = storage - self.storage.sequenceInitialized() - } - - deinit { - self.storage.sequenceDeinitialized() - } - } + struct ChannelAsyncSequence: AsyncSequence { + @usableFromInline + final class _Backing: Sendable { + @usableFromInline + let storage: MultiProducerSingleConsumerChannel._Storage - @usableFromInline - let _backing: _Backing + init(storage: MultiProducerSingleConsumerChannel._Storage) { + self.storage = storage + self.storage.sequenceInitialized() + } - public func makeAsyncIterator() -> Self.Iterator { - .init(storage: self._backing.storage) - } + deinit { + self.storage.sequenceDeinitialized() + } } - /// Converts the channel to an asynchronous sequence for consumption. - /// - /// - Important: The returned asynchronous sequence only supports a single iterator to be created and - /// will fatal error at runtime on subsequent calls to `makeAsyncIterator`. - public consuming func asyncSequence() -> some (AsyncSequence & Sendable) { - ChannelAsyncSequence(_backing: .init(storage: self.storage)) + @usableFromInline + let _backing: _Backing + + public func makeAsyncIterator() -> Self.Iterator { + .init(storage: self._backing.storage) } + } + + /// Converts the channel to an asynchronous sequence for consumption. + /// + /// - Important: The returned asynchronous sequence only supports a single iterator to be created and + /// will fatal error at runtime on subsequent calls to `makeAsyncIterator`. + public consuming func asyncSequence() -> some (AsyncSequence & Sendable) { + ChannelAsyncSequence(_backing: .init(storage: self.storage)) + } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence where Element: Copyable { - struct Iterator: AsyncIteratorProtocol { - @usableFromInline - final class _Backing { - @usableFromInline - let storage: MultiProducerSingleConsumerChannel._Storage - - init(storage: MultiProducerSingleConsumerChannel._Storage) { - self.storage = storage - self.storage.iteratorInitialized() - } - - deinit { - self.storage.iteratorDeinitialized() - } - } + struct Iterator: AsyncIteratorProtocol { + @usableFromInline + final class _Backing { + @usableFromInline + let storage: MultiProducerSingleConsumerChannel._Storage - @usableFromInline - let _backing: _Backing + init(storage: MultiProducerSingleConsumerChannel._Storage) { + self.storage = storage + self.storage.iteratorInitialized() + } - init(storage: MultiProducerSingleConsumerChannel._Storage) { - self._backing = .init(storage: storage) - } - - @inlinable - mutating func next() async throws -> Element? { - do { - return try await self._backing.storage.next(isolation: nil) - } catch { - throw error as! Failure - } - } + deinit { + self.storage.iteratorDeinitialized() + } + } - @inlinable - mutating func next( - isolation actor: isolated (any Actor)? = #isolation - ) async throws(Failure) -> Element? { - do { - return try await self._backing.storage.next(isolation: actor) - } catch { - throw error as! Failure - } - } + @usableFromInline + let _backing: _Backing + + init(storage: MultiProducerSingleConsumerChannel._Storage) { + self._backing = .init(storage: storage) + } + + @inlinable + mutating func next() async throws -> Element? { + do { + return try await self._backing.storage.next(isolation: nil) + } catch { + throw error as! Failure + } + } + + @inlinable + mutating func next( + isolation actor: isolated (any Actor)? = #isolation + ) async throws(Failure) -> Element? { + do { + return try await self._backing.storage.next(isolation: actor) + } catch { + throw error as! Failure + } } + } } // @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index c121e3b1..7e6e1388 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -3,65 +3,65 @@ import AsyncAlgorithms @available(macOS 15.0, *) @main struct Example { - static func main() async throws { - let durationUnboundedMPSC = await ContinuousClock().measure { - await testMPSCChannel(count: 1000000, backpressureStrategy: .unbounded()) - } - print("Unbounded MPSC:", durationUnboundedMPSC) - let durationHighLowMPSC = await ContinuousClock().measure { - await testMPSCChannel(count: 1000000, backpressureStrategy: .watermark(low: 100, high: 500)) - } - print("HighLow MPSC:", durationHighLowMPSC) - let durationAsyncStream = await ContinuousClock().measure { - await testAsyncStream(count: 1000000) - } - print("AsyncStream:", durationAsyncStream) + static func main() async throws { + let durationUnboundedMPSC = await ContinuousClock().measure { + await testMPSCChannel(count: 1_000_000, backpressureStrategy: .unbounded()) } - - static func testMPSCChannel( - count: Int, - backpressureStrategy: MultiProducerSingleConsumerChannel.Source.BackpressureStrategy - ) async { - await withTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: backpressureStrategy - ) - var channel = channelAndSource.channel - var source = Optional.some(consume channelAndSource.source) - - group.addTask { - var source = source.take()! - for i in 0...Source.BackpressureStrategy + ) async { + await withTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: backpressureStrategy + ) + var channel = channelAndSource.channel + var source = Optional.some(consume channelAndSource.source) + + group.addTask { + var source = source.take()! + for i in 0...makeStream() - source?.setOnTerminationCallback { - onTerminationContinuation.finish() - } - - try await source?.send(1) - try await source?.send(2) - source?.finish(throwing: nil) - - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } + _ = try await source1.send(1) + XCTAssertFalse(didTerminate) + _ = consume source1 + XCTAssertFalse(didTerminate) + _ = try await source2.send(2) + XCTAssertFalse(didTerminate) + + _ = await channel.next() + XCTAssertFalse(didTerminate) + _ = await channel.next() + XCTAssertFalse(didTerminate) + _ = consume source2 + _ = await channel.next() + XCTAssertTrue(didTerminate) + } + + func testSourceDeinitialized_whenSourceFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + try await source?.send(1) + try await source?.send(2) + source?.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() - var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) - _ = try await iterator?.next() + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) + _ = try await iterator?.next() - _ = await onTerminationIterator.next() + _ = await onTerminationIterator.next() - _ = try await iterator?.next() - _ = try await iterator?.next() + _ = try await iterator?.next() + _ = try await iterator?.next() - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) - group.cancelAll() - } + group.cancelAll() } + } + + func testSourceDeinitialized_whenFinished() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + source?.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } - func testSourceDeinitialized_whenFinished() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.setOnTerminationCallback { - onTerminationContinuation.finish() - } - - source?.finish(throwing: nil) - - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() - _ = channel.asyncSequence().makeAsyncIterator() + _ = channel.asyncSequence().makeAsyncIterator() - _ = await onTerminationIterator.next() + _ = await onTerminationIterator.next() - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) - group.cancelAll() - } + group.cancelAll() } - - // MARK: Channel deinitialized - - func testChannelDeinitialized() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let source = consume channelAndSource.source - nonisolated(unsafe) var didTerminate = false - source.setOnTerminationCallback { didTerminate = true } - - XCTAssertFalse(didTerminate) - _ = consume channel - XCTAssertTrue(didTerminate) + } + + // MARK: Channel deinitialized + + func testChannelDeinitialized() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + XCTAssertFalse(didTerminate) + _ = consume channel + XCTAssertTrue(didTerminate) + } + + // MARK: - sequenceDeinitialized + + func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let asyncSequence = channel.asyncSequence() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + group.addTask(executorPreference: manualExecutor) { + _ = await asyncSequence.first { _ in true } + } + + withExtendedLifetime(source) {} + _ = consume source + XCTAssertFalse(didTerminate) + manualExecutor.run() + _ = try await group.next() + XCTAssertTrue(didTerminate) } - - // MARK: - sequenceDeinitialized - - func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { - let manualExecutor = ManualTaskExecutor() - try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let asyncSequence = channel.asyncSequence() - let source = consume channelAndSource.source - nonisolated(unsafe) var didTerminate = false - source.setOnTerminationCallback { didTerminate = true } - - group.addTask(executorPreference: manualExecutor) { - _ = await asyncSequence.first { _ in true } - } - - withExtendedLifetime(source) {} - _ = consume source - XCTAssertFalse(didTerminate) - manualExecutor.run() - _ = try await group.next() - XCTAssertTrue(didTerminate) - } + } + + func testSequenceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { + let manualExecutor = ManualTaskExecutor() + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let asyncSequence = channel.asyncSequence() + let source = consume channelAndSource.source + nonisolated(unsafe) var didTerminate = false + source.setOnTerminationCallback { didTerminate = true } + + group.addTask(executorPreference: manualExecutor) { + _ = await asyncSequence.first { _ in true } + } + manualExecutor.run() + XCTAssertFalse(didTerminate) + + withExtendedLifetime(source) {} + _ = consume source + XCTAssertTrue(didTerminate) + manualExecutor.run() + _ = try await group.next() } - - func testSequenceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { - let manualExecutor = ManualTaskExecutor() - try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let asyncSequence = channel.asyncSequence() - let source = consume channelAndSource.source - nonisolated(unsafe) var didTerminate = false - source.setOnTerminationCallback { didTerminate = true } - - group.addTask(executorPreference: manualExecutor) { - _ = await asyncSequence.first { _ in true } - } - manualExecutor.run() - XCTAssertFalse(didTerminate) - - withExtendedLifetime(source) {} - _ = consume source - XCTAssertTrue(didTerminate) - manualExecutor.run() - _ = try await group.next() + } + + // MARK: - iteratorInitialized + + func testIteratorInitialized_whenInitial() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + _ = consume channelAndSource.source + + _ = channel.asyncSequence().makeAsyncIterator() + } + + func testIteratorInitialized_whenChanneling() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + try await source.send(1) + + var iterator = channel.asyncSequence().makeAsyncIterator() + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + } + + func testIteratorInitialized_whenSourceFinished() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + try await source.send(1) + source.finish(throwing: nil) + + var iterator = channel.asyncSequence().makeAsyncIterator() + let element1 = await iterator.next(isolation: nil) + XCTAssertEqual(element1, 1) + let element2 = await iterator.next(isolation: nil) + XCTAssertNil(element2) + } + + func testIteratorInitialized_whenFinished() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + + source.finish(throwing: nil) + + var iterator = channel.asyncSequence().makeAsyncIterator() + let element = await iterator.next(isolation: nil) + XCTAssertNil(element) + } + + // MARK: - iteratorDeinitialized + + func testIteratorDeinitialized_whenInitial() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) } - } + } - // MARK: - iteratorInitialized + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() - func testIteratorInitialized_whenInitial() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - _ = consume channelAndSource.source + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) + iterator = nil + _ = await iterator?.next(isolation: nil) - _ = channel.asyncSequence().makeAsyncIterator() - } + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) - func testIteratorInitialized_whenChanneling() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - try await source.send(1) - - var iterator = channel.asyncSequence().makeAsyncIterator() - let element = await iterator.next(isolation: nil) - XCTAssertEqual(element, 1) + group.cancelAll() } + } + + func testIteratorDeinitialized_whenChanneling() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + try await source.send(1) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) + } + } - func testIteratorInitialized_whenSourceFinished() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - try await source.send(1) - source.finish(throwing: nil) - - var iterator = channel.asyncSequence().makeAsyncIterator() - let element1 = await iterator.next(isolation: nil) - XCTAssertEqual(element1, 1) - let element2 = await iterator.next(isolation: nil) - XCTAssertNil(element2) - } + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() - func testIteratorInitialized_whenFinished() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let source = consume channelAndSource.source + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) + iterator = nil + _ = await iterator?.next(isolation: nil) - source.finish(throwing: nil) + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) - var iterator = channel.asyncSequence().makeAsyncIterator() - let element = await iterator.next(isolation: nil) - XCTAssertNil(element) + group.cancelAll() } - - // MARK: - iteratorDeinitialized - - func testIteratorDeinitialized_whenInitial() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let source = consume channelAndSource.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.setOnTerminationCallback { - onTerminationContinuation.finish() - } - - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) - iterator = nil - _ = await iterator?.next(isolation: nil) - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() + } + + func testIteratorDeinitialized_whenSourceFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + try await source.send(1) + source.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) } - } + } - func testIteratorDeinitialized_whenChanneling() async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.setOnTerminationCallback { - onTerminationContinuation.finish() - } + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() - try await source.send(1) + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) + iterator = nil + _ = await iterator?.next(isolation: nil) - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) - iterator = nil - _ = await iterator?.next(isolation: nil) - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } + group.cancelAll() } - - func testIteratorDeinitialized_whenSourceFinished() async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.setOnTerminationCallback { - onTerminationContinuation.finish() - } - - try await source.send(1) - source.finish(throwing: nil) - - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) - iterator = nil - _ = await iterator?.next(isolation: nil) - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() + } + + func testIteratorDeinitialized_whenFinished() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.setOnTerminationCallback { + onTerminationContinuation.finish() + } + + source.finish(throwing: nil) + + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(for: .seconds(0.2)) } - } + } - func testIteratorDeinitialized_whenFinished() async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - let channel = channelAndSource.channel - let source = consume channelAndSource.source - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.setOnTerminationCallback { - onTerminationContinuation.finish() - } - - source.finish(throwing: nil) + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(for: .seconds(0.2)) - } - } + var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) + iterator = nil + _ = try await iterator?.next() - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) - var iterator = Optional.some(channel.asyncSequence().makeAsyncIterator()) - iterator = nil - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } + group.cancelAll() } - - func testIteratorDeinitialized_whenChanneling_andSuspendedProducer() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 5, high: 10) - ) - var channel: MultiProducerSingleConsumerChannel? = channelAndSource.channel - var source = consume channelAndSource.source - - var iterator = channel?.asyncSequence().makeAsyncIterator() - channel = nil - - _ = try { try source.send(1) }() - - do { - try await withCheckedThrowingContinuation { continuation in - source.send(1) { result in - continuation.resume(with: result) - } - - iterator = nil - } - } catch { - XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) + } + + func testIteratorDeinitialized_whenChanneling_andSuspendedProducer() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 5, high: 10) + ) + var channel: MultiProducerSingleConsumerChannel? = channelAndSource.channel + var source = consume channelAndSource.source + + var iterator = channel?.asyncSequence().makeAsyncIterator() + channel = nil + + _ = try { try source.send(1) }() + + do { + try await withCheckedThrowingContinuation { continuation in + source.send(1) { result in + continuation.resume(with: result) } - _ = try await iterator?.next() + iterator = nil + } + } catch { + XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) } - // MARK: - write - - func testWrite_whenInitial() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - try await source.send(1) - - var iterator = channel.asyncSequence().makeAsyncIterator() - let element = await iterator.next(isolation: nil) - XCTAssertEqual(element, 1) + _ = try await iterator?.next() + } + + // MARK: - write + + func testWrite_whenInitial() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + try await source.send(1) + + var iterator = channel.asyncSequence().makeAsyncIterator() + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + } + + func testWrite_whenChanneling_andNoConsumer() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + try await source.send(1) + try await source.send(2) + + var iterator = channel.asyncSequence().makeAsyncIterator() + let element1 = await iterator.next(isolation: nil) + XCTAssertEqual(element1, 1) + let element2 = await iterator.next(isolation: nil) + XCTAssertEqual(element2, 2) + } + + func testWrite_whenChanneling_andSuspendedConsumer() async throws { + try await withThrowingTaskGroup(of: Int?.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source + + group.addTask { + await channel.next() + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + try await source.send(1) + let element = try await group.next() + XCTAssertEqual(element, 1) } - - func testWrite_whenChanneling_andNoConsumer() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - try await source.send(1) - try await source.send(2) - - var iterator = channel.asyncSequence().makeAsyncIterator() - let element1 = await iterator.next(isolation: nil) - XCTAssertEqual(element1, 1) - let element2 = await iterator.next(isolation: nil) - XCTAssertEqual(element2, 2) + } + + func testWrite_whenChanneling_andSuspendedConsumer_andEmptySequence() async throws { + try await withThrowingTaskGroup(of: Int?.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source + group.addTask { + await channel.next() + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) + + try await source.send(contentsOf: []) + try await source.send(contentsOf: [1]) + let element = try await group.next() + XCTAssertEqual(element, 1) } - - func testWrite_whenChanneling_andSuspendedConsumer() async throws { - try await withThrowingTaskGroup(of: Int?.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - var channel = channelAndSource.channel - var source = consume channelAndSource.source - - group.addTask { - await channel.next() - } - - // This is always going to be a bit racy since we need the call to next() suspend - try await Task.sleep(for: .seconds(0.5)) - - try await source.send(1) - let element = try await group.next() - XCTAssertEqual(element, 1) - } - } - - func testWrite_whenChanneling_andSuspendedConsumer_andEmptySequence() async throws { - try await withThrowingTaskGroup(of: Int?.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - var channel = channelAndSource.channel - var source = consume channelAndSource.source - group.addTask { - await channel.next() - } - - // This is always going to be a bit racy since we need the call to next() suspend - try await Task.sleep(for: .seconds(0.5)) - - try await source.send(contentsOf: []) - try await source.send(contentsOf: [1]) - let element = try await group.next() - XCTAssertEqual(element, 1) - } + } + + func testWrite_whenSourceFinished() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = consume channelAndSource.channel + var source1 = consume channelAndSource.source + var source2 = source1.copy() + + try await source1.send(1) + source1.finish() + do { + try await source2.send(1) + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) } - - func testWrite_whenSourceFinished() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - var channel = consume channelAndSource.channel - var source1 = consume channelAndSource.source - var source2 = source1.copy() - + let element1 = await channel.next() + XCTAssertEqual(element1, 1) + let element2 = await channel.next() + XCTAssertNil(element2) + } + + func testWrite_whenConcurrentProduction() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 5) + ) + var channel = consume channelAndSource.channel + var source1 = consume channelAndSource.source + var source2 = Optional.some(source1.copy()) + + let manualExecutor1 = ManualTaskExecutor() + group.addTask(executorPreference: manualExecutor1) { try await source1.send(1) - source1.finish() - do { - try await source2.send(1) - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) - } - let element1 = await channel.next() - XCTAssertEqual(element1, 1) - let element2 = await channel.next() - XCTAssertNil(element2) - } + } - func testWrite_whenConcurrentProduction() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 5) - ) - var channel = consume channelAndSource.channel - var source1 = consume channelAndSource.source - var source2 = Optional.some(source1.copy()) - - let manualExecutor1 = ManualTaskExecutor() - group.addTask(executorPreference: manualExecutor1) { - try await source1.send(1) - } - - let manualExecutor2 = ManualTaskExecutor() - group.addTask(executorPreference: manualExecutor2) { - var source2 = source2.take()! - try await source2.send(2) - source2.finish() - } + let manualExecutor2 = ManualTaskExecutor() + group.addTask(executorPreference: manualExecutor2) { + var source2 = source2.take()! + try await source2.send(2) + source2.finish() + } - manualExecutor1.run() - let element1 = await channel.next() - XCTAssertEqual(element1, 1) + manualExecutor1.run() + let element1 = await channel.next() + XCTAssertEqual(element1, 1) - manualExecutor2.run() - let element2 = await channel.next() - XCTAssertEqual(element2, 2) + manualExecutor2.run() + let element2 = await channel.next() + XCTAssertEqual(element2, 2) - let element3 = await channel.next() - XCTAssertNil(element3) - } + let element3 = await channel.next() + XCTAssertNil(element3) } + } - // MARK: - enqueueProducer + // MARK: - enqueueProducer - func testEnqueueProducer_whenChanneling_andAndCancelled() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 2) - ) - var channel = channelAndSource.channel - var source = consume channelAndSource.source + func testEnqueueProducer_whenChanneling_andAndCancelled() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - try await source.send(1) + try await source.send(1) - let writeResult = try { try source.send(2) }() + let writeResult = try { try source.send(2) }() - switch consume writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.cancelCallback(callbackToken: callbackToken) + switch consume writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.cancelCallback(callbackToken: callbackToken) - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } - - do { - _ = try await producerStream.first { _ in true } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } - - let element = await channel.next() - XCTAssertEqual(element, 1) + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } } - func testEnqueueProducer_whenChanneling_andAndCancelled_andAsync() async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 2) - ) - var channel = channelAndSource.channel - var source = consume channelAndSource.source - - try await source.send(1) - - group.addTask { - try await source.send(2) - } - - group.cancelAll() - do { - try await group.next() - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } - - let element = await channel.next() - XCTAssertEqual(element, 1) - } + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) } - func testEnqueueProducer_whenChanneling_andInterleaving() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 1) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - var iterator = channel.asyncSequence().makeAsyncIterator() - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let element = await channel.next() + XCTAssertEqual(element, 1) + } - let writeResult = try { try source.send(1) }() + func testEnqueueProducer_whenChanneling_andAndCancelled_andAsync() async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - let element = await iterator.next(isolation: nil) - XCTAssertEqual(element, 1) - - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } + try await source.send(1) - do { - _ = try await producerStream.first { _ in true } - } catch { - XCTFail("Expected no error to be thrown") - } + group.addTask { + try await source.send(2) + } + + group.cancelAll() + do { + try await group.next() + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = await channel.next() + XCTAssertEqual(element, 1) + } + } + + func testEnqueueProducer_whenChanneling_andInterleaving() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + var iterator = channel.asyncSequence().makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.send(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) + + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } } - func testEnqueueProducer_whenChanneling_andSuspending() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 1) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - var iterator = channel.asyncSequence().makeAsyncIterator() - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - let writeResult = try { try source.send(1) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } - - let element = await iterator.next(isolation: nil) - XCTAssertEqual(element, 1) - - do { - _ = try await producerStream.first { _ in true } - } catch { - XCTFail("Expected no error to be thrown") - } + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + func testEnqueueProducer_whenChanneling_andSuspending() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + var iterator = channel.asyncSequence().makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.send(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } } - // MARK: - cancelProducer + let element = await iterator.next(isolation: nil) + XCTAssertEqual(element, 1) - func testCancelProducer_whenChanneling() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 2) - ) - var channel = channelAndSource.channel - var source = consume channelAndSource.source + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + // MARK: - cancelProducer - try await source.send(1) + func testCancelProducer_whenChanneling() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 2) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source - let writeResult = try { try source.send(2) }() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } + try await source.send(1) - source.cancelCallback(callbackToken: callbackToken) - } + let writeResult = try { try source.send(2) }() - do { - _ = try await producerStream.first { _ in true } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } - let element = await channel.next() - XCTAssertEqual(element, 1) + source.cancelCallback(callbackToken: callbackToken) } - // MARK: - finish - - func testFinish_whenChanneling_andConsumerSuspended() async throws { - try await withThrowingTaskGroup(of: Int?.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 1, high: 1) - ) - var channel = channelAndSource.channel - var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source - - group.addTask { - while let element = await channel.next() { - if element == 2 { - return element - } - } - return nil - } - - // This is always going to be a bit racy since we need the call to next() suspend - try await Task.sleep(for: .seconds(0.5)) - - source?.finish(throwing: nil) - source = nil - - let element = try await group.next() - XCTAssertEqual(element, .some(nil)) - } + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) } - func testFinish_whenInitial() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 1, high: 1) - ) - let channel = channelAndSource.channel - let source = consume channelAndSource.source - - source.finish(throwing: CancellationError()) - - do { - for try await _ in channel.asyncSequence() {} - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) + let element = await channel.next() + XCTAssertEqual(element, 1) + } + + // MARK: - finish + + func testFinish_whenChanneling_andConsumerSuspended() async throws { + try await withThrowingTaskGroup(of: Int?.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + var channel = channelAndSource.channel + var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + + group.addTask { + while let element = await channel.next() { + if element == 2 { + return element + } } + return nil + } - } - - // MARK: - Backpressure - - func testBackpressure() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(for: .seconds(0.5)) - let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + source?.finish(throwing: nil) + source = nil - group.addTask { - while true { - backpressureEventContinuation.yield(()) - try await source.send(contentsOf: [1]) - } - } - - var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() - var iterator = channel.asyncSequence().makeAsyncIterator() - - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() - - _ = await iterator.next(isolation: nil) - _ = await iterator.next(isolation: nil) - _ = await iterator.next(isolation: nil) - - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() - - group.cancelAll() - } + let element = try await group.next() + XCTAssertEqual(element, .some(nil)) + } + } + + func testFinish_whenInitial() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 1, high: 1) + ) + let channel = channelAndSource.channel + let source = consume channelAndSource.source + + source.finish(throwing: CancellationError()) + + do { + for try await _ in channel.asyncSequence() {} + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) } - func testBackpressureSync() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - - group.addTask { - while true { - backpressureEventContinuation.yield(()) - try await withCheckedThrowingContinuation { continuation in - source.send(contentsOf: [1]) { result in - continuation.resume(with: result) - } - } - } - } - - var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() - var iterator = channel.asyncSequence().makeAsyncIterator() + } - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() + // MARK: - Backpressure - _ = await iterator.next(isolation: nil) - _ = await iterator.next(isolation: nil) - _ = await iterator.next(isolation: nil) + func testBackpressure() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - group.cancelAll() + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await source.send(contentsOf: [1]) } - } + } - func testWatermarkWithCustomCoount() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: [Int].self, - backpressureStrategy: .watermark(low: 2, high: 4, waterLevelForElement: { $0.count }) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - var iterator = channel.asyncSequence().makeAsyncIterator() + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() - try await source.send([1, 1, 1]) + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() - _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) - try await source.send([1, 1, 1]) + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() - _ = await iterator.next(isolation: nil) + group.cancelAll() } - - func testWatermarWithLotsOfElements() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - // This test should in the future use a custom task executor to schedule to avoid sending - // 1000 elements. - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndSource.channel - var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndSource.source - - group.addTask { - var source = source.take()! - for i in 0...10000 { - try await source.send(i) - } - source.finish() - } - - let asyncSequence = channel.asyncSequence() - - group.addTask { - var sum = 0 - for try await element in asyncSequence { - sum += element - } + } + + func testBackpressureSync() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await withCheckedThrowingContinuation { continuation in + source.send(contentsOf: [1]) { result in + continuation.resume(with: result) } + } } - } + } - func testThrowsError() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - throwing: Error.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() - try await source.send(1) - try await source.send(2) - source.finish(throwing: CancellationError()) + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() - var elements = [Int]() - var iterator = channel.asyncSequence().makeAsyncIterator() + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) - do { - while let element = try await iterator.next() { - elements.append(element) - } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - XCTAssertEqual(elements, [1, 2]) - } + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() - let element = try await iterator.next() - XCTAssertNil(element) + group.cancelAll() } + } + + func testWatermarkWithCustomCoount() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: [Int].self, + backpressureStrategy: .watermark(low: 2, high: 4, waterLevelForElement: { $0.count }) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + var iterator = channel.asyncSequence().makeAsyncIterator() + + try await source.send([1, 1, 1]) + + _ = await iterator.next(isolation: nil) + + try await source.send([1, 1, 1]) + + _ = await iterator.next(isolation: nil) + } + + func testWatermarWithLotsOfElements() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + // This test should in the future use a custom task executor to schedule to avoid sending + // 1000 elements. + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndSource.source + + group.addTask { + var source = source.take()! + for i in 0...10000 { + try await source.send(i) + } + source.finish() + } - func testAsyncSequenceWrite() async throws { - let (stream, continuation) = AsyncStream.makeStream() - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - var channel = channelAndSource.channel - var source = consume channelAndSource.source - - continuation.yield(1) - continuation.yield(2) - continuation.finish() - - try await source.send(contentsOf: stream) - source.finish(throwing: nil) + let asyncSequence = channel.asyncSequence() - let elements = await channel.collect() - XCTAssertEqual(elements, [1, 2]) + group.addTask { + var sum = 0 + for try await element in asyncSequence { + sum += element + } + } + } + } + + func testThrowsError() async throws { + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + throwing: Error.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + try await source.send(1) + try await source.send(2) + source.finish(throwing: CancellationError()) + + var elements = [Int]() + var iterator = channel.asyncSequence().makeAsyncIterator() + + do { + while let element = try await iterator.next() { + elements.append(element) + } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + XCTAssertEqual(elements, [1, 2]) } - // MARK: NonThrowing - - func testNonThrowing() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( - of: Int.self, - backpressureStrategy: .watermark(low: 2, high: 4) - ) - let channel = channelAndSource.channel - var source = consume channelAndSource.source - - let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) - - group.addTask { - while true { - backpressureEventContinuation.yield(()) - try await source.send(contentsOf: [1]) - } - } + let element = try await iterator.next() + XCTAssertNil(element) + } + + func testAsyncSequenceWrite() async throws { + let (stream, continuation) = AsyncStream.makeStream() + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + var channel = channelAndSource.channel + var source = consume channelAndSource.source + + continuation.yield(1) + continuation.yield(2) + continuation.finish() + + try await source.send(contentsOf: stream) + source.finish(throwing: nil) + + let elements = await channel.collect() + XCTAssertEqual(elements, [1, 2]) + } + + // MARK: NonThrowing + + func testNonThrowing() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + of: Int.self, + backpressureStrategy: .watermark(low: 2, high: 4) + ) + let channel = channelAndSource.channel + var source = consume channelAndSource.source + + let (backpressureEventStream, backpressureEventContinuation) = AsyncStream.makeStream(of: Void.self) + + group.addTask { + while true { + backpressureEventContinuation.yield(()) + try await source.send(contentsOf: [1]) + } + } - var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() - var iterator = channel.asyncSequence().makeAsyncIterator() + var backpressureEventIterator = backpressureEventStream.makeAsyncIterator() + var iterator = channel.asyncSequence().makeAsyncIterator() - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() - _ = await iterator.next(isolation: nil) - _ = await iterator.next(isolation: nil) - _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) + _ = await iterator.next(isolation: nil) - await backpressureEventIterator.next() - await backpressureEventIterator.next() - await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() + await backpressureEventIterator.next() - group.cancelAll() - } + group.cancelAll() } + } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel { - /// Collect all elements in the sequence into an array. - fileprivate mutating func collect() async throws(Failure) -> [Element] { - var elements = [Element]() - while let element = try await self.next() { - elements.append(element) - } - return elements + /// Collect all elements in the sequence into an array. + fileprivate mutating func collect() async throws(Failure) -> [Element] { + var elements = [Element]() + while let element = try await self.next() { + elements.append(element) } + return elements + } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension MultiProducerSingleConsumerChannel.Source.SendResult { - func assertIsProducerMore() { - switch self { - case .produceMore: - return () + func assertIsProducerMore() { + switch self { + case .produceMore: + return () - case .enqueueCallback: - XCTFail("Expected produceMore") - } + case .enqueueCallback: + XCTFail("Expected produceMore") } + } - func assertIsEnqueueCallback() { - switch self { - case .produceMore: - XCTFail("Expected enqueueCallback") + func assertIsEnqueueCallback() { + switch self { + case .produceMore: + XCTFail("Expected enqueueCallback") - case .enqueueCallback: - return () - } + case .enqueueCallback: + return () } + } } extension Optional where Wrapped: ~Copyable { - fileprivate mutating func take() -> Self { - let result = consume self - self = nil - return result - } + fileprivate mutating func take() -> Self { + let result = consume self + self = nil + return result + } } diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift index 79956991..1794fe7b 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift @@ -3,15 +3,15 @@ import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ManualTaskExecutor: TaskExecutor { - private let jobs = Mutex>(.init()) - - func enqueue(_ job: UnownedJob) { - self.jobs.withLock { $0.append(job) } - } - - func run() { - while let job = self.jobs.withLock({ $0.popFirst() }) { - job.runSynchronously(on: self.asUnownedTaskExecutor()) - } + private let jobs = Mutex>(.init()) + + func enqueue(_ job: UnownedJob) { + self.jobs.withLock { $0.append(job) } + } + + func run() { + while let job = self.jobs.withLock({ $0.popFirst() }) { + job.runSynchronously(on: self.asUnownedTaskExecutor()) } + } } From b30ec1793536e2b33ec6a0111e9194493af1313b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 13:13:07 +0100 Subject: [PATCH 09/16] Fix CI --- .editorconfig | 8 ++++++++ .github/workflows/pull_request.yml | 1 + ...oducerSingleConsumerChannel+Internal.swift | 4 +++- .../MultiProducerSingleConsumerChannel.swift | 18 ++++++++--------- Sources/Example/Example.swift | 20 +++++++++++++++++++ .../Support/ManualExecutor.swift | 11 ++++++++++ 6 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8b4c83bb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1ed694ad..3c2103e7 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,3 +13,4 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: license_header_check_project_name: "Swift Async Algorithms" + format_check_container_image: "swiftlang/swift:nightly-6.1-noble" # Needed since 6.0.x doesn't support sending keyword diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift index 74253c75..00381fda 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift @@ -159,10 +159,11 @@ extension MultiProducerSingleConsumerChannel { } } + @inlinable init( backpressureStrategy: _InternalBackpressureStrategy ) { - self._stateMachine = .init(.init(backpressureStrategy: backpressureStrategy)) + self._stateMachine = Mutex<_StateMachine>(_StateMachine(backpressureStrategy: backpressureStrategy)) } func channelDeinitialized() { @@ -562,6 +563,7 @@ extension MultiProducerSingleConsumerChannel._Storage { } } + @usableFromInline init( backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy ) { diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index 4208c116..316f9a60 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -274,7 +274,7 @@ extension MultiProducerSingleConsumerChannel { /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable public mutating func send( - contentsOf sequence: consuming sendingS + contentsOf sequence: consuming sending S ) throws -> SendResult where Element == S.Element, S: Sequence, Element: Copyable { try self._storage.send(contentsOf: sequence) } @@ -288,7 +288,7 @@ extension MultiProducerSingleConsumerChannel { /// - Parameter element: The element to send to the channel. /// - Returns: The result that indicates if more elements should be produced at this time. @inlinable - public mutating func send(_ element: consuming sendingElement) throws -> SendResult { + public mutating func send(_ element: consuming sending Element) throws -> SendResult { try self._storage.send(contentsOf: CollectionOfOne(element)) } @@ -334,7 +334,7 @@ extension MultiProducerSingleConsumerChannel { /// invoked during the call to ``send(contentsOf:onProduceMore:)``. @inlinable public mutating func send( - contentsOf sequence: consuming sendingS, + contentsOf sequence: consuming sending S, onProduceMore: @escaping @Sendable (Result) -> Void ) where Element == S.Element, S: Sequence, Element: Copyable { do { @@ -364,7 +364,7 @@ extension MultiProducerSingleConsumerChannel { /// invoked during the call to ``send(_:onProduceMore:)``. @inlinable public mutating func send( - _ element: consuming sendingElement, + _ element: consuming sending Element, onProduceMore: @escaping @Sendable (Result) -> Void ) { do { @@ -394,9 +394,9 @@ extension MultiProducerSingleConsumerChannel { /// - sequence: The elements to send to the channel. @inlinable public mutating func send( - contentsOf sequence: consuming sendingS + contentsOf sequence: consuming sending S ) async throws where Element == S.Element, S: Sequence, Element: Copyable { - let syncSend: (sending S, inout sendingSelf) throws -> SendResult = { try $1.send(contentsOf: $0) } + let syncSend: (sending S, inout sending Self) throws -> SendResult = { try $1.send(contentsOf: $0) } let sendResult = try syncSend(sequence, &self) switch consume sendResult { @@ -430,8 +430,8 @@ extension MultiProducerSingleConsumerChannel { /// - Parameters: /// - element: The element to send to the channel. @inlinable - public mutating func send(_ element: consuming sendingElement) async throws { - let syncSend: (consuming sendingElement, inout sendingSelf) throws -> SendResult = { try $1.send($0) } + public mutating func send(_ element: consuming sending Element) async throws { + let syncSend: (consuming sending Element, inout sending Self) throws -> SendResult = { try $1.send($0) } let sendResult = try syncSend(element, &self) switch consume sendResult { @@ -463,7 +463,7 @@ extension MultiProducerSingleConsumerChannel { /// - Parameters: /// - sequence: The elements to send to the channel. @inlinable - public mutating func send(contentsOf sequence: consuming sendingS) async throws + public mutating func send(contentsOf sequence: consuming sending S) async throws where Element == S.Element, S: AsyncSequence, Element: Copyable, S: Sendable, Element: Sendable { for try await element in sequence { try await self.send(contentsOf: CollectionOfOne(element)) diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index 7e6e1388..e721bb6a 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.0) import AsyncAlgorithms @available(macOS 15.0, *) @@ -65,3 +77,11 @@ struct Example { } } } +#else +@main +struct Example { + static func main() async throws { + fatalError("Example only supports Swift 6.0 and above") + } +} +#endif diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift index 1794fe7b..f7a75e3e 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift @@ -1,3 +1,14 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + import DequeModule import Synchronization From 937217908d18bf1ab9484a73b14fd7a730560354 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 15:00:12 +0100 Subject: [PATCH 10/16] Move to 6.1 and update proposal --- ...-mutli-producer-single-consumer-channel.md | 72 ++++++++++++++++--- ...oducerSingleConsumerChannel+Internal.swift | 2 +- .../MultiProducerSingleConsumerChannel.swift | 66 ++++++++++++++--- Sources/Example/Example.swift | 2 +- 4 files changed, 124 insertions(+), 18 deletions(-) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index d52c7149..9d30c444 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -393,7 +393,7 @@ producer has been terminated will result in an error thrown from the send method ## Detailed design ```swift -#if compiler(>=6.0) +#if compiler(>=6.1) /// An error that is thrown from the various `send` methods of the /// ``MultiProducerSingleConsumerChannel/Source``. /// @@ -401,30 +401,36 @@ producer has been terminated will result in an error thrown from the send method /// trying to send new elements to the source. public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { } -/// A multi producer single consumer channel. +/// A multi-producer single-consumer channel. /// /// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to /// send values to the channel. The channel supports different back pressure strategies to control the /// buffering and demand. The channel will buffer values until its backpressure strategy decides that the /// producer have to wait. /// +/// This channel is also suitable for the single-producer single-consumer use-case /// /// ## Using a MultiProducerSingleConsumerChannel /// -/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with it's source first by calling +/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with its source first by calling /// the ``MultiProducerSingleConsumerChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. /// Afterwards, you can pass the source to the producer and the channel to the consumer. /// /// ``` -/// let (channel, source) = MultiProducerSingleConsumerChannel.makeChannel( +/// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +/// of: Int.self, /// backpressureStrategy: .watermark(low: 2, high: 4) /// ) +/// +/// // The channel and source can be extracted from the returned type +/// let channel = consume channelAndSource.channel +/// let source = consume channelAndSource.source /// ``` /// -/// ### Asynchronous producers +/// ### Asynchronous producing /// -/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-9b5do`` -/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)-4myrz``. Backpressure results in calls +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-8eo96`` +/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)``. Backpressure results in calls /// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. /// /// ``` @@ -441,10 +447,54 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { } /// } /// ``` /// -/// ### Synchronous producers +/// ### Synchronous produceing /// /// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, /// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. +/// +/// ```swift +/// do { +/// let sendResult = try source.send(contentsOf: sequence) +/// +/// switch sendResult { +/// case .produceMore: +/// // Trigger more production in the underlying system +/// +/// case .enqueueCallback(let callbackToken): +/// // There are enough values in the channel already. We need to enqueue +/// // a callback to get notified when we should produce more. +/// source.enqueueCallback(token: callbackToken, onProduceMore: { result in +/// switch result { +/// case .success: +/// // Trigger more production in the underlying system +/// case .failure(let error): +/// // Terminate the underlying producer +/// } +/// }) +/// } +/// } catch { +/// // `send(contentsOf:)` throws if the channel already terminated +/// } +/// ``` +/// +/// ### Multiple producers +/// +/// To support multiple producers the source offers a ``Source/copy()`` method to produce a new source. +/// +/// ### Terminating the production of values +/// +/// The consumer can be terminated through multiple ways: +/// - Calling ``Source/finish(throwing:)``. +/// - Deiniting all sources. +/// +/// In both cases, if there are still elements buffered by the channel, then the consumer will receive +/// all buffered elements. Afterwards it will be terminated. +/// +/// ### Observing termination of the consumer +/// +/// When the consumer stops consumption by either deiniting the channel or the task calling ``next(isolation:)`` +/// getting cancelled, the source will get notified about the termination if a termination callback has been set +/// before by calling ``Source/setOnTerminationCallback(_:)``. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct MultiProducerSingleConsumerChannel: ~Copyable { /// A struct containing the initialized channel and source. @@ -783,6 +833,12 @@ has been offered with the `Continuation` based approach and introduced new factory methods to solve some of the usability ergonomics with the initializer based APIs. +### Provide the type on older compilers + +To achieve maximum performance the implementation is using `~Copyable` extensively. +On Swift versions before 6.1, there is a https://github.com/swiftlang/swift/issues/78048 when using; hence, this type +is only usable with Swift 6.1 and later compilers. + ## Acknowledgements - [Johannes Weiss](https://github.com/weissi) - For making me aware how diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift index 00381fda..00762f38 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.0) +#if compiler(>=6.1) import DequeModule import Synchronization diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index 316f9a60..a7f6ec29 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.0) +#if compiler(>=6.1) /// An error that is thrown from the various `send` methods of the /// ``MultiProducerSingleConsumerChannel/Source``. /// @@ -20,30 +20,36 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { init() {} } -/// A multi producer single consumer channel. +/// A multi-producer single-consumer channel. /// /// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to /// send values to the channel. The channel supports different back pressure strategies to control the /// buffering and demand. The channel will buffer values until its backpressure strategy decides that the /// producer have to wait. /// +/// This channel is also suitable for the single-producer single-consumer use-case /// /// ## Using a MultiProducerSingleConsumerChannel /// -/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with it's source first by calling +/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with its source first by calling /// the ``MultiProducerSingleConsumerChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. /// Afterwards, you can pass the source to the producer and the channel to the consumer. /// /// ``` -/// let (channel, source) = MultiProducerSingleConsumerChannel.makeChannel( +/// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +/// of: Int.self, /// backpressureStrategy: .watermark(low: 2, high: 4) /// ) +/// +/// // The channel and source can be extracted from the returned type +/// let channel = consume channelAndSource.channel +/// let source = consume channelAndSource.source /// ``` /// -/// ### Asynchronous producers +/// ### Asynchronous producing /// -/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-9b5do`` -/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)-4myrz``. Backpressure results in calls +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-8eo96`` +/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)``. Backpressure results in calls /// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. /// /// ``` @@ -60,10 +66,54 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { /// } /// ``` /// -/// ### Synchronous producers +/// ### Synchronous produceing /// /// Values can also be send to the source from synchronous context. Backpressure is also exposed on the synchronous contexts; however, /// it is up to the caller to decide how to properly translate the backpressure to underlying producer e.g. by blocking the thread. +/// +/// ```swift +/// do { +/// let sendResult = try source.send(contentsOf: sequence) +/// +/// switch sendResult { +/// case .produceMore: +/// // Trigger more production in the underlying system +/// +/// case .enqueueCallback(let callbackToken): +/// // There are enough values in the channel already. We need to enqueue +/// // a callback to get notified when we should produce more. +/// source.enqueueCallback(token: callbackToken, onProduceMore: { result in +/// switch result { +/// case .success: +/// // Trigger more production in the underlying system +/// case .failure(let error): +/// // Terminate the underlying producer +/// } +/// }) +/// } +/// } catch { +/// // `send(contentsOf:)` throws if the channel already terminated +/// } +/// ``` +/// +/// ### Multiple producers +/// +/// To support multiple producers the source offers a ``Source/copy()`` method to produce a new source. +/// +/// ### Terminating the production of values +/// +/// The consumer can be terminated through multiple ways: +/// - Calling ``Source/finish(throwing:)``. +/// - Deiniting all sources. +/// +/// In both cases, if there are still elements buffered by the channel, then the consumer will receive +/// all buffered elements. Afterwards it will be terminated. +/// +/// ### Observing termination of the consumer +/// +/// When the consumer stops consumption by either deiniting the channel or the task calling ``next(isolation:)`` +/// getting cancelled, the source will get notified about the termination if a termination callback has been set +/// before by calling ``Source/setOnTerminationCallback(_:)``. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct MultiProducerSingleConsumerChannel: ~Copyable { /// The backing storage. diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index e721bb6a..ff1a1f60 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if compiler(>=6.0) +#if compiler(>=6.1) import AsyncAlgorithms @available(macOS 15.0, *) From 8d2f4d63794c26d847c8372dd64413b304e45fba Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 15:02:34 +0100 Subject: [PATCH 11/16] Guard tests --- .../MultiProducerSingleConsumerChannelTests.swift | 2 ++ Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift index cced207d..83454a31 100644 --- a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +#if compiler(>=6.1) import AsyncAlgorithms import XCTest @@ -1114,3 +1115,4 @@ extension Optional where Wrapped: ~Copyable { return result } } +#endif diff --git a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift index f7a75e3e..b07fb835 100644 --- a/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift +++ b/Tests/AsyncAlgorithmsTests/Support/ManualExecutor.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +#if compiler(>=6.0) import DequeModule import Synchronization @@ -26,3 +27,4 @@ final class ManualTaskExecutor: TaskExecutor { } } } +#endif From 357e9efc48a31c589ce5770866a18b76ad848396 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 15:30:28 +0100 Subject: [PATCH 12/16] Minor edits to the proposal --- .../0016-mutli-producer-single-consumer-channel.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index 9d30c444..dbce76bb 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -2,11 +2,10 @@ * Proposal: [SAA-0016](0016-multi-producer-single-consumer-channel.md) * Authors: [Franz Busch](https://github.com/FranzBusch) -* Review Manager: TBD * Status: **Implemented** ## Revision -- 2025/03/24: Adopt `~Copyable` for better performance. +- 2025/03/24: Adopt `~Copyable` for correct semantics and better performance. - 2023/12/18: Migrate proposal from Swift Evolution to Swift Async Algorithms. - 2023/12/19: Add element size dependent strategy - 2024/05/19: Rename to multi producer single consumer channel @@ -25,8 +24,8 @@ with the goal to model asynchronous multi-producer-single-consumer systems. After using the `AsyncSequence` protocol, the `Async[Throwing]Stream` types, and the `Async[Throwing]Channel` types extensively over the past years, we learned that there is a gap in the ecosystem for a type that provides strict -multi-producer-single-consumer guarantees with backpressure support. -Additionally, any type stream/channel like type needs to have a clear definition +multi-producer-single-consumer guarantees with external backpressure support. +Additionally, any stream/channel like type needs to have a clear definition about the following behaviors: 1. Backpressure @@ -138,9 +137,9 @@ protocols are not supporting `~Copyable` types we provide a way to convert the proposed channel to an asynchronous sequence. This leaves us room to support any potential future asynchronous streaming protocol that supports `~Copyable`. -### Creating an MultiProducerSingleConsumerChannel +### Creating a MultiProducerSingleConsumerChannel -You can create an `MultiProducerSingleConsumerChannel` instance using the new +You can create an `MultiProducerSingleConsumerChannel` instance using the `makeChannel(of: backpressureStrategy:)` method. This method returns you the channel and the source. The source can be used to send new values to the asynchronous channel. The new API specifically provides a @@ -839,7 +838,7 @@ To achieve maximum performance the implementation is using `~Copyable` extensive On Swift versions before 6.1, there is a https://github.com/swiftlang/swift/issues/78048 when using; hence, this type is only usable with Swift 6.1 and later compilers. -## Acknowledgements +## Acknowledgements - [Johannes Weiss](https://github.com/weissi) - For making me aware how important this problem is and providing great ideas on how to shape the API. From c3911f1d498f107e1fd3c501d96e793d98da53ac Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 15:48:51 +0100 Subject: [PATCH 13/16] Fix revision order --- Evolution/0016-mutli-producer-single-consumer-channel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index dbce76bb..23a35d60 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -5,11 +5,11 @@ * Status: **Implemented** ## Revision -- 2025/03/24: Adopt `~Copyable` for correct semantics and better performance. - 2023/12/18: Migrate proposal from Swift Evolution to Swift Async Algorithms. - 2023/12/19: Add element size dependent strategy - 2024/05/19: Rename to multi producer single consumer channel - 2024/05/28: Add unbounded strategy +- 2025/03/24: Adopt `~Copyable` for correct semantics and better performance. ## Introduction From 02b6c86c735bc6ddf71962cba72469bfb0f80b04 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 29 Mar 2025 17:09:05 +0100 Subject: [PATCH 14/16] FIxup setOnTerminationCallback --- .../0016-mutli-producer-single-consumer-channel.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index 23a35d60..ad85360e 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -328,7 +328,7 @@ let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( ) var channel = consume channelAndSource.channel var source = consume channelAndSource.source -source.onTermination = { print("Terminated") } +source.setOnTerminationCallback { print("Terminated") } let task = Task { await channel.next() @@ -344,7 +344,7 @@ let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( ) var channel = consume channelAndSource.channel var source = consume channelAndSource.source -source.onTermination = { print("Terminated") } +source.setOnTerminationCallback { print("Terminated") } _ = consume channel // Prints Terminated ``` @@ -356,7 +356,7 @@ let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( ) var channel = consume channelAndSource.channel var source = consume channelAndSource.source -source.onTermination = { print("Terminated") } +source.setOnTerminationCallback { print("Terminated") } _ = try await source.send(1) source.finish() @@ -374,7 +374,7 @@ let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( var channel = consume channelAndSource.channel var source1 = consume channelAndSource.source var source2 = source1.copy() -source1.onTermination = { print("Terminated") } +source1.setOnTerminationCallback { print("Terminated") } _ = try await source1.send(1) _ = consume source1 @@ -601,9 +601,7 @@ extension MultiProducerSingleConsumerChannel { /// A callback to invoke when the channel finished. /// /// This is called after the last element has been consumed by the channel. - public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) { - self._storage.onTermination = callback - } + public func setOnTerminationCallback(_ callback: @escaping @Sendable () -> Void) /// Creates a new source which can be used to send elements to the channel concurrently. /// From 3523cd33b426339937dd57b33a018f7a9de69dee Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 2 Apr 2025 13:53:25 +0900 Subject: [PATCH 15/16] Address review feedback --- .../0016-mutli-producer-single-consumer-channel.md | 2 +- .../MultiProducerSingleConsumerChannel.swift | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index ad85360e..8229070c 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -760,7 +760,7 @@ production APIs and has an effective buffer of one per producer. This means that any producer will be suspended until its value has been consumed. `AsyncChannel` can handle multiple consumers and resumes them in FIFO order. -### swift-nio: NIOAsyncSequenceProducer +### swift-nio: NIOAsyncSequenceProducer The NIO team have created their own root asynchronous sequence with the goal to provide a high performance sequence that can be used to bridge a NIO `Channel` diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift index a7f6ec29..6f2e5c22 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift @@ -161,7 +161,7 @@ public struct MultiProducerSingleConsumerChannel: ~Copy of elementType: Element.Type = Element.self, throwing failureType: Failure.Type = Never.self, backpressureStrategy: Source.BackpressureStrategy - ) -> ChannelAndStream { + ) -> sending ChannelAndStream { let storage = _Storage( backpressureStrategy: backpressureStrategy.internalBackpressureStrategy ) @@ -209,7 +209,7 @@ extension MultiProducerSingleConsumerChannel { /// A struct to send values to the channel. /// /// Use this source to provide elements to the channel by calling one of the `send` methods. - public struct Source: ~Copyable, Sendable { + public struct Source: ~Copyable { /// A struct representing the backpressure of the channel. public struct BackpressureStrategy: Sendable { var internalBackpressureStrategy: _InternalBackpressureStrategy @@ -234,8 +234,8 @@ extension MultiProducerSingleConsumerChannel { /// - high: When the number of buffered elements rises above the high watermark, producers will be suspended. /// - waterLevelForElement: A closure used to compute the contribution of each buffered element to the current water level. /// - /// - Note, `waterLevelForElement` will be called on each element when it is written into the source and when - /// it is consumed from the channel, so it is recommended to provide a function that runs in constant time. + /// - Important: `waterLevelForElement` will be called during a lock on each element when it is written into the source and when + /// it is consumed from the channel, so it must be side-effect free and at best constant in time. public static func watermark( low: Int, high: Int, @@ -446,7 +446,7 @@ extension MultiProducerSingleConsumerChannel { public mutating func send( contentsOf sequence: consuming sending S ) async throws where Element == S.Element, S: Sequence, Element: Copyable { - let syncSend: (sending S, inout sending Self) throws -> SendResult = { try $1.send(contentsOf: $0) } + let syncSend: (sending S, inout Self) throws -> SendResult = { try $1.send(contentsOf: $0) } let sendResult = try syncSend(sequence, &self) switch consume sendResult { @@ -481,7 +481,7 @@ extension MultiProducerSingleConsumerChannel { /// - element: The element to send to the channel. @inlinable public mutating func send(_ element: consuming sending Element) async throws { - let syncSend: (consuming sending Element, inout sending Self) throws -> SendResult = { try $1.send($0) } + let syncSend: (consuming sending Element, inout Self) throws -> SendResult = { try $1.send($0) } let sendResult = try syncSend(element, &self) switch consume sendResult { From 0b39d1ea9953222a68c252fc133d96d9517eea98 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Sat, 5 Apr 2025 23:19:05 +0900 Subject: [PATCH 16/16] Rename to `MultiProducerSingleConsumerAsyncChannel` --- ...-mutli-producer-single-consumer-channel.md | 70 +++++++------- ...SingleConsumerAsyncChannel+Internal.swift} | 54 +++++------ ...iProducerSingleConsumerAsyncChannel.swift} | 56 +++++------ Sources/Example/Example.swift | 4 +- ...ucerSingleConsumerAsyncChannelTests.swift} | 94 +++++++++---------- 5 files changed, 139 insertions(+), 139 deletions(-) rename Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/{MultiProducerSingleConsumerChannel+Internal.swift => MultiProducerSingleConsumerAsyncChannel+Internal.swift} (96%) rename Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/{MultiProducerSingleConsumerChannel.swift => MultiProducerSingleConsumerAsyncChannel.swift} (90%) rename Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/{MultiProducerSingleConsumerChannelTests.swift => MultiProducerSingleConsumerAsyncChannelTests.swift} (89%) diff --git a/Evolution/0016-mutli-producer-single-consumer-channel.md b/Evolution/0016-mutli-producer-single-consumer-channel.md index 8229070c..20a23eab 100644 --- a/Evolution/0016-mutli-producer-single-consumer-channel.md +++ b/Evolution/0016-mutli-producer-single-consumer-channel.md @@ -1,4 +1,4 @@ -# MultiProducerSingleConsumerChannel +# MultiProducerSingleConsumerAsyncChannel * Proposal: [SAA-0016](0016-multi-producer-single-consumer-channel.md) * Authors: [Franz Busch](https://github.com/FranzBusch) @@ -129,7 +129,7 @@ The above motivation lays out the expected behaviors for any consumer/producer system and compares them to the behaviors of `Async[Throwing]Stream` and `Async[Throwing]Channel`. -This section proposes a new type called `MultiProducerSingleConsumerChannel` +This section proposes a new type called `MultiProducerSingleConsumerAsyncChannel` that implement all of the above-mentioned behaviors. Importantly, this proposed solution is taking advantage of `~Copyable` types to model the multi-producer-single-consumer behavior. While the current `AsyncSequence` @@ -137,16 +137,16 @@ protocols are not supporting `~Copyable` types we provide a way to convert the proposed channel to an asynchronous sequence. This leaves us room to support any potential future asynchronous streaming protocol that supports `~Copyable`. -### Creating a MultiProducerSingleConsumerChannel +### Creating a MultiProducerSingleConsumerAsyncChannel -You can create an `MultiProducerSingleConsumerChannel` instance using the +You can create an `MultiProducerSingleConsumerAsyncChannel` instance using the `makeChannel(of: backpressureStrategy:)` method. This method returns you the channel and the source. The source can be used to send new values to the asynchronous channel. The new API specifically provides a multi-producer/single-consumer pattern. ```swift -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -192,7 +192,7 @@ do { ``` The above API offers the most control and highest performance when bridging a -synchronous producer to a `MultiProducerSingleConsumerChannel`. First, you have +synchronous producer to a `MultiProducerSingleConsumerAsyncChannel`. First, you have to send values using the `send(contentsOf:)` which returns a `SendResult`. The result either indicates that more values should be produced or that a callback should be enqueued by calling the `enqueueCallback(callbackToken: @@ -221,7 +221,7 @@ try await source.send(contentsOf: sequence) ``` With the above APIs, we should be able to effectively bridge any system into a -`MultiProducerSingleConsumerChannel` regardless if the system is callback-based, +`MultiProducerSingleConsumerAsyncChannel` regardless if the system is callback-based, blocking, or asynchronous. ### Multi producer @@ -232,7 +232,7 @@ region than the original source allowing to pass it into a different isolation region to concurrently produce elements. ```swift -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -262,7 +262,7 @@ this: ```swift // Termination through calling finish -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -280,7 +280,7 @@ If the channel has a failure type it can also be finished with an error. ```swift // Termination through calling finish -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: SomeError.self, backpressureStrategy: .watermark(low: 2, high: 4) @@ -301,7 +301,7 @@ will happen automatically when the source is last used or explicitly consumed. ```swift // Termination through deiniting the source -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -322,7 +322,7 @@ callback. Termination of the producer happens in the following scenarios: ```swift // Termination through task cancellation -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -338,7 +338,7 @@ task.cancel() // Prints Terminated ```swift // Termination through deiniting the channel -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -350,7 +350,7 @@ _ = consume channel // Prints Terminated ```swift // Termination through finishing the source and consuming the last element -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -367,7 +367,7 @@ await channel.next() // Prints Terminated ```swift // Termination through deiniting the last source and consuming the last element -let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -394,29 +394,29 @@ producer has been terminated will result in an error thrown from the send method ```swift #if compiler(>=6.1) /// An error that is thrown from the various `send` methods of the -/// ``MultiProducerSingleConsumerChannel/Source``. +/// ``MultiProducerSingleConsumerAsyncChannel/Source``. /// /// This error is thrown when the channel is already finished when /// trying to send new elements to the source. -public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { } +public struct MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError: Error { } /// A multi-producer single-consumer channel. /// -/// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to +/// The ``MultiProducerSingleConsumerAsyncChannel`` provides a ``MultiProducerSingleConsumerAsyncChannel/Source`` to /// send values to the channel. The channel supports different back pressure strategies to control the /// buffering and demand. The channel will buffer values until its backpressure strategy decides that the /// producer have to wait. /// /// This channel is also suitable for the single-producer single-consumer use-case /// -/// ## Using a MultiProducerSingleConsumerChannel +/// ## Using a MultiProducerSingleConsumerAsyncChannel /// -/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with its source first by calling -/// the ``MultiProducerSingleConsumerChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. +/// To use a ``MultiProducerSingleConsumerAsyncChannel`` you have to create a new channel with its source first by calling +/// the ``MultiProducerSingleConsumerAsyncChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. /// Afterwards, you can pass the source to the producer and the channel to the consumer. /// /// ``` -/// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +/// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( /// of: Int.self, /// backpressureStrategy: .watermark(low: 2, high: 4) /// ) @@ -428,8 +428,8 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { } /// /// ### Asynchronous producing /// -/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-8eo96`` -/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)``. Backpressure results in calls +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerAsyncChannel/Source/send(_:)-8eo96`` +/// and ``MultiProducerSingleConsumerAsyncChannel/Source/send(contentsOf:)``. Backpressure results in calls /// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. /// /// ``` @@ -495,14 +495,14 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { } /// getting cancelled, the source will get notified about the termination if a termination callback has been set /// before by calling ``Source/setOnTerminationCallback(_:)``. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct MultiProducerSingleConsumerChannel: ~Copyable { +public struct MultiProducerSingleConsumerAsyncChannel: ~Copyable { /// A struct containing the initialized channel and source. /// /// This struct can be deconstructed by consuming the individual /// components from it. /// /// ```swift - /// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + /// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( /// of: Int.self, /// backpressureStrategy: .watermark(low: 5, high: 10) /// ) @@ -512,12 +512,12 @@ public struct MultiProducerSingleConsumerChannel: ~Copy @frozen public struct ChannelAndStream : ~Copyable { /// The channel. - public var channel: MultiProducerSingleConsumerChannel + public var channel: MultiProducerSingleConsumerAsyncChannel /// The source. public var source: Source } - /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. + /// Initializes a new ``MultiProducerSingleConsumerAsyncChannel`` and an ``MultiProducerSingleConsumerAsyncChannel/Source``. /// /// - Parameters: /// - elementType: The element type of the channel. @@ -547,7 +547,7 @@ public struct MultiProducerSingleConsumerChannel: ~Copy } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerAsyncChannel { /// A struct to send values to the channel. /// /// Use this source to provide elements to the channel by calling one of the `send` methods. @@ -583,10 +583,10 @@ extension MultiProducerSingleConsumerChannel { /// A type that indicates the result of sending elements to the source. public enum SendResult: ~Copyable, Sendable { /// An opaque token that is returned when the channel's backpressure strategy indicated that production should - /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. + /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerAsyncChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. /// - /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` - /// and ``MultiProducerSingleConsumerChannel/Source/cancelCallback(callbackToken:)``. + /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerAsyncChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` + /// and ``MultiProducerSingleConsumerAsyncChannel/Source/cancelCallback(callbackToken:)``. public struct CallbackToken: Sendable, Hashable { } /// Indicates that more elements should be produced and send to the source. @@ -594,7 +594,7 @@ extension MultiProducerSingleConsumerChannel { /// Indicates that a callback should be enqueued. /// - /// The associated token should be passed to the ````MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. + /// The associated token should be passed to the ````MultiProducerSingleConsumerAsyncChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. case enqueueCallback(CallbackToken) } @@ -728,7 +728,7 @@ extension MultiProducerSingleConsumerChannel { /// Indicates that the production terminated. /// - /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerChannel/next(isolation:)`` will return + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerAsyncChannel/next(isolation:)`` will return /// `nil` or throw an error. /// /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept @@ -741,7 +741,7 @@ extension MultiProducerSingleConsumerChannel { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerAsyncChannel { /// Converts the channel to an asynchronous sequence for consumption. /// /// - Important: The returned asynchronous sequence only supports a single iterator to be created and diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift similarity index 96% rename from Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift rename to Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift index 00762f38..419a2f31 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel+Internal.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel+Internal.swift @@ -14,7 +14,7 @@ import DequeModule import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerAsyncChannel { @usableFromInline enum _InternalBackpressureStrategy: Sendable, CustomStringConvertible { @usableFromInline @@ -140,7 +140,7 @@ extension MultiProducerSingleConsumerChannel { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerAsyncChannel { @usableFromInline final class _Storage: Sendable { @usableFromInline @@ -179,9 +179,9 @@ extension MultiProducerSingleConsumerChannel { for producerContinuation in producerContinuations { switch producerContinuation { case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } onTermination?() @@ -210,9 +210,9 @@ extension MultiProducerSingleConsumerChannel { for producerContinuation in producerContinuations { switch producerContinuation { case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } onTermination?() @@ -241,9 +241,9 @@ extension MultiProducerSingleConsumerChannel { for producerContinuation in producerContinuations { switch producerContinuation { case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } onTermination?() @@ -283,7 +283,7 @@ extension MultiProducerSingleConsumerChannel { @inlinable func send( contentsOf sequence: sending some Sequence - ) throws -> MultiProducerSingleConsumerChannel.Source.SendResult { + ) throws -> MultiProducerSingleConsumerAsyncChannel.Source.SendResult { let action = self._stateMachine.withLock { $0.send(sequence) } @@ -304,7 +304,7 @@ extension MultiProducerSingleConsumerChannel { return .enqueueCallback(.init(id: callbackToken)) case .throwFinishedError: - throw MultiProducerSingleConsumerChannelAlreadyFinishedError() + throw MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError() } } @@ -396,9 +396,9 @@ extension MultiProducerSingleConsumerChannel { for producerContinuation in producerContinuations { switch producerContinuation { case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } @@ -509,9 +509,9 @@ extension MultiProducerSingleConsumerChannel { for producerContinuation in producerContinuations { switch producerContinuation { case .closure(let onProduceMore): - onProduceMore(.failure(MultiProducerSingleConsumerChannelAlreadyFinishedError())) + onProduceMore(.failure(MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError())) case .continuation(let continuation): - continuation.resume(throwing: MultiProducerSingleConsumerChannelAlreadyFinishedError()) + continuation.resume(throwing: MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } onTermination?() @@ -525,7 +525,7 @@ extension MultiProducerSingleConsumerChannel { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel._Storage { +extension MultiProducerSingleConsumerAsyncChannel._Storage { /// The state machine of the channel. @usableFromInline struct _StateMachine: ~Copyable { @@ -565,7 +565,7 @@ extension MultiProducerSingleConsumerChannel._Storage { @usableFromInline init( - backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy + backpressureStrategy: MultiProducerSingleConsumerAsyncChannel._InternalBackpressureStrategy ) { self._state = .channeling( .init( @@ -896,7 +896,7 @@ extension MultiProducerSingleConsumerChannel._Storage { ) } else { // An iterator needs to be initialized before it can be deinitialized. - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") } case .sourceFinished(let sourceFinished): @@ -916,7 +916,7 @@ extension MultiProducerSingleConsumerChannel._Storage { return .callOnTermination(sourceFinished.onTermination) } else { // An iterator needs to be initialized before it can be deinitialized. - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") } case .finished(let finished): @@ -1074,14 +1074,14 @@ extension MultiProducerSingleConsumerChannel._Storage { // It can happen that the source got finished or the consumption fully finishes. self = .init(state: .sourceFinished(sourceFinished)) - return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) case .finished(let finished): // Since we are unlocking between sending elements and suspending the send // It can happen that the source got finished or the consumption fully finishes. self = .init(state: .finished(finished)) - return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + return .resumeProducerWithError(onProduceMore, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } @@ -1124,14 +1124,14 @@ extension MultiProducerSingleConsumerChannel._Storage { // It can happen that the source got finished or the consumption fully finishes. self = .init(state: .sourceFinished(sourceFinished)) - return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) case .finished(let finished): // Since we are unlocking between sending elements and suspending the send // It can happen that the source got finished or the consumption fully finishes. self = .init(state: .finished(finished)) - return .resumeProducerWithError(continuation, MultiProducerSingleConsumerChannelAlreadyFinishedError()) + return .resumeProducerWithError(continuation, MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError()) } } @@ -1274,7 +1274,7 @@ extension MultiProducerSingleConsumerChannel._Storage { case .channeling(var channeling): guard channeling.consumerContinuation == nil else { // We have multiple AsyncIterators iterating the sequence - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") } guard let element = channeling.buffer.popFirst() else { @@ -1356,7 +1356,7 @@ extension MultiProducerSingleConsumerChannel._Storage { case .channeling(var channeling): guard channeling.consumerContinuation == nil else { // We have multiple AsyncIterators iterating the sequence - fatalError("MultiProducerSingleConsumerChannel internal inconsistency") + fatalError("MultiProducerSingleConsumerAsyncChannel internal inconsistency") } // We have to check here again since we might have a producer interleave next and suspendNext @@ -1472,14 +1472,14 @@ extension MultiProducerSingleConsumerChannel._Storage { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel._Storage._StateMachine { +extension MultiProducerSingleConsumerAsyncChannel._Storage._StateMachine { @usableFromInline enum _State: ~Copyable { @usableFromInline struct Channeling: ~Copyable { /// The backpressure strategy. @usableFromInline - var backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy + var backpressureStrategy: MultiProducerSingleConsumerAsyncChannel._InternalBackpressureStrategy /// Indicates if the iterator was initialized. @usableFromInline @@ -1527,7 +1527,7 @@ extension MultiProducerSingleConsumerChannel._Storage._StateMachine { @inlinable init( - backpressureStrategy: MultiProducerSingleConsumerChannel._InternalBackpressureStrategy, + backpressureStrategy: MultiProducerSingleConsumerAsyncChannel._InternalBackpressureStrategy, iteratorInitialized: Bool, sequenceInitialized: Bool, onTermination: (@Sendable () -> Void)? = nil, diff --git a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift similarity index 90% rename from Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift rename to Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift index 6f2e5c22..24150047 100644 --- a/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannel.swift +++ b/Sources/AsyncAlgorithms/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannel.swift @@ -11,32 +11,32 @@ #if compiler(>=6.1) /// An error that is thrown from the various `send` methods of the -/// ``MultiProducerSingleConsumerChannel/Source``. +/// ``MultiProducerSingleConsumerAsyncChannel/Source``. /// /// This error is thrown when the channel is already finished when /// trying to send new elements to the source. -public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { +public struct MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError: Error { @usableFromInline init() {} } /// A multi-producer single-consumer channel. /// -/// The ``MultiProducerSingleConsumerChannel`` provides a ``MultiProducerSingleConsumerChannel/Source`` to +/// The ``MultiProducerSingleConsumerAsyncChannel`` provides a ``MultiProducerSingleConsumerAsyncChannel/Source`` to /// send values to the channel. The channel supports different back pressure strategies to control the /// buffering and demand. The channel will buffer values until its backpressure strategy decides that the /// producer have to wait. /// /// This channel is also suitable for the single-producer single-consumer use-case /// -/// ## Using a MultiProducerSingleConsumerChannel +/// ## Using a MultiProducerSingleConsumerAsyncChannel /// -/// To use a ``MultiProducerSingleConsumerChannel`` you have to create a new channel with its source first by calling -/// the ``MultiProducerSingleConsumerChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. +/// To use a ``MultiProducerSingleConsumerAsyncChannel`` you have to create a new channel with its source first by calling +/// the ``MultiProducerSingleConsumerAsyncChannel/makeChannel(of:throwing:BackpressureStrategy:)`` method. /// Afterwards, you can pass the source to the producer and the channel to the consumer. /// /// ``` -/// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( +/// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( /// of: Int.self, /// backpressureStrategy: .watermark(low: 2, high: 4) /// ) @@ -48,8 +48,8 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { /// /// ### Asynchronous producing /// -/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerChannel/Source/send(_:)-8eo96`` -/// and ``MultiProducerSingleConsumerChannel/Source/send(contentsOf:)``. Backpressure results in calls +/// Values can be send to the source from asynchronous contexts using ``MultiProducerSingleConsumerAsyncChannel/Source/send(_:)-8eo96`` +/// and ``MultiProducerSingleConsumerAsyncChannel/Source/send(contentsOf:)``. Backpressure results in calls /// to the `send` methods to be suspended. Once more elements should be produced the `send` methods will be resumed. /// /// ``` @@ -115,7 +115,7 @@ public struct MultiProducerSingleConsumerChannelAlreadyFinishedError: Error { /// getting cancelled, the source will get notified about the termination if a termination callback has been set /// before by calling ``Source/setOnTerminationCallback(_:)``. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct MultiProducerSingleConsumerChannel: ~Copyable { +public struct MultiProducerSingleConsumerAsyncChannel: ~Copyable { /// The backing storage. @usableFromInline let storage: _Storage @@ -126,7 +126,7 @@ public struct MultiProducerSingleConsumerChannel: ~Copy /// components from it. /// /// ```swift - /// let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + /// let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( /// of: Int.self, /// backpressureStrategy: .watermark(low: 5, high: 10) /// ) @@ -136,12 +136,12 @@ public struct MultiProducerSingleConsumerChannel: ~Copy @frozen public struct ChannelAndStream: ~Copyable { /// The channel. - public var channel: MultiProducerSingleConsumerChannel + public var channel: MultiProducerSingleConsumerAsyncChannel /// The source. public var source: Source init( - channel: consuming MultiProducerSingleConsumerChannel, + channel: consuming MultiProducerSingleConsumerAsyncChannel, source: consuming Source ) { self.channel = channel @@ -149,7 +149,7 @@ public struct MultiProducerSingleConsumerChannel: ~Copy } } - /// Initializes a new ``MultiProducerSingleConsumerChannel`` and an ``MultiProducerSingleConsumerChannel/Source``. + /// Initializes a new ``MultiProducerSingleConsumerAsyncChannel`` and an ``MultiProducerSingleConsumerAsyncChannel/Source``. /// /// - Parameters: /// - elementType: The element type of the channel. @@ -205,7 +205,7 @@ public struct MultiProducerSingleConsumerChannel: ~Copy } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerAsyncChannel { /// A struct to send values to the channel. /// /// Use this source to provide elements to the channel by calling one of the `send` methods. @@ -263,10 +263,10 @@ extension MultiProducerSingleConsumerChannel { /// A type that indicates the result of sending elements to the source. public enum SendResult: ~Copyable, Sendable { /// An opaque token that is returned when the channel's backpressure strategy indicated that production should - /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. + /// be suspended. Use this token to enqueue a callback by calling the ``MultiProducerSingleConsumerAsyncChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` method. /// - /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` - /// and ``MultiProducerSingleConsumerChannel/Source/cancelCallback(callbackToken:)``. + /// - Important: This token must only be passed once to ``MultiProducerSingleConsumerAsyncChannel/Source/enqueueCallback(callbackToken:onProduceMore:)`` + /// and ``MultiProducerSingleConsumerAsyncChannel/Source/cancelCallback(callbackToken:)``. public struct CallbackToken: Sendable, Hashable { @usableFromInline let _id: UInt64 @@ -282,7 +282,7 @@ extension MultiProducerSingleConsumerChannel { /// Indicates that a callback should be enqueued. /// - /// The associated token should be passed to the ````MultiProducerSingleConsumerChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. + /// The associated token should be passed to the ````MultiProducerSingleConsumerAsyncChannel/Source/enqueueCallback(callbackToken:onProduceMore:)```` method. case enqueueCallback(CallbackToken) } @@ -522,7 +522,7 @@ extension MultiProducerSingleConsumerChannel { /// Indicates that the production terminated. /// - /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerChannel/next(isolation:)`` will return + /// After all buffered elements are consumed the subsequent call to ``MultiProducerSingleConsumerAsyncChannel/next(isolation:)`` will return /// `nil` or throw an error. /// /// Calling this function more than once has no effect. After calling finish, the channel enters a terminal state and doesn't accept @@ -538,14 +538,14 @@ extension MultiProducerSingleConsumerChannel { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel where Element: Copyable { +extension MultiProducerSingleConsumerAsyncChannel where Element: Copyable { struct ChannelAsyncSequence: AsyncSequence { @usableFromInline final class _Backing: Sendable { @usableFromInline - let storage: MultiProducerSingleConsumerChannel._Storage + let storage: MultiProducerSingleConsumerAsyncChannel._Storage - init(storage: MultiProducerSingleConsumerChannel._Storage) { + init(storage: MultiProducerSingleConsumerAsyncChannel._Storage) { self.storage = storage self.storage.sequenceInitialized() } @@ -573,14 +573,14 @@ extension MultiProducerSingleConsumerChannel where Element: Copyable { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence where Element: Copyable { +extension MultiProducerSingleConsumerAsyncChannel.ChannelAsyncSequence where Element: Copyable { struct Iterator: AsyncIteratorProtocol { @usableFromInline final class _Backing { @usableFromInline - let storage: MultiProducerSingleConsumerChannel._Storage + let storage: MultiProducerSingleConsumerAsyncChannel._Storage - init(storage: MultiProducerSingleConsumerChannel._Storage) { + init(storage: MultiProducerSingleConsumerAsyncChannel._Storage) { self.storage = storage self.storage.iteratorInitialized() } @@ -593,7 +593,7 @@ extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence where Element: @usableFromInline let _backing: _Backing - init(storage: MultiProducerSingleConsumerChannel._Storage) { + init(storage: MultiProducerSingleConsumerAsyncChannel._Storage) { self._backing = .init(storage: storage) } @@ -620,5 +620,5 @@ extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence where Element: } // @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel.ChannelAsyncSequence: Sendable {} +extension MultiProducerSingleConsumerAsyncChannel.ChannelAsyncSequence: Sendable {} #endif diff --git a/Sources/Example/Example.swift b/Sources/Example/Example.swift index ff1a1f60..3eb472bf 100644 --- a/Sources/Example/Example.swift +++ b/Sources/Example/Example.swift @@ -32,10 +32,10 @@ struct Example { static func testMPSCChannel( count: Int, - backpressureStrategy: MultiProducerSingleConsumerChannel.Source.BackpressureStrategy + backpressureStrategy: MultiProducerSingleConsumerAsyncChannel.Source.BackpressureStrategy ) async { await withTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: backpressureStrategy ) diff --git a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift similarity index 89% rename from Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift rename to Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift index 83454a31..3c5ad977 100644 --- a/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerChannelTests.swift +++ b/Tests/AsyncAlgorithmsTests/MultiProducerSingleConsumerChannel/MultiProducerSingleConsumerAsyncChannelTests.swift @@ -14,13 +14,13 @@ import AsyncAlgorithms import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class MultiProducerSingleConsumerChannelTests: XCTestCase { +final class MultiProducerSingleConsumerAsyncChannelTests: XCTestCase { // MARK: - sourceDeinitialized func testSourceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -48,7 +48,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSourceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -74,7 +74,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testSourceDeinitialized_whenMultipleSources() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -104,13 +104,13 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSourceDeinitialized_whenSourceFinished() async throws { try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 5, high: 10) ) let channel = channelAndSource.channel - var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + var source: MultiProducerSingleConsumerAsyncChannel.Source? = consume channelAndSource.source let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.setOnTerminationCallback { @@ -148,13 +148,13 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSourceDeinitialized_whenFinished() async throws { await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 5, high: 10) ) let channel = channelAndSource.channel - let source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + let source: MultiProducerSingleConsumerAsyncChannel.Source? = consume channelAndSource.source let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() source?.setOnTerminationCallback { @@ -187,7 +187,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: Channel deinitialized func testChannelDeinitialized() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -206,7 +206,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSequenceDeinitialized_whenChanneling_andNoSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -232,7 +232,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testSequenceDeinitialized_whenChanneling_andSuspendedConsumer() async throws { let manualExecutor = ManualTaskExecutor() try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -259,7 +259,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - iteratorInitialized func testIteratorInitialized_whenInitial() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -270,7 +270,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorInitialized_whenChanneling() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -285,7 +285,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorInitialized_whenSourceFinished() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -303,7 +303,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorInitialized_whenFinished() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -321,7 +321,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testIteratorDeinitialized_whenInitial() async throws { await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -356,7 +356,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testIteratorDeinitialized_whenChanneling() async throws { try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -393,7 +393,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testIteratorDeinitialized_whenSourceFinished() async throws { try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 5, high: 10) ) @@ -431,7 +431,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testIteratorDeinitialized_whenFinished() async throws { try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 5, high: 10) @@ -468,12 +468,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testIteratorDeinitialized_whenChanneling_andSuspendedProducer() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 5, high: 10) ) - var channel: MultiProducerSingleConsumerChannel? = channelAndSource.channel + var channel: MultiProducerSingleConsumerAsyncChannel? = channelAndSource.channel var source = consume channelAndSource.source var iterator = channel?.asyncSequence().makeAsyncIterator() @@ -490,7 +490,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { iterator = nil } } catch { - XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) + XCTAssertTrue(error is MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError) } _ = try await iterator?.next() @@ -499,7 +499,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - write func testWrite_whenInitial() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -514,7 +514,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testWrite_whenChanneling_andNoConsumer() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -533,7 +533,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testWrite_whenChanneling_andSuspendedConsumer() async throws { try await withThrowingTaskGroup(of: Int?.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -555,7 +555,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testWrite_whenChanneling_andSuspendedConsumer_andEmptySequence() async throws { try await withThrowingTaskGroup(of: Int?.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -576,7 +576,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testWrite_whenSourceFinished() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -590,7 +590,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { try await source2.send(1) XCTFail("Expected an error to be thrown") } catch { - XCTAssertTrue(error is MultiProducerSingleConsumerChannelAlreadyFinishedError) + XCTAssertTrue(error is MultiProducerSingleConsumerAsyncChannelAlreadyFinishedError) } let element1 = await channel.next() XCTAssertEqual(element1, 1) @@ -600,7 +600,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testWrite_whenConcurrentProduction() async throws { await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 5) ) @@ -636,7 +636,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - enqueueProducer func testEnqueueProducer_whenChanneling_andAndCancelled() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 2) ) @@ -673,7 +673,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testEnqueueProducer_whenChanneling_andAndCancelled_andAsync() async throws { try await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 2) ) @@ -700,7 +700,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testEnqueueProducer_whenChanneling_andInterleaving() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 1) ) @@ -732,7 +732,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testEnqueueProducer_whenChanneling_andSuspending() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 1) ) @@ -766,7 +766,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { // MARK: - cancelProducer func testCancelProducer_whenChanneling() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 2) ) @@ -805,12 +805,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testFinish_whenChanneling_andConsumerSuspended() async throws { try await withThrowingTaskGroup(of: Int?.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 1, high: 1) ) var channel = channelAndSource.channel - var source: MultiProducerSingleConsumerChannel.Source? = consume channelAndSource.source + var source: MultiProducerSingleConsumerAsyncChannel.Source? = consume channelAndSource.source group.addTask { while let element = await channel.next() { @@ -833,7 +833,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testFinish_whenInitial() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 1, high: 1) @@ -856,7 +856,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testBackpressure() async throws { await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -894,7 +894,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testBackpressureSync() async throws { await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -935,7 +935,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testWatermarkWithCustomCoount() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: [Int].self, backpressureStrategy: .watermark(low: 2, high: 4, waterLevelForElement: { $0.count }) ) @@ -956,12 +956,12 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { await withThrowingTaskGroup(of: Void.self) { group in // This test should in the future use a custom task executor to schedule to avoid sending // 1000 elements. - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) let channel = channelAndSource.channel - var source: MultiProducerSingleConsumerChannel.Source! = consume channelAndSource.source + var source: MultiProducerSingleConsumerAsyncChannel.Source! = consume channelAndSource.source group.addTask { var source = source.take()! @@ -983,7 +983,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } func testThrowsError() async throws { - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, throwing: Error.self, backpressureStrategy: .watermark(low: 2, high: 4) @@ -1014,7 +1014,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testAsyncSequenceWrite() async throws { let (stream, continuation) = AsyncStream.makeStream() - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -1036,7 +1036,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { func testNonThrowing() async throws { await withThrowingTaskGroup(of: Void.self) { group in - let channelAndSource = MultiProducerSingleConsumerChannel.makeChannel( + let channelAndSource = MultiProducerSingleConsumerAsyncChannel.makeChannel( of: Int.self, backpressureStrategy: .watermark(low: 2, high: 4) ) @@ -1074,7 +1074,7 @@ final class MultiProducerSingleConsumerChannelTests: XCTestCase { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel { +extension MultiProducerSingleConsumerAsyncChannel { /// Collect all elements in the sequence into an array. fileprivate mutating func collect() async throws(Failure) -> [Element] { var elements = [Element]() @@ -1086,7 +1086,7 @@ extension MultiProducerSingleConsumerChannel { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension MultiProducerSingleConsumerChannel.Source.SendResult { +extension MultiProducerSingleConsumerAsyncChannel.Source.SendResult { func assertIsProducerMore() { switch self { case .produceMore: