Skip to content

Commit df33406

Browse files
authored
gracefully cancel a request (#256)
* gracefully cancel a request Adds a way to gracefully cancel an ongoing request. The `request` method accepts an additional `interrupt` keyword which can be a `Base.Event`. When it is triggered, the [`curl_multi_remove_handle`](https://curl.se/libcurl/c/curl_multi_remove_handle.html) is invoked, which interrupts the easy handle gracefully. It closes the `output` and `progress` channels of the `Easy` handle to unblock the waiting request task, which then terminates with a `RequestError`.
1 parent 1061ecc commit df33406

File tree

4 files changed

+61
-2
lines changed

4 files changed

+61
-2
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ request(url;
9797
[ debug = <none>, ]
9898
[ throw = true, ]
9999
[ downloader = <default>, ]
100+
[ interrupt = <none>, ]
100101
) -> Union{Response, RequestError}
101102
```
102103
- `url :: AbstractString`
@@ -110,6 +111,7 @@ request(url;
110111
- `debug :: (type, message) --> Any`
111112
- `throw :: Bool`
112113
- `downloader :: Downloader`
114+
- `interrupt :: Base.Event`
113115

114116
Make a request to the given url, returning a `Response` object capturing the
115117
status, headers and other information about the response. The body of the
@@ -129,6 +131,11 @@ be downloaded (indicated by non-2xx status code), `request` returns a `Response`
129131
object no matter what the status code of the response is. If there is an error
130132
with getting a response at all, then a `RequestError` is thrown or returned.
131133

134+
If the `interrupt` keyword argument is provided, it must be a `Base.Event` object.
135+
If the event is triggered while the request is in progress, the request will be
136+
cancelled and an error will be thrown. This can be used to interrupt a long
137+
running request, for example if the user wants to cancel a download.
138+
132139
### default_downloader!
133140

134141
```jl

src/Curl/Multi.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ function socket_callback(
192192
end
193193
end
194194
@isdefined(errormonitor) && errormonitor(task)
195+
else
196+
lock(multi.lock) do
197+
check_multi_info(multi)
198+
end
195199
end
196200
@isdefined(old_watcher) && close(old_watcher)
197201
return 0

src/Downloads.jl

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ end
286286
[ debug = <none>, ]
287287
[ throw = true, ]
288288
[ downloader = <default>, ]
289+
[ interrupt = <none>, ]
289290
) -> Union{Response, RequestError}
290291
291292
url :: AbstractString
@@ -299,6 +300,7 @@ end
299300
debug :: (type, message) --> Any
300301
throw :: Bool
301302
downloader :: Downloader
303+
interrupt :: Base.Event
302304
303305
Make a request to the given url, returning a `Response` object capturing the
304306
status, headers and other information about the response. The body of the
@@ -317,6 +319,11 @@ Note that unlike `download` which throws an error if the requested URL could not
317319
be downloaded (indicated by non-2xx status code), `request` returns a `Response`
318320
object no matter what the status code of the response is. If there is an error
319321
with getting a response at all, then a `RequestError` is thrown or returned.
322+
323+
If the `interrupt` keyword argument is provided, it must be a `Base.Event` object.
324+
If the event is triggered while the request is in progress, the request will be
325+
cancelled and an error will be thrown. This can be used to interrupt a long
326+
running request, for example if the user wants to cancel a download.
320327
"""
321328
function request(
322329
url :: AbstractString;
@@ -330,6 +337,7 @@ function request(
330337
debug :: Union{Function, Nothing} = nothing,
331338
throw :: Bool = true,
332339
downloader :: Union{Downloader, Nothing} = nothing,
340+
interrupt :: Union{Nothing, Base.Event} = nothing,
333341
) :: Union{Response, RequestError}
334342
if downloader === nothing
335343
lock(DOWNLOAD_LOCK) do
@@ -388,6 +396,20 @@ function request(
388396

389397
# do the request
390398
add_handle(downloader.multi, easy)
399+
interrupted = false
400+
if interrupt !== nothing
401+
interrupt_task = @async begin
402+
# wait for the interrupt event
403+
wait(interrupt)
404+
# cancel the request
405+
remove_handle(downloader.multi, easy)
406+
close(easy.output)
407+
close(easy.progress)
408+
interrupted = true
409+
end
410+
else
411+
interrupt_task = nothing
412+
end
391413
try # ensure handle is removed
392414
@sync begin
393415
@async for buf in easy.output
@@ -403,14 +425,28 @@ function request(
403425
end
404426
end
405427
finally
406-
remove_handle(downloader.multi, easy)
428+
if !interrupted
429+
if interrupt_task !== nothing
430+
# trigger interrupt
431+
notify(interrupt)
432+
wait(interrupt_task)
433+
else
434+
remove_handle(downloader.multi, easy)
435+
end
436+
end
407437
end
408438

409439
# return the response or throw an error
410440
response = Response(get_response_info(easy)...)
411441
easy.code == Curl.CURLE_OK && return response
412442
message = get_curl_errstr(easy)
413-
response = RequestError(url, easy.code, message, response)
443+
if easy.code == typemax(Curl.CURLcode)
444+
# uninitialized code, likely a protocol error
445+
code = Int(0)
446+
else
447+
code = Int(easy.code)
448+
end
449+
response = RequestError(url, code, message, response)
414450
throw && Base.throw(response)
415451
end
416452
end

test/runtests.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,18 @@ include("setup.jl")
468468
end
469469
end
470470

471+
@testset "interrupt" begin
472+
url = "$server/delay/10"
473+
interrupt = Base.Event()
474+
download_task = @async request(url; interrupt=interrupt)
475+
sleep(0.1)
476+
@test !istaskdone(download_task)
477+
notify(interrupt)
478+
timedwait(()->istaskdone(download_task), 5.0)
479+
@test istaskdone(download_task)
480+
@test download_task.result isa RequestError
481+
end
482+
471483
@testset "progress" begin
472484
url = "$server/drip"
473485
progress = []

0 commit comments

Comments
 (0)