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?