Writing a `CheckedContinuation` to a `Channel` without leaking?

i’m currently running into a

_Concurrency/CheckedContinuation.swift:164: 
Fatal error: SWIFT TASK CONTINUATION MISUSE: 
request(_:sections:deadline:) tried to resume its continuation 
more than once, returning 
failure(MongoIO.MongoIO.ChannelError.io(
    read(descriptor:pointer:size:): Connection reset by peer (errno: 104), 
    written: false))!

this corresponds to the following code location, where i am writing a CheckedContinuation to an any Channel:

await withCheckedContinuation
{
    (continuation:CheckedContinuation<
        Result<MongoWire.Message<ByteBufferView>, MongoIO.ChannelError>,
        Never>) in

    channel.writeAndFlush(MongoIO.Action.request(sections, continuation))
        .whenComplete
    {
        if  case .failure(let error) = $0
        {
            continuation.resume(returning: .failure(.io(error, written: false)))
        }
    }
}

as you can see, i’m using the failure(_:) case to resume the continuation if the writeAndFlush operation fails, and this apparently took place even though the continuation also made it into the channel pipeline and was resumed there as well.

is it actually possible for an OutboundIn value to enter the pipeline even though writeAndFlush failed?

Just from this code snippet it is hard to tell but it looks like there is another place that resumes the continuation.

Yes, the failing of a write only happens after the write travelled the channel pipeline and gets either succeeded or failed at some point. Most likely you pass the write into the channel pipeline and where ever your resume it you also fail the write which triggers the double resume.

correct, the channel handler in the pipeline also contains logic that resumes the continuation.

ah, that’s what i suspected. how should i handle this case? switching on the returned Result case doesn’t seem sufficient to detect if the continuation needs resuming.

I think you should either only resume the continuation in one place. This can be either your channel handler or the whenComplete callback on the write’s future. I personally would probably do the latter for simplicity though the former can have better performance since you can avoid allocating a promise.

the continuation is the continuation that receives the response gathered by the channel handler (a MongoWire.Message<ByteBufferView>), so it can’t yield a successful result from the whenComplete callback, because the response has not been received yet.

however, the channel handler can’t be exclusively responsible for resuming the continuation either, because if the continuation never makes it into the pipeline in the first place, then the caller will hang indefinitely.

Right I missed that the continuation is typed and not Void. Without known the details have where the continuation is being constructed and what the other handlers in the pipeline are doing it is hard to say what the right thing to do is.
Overall you just have to make sure that it is only resumed once. This is one of the reasons why we created the NIOAsyncChannel since bridging one off continuations into a channel pipeline can become tricky.

One thing that you could do is pass an additional EventLoopPromise down the channel pipeline instead of the continuation and add a whenComplete to that promise. You can properly type that promise and a promise protects you against double resumption.

1 Like

yeah, the withCheckedThrowingContinuation call is just one of those older “NIO-to-async” adapters, so using an EventLoopPromise and then awaiting on the future’s get() is probably the way to go. no need to have a CheckedContinuation at all actually.

it is an older codebase written around 2022 or so. if it were a new project i would certainly use NIOAsyncChannel instead.

1 Like