Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canceling a download Task doesn't actually cancel it? #753

Open
gregcotten opened this issue Jul 20, 2024 · 3 comments
Open

Canceling a download Task doesn't actually cancel it? #753

gregcotten opened this issue Jul 20, 2024 · 3 comments

Comments

@gregcotten
Copy link

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?

@gregcotten
Copy link
Author

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!

@Lukasa
Copy link
Collaborator

Lukasa commented Jul 22, 2024

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

@Lukasa
Copy link
Collaborator

Lukasa commented Jul 22, 2024

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants