I’m working with a single AsyncSequence that needs to be consumed by two other ASs. To coordinate this, I set up an actor that owns the underlying iterator and exposes methods that forward next() calls. (Following is just an example of interest to the conversation, see Add streamable multipart part by ptoffy · Pull Request #145 · vapor/multipart-kit · GitHub for the full code)
Cannot call mutating async function 'next(isolation:)' on actor-isolated property 'backingIterator'
I can appreciate that this protects from re-entrancy as the await might allow mutation of backingIterator from another actor method, however the only "safe" fix I've found to solve the issue is replacing nextChunk() with
func nextChunk() async throws -> BackingSequence.Element? {
var temp = backingIterator
let next = try await temp.next(isolation: self)
backingIterator = temp
return next
}
This is not ideal for performance reasons. The other solution would be to wrap the backingIterator in a class which would defy isolation altogether.
Is there an actor-safe way to call a mutating async method on a stored property? Or would any other solution just be slower than simply copying the iterator?
IMO reusing the same iterator across multiple Tasks or iterating the same async sequence concurrently can lead to very confusing behaviors – is that what you want to support in this case? i found this related-seeming comment in a PR from the async-algorithms repo interesting. IIUC the gist is that in many (most?) cases, an AsyncSequence itself can be Sendable, but its AsyncIterator typically is not or should not be, to enforce use from a single Task at a time (it's possible i'm misinterpreting, perhaps @FranzBusch can weigh in).
going back to your use case – why is it that you want to forward to the same base iterator vs having a separate one for each of your consuming sequences?
So essentially what I'm trying to do is, given a single source sequence (e.g AsyncSequence<[UInt8]>) build another sequence (let's call it PartAsyncSequence) that returns structured parts out of the original one:
struct Part {
let body: PartBodyAsyncSequence<[UInt8]>
// ... other fields
}
You'll notice how the Part itself also holds a sequence. To be able to also consume that sequence, I need some kind of coordination to correctly read from the original one, since both sub-sequences will consume the upstream one.
If I'm consuming a PartAsyncSequence, I want my iterator to return the whole Part, in contrast if I'm reading from PartBodyAsyncSequence, I want the iterator to return single body chunks. I hope this makes sense.
I'm more than happy to consider alternative solutions, the shared iterator actor was just my first instinct.