ekfa/apps/efka/src/iot/efka_iot_deploy_stream.erl
2026-07-09 10:38:16 +08:00

120 lines
4.3 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @doc One container deploy feedback stream from iot.
%%% @end
%%%-------------------------------------------------------------------
-module(efka_iot_deploy_stream).
-export([start_stream/1]).
-export([run/1]).
-define(REQUEST_TIMEOUT, 10000).
-define(MAX_REQUEST_BYTES, 16 * 1024 * 1024).
-type stream_id() :: pos_integer().
-spec start_stream(StreamId :: stream_id()) -> {ok, {pid(), reference()}}.
start_stream(StreamId) when is_integer(StreamId), StreamId > 0 ->
{ok, spawn_monitor(?MODULE, run, [StreamId])}.
-spec run(StreamId :: stream_id()) -> ok.
run(StreamId) when is_integer(StreamId), StreamId > 0 ->
try run0(StreamId) of
ok ->
ok
catch
Class:Reason:Stack ->
logger:warning("[efka_iot_deploy_stream] stream_id: ~p crashed, class: ~p, reason: ~p, stack: ~p",
[StreamId, Class, Reason, Stack]),
send_error_and_close(StreamId, iolist_to_binary(io_lib:format("~p:~p", [Class, Reason]))),
ok
after
efka_iot_client:stream_done(StreamId)
end.
-spec run0(stream_id()) -> ok.
run0(StreamId) ->
efka_iot_client:send_stream(StreamId, opened),
case receive_request(StreamId, <<>>) of
{ok, Request} ->
handle_request(StreamId, Request);
{error, reset} ->
ok;
{error, Reason} ->
send_error_and_close(StreamId, reason_to_binary(Reason))
end.
-spec receive_request(stream_id(), binary()) -> {ok, binary()} | {error, term()}.
receive_request(StreamId, Acc) ->
receive
{stream, StreamId, {data, Data}} when is_binary(Data) ->
NAcc = <<Acc/binary, Data/binary>>,
case byte_size(NAcc) =< ?MAX_REQUEST_BYTES of
true ->
receive_request(StreamId, NAcc);
false ->
{error, request_too_large}
end;
{stream, StreamId, fin} ->
{ok, Acc};
{stream, StreamId, {reset, _Reason}} ->
{error, reset};
Info ->
logger:debug("[efka_iot_deploy_stream] stream_id: ~p ignore unknown info: ~p", [StreamId, Info]),
receive_request(StreamId, Acc)
after ?REQUEST_TIMEOUT ->
{error, request_timeout}
end.
-spec handle_request(stream_id(), binary()) -> ok.
handle_request(StreamId, RequestBin) ->
case decode_request(RequestBin) of
{ok, TaskId, Params} ->
deploy(StreamId, TaskId, Params);
{error, Reason} ->
send_error_and_close(StreamId, reason_to_binary(Reason))
end.
-spec decode_request(binary()) -> {ok, non_neg_integer(), map()} | {error, term()}.
decode_request(RequestBin) ->
try json:decode(RequestBin) of
#{<<"task_id">> := TaskId, <<"params">> := Params}
when is_integer(TaskId), TaskId >= 0, is_map(Params) ->
{ok, TaskId, Params};
_ ->
{error, invalid_deploy_request}
catch
Class:Reason ->
{error, {bad_json, Class, Reason}}
end.
-spec deploy(stream_id(), non_neg_integer(), map()) -> ok.
deploy(StreamId, TaskId, Params) ->
try
ContainerName = maps:get(<<"container_name">>, Params),
{ok, RootDir} = docker_helper:root_dir(),
{ok, ContainerDir} = docker_helper:ensure_container_dir(RootDir, ContainerName),
docker_deployer:deploy(TaskId, ContainerDir, Params, {stream, StreamId})
catch
Class:Reason ->
Error = iolist_to_binary(io_lib:format("deploy stream failed: ~p:~p", [Class, Reason])),
send_error_and_close(StreamId, Error)
end.
-spec send_error_and_close(stream_id(), binary()) -> ok.
send_error_and_close(StreamId, Reason) ->
ok = efka_iot_client:send_stream(StreamId, {data, iolist_to_binary(json:encode(#{
<<"type">> => <<"error">>,
<<"message">> => Reason
}))}),
ok = efka_iot_client:send_stream(StreamId, {data, iolist_to_binary(json:encode(#{
<<"type">> => <<"close">>,
<<"reason">> => <<"fail">>
}))}),
efka_iot_client:send_stream(StreamId, fin).
-spec reason_to_binary(term()) -> binary().
reason_to_binary(Reason) when is_binary(Reason) ->
Reason;
reason_to_binary(Reason) ->
unicode:characters_to_binary(io_lib:format("~p", [Reason])).