-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
396: Alternate Downloads.jl-based backend (alternative to HTTP.jl) r=mattBrzezinski a=ericphanson I was running into issues with HTTP when trying to make many concurrent requests to s3, so I thought I'd try libcurl through the Downloads.jl stdlib. Turned out to be pretty easy to at least get something basic working, e.g. I can pull down a test file from s3 with no problems with this little bit of code-- and you can even pass a `Downloads.Downloader` along to have more control over the client. I haven't had a chance to do more thorough testing, but I thought I'd put the PR up so maybe we can see what CI says to test out the Downloads-based backend. (Right now Downloads.jl is set to the default in this PR for testing purposes, but I think we should keep HTTP.jl as the default unless it really is a big upgrade). Co-authored-by: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Co-authored-by: Chris de Graaf <me@cdg.dev>
- Loading branch information
Showing
9 changed files
with
149 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
struct DownloadsBackend <: AWS.AbstractBackend | ||
downloader::Union{Nothing, Downloads.Downloader} | ||
end | ||
|
||
DownloadsBackend() = DownloadsBackend(nothing) | ||
|
||
const AWS_DOWNLOADER = Ref{Union{Nothing, Downloader}}(nothing) | ||
const AWS_DOWNLOAD_LOCK = ReentrantLock() | ||
|
||
# Here we mimic Download.jl's own setup for using a global downloader. | ||
# We do this to have our own downloader (separate from Downloads.jl's global downloader) | ||
# because we add a hook to avoid redirects in order to try to match the HTTPBackend's | ||
# implementation, and we don't want to mutate the global downloader from Downloads.jl. | ||
# https://github.com/JuliaLang/Downloads.jl/blob/84e948c02b8a0625552a764bf90f7d2ee97c949c/src/Downloads.jl#L293-L301 | ||
function get_downloader(downloader=nothing) | ||
lock(AWS_DOWNLOAD_LOCK) do | ||
yield() # let other downloads finish | ||
downloader isa Downloader && return | ||
while true | ||
downloader = AWS_DOWNLOADER[] | ||
downloader isa Downloader && return | ||
D = Downloader() | ||
D.easy_hook = (easy, info) -> Curl.setopt(easy, Curl.CURLOPT_FOLLOWLOCATION, false) | ||
AWS_DOWNLOADER[] = D | ||
end | ||
end | ||
return downloader | ||
end | ||
|
||
# https://github.com/JuliaWeb/HTTP.jl/blob/2a03ca76376162ffc3423ba7f15bd6d966edff9b/src/MessageRequest.jl#L84-L85 | ||
body_length(x::AbstractVector{UInt8}) = length(x) | ||
body_length(x::AbstractString) = sizeof(x) | ||
|
||
read_body(x::IOBuffer) = take!(x) | ||
function read_body(x::IO) | ||
close(x) | ||
return read(x) | ||
end | ||
|
||
function _http_request(backend::DownloadsBackend, request) | ||
# If we pass `output`, older versions of Downloads.jl will | ||
# expect a message body in the response. Specifically, it sets | ||
# <https://curl.se/libcurl/c/CURLOPT_NOBODY.html> | ||
# only when we do not pass the `output` argument. | ||
# See <https://github.com/JuliaLang/Downloads.jl/issues/131>. | ||
# | ||
# When the method is `HEAD`, the response may have a Content-Length | ||
# but not send any content back (which appears to be correct, | ||
# <https://stackoverflow.com/a/18925736/12486544>). | ||
# | ||
# Thus, if we did not set `CURLOPT_NOBODY`, and it gets a Content-Length | ||
# back, it will hang waiting for that body. | ||
# | ||
# Therefore, we do not pass an `output` when the `request_method` is `HEAD`. | ||
# (Note: this is fixed on the latest Downloads.jl, but we include this workaround | ||
# for compatability). | ||
if request.response_stream === nothing | ||
request.response_stream = IOBuffer() | ||
end | ||
output_arg = if request.request_method == "HEAD" | ||
NamedTuple() | ||
else | ||
(; output=request.response_stream) | ||
end | ||
|
||
# If we're going to return the stream, we don't want to read the body into an | ||
# HTTP.Response we're never going to use. If we do that, the returned stream | ||
# will have no data available (and reading from it could hang forever). | ||
body_arg = if request.request_method == "HEAD" || request.return_stream | ||
() -> NamedTuple() | ||
else | ||
() -> (; body = read_body(request.response_stream)) | ||
end | ||
|
||
# HTTP.jl sets this header automatically. | ||
request.headers["Content-Length"] = string(body_length(request.content)) | ||
|
||
# We pass an `input` only when we have content we wish to send. | ||
input = IOBuffer() | ||
input_arg = if !isempty(request.content) | ||
write(input, request.content) | ||
(; input=input) | ||
else | ||
NamedTuple() | ||
end | ||
|
||
@repeat 4 try | ||
downloader = @something(backend.downloader, get_downloader()) | ||
# set the hook so that we don't follow redirects. Only | ||
# need to do this on per-request downloaders, because we | ||
# set our global one with this hook already. | ||
if backend.downloader !== nothing && downloader.easy_hook === nothing | ||
downloader.easy_hook = (easy, info) -> Curl.setopt(easy, Curl.CURLOPT_FOLLOWLOCATION, false) | ||
end | ||
|
||
# We seekstart on every attempt, otherwise every attempt | ||
# but the first will send an empty payload. | ||
seekstart(input) | ||
|
||
response = Downloads.request(request.url; input_arg..., output_arg..., | ||
method = request.request_method, | ||
headers = request.headers, verbose=false, throw=true, | ||
downloader=downloader) | ||
|
||
http_response = HTTP.Response(response.status, response.headers; body_arg()..., request=nothing) | ||
|
||
if HTTP.iserror(http_response) | ||
target = HTTP.resource(HTTP.URI(request.url)) | ||
throw(HTTP.StatusError(http_response.status, request.request_method, target, http_response)) | ||
end | ||
return http_response | ||
catch e | ||
@delay_retry if ((isa(e, HTTP.StatusError) && AWS._http_status(e) >= 500) || isa(e, Downloads.RequestError)) end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters