I'm experimenting with withTimeout(in:), and it's raised an interesting question about passing mutable values to async functions, and specifically passing AsyncIterators.
I have a custom AsyncSequence called LedgerResponseSequence. It is a sequence of responses to commands I send on another channel, and wraps a FileHandle:
let ledgerOutput = readPipe.fileHandleForReading
var responses = LedgerResponseSequence(reader: ledgerOutput).makeAsyncIterator()
In a loop, I consume commands from an AsyncChannel, send them, and then wait for a response and send it to the caller on a one-shot AsyncChannel (like I would in Go):
for try await command in commandChannel {
if Task.isCancelled { ... fail command and return ... }
sendCommand(command)
// Wait for response
do {
guard let response = try await responses.next() else { throw CancellationError() }
await command.response.send(response)
command.response.finish()
} catch {
await command.response.fail(error)
}
}
So far, so good. Then I naively added withTimeout
:
do {
// Wrap previous code with a timeout
try await withTimeout(in: .seconds(2)) {
guard let response = try await responses.next() else {
throw CancellationError()
}
// etc.
}
}
Unsurprisingly, this fails to compile with Mutation of captured var 'responses' in concurrently-executing code
. That's probably fine and correct.
The question is the correct way to deal with this. My current solution is to hand-roll an inout
parameter by returning a mutated copy of the AsyncIterator:
responses = try await withTimeout(in: .seconds(2)) { [responses] in
var responses = responses
// ... etc ... mutating responses
return responses
}
This works, but is it correct? (It technically also "works" if I just mutate a copy and don't both returning it, but that feels like relying on implementation details of this particular iterator.)