JuliaWeb / HTTP.jl

HTTP for Julia
https://juliaweb.github.io/HTTP.jl/stable/
Other
626 stars 177 forks source link

HTTP.request negates interactive thread usage. #1153

Closed samtkaplan closed 3 months ago

samtkaplan commented 4 months ago

I observe that using HTTP.request within an interactive thread makes the request run, in part, on a default thread. The result is that when the default thread(s) are busy, the request is delayed (i.e. not "interactive"). Below is an example where we keep a thread in the default thread-pool busy by computing an approximation to pi (via the compute method), and we use a thread in the interactive thread-pool to make an http request once every two seconds (via the interact method). We can exercise this pattern using HTTP.jl or LibCURL.jl for the http request. In the case of HTTP.jl the interact method is blocked until the compute method finishes where-as in the case of LibCurl.jl, the interact and compute methods run simultaneously, as expected.

using HTTP,LibCURL

function compute(tic)
    @info "compute" Threads.threadpool() Threads.threadid()
    x = 1
    c = -1
    d = 3
    while true
        if time() - tic > 30
            break
        end
        x += c/d
        c *= -1
        d += 2
    end
    @info "approximation to pi is $(4*x)"
end

function interact(tic, ishttp)
    @info "interact" Threads.threadpool() Threads.threadid()
    while true
        if ishttp
            r = HTTP.request("GET", "https://example.com")
            @info "status=$(r.status)"
        else
            curl = curl_easy_init()
            curl_easy_setopt(curl, CURLOPT_URL, "https://example.com")
            curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0)
            redirect_stdout(devnull) do
                curl_easy_perform(curl)
            end
            status = Clong[300]
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status)
            @info "status=$(status[1])"
            curl_easy_cleanup(curl)
        end
        sleep(2)
        if time() - tic > 32
            break
        end
    end
    nothing
end

ishttp = parse(Bool, ARGS[1])

tic = time()
t1 = Threads.@spawn :interactive interact(tic, ishttp)
sleep(.001) # not sure why this is needed, but without it, the interactive thread sometimes does not run.
t2 = Threads.@spawn :default compute(tic)

wait.((t1,t2))

Here is the result using ishttp=true:

$ julia -t 1,1 --project=@http httptrouble.jl true
┌ Info: interact
│   Threads.threadpool() = :interactive
└   Threads.threadid() = 1
┌ Info: compute
│   Threads.threadpool() = :default
└   Threads.threadid() = 2
[ Info: approximation to pi is 3.1415926553757925
[ Info: status=200

Here is the result using ishttp=false:

$ julia -t 1,1 --project=@http httptrouble.jl false
┌ Info: interact
│   Threads.threadpool() = :interactive
└   Threads.threadid() = 1
┌ Info: compute
│   Threads.threadpool() = :default
└   Threads.threadid() = 2
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: status=200
[ Info: approximation to pi is 3.141592655058851
[ Info: status=200

For what it is worth, the real-world use case here is monitoring for spot eviction events when using Azure cloud virtual machines.

I suppose the solution is to somehow propagate the interactive thread-pool usage through the HTTP.jl layers so that the appropriate thread pool can be used when Threads.@spawn is called, but I'm not super familiar with the structure of the layers in HTTP.jl, and I'm not sure what the best mechanism for propagating the interactive thread usage is. But, perhaps the simplest solution is to use something like Threads.@spawn Threads.threadpool() ... in places like this.