Achieving parallel web request handling with Julia (httpserver.jl or other)

I’ve written a web server in Julia using httpserver.jl. For simplicity, let’s say that I have one endpoint that runs a Julia function to crunch some numbers and then return them to the browser. Let’s also assume for sake of discussion that the function takes 60 seconds to execute and complete.

While the first request is being processed, any subsequent requests will sit and spin away waiting for the first one to finish. The browser hangs until the first request completes and then the second one gets processed and returned and so on.

Assuming the machine has enough resources to handle the load, what is the current recommended way to achieve parallel request processing with a Julia web server?

Thanks in advance for the help!

2 Likes

There’s some support in Base for allowing ports to be “reused”; i.e. multiple processes could listen on the same port and requests would be accepted in parallel. I haven’t personally used it yet, and I think support is limited (only works on linux?), but it’s probably the most robust route if you really need a parallel server.

In the HTTP.jl package, which is a rewrite and modernizing of HttpServer.jl, among other things, the Servers package provides quick and easy ways to spin servers up and pass in “handler” functions. When requests are received, they are processed asynchronously using @schedule blocks, so multiple requests can be processed simultaneously.

In one of my own personal applications, I spawn additional julia workers and spawn long-running computations off on them. Typically, http isn’t really meant for long-running/waiting requests like this: you’d probably be better off restructuring the client to make an initial compute request and then periodically poll to see if the computation is finished. You could also consider using websockets for a hybrid approach of persistent connection with the ability to bi-directionally “push” messages.

4 Likes

Just echoing @quinnj response. I have a rule of thumb:

  • If it takes less than 3s, handle it through http
  • Otherwise, schedule a worker

Probably can do this on heroku with a web and worker dyno (it would be free if you got it working)

(the grain of salt is i’ve only hosted using heroku with rails.)


For interesting links check out,

1 Like

I agree with the above, and would add that it’s possible to schedule workers within a single hosted container if you don’t want to mess with separate worker dynos (or the equivalent in other platforms).

1 Like

Thank you very much, everyone. I will investigate all of these routes :slight_smile: Cheers.

It was really interesting discussion (and nice work!) year and half ago. You wrote:

I would like to know your opinion about Escher.jl and Genie.jl (or other pure Julia solution) if they are ready now…

Or would you still prefer Angular and if yes then why? Thanks! :slight_smile:

FYI there was also this proposal of a google summer code there https://julialang.org/soc/projects/general.html#middlewares-for-common-web-application-chores-in-muxjl

Middlewares for common web application chores in Mux.jl

Implementation of mid-level features - specifically routing, load-balancing,
cookie/session handling, and authentication - in Mux.jl. The implementation
should be extensible enough to allow easy interfacing with different kinds of
caching, databases or authentication backends.
(See Clojure/Ring for inspiration).

Expected Results: Improvements to the Mux.jl package.

Required Skills: Experience with web development.

Recommended Skills: Knowledge of various web frameworks, especially the design decisions and tradeoffs behind them.

Mentors: Mike Innes

It seems it wasn’t allocated this year in the recently published gsoc 2018 student list
I don’t know if there was (or will be) any others evolutions

As far as I can tell there are still more students to announce themselves. I’m pretty sure we haven’t 27 introductions yet. Maybe the MUX project is still coming.

I have little experience in web development but I’ll be developing a web app in WebIO for this year GSoC and was planning to deploy it with Mux eventually. It’d be interesting to know if Mux is still the recommended way/ how robustly one can use it to deploy apps or otherwise how easy/hard it’d be for WebIO to support the new HTTP.jl framework.

Escher is no longer being developed as a package, although parts of it are being reused. I haven’t used Genie.jl but it doesn’t seem like a good fit for my typical use case.

I usually have some large front-end application that needs fairly complex routing, components, and so on, which is why I use standard tools like Angular/React and existing design frameworks. Occasionally I need to create a new API that uses julia, and for that my use case is very similar to the one Jacob mentioned above.

I’m a big fan of the new HTTP.jl and would recommend it over Mux. HTTP provides a lot of the same functionality for interacting with headers, creating responses, etc, although it doesn’t have a high-level interface for stacking middleware and setting up routes. I set up a bit of this in Joseki.jl but you can also just use HTTP directly.

I’m not sure what are the technical obstacles, but would you consider providing support for WebIO.jl in Joseki? This is the file that defines Mux support for example, not sure how hard it would be to port it. The advantage is that one could then publish all InteractNext based GUIs using your Joseki package.

It would be possible, but Joseki is narrowly focused on API creation so I think it would make more sense to create provider directly with HTTP.

1 Like

Do you think that it is

  • a) technically possible
  • b) probable

that Julia will solve 2 language problem and allow people to avoid javascript (maybe css too) in creating web apps?

(Btw joseki.jl seems nice! :slight_smile: Do you plan tesuji.jl in the future? :wink: )

Sorry for reviving this topic, but I have a similar task in mind. I want to build a server that will process images coming from a random number of smartphones(and storing them locally) and send back a response to each phone. The number crunching part takes ~2-3s.
I have 16 cores, so I imagine I could handle 15 requests in parallel.
I tried the(minimal) example below, but I cannot communicate with the server:

using Distributed
addprocs(1)
@everywhere using HTTP

@everywhere function runserver()
    HTTP.serve("0.0.0.0",8081,reuseaddr=true) do request::HTTP.Request
        raw = HTTP.payload(request)
        answer = process_stuff(raw)
        try
            return HTTP.Response(answer)
        catch e
            return HTTP.Response(404, "Error: $e")
        end
    end
end

@everywhere function process_stuff(raw)
    sleep(3)
    return "Done!"
end

@async runserver()
@spawnat 2 runserver()

Can someone post a minimal working example for handling multiple requests in parallel?

Thank you!

Yep, append the next line at the end of your sample:
Base.JLOptions().isinteractive == 0 && wait()

And it works :slight_smile: # the julia main process exited and killed the async child processes.

In case people need it, I finally managed to make it work:

  • the HTTP requests are handled by the CPU cores,
  • I can see it in the logs,
  • I can measure the improvement in how fast concurrent HTTP requests get answered.

A few notes first:

When I am talking about CPU cores I refer to what is displayed either

From what I see in Julia’s documentation a “process” is more or less the same thing as a “CPU core” (see Distributed.myid ). I use them indistinctively here.

The important functions/macros :

  • Sys.CPU_THREADS tells us how many CPU cores the machine has (same as in /proc/cpuinfo). We can use this number to know how many processes we can add.
  • Distributed.addprocs to add processes
  • @everywhere to load all packages and project code (including the web APIs!..that was my mistake) in all the processes
  • @spawnat to start the HTTP server on a given process
  • Distributed.myid() allows us to check on which CPU core a piece of code is being executed.

I use julia 1.3.1 but it probably also works with older version because I only use functions from the Distributed package (not Base.Threads).
I use Mux.serve but that would also work with HTTP.serve

This being said here is a simplfied version of my main file

using Pkg
Pkg.activate(".")
using Revise # NOTE: In order for the web APIs to take into account changes 
             #       you execute `@everywhere include("src/web-api-definition.jl")` (see below)
 

using Distributed
addprocs(Sys.CPU_THREADS - 1) # Add the number of CPU cores minus one (for the
                              #  current process)
@everywhere using Distributed # Add Distributed to all processes because we 
                              #   want to use Distributed.myid() in the processes

@everywhere using Lib1, Lib2, LibX # Add packages
@everywhere push!(LOAD_PATH, "/home/myuser/CODE/MyLib.jl/") # Add your packages

using Mux, HTTP

@everywhere include("src/web-api-definition.jl") # This file contains the definition of the variable
                                                 #   web_api used below (eg. `@app web_api = (...)`) see documentation 
                                                 #   of Mux. I executed 

# Start a HTTP server on every available process. All HTTP servers listen
#   on the same port, hence `reuseaddr = true`
for i in 1:nprocs()
    @spawnat i Mux.serve(web_api, Mux.localhost, 8082
                        ;reuseaddr = true)
  end

Now, if you want to check that the HTTP requests are handled by the different processes, you can use
@info "Running on process[$(Distributed.myid()]"

To check what improvement it brings to the caller, I used curl calls. Side note, at first it seemed to me that there was no difference between having 1 CPU core and 8 CPU cores handling the requests but that was because the julia function called by the API was misconceived: I was using sleep(numOfSeconds) in the function in order to simulate a function that takes some time to execute but sleep() actually let the process go take care of another request. In order to simulate a long running function you need to keep the process busy, for example:

function doSomething(numOfSeconds::Float64)

    @info "Running on process[$(Distributed.myid()]"

    startTime = Dates.now()

    diffRes = 0.0
    counter = 0
    while diffRes < numOfSeconds
        counter+=1
        diffRes =  Dates.now() - startTime
        diffRes = convert(Float64,diffRes / Millisecond(1000))
    end

    return string(counter)

end
1 Like