Обслуживание веб-приложений
В этом руководстве демонстрируются настройка и обслуживание моделей JuMP через REST API.
В примере создаваемого приложения решается простая частично целочисленная программа, параметризованная нижней границей переменной. Для вызова службы пользователи отправляют HTTP-запрос POST с содержимым JSON, в котором указывается нижняя граница. Возвращаемое значение представляет собой решение частично целочисленной программы в формате JSON.
Для начала понадобятся JuMP и решатель:
using JuMP
import HiGHS
Кроме того, понадобятся пакеты HTTP.jl для сервера REST и JSON.jl для преобразования передаваемых данных.
import HTTP
import JSON
Сторона сервера
Основными компонентами сервера REST являются конечные точки. Это функции, которые принимают словарь Dict{String,Any}
входных параметров и возвращают Dict{String,Any}
в качестве выходных данных. Тип Dict{String,Any}
выбран по той причине, что данные будут считываться в формат JSON и из него.
Вот очень простая конечная точка: она принимает params
в качестве входных данных, формулирует и решает простейшую частично целочисленную программу, а затем возвращает словарь с результатом.
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),
)
# Ключ `x` включается только в том случае, если у него есть значение.
if primal_status(model) == FEASIBLE_POINT
ret["x"] = value(x)
end
return ret
end
endpoint_solve (generic function with 1 method)
При вызове этой функции мы получаем следующее:
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" => OPTIMAL
endpoint_solve(Dict{String,Any}())
Dict{String, Any} with 2 entries:
"status" => "failure"
"reason" => "missing lower_bound param"
Вторая функция должна принимать объект HTTP.Request
и возвращать объект HTTP.Response
.
function serve_solve(request::HTTP.Request)
data = JSON.parse(String(request.body))
solution = endpoint_solve(data)
return HTTP.Response(200, JSON.json(solution))
end
serve_solve (generic function with 1 method)
Наконец, нужен HTTP-сервер. В HTTP.jl его можно реализовать по-разному. Мы используем Sockets.listen
явным образом, чтобы можно было вручную контролировать выключение сервера.
function setup_server(host, port)
server = HTTP.Sockets.listen(host, port)
HTTP.serve!(host, port; server = server) do request
try
# Расширим сервер, добавив другие конечные точки.
if request.target == "/api/solve"
return serve_solve(request)
else
return HTTP.Response(404, "target $(request.target) not found")
end
catch err
# Регистрируем сведения об исключении на стороне сервера
@info "Unhandled exception: $err"
# Возвращаем ответ клиенту
return HTTP.Response(500, "internal error")
end
end
return server
end
setup_server (generic function with 1 method)
HTTP.jl обслуживает запросы не в отдельном потоке. Поэтому длительно выполняемое задание будет блокировать главный поток, не позволяя отправлять запросы нескольким пользователям одновременно. Обходное решение см. в проблеме HTTP.jl № 798 или посмотрите видеозапись Building Microservices and Applications in Julia с JuliaCon 2020. |
server = setup_server(HTTP.ip"127.0.0.1", 8080)
Sockets.TCPServer(RawFD(23) active)
Сторона клиента
Теперь, когда у нас есть сервер, мы можем отправлять ему запросы с помощью этой функции:
function send_request(data::Dict; endpoint::String = "solve")
ret = HTTP.request(
"POST",
# Значения должны соответствовать URL-адресу и конечной точке, определенным для сервера.
"http://127.0.0.1:8080/api/$endpoint",
["Content-Type" => "application/json"],
JSON.json(data),
)
if ret.status != 200
# Это может произойти в случае истечения времени ожидания, сетевых ошибок и т. д.
return Dict(
"status" => "failure",
"code" => ret.status,
"body" => String(ret.body),
)
end
return JSON.parse(String(ret.body))
end
send_request (generic function with 1 method)
Посмотрим, что произойдет:
send_request(Dict("lower_bound" => 0))
Dict{String, Any} with 4 entries:
"status" => "okay"
"x" => 0.0
"primal_status" => "FEASIBLE_POINT"
"terminaton_status" => "OPTIMAL"
send_request(Dict("lower_bound" => 1.2))
Dict{String, Any} with 4 entries:
"status" => "okay"
"x" => 2.0
"primal_status" => "FEASIBLE_POINT"
"terminaton_status" => "OPTIMAL"
Если не отправить lower_bound
, ответ будет следующим.
send_request(Dict("invalid_param" => 1.2))
Dict{String, Any} with 2 entries:
"status" => "failure"
"reason" => "missing lower_bound param"
Если отправить не числовое значение lower_bound
, ответ будет следующим.
send_request(Dict("lower_bound" => "1.2"))
Dict{String, Any} with 2 entries:
"status" => "failure"
"reason" => "lower_bound is not a number"
Наконец, можно завершить работу HTTP-сервера:
close(server)
[ Info: Server on 127.0.0.1:8080 closing
Дальнейшие действия
Более сложные примеры, связанные с HTTP-серверами, см. в документации по HTTP.jl.
Описание способа интеграции с более крупными моделями JuMP см. в разделе Design patterns for larger models.