swift-server/async-http-client

Canceling a download `Task` doesn't actually cancel it?

Opened this issue · 3 comments

Stubbing this for now with hope to provide a reproducible test case when I have time...

I'm hoping to use structured concurrency Task cancellation to actually cancel a download, so I did something like this:

let download = Task {
    let delegate = try FileDownloadDelegate(path: tempDownloadPath)

    let downloadTask = HTTPClient.shared.execute(request: request, delegate: delegate, reportProgress: {
        print("progress: \($0.receivedBytes)")
    })

    _ = try await withTaskCancellationHandler {
        try await downloadTask.get()
    } onCancel: {
        print("cancelling download...")
        downloadTask.cancel()
        print("cancelled download")
    }
}

await Task.sleep(for: .seconds(5))
download.cancel()
try await download.value

The actual download progresses until it's done downloading the file, no matter what, and then downloadTask.get() returns without throwing an error.

What am I doing wrong here?

Follow up: canceling the task passed as a parameter in reportProgress can actually stop the download and fail the downloadTask.get(). Why can't I cancel the download outside of that scope? Obviously that would be something you'd want to do!

Sorry for this sitting unanswered over the weekend @gregcotten, let me see if I can reproduce this.

Ok, so I don't reproduce this locally with a simple alternative. Here's the complete code I'm running:

import NIOCore
import NIOPosix
import NIOHTTP1
import AsyncHTTPClient

private final class HTTPServer: ChannelInboundHandler {
    typealias InboundIn = HTTPServerRequestPart
    typealias OutboundOut = HTTPServerResponsePart

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.unwrapInboundIn(data) {
        case .head, .body:
            ()
        case .end:
            self.sendResponse(context: context)
        }
    }

    private func sendResponse(context: ChannelHandlerContext) {
        let head = HTTPResponseHead(version: .http1_1, status: .ok, headers: ["Content-Length": "12"])
        context.write(self.wrapOutboundOut(.head(head)), promise: nil)
        context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(ByteBuffer(string: "hello")))), promise: nil)
        context.eventLoop.scheduleTask(in: .seconds(15)) {
            context.writeAndFlush(self.wrapOutboundOut(.body(.byteBuffer(ByteBuffer(string: " ")))), promise: nil)
        }
        context.eventLoop.scheduleTask(in: .seconds(30)) {
            context.write(self.wrapOutboundOut(.body(.byteBuffer(ByteBuffer(string: "world!")))), promise: nil)
            context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
        }
    }
}

@main
struct Main {
    static func main() async throws {
        let server = try await Self.runServer()
        let client = HTTPClient(eventLoopGroup: .singletonMultiThreadedEventLoopGroup)
        let request = try HTTPClient.Request(url: "http://localhost:\(server.localAddress!.port!)/")

        let download = Task {
            let delegate = try FileDownloadDelegate(path: "/tmp/test.txt", reportProgress: {
                print("progress: \($0.receivedBytes)")
            })

            let downloadTask = client.execute(request: request, delegate: delegate)

            _ = try await withTaskCancellationHandler {
                try await downloadTask.get()
            } onCancel: {
                print("cancelling download...")
                downloadTask.cancel()
                print("cancelled download")
            }
        }

        try await Task.sleep(for: .seconds(5))
        download.cancel()
        try await download.value
    }

    private static func runServer() async throws -> Channel {
        return try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup)
            .childChannelInitializer { channel in
                channel.pipeline.configureHTTPServerPipeline().flatMap {
                    channel.pipeline.addHandler(HTTPServer())
                }
            }
            .bind(host: "localhost", port: 0)
            .get()
    }
}

This code correctly throws at the top level, showing the following output in the console:

progress: 5
cancelling download...
cancelled download
Swift/ErrorType.swift:253: Fatal error: Error raised at top level: HTTPClientError.cancelled

You appear to be using slightly different APIs though, I can't find the APIs you're using on AHC. Do you know where they're coming from? If you run my sample code, do you see the same output as me?