Документация Engee

Обслуживание веб-приложений

В этом руководстве демонстрируются настройка и обслуживание моделей 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.