%%%------------------------------------------------------------------- %%% @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 = <>, 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])).