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 JuMP
import HiGHS
import HTTP
import 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 ret
endendpoint_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)
end
endwrap_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 needed
HTTP.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) @0x00007fa76575c970, 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.