Serving web apps
This tutorial was generated using Literate.jl. Download the source as a .jl file.
This tutorial demonstrates how to set up and serve JuMP models via a REST API using HTTP.jl and JSON.jl. The example service accepts an HTTP POST request with a JSON body, solves a parameterized mixed-integer program, and returns the solution as JSON.
Learning intentions:
- Structure a JuMP solver as a self-contained endpoint function that validates JSON input, builds and solves a model, and returns results as a dictionary
- Wrap the endpoint in an HTTP request handler that parses JSON, runs the solver in a separate thread, and returns an HTTP response
- Register the endpoint on an HTTP router, start the server, and send test requests to verify the full round-trip
Required packages
This tutorial uses the following packages:
using JuMPimport HiGHSimport HTTPimport JSONThe server side
The core components of our REST server are endpoints. These are functions which accept a Dict{String,Any} of input parameters, and return a Dict{String,Any} as output. The types are Dict{String,Any} because we're going to read these to and from JSON.
Here's a very simple endpoint: it accepts params as input, formulates and solves a trivial mixed-integer program, and then returns a dictionary with the result.
function endpoint_solve(params::Dict{String,Any}) if !haskey(params, "lower_bound") return Dict{String,Any}( "status" => "failure", "reason" => "missing lower_bound param", ) elseif !(params["lower_bound"] isa Real) return Dict{String,Any}( "status" => "failure", "reason" => "lower_bound is not a number", ) end model = Model(HiGHS.Optimizer) set_silent(model) @variable(model, x >= params["lower_bound"], Int) optimize!(model) ret = Dict{String,Any}( "status" => "okay", "terminaton_status" => termination_status(model), "primal_status" => primal_status(model), ) # Only include the `x` key if it has a value. if primal_status(model) == FEASIBLE_POINT ret["x"] = value(x) end return retendendpoint_solve (generic function with 1 method)When we call this, we get:
endpoint_solve(Dict{String,Any}("lower_bound" => 1.2))Dict{String, Any} with 4 entries:
"status" => "okay"
"x" => 2.0
"primal_status" => FEASIBLE_POINT
"terminaton_status" => OPTIMALendpoint_solve(Dict{String,Any}())Dict{String, Any} with 2 entries:
"status" => "failure"
"reason" => "missing lower_bound param"We now need to turn each endpoint into a function that accepts an HTTP.Request, parses the JSON input, runs the endpoint, converts the result to JSON, and returns an HTTP.Response. In addition, the computation is handled in a separate thread, and we catch any unhandled exceptions.
function wrap_endpoint(endpoint::Function) function serve_request(request::HTTP.Request)::HTTP.Response task = Threads.@spawn try input = request.body |> String |> JSON.parse ret = convert(Dict{String,Any}, input) |> endpoint |> JSON.json HTTP.Response(200, ret) catch err HTTP.Response(500, "internal error: $err") end return fetch(task) endendwrap_endpoint (generic function with 1 method)Finally, we need an HTTP server. There are a variety of ways you can do this in HTTP.jl. Here's one way:
router = HTTP.Router()# Register other routes as neededHTTP.register!(router, "/api/solve", wrap_endpoint(endpoint_solve))server = HTTP.serve!(router, HTTP.ip"127.0.0.1", 8080)HTTP.Servers.Server{HTTP.Servers.Listener{Nothing, Sockets.TCPServer}}(HTTP.Servers.Listener{Nothing, Sockets.TCPServer}(Sockets.InetAddr{Sockets.IPv4}(ip"127.0.0.1", 8080), "127.0.0.1", "8080", nothing, Sockets.TCPServer(RawFD(28) active)), nothing, Set{HTTP.Connections.Connection}(), Task (runnable, started) @0x00007f0fe706c790, ReentrantLock())The client side
Now that we have a server, we can send it requests via this function:
function send_request(data::Dict; endpoint::String = "solve") ret = HTTP.request( "POST", # This should match the URL and endpoint we defined for our server. "http://127.0.0.1:8080/api/$endpoint", ["Content-Type" => "application/json"], JSON.json(data), ) if ret.status != 200 # This could happen if there are time-outs, network errors, etc. return Dict( "status" => "failure", "code" => ret.status, "body" => String(ret.body), ) end return JSON.parse(String(ret.body))endsend_request (generic function with 1 method)Let's see what happens:
send_request(Dict("lower_bound" => 0))JSON.Object{String, Any} with 4 entries:
"primal_status" => "FEASIBLE_POINT"
"status" => "okay"
"terminaton_status" => "OPTIMAL"
"x" => 0.0send_request(Dict("lower_bound" => 1.2))JSON.Object{String, Any} with 4 entries:
"primal_status" => "FEASIBLE_POINT"
"status" => "okay"
"terminaton_status" => "OPTIMAL"
"x" => 2.0If we don't send a lower_bound, we get:
send_request(Dict("invalid_param" => 1.2))JSON.Object{String, Any} with 2 entries:
"reason" => "missing lower_bound param"
"status" => "failure"If we don't send a lower_bound that is a number, we get:
send_request(Dict("lower_bound" => "1.2"))JSON.Object{String, Any} with 2 entries:
"reason" => "lower_bound is not a number"
"status" => "failure"Finally, we can shutdown our HTTP server:
close(server)[ Info: Server on 127.0.0.1:8080 closingNext steps
For more complicated examples relating to HTTP servers, consult the HTTP.jl documentation.
To see how you can integrate this with a larger JuMP model, read Design patterns for larger models.