Compare commits
14 Commits
docker_mst
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 537dd71e75 | |||
| d2b30cc995 | |||
| d3e89eceea | |||
| 33952d176f | |||
| 27dff14451 | |||
| 4ed3a67f29 | |||
| 41dcccb7d7 | |||
| bbe35e3b87 | |||
| 29f5bfb17c | |||
| 037eb775b4 | |||
| d65755b890 | |||
| 3defc05815 | |||
| f96dd3fea7 | |||
| 25bb8b0514 |
1167
HTTP_API_README.md
Normal file
1167
HTTP_API_README.md
Normal file
File diff suppressed because it is too large
Load Diff
12
README.md
12
README.md
@ -1,12 +0,0 @@
|
||||
iot
|
||||
=====
|
||||
|
||||
An OTP application
|
||||
|
||||
## erlang client sdk
|
||||
https://github.com/emqx/emqtt
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
$ rebar3 compile
|
||||
@ -10,6 +10,7 @@
|
||||
|
||||
-record(http_endpoint, {
|
||||
url = <<>> :: binary(),
|
||||
token = <<>> :: binary(),
|
||||
pool_size = 10 :: integer()
|
||||
}).
|
||||
|
||||
@ -33,7 +34,7 @@
|
||||
-record(endpoint, {
|
||||
id :: integer(),
|
||||
%% 全局唯一,在路由规则中通过名称来指定
|
||||
name :: binary(),
|
||||
matcher :: binary(),
|
||||
%% 标题描述
|
||||
title = <<>> :: binary(),
|
||||
%% 配置项, 格式: #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}
|
||||
24
apps/endpoint/src/endpoint.app.src
Normal file
24
apps/endpoint/src/endpoint.app.src
Normal file
@ -0,0 +1,24 @@
|
||||
{application, endpoint,
|
||||
[{description, "Endpoint OTP application"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, [
|
||||
endpoint_sup,
|
||||
endpoint_adapter_sup,
|
||||
endpoint_subscription,
|
||||
endpoint_log
|
||||
]},
|
||||
{mod, {endpoint_app, []}},
|
||||
{applications, [
|
||||
emqtt,
|
||||
brod,
|
||||
hackney,
|
||||
gproc,
|
||||
crypto,
|
||||
kernel,
|
||||
stdlib
|
||||
]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
{licenses, ["Apache 2.0"]},
|
||||
{links, []}
|
||||
]}.
|
||||
359
apps/endpoint/src/endpoint.erl
Normal file
359
apps/endpoint/src/endpoint.erl
Normal file
@ -0,0 +1,359 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint).
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
-export([get_name/1, get_pid/1, forward/2, reload/2, clean_up/1]).
|
||||
-export([get_alias_pid/1, is_support/1, get_protocol/1]).
|
||||
-export([endpoint_record/1, parse_config/2]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(Endpoint :: #endpoint{}) -> {'ok', pid()} | 'ignore' | {'error', term()}.
|
||||
start_link(Endpoint = #endpoint{id = Id, config = #http_endpoint{}}) ->
|
||||
LocalName = get_name(Id),
|
||||
endpoint_http:start_link(LocalName, Endpoint);
|
||||
start_link(Endpoint = #endpoint{id = Id, config = #mqtt_endpoint{}}) ->
|
||||
LocalName = get_name(Id),
|
||||
endpoint_mqtt:start_link(LocalName, Endpoint);
|
||||
start_link(Endpoint = #endpoint{id = Id, config = #kafka_endpoint{}}) ->
|
||||
LocalName = get_name(Id),
|
||||
endpoint_kafka:start_link(LocalName, Endpoint).
|
||||
|
||||
-spec get_name(Id :: integer()) -> term().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
{endpoint, Id}.
|
||||
|
||||
-spec get_pid(Id :: integer()) -> undefined | pid().
|
||||
get_pid(Id) when is_integer(Id) ->
|
||||
gproc:whereis_name({n, l, get_name(Id)}).
|
||||
|
||||
-spec get_alias_name(Name :: binary()) -> term().
|
||||
get_alias_name(Name) when is_binary(Name) ->
|
||||
{endpoint_alias, Name}.
|
||||
|
||||
-spec get_alias_pid(Name :: binary()) -> undefined | pid().
|
||||
get_alias_pid(Name) when is_binary(Name) ->
|
||||
gproc:whereis_name({n, l, get_alias_name(Name)}).
|
||||
|
||||
-spec forward(Pid :: pid(), Metric :: binary()) -> ok.
|
||||
forward(Pid, Metric) when is_pid(Pid), is_binary(Metric) ->
|
||||
gen_server:cast(Pid, {forward, Metric}).
|
||||
|
||||
reload(Pid, NEndpoint = #endpoint{}) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {reload, NEndpoint}).
|
||||
|
||||
-spec clean_up(Pid :: pid()) -> ok.
|
||||
clean_up(Pid) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, cleanup).
|
||||
|
||||
-spec get_protocol(Endpoint :: #endpoint{}) -> atom().
|
||||
get_protocol(#endpoint{config = #http_endpoint{}}) ->
|
||||
http;
|
||||
get_protocol(#endpoint{config = #mqtt_endpoint{}}) ->
|
||||
mqtt;
|
||||
get_protocol(#endpoint{config = #kafka_endpoint{}}) ->
|
||||
kafka.
|
||||
|
||||
-spec is_support(Protocol :: atom()) -> boolean().
|
||||
is_support(Protocol) when is_atom(Protocol) ->
|
||||
{ok, Props} = application:get_env(endpoint, endpoints),
|
||||
SupportProtocols = proplists:get_value(support_protocols, Props, []),
|
||||
lists:member(Protocol, SupportProtocols).
|
||||
|
||||
-spec endpoint_record(EndpointInfo :: #{}) -> error | {ok, Endpoint :: #endpoint{}}.
|
||||
endpoint_record(#{<<"id">> := Id, <<"matcher">> := Matcher, <<"title">> := Title, <<"type">> := Type, <<"config">> := ConfigJson,
|
||||
<<"status">> := Status, <<"updated_at">> := UpdatedAt, <<"created_at">> := CreatedAt}) ->
|
||||
case parse_config(Type, ConfigJson) of
|
||||
{ok, Config} ->
|
||||
{ok, #endpoint {
|
||||
id = Id,
|
||||
matcher = Matcher,
|
||||
title = Title,
|
||||
config = Config,
|
||||
status = Status,
|
||||
updated_at = UpdatedAt,
|
||||
created_at = CreatedAt
|
||||
}};
|
||||
{error, _Reason} ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec parse_config(Protocol :: binary(), Config :: map()) -> {ok, #mqtt_endpoint{} | #kafka_endpoint{} | #http_endpoint{}} | {error, Errors :: [Error :: binary()]}.
|
||||
parse_config(<<"mqtt">>, #{<<"host">> := Host, <<"port">> := Port0, <<"client_id">> := ClientId, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}) ->
|
||||
{Port, PortErrors} = case parse_integer(Port0) of
|
||||
{ok, ParsedPort} ->
|
||||
{ParsedPort, []};
|
||||
error ->
|
||||
{undefined, [<<"port invalid">>]}
|
||||
end,
|
||||
|
||||
CheckTerms = case Port of
|
||||
undefined ->
|
||||
[{host, Host}, {username, Username}, {password, Password}, {topic, Topic}, {qos, Qos}];
|
||||
_ ->
|
||||
[{host, Host}, {port, Port}, {username, Username}, {password, Password}, {topic, Topic}, {qos, Qos}]
|
||||
end,
|
||||
Errors = PortErrors ++ lists:filtermap(fun(Term) ->
|
||||
case check_mqtt_argument(Term) of
|
||||
ok ->
|
||||
false;
|
||||
{error, Error} ->
|
||||
{true, Error}
|
||||
end
|
||||
end, CheckTerms),
|
||||
case Errors =:= [] of
|
||||
true ->
|
||||
{ok, #mqtt_endpoint{
|
||||
host = Host,
|
||||
port = Port,
|
||||
client_id = ClientId,
|
||||
username = Username,
|
||||
password = Password,
|
||||
topic = Topic,
|
||||
qos = Qos
|
||||
}};
|
||||
false ->
|
||||
{error, Errors}
|
||||
end;
|
||||
parse_config(<<"http">>, C = #{<<"url">> := Url, <<"pool_size">> := PoolSize}) ->
|
||||
Token = maps:get(<<"token">>, C, <<>>),
|
||||
Errors = lists:filtermap(fun(Term) ->
|
||||
case check_http_argument(Term) of
|
||||
ok ->
|
||||
false;
|
||||
{error, Error} ->
|
||||
{true, Error}
|
||||
end
|
||||
end, [{url, Url}, {token, Token}, {pool_size, PoolSize}]),
|
||||
case Errors =:= [] of
|
||||
true ->
|
||||
{ok, #http_endpoint{
|
||||
url = Url,
|
||||
token = Token,
|
||||
pool_size = PoolSize
|
||||
}};
|
||||
false ->
|
||||
{error, Errors}
|
||||
end;
|
||||
parse_config(<<"kafka">>, #{<<"sasl_config">> := #{<<"username">> := Username, <<"password">> := Password, <<"mechanism">> := Mechanism0}, <<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
|
||||
Errors = lists:filtermap(fun(Term) ->
|
||||
case check_kafka_argument(Term) of
|
||||
ok ->
|
||||
false;
|
||||
{error, Error} ->
|
||||
{true, Error}
|
||||
end
|
||||
end, [{username, Username}, {password, Password}, {topic, Topic}, {mechanism, Mechanism0}, {bootstrap_servers, BootstrapServers}]),
|
||||
case Errors =:= [] of
|
||||
true ->
|
||||
Mechanism = parse_kafka_mechanism(Mechanism0),
|
||||
{ok, #kafka_endpoint{
|
||||
sasl_config = {Mechanism, Username, Password},
|
||||
bootstrap_servers = parse_kafka_bootstrap_servers(BootstrapServers),
|
||||
topic = Topic
|
||||
}};
|
||||
false ->
|
||||
{error, Errors}
|
||||
end;
|
||||
parse_config(<<"kafka">>, #{<<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
|
||||
Errors = lists:filtermap(fun(Term) ->
|
||||
case check_kafka_argument(Term) of
|
||||
ok ->
|
||||
false;
|
||||
{error, Error} ->
|
||||
{true, Error}
|
||||
end
|
||||
end, [{topic, Topic}, {bootstrap_servers, BootstrapServers}]),
|
||||
case Errors =:= [] of
|
||||
true ->
|
||||
{ok, #kafka_endpoint{
|
||||
sasl_config = undefined,
|
||||
bootstrap_servers = parse_kafka_bootstrap_servers(BootstrapServers),
|
||||
topic = Topic
|
||||
}};
|
||||
false ->
|
||||
{error, Errors}
|
||||
end;
|
||||
parse_config(_, _) ->
|
||||
{error, invalid_config}.
|
||||
|
||||
-spec parse_kafka_bootstrap_servers(BootstrapServers :: [binary()]) -> Servers :: [{Host :: string(), Port :: integer()}].
|
||||
parse_kafka_bootstrap_servers(BootstrapServers) when is_list(BootstrapServers) ->
|
||||
lists:map(fun(S) ->
|
||||
{ok, Host, Port} = parse_kafka_bootstrap_server(S),
|
||||
{Host, Port}
|
||||
end, BootstrapServers).
|
||||
|
||||
-spec parse_kafka_mechanism(Mechanism0 :: binary()) -> atom().
|
||||
parse_kafka_mechanism(Mechanism0) when is_binary(Mechanism0) ->
|
||||
case Mechanism0 of
|
||||
<<"sha_256">> ->
|
||||
scram_sha_256;
|
||||
<<"sha_512">> ->
|
||||
scram_sha_512;
|
||||
<<"plain">> ->
|
||||
plain;
|
||||
_ ->
|
||||
plain
|
||||
end.
|
||||
|
||||
-spec check_kafka_argument(tuple()) -> ok | {error, Reason :: binary()}.
|
||||
check_kafka_argument({mechanism, Mechanism}) ->
|
||||
case lists:member(Mechanism, [<<"sha_256">>, <<"sha_512">>, <<"plain">>]) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"mechanism invalid">>}
|
||||
end;
|
||||
check_kafka_argument({username, Username}) ->
|
||||
case Username /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"username is empty">>}
|
||||
end;
|
||||
check_kafka_argument({password, Password}) ->
|
||||
case Password /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"password is empty">>}
|
||||
end;
|
||||
check_kafka_argument({topic, Topic}) ->
|
||||
case Topic /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"topic is empty">>}
|
||||
end;
|
||||
check_kafka_argument({bootstrap_servers, BootstrapServers}) ->
|
||||
case is_list(BootstrapServers) andalso length(BootstrapServers) > 0 of
|
||||
true ->
|
||||
InvalidServers = lists:filtermap(fun(S) ->
|
||||
case parse_kafka_bootstrap_server(S) of
|
||||
{ok, _Host, _Port} ->
|
||||
false;
|
||||
error ->
|
||||
{true, S}
|
||||
end
|
||||
end, BootstrapServers),
|
||||
|
||||
case InvalidServers =:= [] of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, iolist_to_binary([<<"bootstrap_servers: ">>, lists:join(<<",">>, InvalidServers), <<" format is error">>])}
|
||||
end;
|
||||
false ->
|
||||
{error, <<"bootstrap_servers is empty">>}
|
||||
end.
|
||||
|
||||
-spec check_http_argument(tuple()) -> ok | {error, Reason :: binary()}.
|
||||
check_http_argument({url, Url}) ->
|
||||
case is_binary(Url) andalso Url /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"url is empty">>}
|
||||
end;
|
||||
check_http_argument({token, Token}) ->
|
||||
case is_binary(Token) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"token invalid">>}
|
||||
end;
|
||||
check_http_argument({pool_size, PoolSize}) ->
|
||||
case is_integer(PoolSize) andalso PoolSize > 0 of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"pool_size invalid">>}
|
||||
end.
|
||||
|
||||
-spec check_mqtt_argument(tuple()) -> ok | {error, Reason :: binary()}.
|
||||
check_mqtt_argument({host, Host}) ->
|
||||
case Host /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"host is empty">>}
|
||||
end;
|
||||
check_mqtt_argument({port, Port}) ->
|
||||
case is_integer(Port) andalso Port > 0 of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"port invalid">>}
|
||||
end;
|
||||
check_mqtt_argument({username, Username}) ->
|
||||
case Username /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"username is empty">>}
|
||||
end;
|
||||
check_mqtt_argument({password, Password}) ->
|
||||
case Password /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"password is empty">>}
|
||||
end;
|
||||
check_mqtt_argument({topic, Topic}) ->
|
||||
case Topic /= <<>> of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"topic is empty">>}
|
||||
end;
|
||||
check_mqtt_argument({qos, Qos}) ->
|
||||
case is_integer(Qos) andalso lists:member(Qos, [0, 1, 2]) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"qos invalid">>}
|
||||
end.
|
||||
|
||||
-spec parse_integer(term()) -> {ok, integer()} | error.
|
||||
parse_integer(Value) when is_integer(Value) ->
|
||||
{ok, Value};
|
||||
parse_integer(Value) when is_binary(Value) ->
|
||||
try binary_to_integer(Value) of
|
||||
Int ->
|
||||
{ok, Int}
|
||||
catch
|
||||
_:_ ->
|
||||
error
|
||||
end;
|
||||
parse_integer(_) ->
|
||||
error.
|
||||
|
||||
-spec parse_kafka_bootstrap_server(term()) -> {ok, string(), integer()} | error.
|
||||
parse_kafka_bootstrap_server(Server) when is_binary(Server) ->
|
||||
case binary:split(Server, <<":">>) of
|
||||
[Host0, Port0] ->
|
||||
Host = binary_to_list(Host0),
|
||||
case parse_integer(Port0) of
|
||||
{ok, Port} when Host =/= [], Port > 0 ->
|
||||
{ok, Host, Port};
|
||||
_ ->
|
||||
error
|
||||
end;
|
||||
_ ->
|
||||
error
|
||||
end;
|
||||
parse_kafka_bootstrap_server(_) ->
|
||||
error.
|
||||
83
apps/endpoint/src/endpoint_adapter_sup.erl
Normal file
83
apps/endpoint/src/endpoint_adapter_sup.erl
Normal file
@ -0,0 +1,83 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc endpoint top level supervisor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(endpoint_adapter_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
-include("endpoint.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([ensured_endpoint_started/1, delete_endpoint/1]).
|
||||
-export([start_endpoints/1]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%% sup_flags() = #{strategy => strategy(), % optional
|
||||
%% intensity => non_neg_integer(), % optional
|
||||
%% period => pos_integer()} % optional
|
||||
%% child_spec() = #{id => child_id(), % mandatory
|
||||
%% start => mfargs(), % mandatory
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
{ok, {SupFlags, []}}.
|
||||
|
||||
-spec start_endpoints(Endpoints :: list()) -> ok.
|
||||
start_endpoints(Endpoints) when is_list(Endpoints) ->
|
||||
lists:foreach(fun(Endpoint) ->
|
||||
Spec = child_spec(Endpoint),
|
||||
{ok, _} = supervisor:start_child(?MODULE, Spec)
|
||||
end, Endpoints),
|
||||
ok.
|
||||
|
||||
-spec ensured_endpoint_started(Endpoint :: #endpoint{}) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensured_endpoint_started(Endpoint = #endpoint{}) ->
|
||||
case supervisor:start_child(?MODULE, child_spec(Endpoint)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
-spec delete_endpoint(Id :: integer()) -> ok | {error, Reason :: any()}.
|
||||
delete_endpoint(Id) when is_integer(Id) ->
|
||||
Name = endpoint:get_name(Id),
|
||||
case supervisor:terminate_child(?MODULE, Name) of
|
||||
ok ->
|
||||
delete_endpoint_child(Name);
|
||||
{error, not_found} ->
|
||||
delete_endpoint_child(Name);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
delete_endpoint_child(Name) ->
|
||||
case supervisor:delete_child(?MODULE, Name) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, not_found} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
child_spec(Endpoint = #endpoint{id = Id}) ->
|
||||
Name = endpoint:get_name(Id),
|
||||
#{id => Name,
|
||||
start => {endpoint, start_link, [Endpoint]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['endpoint']}.
|
||||
15
apps/endpoint/src/endpoint_app.erl
Normal file
15
apps/endpoint/src/endpoint_app.erl
Normal file
@ -0,0 +1,15 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc endpoint application entrypoint.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
endpoint_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
304
apps/endpoint/src/endpoint_buffer.erl
Normal file
304
apps/endpoint/src/endpoint_buffer.erl
Normal file
@ -0,0 +1,304 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_buffer).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
%% 最大重试次数,不包含首次发送
|
||||
-define(MAX_RETRY_TIMES, 3).
|
||||
%% 与 endpoint_outbox 的单条记录限制保持一致。
|
||||
-define(MAX_PAYLOAD_BYTES, 32 * 1024).
|
||||
|
||||
-export([new/2, append/2, append_only/2, trigger_next/1, trigger_n/1, handle_timeout/4, cleanup/1, recover_inflight/1, ack/2, stat/1, resize/2]).
|
||||
-export_type([buffer/0]).
|
||||
|
||||
-type flight_source() :: outbox | memory.
|
||||
-type timer_entry() :: {reference(), binary(), non_neg_integer(), flight_source()}.
|
||||
|
||||
-record(buffer, {
|
||||
endpoint :: #endpoint{},
|
||||
outbox :: endpoint_outbox:outbox(),
|
||||
%% 当前待 ack 的数据及其重试定时器 #{Id => {TimerRef, Payload, RetryTimes, Source}}
|
||||
timer_map = #{} :: #{integer() => timer_entry()},
|
||||
%% 窗口大小,允许最大的未确认消息数
|
||||
window_size = 10,
|
||||
%% 未确认的消息数
|
||||
flight_num = 0,
|
||||
%% 记录成功处理的消息数
|
||||
acc_num = 0
|
||||
}).
|
||||
|
||||
-type buffer() :: #buffer{}.
|
||||
|
||||
-spec new(Endpoint :: #endpoint{}, WindowSize :: integer()) -> Buffer :: #buffer{}.
|
||||
new(Endpoint = #endpoint{id = Id}, WindowSize) when is_integer(WindowSize), WindowSize > 0 ->
|
||||
%% 读取配置文件
|
||||
{ok, Endpoints} = application:get_env(endpoint, endpoints),
|
||||
RootDir = proplists:get_value(root_dir, Endpoints),
|
||||
OutboxDir = filename:join(RootDir, integer_to_list(Id)),
|
||||
|
||||
{ok, Outbox} = endpoint_outbox:open(OutboxDir, #{}),
|
||||
#buffer{outbox = Outbox, endpoint = Endpoint, window_size = WindowSize}.
|
||||
|
||||
-spec append(Payload :: binary(), Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
append(Payload, Buffer = #buffer{}) when is_binary(Payload) ->
|
||||
case validate_payload_size(Payload, Buffer) of
|
||||
ok ->
|
||||
%% Persist before dispatch so a VM crash does not lose in-flight data.
|
||||
case append_to_outbox(Payload, Buffer) of
|
||||
{ok, NBuffer} ->
|
||||
trigger_next(NBuffer);
|
||||
{dropped, NBuffer} ->
|
||||
trigger_next(NBuffer);
|
||||
{error, NBuffer} ->
|
||||
NBuffer
|
||||
end;
|
||||
error ->
|
||||
Buffer
|
||||
end.
|
||||
|
||||
-spec append_only(Payload :: binary(), Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
append_only(Payload, Buffer = #buffer{}) when is_binary(Payload) ->
|
||||
case validate_payload_size(Payload, Buffer) of
|
||||
ok ->
|
||||
case append_to_outbox(Payload, Buffer) of
|
||||
{ok, NBuffer} ->
|
||||
NBuffer;
|
||||
{dropped, NBuffer} ->
|
||||
NBuffer;
|
||||
{error, NBuffer} ->
|
||||
NBuffer
|
||||
end;
|
||||
error ->
|
||||
Buffer
|
||||
end.
|
||||
|
||||
-spec trigger_n(Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
trigger_n(Buffer = #buffer{window_size = WindowSize}) ->
|
||||
%% 最多允许window_size
|
||||
lists:foldl(fun(_, Buffer0) -> trigger_next(Buffer0) end, Buffer, lists:seq(1, WindowSize)).
|
||||
|
||||
%% 触发读取下一条数据
|
||||
-spec trigger_next(Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
trigger_next(Buffer = #buffer{outbox = Outbox, flight_num = FlightNum, window_size = WindowSize}) ->
|
||||
case FlightNum < WindowSize of
|
||||
false ->
|
||||
Buffer;
|
||||
true ->
|
||||
case endpoint_outbox:next(Outbox) of
|
||||
eof ->
|
||||
Buffer;
|
||||
{ok, Id, Payload, NOutbox} ->
|
||||
ReceiverPid = self(),
|
||||
ReceiverPid ! {next_data, Id, Payload},
|
||||
schedule_retry(Id, Payload, 0, outbox, Buffer#buffer{outbox = NOutbox, flight_num = FlightNum + 1});
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_buffer] read next outbox failed, endpoint_id: ~p, reason: ~p",
|
||||
[buffer_endpoint_id(Buffer), Reason]),
|
||||
Buffer
|
||||
end
|
||||
end.
|
||||
|
||||
-spec handle_timeout(reference(), integer(), binary(), #buffer{}) -> #buffer{}.
|
||||
handle_timeout(TimerRef, Id, _Payload, Buffer = #buffer{timer_map = TimerMap})
|
||||
when is_reference(TimerRef), is_integer(Id) ->
|
||||
case maps:take(Id, TimerMap) of
|
||||
{{TimerRef, Payload, RetryTimes, Source}, NTimerMap} when RetryTimes < ?MAX_RETRY_TIMES ->
|
||||
logger:warning("[endpoint_buffer] retry message, endpoint_id: ~p, id: ~p, retry: ~p/~p",
|
||||
[buffer_endpoint_id(Buffer), Id, RetryTimes + 1, ?MAX_RETRY_TIMES]),
|
||||
ReceiverPid = self(),
|
||||
ReceiverPid ! {next_data, Id, Payload},
|
||||
schedule_retry(Id, Payload, RetryTimes + 1, Source, Buffer#buffer{timer_map = NTimerMap});
|
||||
{{TimerRef, Payload, RetryTimes, outbox}, NTimerMap} ->
|
||||
logger:warning("[endpoint_buffer] drop message after retries exhausted, endpoint_id: ~p, id: ~p, retries: ~p",
|
||||
[buffer_endpoint_id(Buffer), Id, RetryTimes]),
|
||||
drop_outbox_message(Id, Payload, RetryTimes, Buffer#buffer{timer_map = NTimerMap});
|
||||
{{TimerRef, _, RetryTimes, memory}, NTimerMap} ->
|
||||
logger:warning("[endpoint_buffer] drop memory message after retries exhausted, endpoint_id: ~p, id: ~p, retries: ~p",
|
||||
[buffer_endpoint_id(Buffer), Id, RetryTimes]),
|
||||
drop_memory_message(Buffer#buffer{timer_map = NTimerMap});
|
||||
{{_OtherTimerRef, _OtherPayload, _RetryTimes, _Source}, _NTimerMap} ->
|
||||
Buffer;
|
||||
error ->
|
||||
Buffer
|
||||
end.
|
||||
|
||||
-spec ack(Id :: integer(), Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
ack(Id, Buffer = #buffer{timer_map = TimerMap, outbox = Outbox, acc_num = AccNum, flight_num = FlightNum}) when is_integer(Id) ->
|
||||
case maps:take(Id, TimerMap) of
|
||||
{{TimerRef, _Payload, _RetryTimes, memory}, NTimerMap} ->
|
||||
_ = erlang:cancel_timer(TimerRef),
|
||||
NBuffer = Buffer#buffer{
|
||||
timer_map = NTimerMap,
|
||||
acc_num = AccNum + 1,
|
||||
flight_num = max(FlightNum - 1, 0)
|
||||
},
|
||||
trigger_next(NBuffer);
|
||||
{{TimerRef, Payload, RetryTimes, outbox}, NTimerMap} ->
|
||||
_ = erlang:cancel_timer(TimerRef),
|
||||
case endpoint_outbox:ack(Id, Outbox) of
|
||||
{ok, NOutbox} ->
|
||||
NBuffer = Buffer#buffer{
|
||||
timer_map = NTimerMap,
|
||||
outbox = NOutbox,
|
||||
acc_num = AccNum + 1,
|
||||
flight_num = max(FlightNum - 1, 0)
|
||||
},
|
||||
trigger_next(NBuffer);
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_buffer] ack outbox failed, endpoint_id: ~p, id: ~p, reason: ~p",
|
||||
[buffer_endpoint_id(Buffer), Id, Reason]),
|
||||
schedule_retry(Id, Payload, RetryTimes, outbox, Buffer#buffer{timer_map = NTimerMap})
|
||||
end;
|
||||
error ->
|
||||
Buffer
|
||||
end.
|
||||
|
||||
%% 获取当前统计信息
|
||||
-spec stat(Buffer :: #buffer{}) -> map().
|
||||
stat(#buffer{acc_num = AccNum, outbox = Outbox, flight_num = FlightNum, timer_map = TimerMap}) ->
|
||||
OutboxStat = endpoint_outbox:stat(Outbox),
|
||||
WriteSeq = maps:get(write_seq, OutboxStat, 0),
|
||||
AckedSeq = maps:get(acked_seq, OutboxStat, 0),
|
||||
OutboxFlightNum = count_inflight(outbox, TimerMap),
|
||||
MemoryFlightNum = count_inflight(memory, TimerMap),
|
||||
QueueNum = max(WriteSeq - AckedSeq - OutboxFlightNum, 0),
|
||||
OutboxStat#{
|
||||
<<"acc_num">> => AccNum,
|
||||
<<"queue_num">> => QueueNum,
|
||||
<<"inflight_num">> => FlightNum,
|
||||
<<"outbox_inflight_num">> => OutboxFlightNum,
|
||||
<<"memory_inflight_num">> => MemoryFlightNum
|
||||
}.
|
||||
|
||||
-spec cleanup(Buffer :: #buffer{}) -> #buffer{}.
|
||||
cleanup(Buffer = #buffer{timer_map = TimerMap}) ->
|
||||
cancel_timers(TimerMap),
|
||||
NBuffer0 = persist_memory_inflight(Buffer),
|
||||
reset_reader_after_recover(NBuffer0#buffer{timer_map = #{}, flight_num = 0}).
|
||||
|
||||
-spec recover_inflight(Buffer :: #buffer{}) -> #buffer{}.
|
||||
recover_inflight(Buffer = #buffer{timer_map = TimerMap}) ->
|
||||
cancel_timers(TimerMap),
|
||||
NBuffer0 = persist_memory_inflight(Buffer),
|
||||
case endpoint_outbox:reset_reader(NBuffer0#buffer.outbox) of
|
||||
{ok, NOutbox} ->
|
||||
NBuffer0#buffer{outbox = NOutbox, timer_map = #{}, flight_num = 0}
|
||||
end.
|
||||
|
||||
-spec resize(Buffer :: #buffer{}, WindowSize :: integer()) -> #buffer{}.
|
||||
resize(Buffer = #buffer{}, WindowSize) when is_integer(WindowSize), WindowSize > 0 ->
|
||||
trigger_n(Buffer#buffer{window_size = WindowSize}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec buffer_endpoint_id(buffer()) -> integer().
|
||||
buffer_endpoint_id(#buffer{endpoint = #endpoint{id = Id}}) ->
|
||||
Id.
|
||||
|
||||
-spec validate_payload_size(binary(), buffer()) -> ok | error.
|
||||
validate_payload_size(Payload, Buffer) ->
|
||||
case byte_size(Payload) =< ?MAX_PAYLOAD_BYTES of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
logger:warning("[endpoint_buffer] payload too large, endpoint_id: ~p, size: ~p, max_size: ~p",
|
||||
[buffer_endpoint_id(Buffer), byte_size(Payload), ?MAX_PAYLOAD_BYTES]),
|
||||
error
|
||||
end.
|
||||
|
||||
-spec append_to_outbox(binary(), buffer()) -> {ok | dropped | error, buffer()}.
|
||||
append_to_outbox(Payload, Buffer = #buffer{outbox = Outbox}) ->
|
||||
case endpoint_outbox:append(Payload, Outbox) of
|
||||
{ok, _Seq, NOutbox} ->
|
||||
{ok, Buffer#buffer{outbox = NOutbox}};
|
||||
{dropped, capacity_reached, NOutbox} ->
|
||||
logger:warning("[endpoint_buffer] outbox capacity reached, endpoint_id: ~p", [buffer_endpoint_id(Buffer)]),
|
||||
{dropped, Buffer#buffer{outbox = NOutbox}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_buffer] append outbox failed, endpoint_id: ~p, reason: ~p",
|
||||
[buffer_endpoint_id(Buffer), Reason]),
|
||||
{error, Buffer}
|
||||
end.
|
||||
|
||||
-spec schedule_retry(integer(), binary(), non_neg_integer(), flight_source(), buffer()) -> buffer().
|
||||
schedule_retry(Id, Payload, RetryTimes, Source, Buffer = #buffer{timer_map = TimerMap})
|
||||
when is_integer(Id), is_binary(Payload), is_integer(RetryTimes), RetryTimes >= 0 ->
|
||||
TimerRef = erlang:start_timer(?RETRY_INTERVAL, self(), {endpoint_buffer_retry, Id, Payload}),
|
||||
Buffer#buffer{timer_map = maps:put(Id, {TimerRef, Payload, RetryTimes, Source}, TimerMap)}.
|
||||
|
||||
-spec cancel_timers(#{integer() => timer_entry()}) -> ok.
|
||||
cancel_timers(TimerMap) ->
|
||||
lists:foreach(fun({_Id, {TimerRef, _Payload, _RetryTimes, _Source}}) ->
|
||||
_ = erlang:cancel_timer(TimerRef),
|
||||
ok
|
||||
end, maps:to_list(TimerMap)),
|
||||
ok.
|
||||
|
||||
-spec drop_outbox_message(integer(), binary(), non_neg_integer(), buffer()) -> buffer().
|
||||
drop_outbox_message(Id, Payload, RetryTimes, Buffer = #buffer{outbox = Outbox, acc_num = AccNum, flight_num = FlightNum}) ->
|
||||
case endpoint_outbox:ack(Id, Outbox) of
|
||||
{ok, NOutbox} ->
|
||||
trigger_next(Buffer#buffer{
|
||||
outbox = NOutbox,
|
||||
acc_num = AccNum + 1,
|
||||
flight_num = max(FlightNum - 1, 0)
|
||||
});
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_buffer] ack dropped message failed, endpoint_id: ~p, id: ~p, reason: ~p",
|
||||
[buffer_endpoint_id(Buffer), Id, Reason]),
|
||||
schedule_retry(Id, Payload, RetryTimes, outbox, Buffer)
|
||||
end.
|
||||
|
||||
-spec drop_memory_message(buffer()) -> buffer().
|
||||
drop_memory_message(Buffer = #buffer{acc_num = AccNum, flight_num = FlightNum}) ->
|
||||
trigger_next(Buffer#buffer{
|
||||
acc_num = AccNum + 1,
|
||||
flight_num = max(FlightNum - 1, 0)
|
||||
}).
|
||||
|
||||
-spec persist_memory_inflight(buffer()) -> buffer().
|
||||
persist_memory_inflight(Buffer = #buffer{timer_map = TimerMap}) ->
|
||||
MemoryInflight = lists:sort(
|
||||
fun({IdA, _PayloadA}, {IdB, _PayloadB}) -> IdA > IdB end,
|
||||
[{Id, Payload} || {Id, {_TimerRef, Payload, _RetryTimes, memory}} <- maps:to_list(TimerMap)]
|
||||
),
|
||||
lists:foldl(fun({_Id, Payload}, AccBuffer) ->
|
||||
case append_to_outbox(Payload, AccBuffer) of
|
||||
{ok, NBuffer} ->
|
||||
NBuffer;
|
||||
{dropped, NBuffer} ->
|
||||
NBuffer;
|
||||
{error, NBuffer} ->
|
||||
NBuffer
|
||||
end
|
||||
end, Buffer, MemoryInflight).
|
||||
|
||||
-spec reset_reader_after_recover(buffer()) -> buffer().
|
||||
reset_reader_after_recover(Buffer = #buffer{outbox = Outbox}) ->
|
||||
case endpoint_outbox:reset_reader(Outbox) of
|
||||
{ok, NOutbox} ->
|
||||
Buffer#buffer{outbox = NOutbox}
|
||||
end.
|
||||
|
||||
-spec count_inflight(flight_source(), #{integer() => timer_entry()}) -> non_neg_integer().
|
||||
count_inflight(Source, TimerMap) ->
|
||||
maps:fold(fun(_Id, {_TimerRef, _Payload, _RetryTimes, EntrySource}, Acc) ->
|
||||
case EntrySource =:= Source of
|
||||
true ->
|
||||
Acc + 1;
|
||||
false ->
|
||||
Acc
|
||||
end
|
||||
end, 0, TimerMap).
|
||||
@ -13,7 +13,7 @@
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/3]).
|
||||
-export([start_link/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
@ -30,10 +30,10 @@
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(LocalName :: atom(), AliasName :: atom(), Endpoint :: #endpoint{}) ->
|
||||
-spec(start_link(LocalName :: term(), Endpoint :: #endpoint{}) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(LocalName, AliasName, Endpoint = #endpoint{config = #http_endpoint{}}) when is_atom(LocalName), is_atom(AliasName) ->
|
||||
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
|
||||
start_link(LocalName, Endpoint = #endpoint{config = #http_endpoint{}}) ->
|
||||
gen_server:start_link({via, gproc, {n, l, LocalName}}, ?MODULE, [Endpoint], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
@ -44,9 +44,10 @@ start_link(LocalName, AliasName, Endpoint = #endpoint{config = #http_endpoint{}}
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([AliasName, Endpoint]) ->
|
||||
Buffer = endpoint_buffer:new(Endpoint, 10),
|
||||
true = gproc:reg({n, l, AliasName}),
|
||||
init([Endpoint = #endpoint{matcher = Matcher, config = #http_endpoint{pool_size = PoolSize}}]) ->
|
||||
ok = endpoint_util:set_metadata(),
|
||||
endpoint_subscription:subscribe(Matcher, self()),
|
||||
Buffer = endpoint_buffer:new(Endpoint, PoolSize),
|
||||
{ok, #state{endpoint = Endpoint, buffer = Buffer}}.
|
||||
|
||||
%% @private
|
||||
@ -69,13 +70,17 @@ handle_call(get_stat, _From, State = #state{buffer = Buffer}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({forward, ServiceId, Metric}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:append({ServiceId, Metric}, Buffer),
|
||||
handle_cast({forward, Metric}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:append(Metric, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}};
|
||||
handle_cast({reload, NEndpoint = #endpoint{matcher = NMatcher, config = #http_endpoint{pool_size = PoolSize}}}, State = #state{endpoint = #endpoint{matcher = Matcher}, buffer = Buffer}) ->
|
||||
ensure_subscription(Matcher, NMatcher),
|
||||
NBuffer = endpoint_buffer:resize(Buffer, PoolSize),
|
||||
{noreply, State#state{endpoint = NEndpoint, buffer = NBuffer}};
|
||||
|
||||
handle_cast(cleanup, State = #state{buffer = Buffer}) ->
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
{noreply, State}.
|
||||
NBuffer = endpoint_buffer:cleanup(Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
@ -83,28 +88,25 @@ handle_cast(cleanup, State = #state{buffer = Buffer}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({next_data, Id, {ServiceId, Metric}}, State = #state{buffer = Buffer, endpoint = #endpoint{config = #http_endpoint{url = Url}}}) ->
|
||||
Headers = [
|
||||
{<<"Content-Type">>, <<"application/octet-stream">>},
|
||||
{<<"Service-Id">>, ServiceId}
|
||||
],
|
||||
handle_info({next_data, Id, Metric}, State = #state{buffer = Buffer, endpoint = #endpoint{config = #http_endpoint{url = Url, token = Token}}}) ->
|
||||
Headers = [{<<"Content-Type">>, <<"application/json">>}] ++ patch_headers(Metric, Token),
|
||||
case hackney:request(post, Url, Headers, Metric) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
lager:debug("[endpoint_http] url: ~p, response is: ~p", [Url, RespBody]),
|
||||
{ok, HttpCode, _, ClientRef} when HttpCode >= 200, HttpCode < 300 ->
|
||||
RespBody = read_response_body(ClientRef),
|
||||
logger:debug("[endpoint_http] url: ~p, response is: ~p", [Url, RespBody]),
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
|
||||
{noreply, State#state{buffer = NBuffer}};
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
lager:debug("[endpoint_http] url: ~p, http_code: ~p, response is: ~p", [Url, HttpCode, RespBody]),
|
||||
RespBody = read_response_body(ClientRef),
|
||||
logger:warning("[endpoint_http] url: ~p, http_code: ~p, response is: ~p", [Url, HttpCode, RespBody]),
|
||||
{noreply, State};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_http] url: ~p, get error: ~p", [Url, Reason]),
|
||||
logger:warning("[endpoint_http] url: ~p, get error: ~p", [Url, Reason]),
|
||||
{noreply, State}
|
||||
end.
|
||||
end;
|
||||
handle_info({timeout, TimerRef, {endpoint_buffer_retry, Id, Payload}}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:handle_timeout(TimerRef, Id, Payload, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
@ -113,7 +115,8 @@ handle_info({next_data, Id, {ServiceId, Metric}}, State = #state{buffer = Buffer
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
terminate(_Reason, #state{buffer = Buffer}) ->
|
||||
_ = endpoint_buffer:cleanup(Buffer),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
@ -127,3 +130,28 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec ensure_subscription(binary(), binary()) -> ok.
|
||||
ensure_subscription(Matcher, Matcher) ->
|
||||
ok;
|
||||
ensure_subscription(Matcher, NMatcher) ->
|
||||
ok = endpoint_subscription:unsubscribe(Matcher, self()),
|
||||
endpoint_subscription:subscribe(NMatcher, self()).
|
||||
|
||||
-spec read_response_body(reference()) -> binary() | term().
|
||||
read_response_body(ClientRef) ->
|
||||
case hackney:body(ClientRef) of
|
||||
{ok, RespBody} ->
|
||||
hackney:close(ClientRef),
|
||||
RespBody;
|
||||
{error, Reason} ->
|
||||
hackney:close(ClientRef),
|
||||
Reason
|
||||
end.
|
||||
|
||||
-spec patch_headers(Metric :: binary(), any()) -> list().
|
||||
patch_headers(Metric, Token) when is_binary(Token), Token /= <<>> ->
|
||||
Sign = endpoint_util:sha256(erlang:iolist_to_binary([Token, Metric, Token])),
|
||||
[{<<"X-Signature">>, Sign}];
|
||||
patch_headers(_, _) ->
|
||||
[].
|
||||
261
apps/endpoint/src/endpoint_kafka.erl
Normal file
261
apps/endpoint/src/endpoint_kafka.erl
Normal file
@ -0,0 +1,261 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_kafka).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([callback_mode/0, init/1, terminate/3, code_change/4]).
|
||||
-export([disconnected/3, connected/3]).
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
|
||||
-record(state, {
|
||||
endpoint :: #endpoint{},
|
||||
buffer :: endpoint_buffer:buffer(),
|
||||
client_id :: atom(),
|
||||
client_pid :: undefined | pid()
|
||||
}).
|
||||
|
||||
-type kafka_state() :: disconnected | connected.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(LocalName :: term(), Endpoint :: #endpoint{}) ->
|
||||
{ok, pid()} | ignore | {error, term()}.
|
||||
start_link(LocalName, Endpoint = #endpoint{}) ->
|
||||
gen_statem:start_link({via, gproc, {n, l, LocalName}}, ?MODULE, [Endpoint], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec callback_mode() -> state_functions.
|
||||
callback_mode() ->
|
||||
state_functions.
|
||||
|
||||
-spec init(term()) -> gen_statem:init_result(kafka_state(), #state{}).
|
||||
init([Endpoint = #endpoint{id = Id, matcher = Matcher}]) ->
|
||||
ok = endpoint_util:set_metadata(),
|
||||
erlang:process_flag(trap_exit, true),
|
||||
ok = endpoint_subscription:subscribe(Matcher, self()),
|
||||
Buffer = endpoint_buffer:new(Endpoint, 10),
|
||||
ClientId = list_to_atom("brod_client:" ++ integer_to_list(Id)),
|
||||
{ok, disconnected, #state{endpoint = Endpoint, buffer = Buffer, client_id = ClientId},
|
||||
[{state_timeout, 0, connect}]}.
|
||||
|
||||
-spec disconnected(gen_statem:event_type(), term(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
disconnected({call, From}, get_stat, State = #state{buffer = Buffer}) ->
|
||||
reply_stat(From, Buffer, State);
|
||||
disconnected(cast, {forward, Metric}, State = #state{buffer = Buffer}) ->
|
||||
store_metric(Metric, Buffer, State);
|
||||
disconnected(cast, cleanup, State = #state{buffer = Buffer}) ->
|
||||
cleanup_buffer(Buffer, State);
|
||||
disconnected(cast, {reload, NEndpoint = #endpoint{matcher = NMatcher}},
|
||||
State = #state{endpoint = #endpoint{matcher = Matcher}, client_id = ClientId}) ->
|
||||
reload_endpoint(Matcher, NMatcher, ClientId, NEndpoint, State);
|
||||
disconnected(state_timeout, connect, State) ->
|
||||
try_connect(State);
|
||||
disconnected(info, {timeout, TimerRef, {endpoint_buffer_retry, Id, Payload}}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:handle_timeout(TimerRef, Id, Payload, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}};
|
||||
disconnected(info, {next_data, _Id, _Tuple}, State) ->
|
||||
{keep_state, State};
|
||||
disconnected(info, {ack, Id}, State = #state{buffer = Buffer}) ->
|
||||
ack_buffer(Id, Buffer, State);
|
||||
disconnected(info, Info, State) ->
|
||||
unknown_info(Info, disconnected, State);
|
||||
disconnected(EventType, EventContent, State) ->
|
||||
unknown_event(EventType, EventContent, disconnected, State).
|
||||
|
||||
-spec connected(gen_statem:event_type(), term(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
connected({call, From}, get_stat, State = #state{buffer = Buffer}) ->
|
||||
reply_stat(From, Buffer, State);
|
||||
connected(cast, {forward, Metric}, State = #state{buffer = Buffer}) ->
|
||||
forward_metric(Metric, Buffer, State);
|
||||
connected(cast, cleanup, State = #state{buffer = Buffer}) ->
|
||||
cleanup_buffer(Buffer, State);
|
||||
connected(cast, {reload, NEndpoint = #endpoint{matcher = NMatcher}},
|
||||
State = #state{endpoint = #endpoint{matcher = Matcher}, client_id = ClientId}) ->
|
||||
reload_endpoint(Matcher, NMatcher, ClientId, NEndpoint, State);
|
||||
connected(info, {next_data, Id, Metric},
|
||||
State = #state{
|
||||
buffer = Buffer,
|
||||
client_pid = ClientPid,
|
||||
client_id = ClientId,
|
||||
endpoint = #endpoint{config = #kafka_endpoint{topic = Topic}}
|
||||
}) ->
|
||||
ReceiverPid = self(),
|
||||
AckCb = fun(Partition, BaseOffset) ->
|
||||
logger:debug("[endpoint_kafka] ack partion: ~p, offset: ~p", [Partition, BaseOffset]),
|
||||
ReceiverPid ! {ack, Id}
|
||||
end,
|
||||
case catch brod:produce_cb(ClientPid, Topic, random, <<>>, Metric, AckCb) of
|
||||
{ok, _CallRef} ->
|
||||
{keep_state, State};
|
||||
{ok, _CallRef, _ProducerPid} ->
|
||||
{keep_state, State};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_kafka] produce topic: ~p, get error: ~p", [Topic, Reason]),
|
||||
stop_kafka_client(ClientId),
|
||||
NBuffer = endpoint_buffer:recover_inflight(Buffer),
|
||||
{next_state, disconnected, State#state{client_pid = undefined, buffer = NBuffer},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]};
|
||||
{'EXIT', Reason} ->
|
||||
logger:warning("[endpoint_kafka] produce topic: ~p, exit with reason: ~p", [Topic, Reason]),
|
||||
stop_kafka_client(ClientId),
|
||||
NBuffer = endpoint_buffer:recover_inflight(Buffer),
|
||||
{next_state, disconnected, State#state{client_pid = undefined, buffer = NBuffer},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]}
|
||||
end;
|
||||
connected(info, {timeout, TimerRef, {endpoint_buffer_retry, Id, Payload}}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:handle_timeout(TimerRef, Id, Payload, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}};
|
||||
connected(info, {ack, Id}, State = #state{buffer = Buffer}) ->
|
||||
ack_buffer(Id, Buffer, State);
|
||||
connected(info, {'EXIT', ClientPid, Reason},
|
||||
State = #state{client_pid = ClientPid, endpoint = #endpoint{title = Title}, buffer = Buffer}) ->
|
||||
logger:warning("[endpoint_kafka] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
|
||||
NBuffer = endpoint_buffer:recover_inflight(Buffer),
|
||||
{next_state, disconnected, State#state{client_pid = undefined, buffer = NBuffer},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]};
|
||||
connected(info, Info, State) ->
|
||||
unknown_info(Info, connected, State);
|
||||
connected(EventType, EventContent, State) ->
|
||||
unknown_event(EventType, EventContent, connected, State).
|
||||
|
||||
-spec terminate(term(), kafka_state(), #state{}) -> term().
|
||||
terminate(Reason, _StateName, #state{endpoint = #endpoint{title = Title}, buffer = Buffer, client_id = ClientId}) ->
|
||||
logger:debug("[endpoint_kafka] endpoint: ~p, terminate with reason: ~p", [Title, Reason]),
|
||||
stop_kafka_client(ClientId),
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
ok.
|
||||
|
||||
-spec code_change(term() | {down, term()}, kafka_state(), #state{}, term()) ->
|
||||
{ok, kafka_state(), #state{}} | {error, term()}.
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec reply_stat(gen_statem:from(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
reply_stat(From, Buffer, State) ->
|
||||
Stat = endpoint_buffer:stat(Buffer),
|
||||
{keep_state, State, [{reply, From, {ok, Stat}}]}.
|
||||
|
||||
-spec forward_metric(binary(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
forward_metric(Metric, Buffer, State) ->
|
||||
NBuffer = endpoint_buffer:append(Metric, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec store_metric(binary(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
store_metric(Metric, Buffer, State) ->
|
||||
NBuffer = endpoint_buffer:append_only(Metric, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec cleanup_buffer(endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
cleanup_buffer(Buffer, _State) ->
|
||||
NBuffer = endpoint_buffer:cleanup(Buffer),
|
||||
{keep_state, _State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec ack_buffer(integer(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
ack_buffer(Id, Buffer, State) ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec reload_endpoint(binary(), binary(), atom(), #endpoint{}, #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
reload_endpoint(Matcher, NMatcher, ClientId, NEndpoint, State = #state{}) ->
|
||||
ensure_subscription(Matcher, NMatcher),
|
||||
stop_kafka_client(ClientId),
|
||||
NBuffer = endpoint_buffer:recover_inflight(State#state.buffer),
|
||||
{next_state, disconnected, State#state{endpoint = NEndpoint, client_pid = undefined, buffer = NBuffer},
|
||||
[{state_timeout, 0, connect}]}.
|
||||
|
||||
-spec try_connect(#state{}) -> gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
try_connect(State = #state{
|
||||
buffer = Buffer,
|
||||
client_id = ClientId,
|
||||
endpoint = #endpoint{
|
||||
title = Title,
|
||||
config = #kafka_endpoint{
|
||||
sasl_config = SaslConfig,
|
||||
bootstrap_servers = BootstrapServers,
|
||||
topic = Topic
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
logger:debug("[endpoint_kafka] endpoint: ~p, create postman", [Title]),
|
||||
BaseConfig = [
|
||||
{reconnect_cool_down_seconds, 5},
|
||||
{socket_options, [{keepalive, true}]}
|
||||
],
|
||||
ClientConfig = case SaslConfig of
|
||||
{Mechanism, Username, Password} ->
|
||||
[{sasl, {Mechanism, Username, Password}} | BaseConfig];
|
||||
undefined ->
|
||||
BaseConfig
|
||||
end,
|
||||
case catch brod:start_link_client(BootstrapServers, ClientId, ClientConfig) of
|
||||
{ok, ClientPid} ->
|
||||
case brod:start_producer(ClientId, Topic, []) of
|
||||
ok ->
|
||||
NBuffer = endpoint_buffer:trigger_n(Buffer),
|
||||
{next_state, connected, State#state{buffer = NBuffer, client_pid = ClientPid}};
|
||||
{error, Reason} ->
|
||||
logger:debug("[endpoint_kafka] start_producer: ~p, get error: ~p", [ClientId, Reason]),
|
||||
stop_kafka_client(ClientId),
|
||||
{keep_state, State#state{client_pid = undefined},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]}
|
||||
end;
|
||||
Error ->
|
||||
logger:debug("[endpoint_kafka] start_client: ~p, get error: ~p", [ClientId, Error]),
|
||||
{keep_state, State#state{client_pid = undefined},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]}
|
||||
end.
|
||||
|
||||
-spec unknown_info(term(), kafka_state(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
unknown_info(Info, StateName, State) ->
|
||||
logger:warning("[endpoint_kafka] unknown message: ~p, status: ~p", [Info, StateName]),
|
||||
{keep_state, State}.
|
||||
|
||||
-spec unknown_event(gen_statem:event_type(), term(), kafka_state(), #state{}) ->
|
||||
gen_statem:event_handler_result(kafka_state(), #state{}).
|
||||
unknown_event(EventType, EventContent, StateName, State) ->
|
||||
logger:warning("[endpoint_kafka] unknown event: ~p, content: ~p, status: ~p", [EventType, EventContent, StateName]),
|
||||
{keep_state, State}.
|
||||
|
||||
-spec ensure_subscription(binary(), binary()) -> ok.
|
||||
ensure_subscription(Matcher, Matcher) ->
|
||||
ok;
|
||||
ensure_subscription(Matcher, NMatcher) ->
|
||||
ok = endpoint_subscription:unsubscribe(Matcher, self()),
|
||||
endpoint_subscription:subscribe(NMatcher, self()).
|
||||
|
||||
-spec stop_kafka_client(atom()) -> ok.
|
||||
stop_kafka_client(ClientId) when is_atom(ClientId) ->
|
||||
_ = catch brod:stop_client(ClientId),
|
||||
ok.
|
||||
191
apps/endpoint/src/endpoint_log.erl
Normal file
191
apps/endpoint/src/endpoint_log.erl
Normal file
@ -0,0 +1,191 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Endpoint diagnostic log.
|
||||
%%%
|
||||
%%% 当前用于记录 publish 时没有匹配到任何订阅者的数据,避免这类信息进入
|
||||
%%% endpoint_subscription 的热路径同步磁盘写入。
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_log).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0, unmatched_publish/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(LOG_NAME, endpoint_unmatched_publish_log).
|
||||
-define(LOG_FILE_NAME, "unmatched_publish.log").
|
||||
-define(DEFAULT_MAX_BYTES, 10485760).
|
||||
-define(DEFAULT_MAX_FILES, 10).
|
||||
-define(MAX_LOG_CONTENT_BYTES, 32 * 1024).
|
||||
|
||||
-record(state, {
|
||||
enabled = false :: boolean(),
|
||||
log_name = ?LOG_NAME :: atom()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
-spec unmatched_publish(RouteKey :: binary(), Content :: binary()) -> ok.
|
||||
unmatched_publish(RouteKey, Content) when is_binary(RouteKey), is_binary(Content) ->
|
||||
case erlang:whereis(?SERVER) of
|
||||
undefined ->
|
||||
ok;
|
||||
Pid when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {unmatched_publish, RouteKey, Content}),
|
||||
ok
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec init(term()) -> {ok, #state{}}.
|
||||
init([]) ->
|
||||
ok = endpoint_util:set_metadata(),
|
||||
case open_log() of
|
||||
ok ->
|
||||
{ok, #state{enabled = true}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_log] open disk_log failed, reason: ~p", [Reason]),
|
||||
{ok, #state{enabled = false}}
|
||||
end.
|
||||
|
||||
-spec handle_call(term(), gen_server:from(), #state{}) ->
|
||||
{reply, ok, #state{}}.
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
|
||||
handle_cast({unmatched_publish, RouteKey, Content}, State = #state{enabled = true, log_name = LogName}) ->
|
||||
Entry = encode_unmatched_publish(RouteKey, Content),
|
||||
case disk_log:blog(LogName, Entry) of
|
||||
ok ->
|
||||
{noreply, State};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_log] write unmatched publish log failed, reason: ~p", [Reason]),
|
||||
{noreply, State#state{enabled = false}}
|
||||
end;
|
||||
handle_cast({unmatched_publish, _RouteKey, _Content}, State) ->
|
||||
{noreply, State};
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(term(), #state{}) -> {noreply, #state{}}.
|
||||
handle_info(Info, State) ->
|
||||
logger:debug("[endpoint_log] unknown info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(term(), #state{}) -> ok.
|
||||
terminate(_Reason, #state{enabled = true, log_name = LogName}) ->
|
||||
_ = disk_log:close(LogName),
|
||||
ok;
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
-spec code_change(term() | {down, term()}, #state{}, term()) ->
|
||||
{ok, #state{}} | {error, term()}.
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec open_log() -> ok | {error, term()}.
|
||||
open_log() ->
|
||||
Config = log_config(),
|
||||
case log_dir(Config) of
|
||||
{ok, Path} ->
|
||||
File = filename:join(Path, ?LOG_FILE_NAME),
|
||||
ok = filelib:ensure_dir(File),
|
||||
case disk_log:open([
|
||||
{name, ?LOG_NAME},
|
||||
{file, File},
|
||||
{type, wrap},
|
||||
{format, external},
|
||||
{size, {config_pos_integer(max_bytes, Config, ?DEFAULT_MAX_BYTES),
|
||||
config_pos_integer(max_files, Config, ?DEFAULT_MAX_FILES)}},
|
||||
{linkto, self()},
|
||||
{repair, true}
|
||||
]) of
|
||||
{ok, ?LOG_NAME} ->
|
||||
ok;
|
||||
{repaired, ?LOG_NAME, _Recovered, _BadBytes} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec log_config() -> proplists:proplist().
|
||||
log_config() ->
|
||||
case application:get_env(endpoint, endpoint_log) of
|
||||
{ok, Config} when is_list(Config) ->
|
||||
Config;
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec log_dir(proplists:proplist()) -> {ok, file:filename_all()} | {error, term()}.
|
||||
log_dir(Config) ->
|
||||
case proplists:get_value(path, Config) of
|
||||
Path when is_list(Path); is_binary(Path) ->
|
||||
{ok, Path};
|
||||
_ ->
|
||||
{error, {missing_config, endpoint_log, path}}
|
||||
end.
|
||||
|
||||
-spec config_pos_integer(atom(), proplists:proplist(), pos_integer()) -> pos_integer().
|
||||
config_pos_integer(Key, Config, Default) ->
|
||||
case proplists:get_value(Key, Config, Default) of
|
||||
Value when is_integer(Value), Value > 0 ->
|
||||
Value;
|
||||
_ ->
|
||||
Default
|
||||
end.
|
||||
|
||||
-spec encode_unmatched_publish(binary(), binary()) -> binary().
|
||||
encode_unmatched_publish(RouteKey, Content) ->
|
||||
{LoggedContent, Truncated} = maybe_truncate(Content),
|
||||
iolist_to_binary([
|
||||
format_local_time(), <<"\t">>,
|
||||
<<"unmatched_publish">>, <<"\t">>,
|
||||
RouteKey, <<"\t">>,
|
||||
integer_to_binary(byte_size(Content)), <<"\t">>,
|
||||
boolean_to_binary(Truncated), <<"\t">>,
|
||||
base64:encode(LoggedContent), <<"\n">>
|
||||
]).
|
||||
|
||||
-spec boolean_to_binary(boolean()) -> binary().
|
||||
boolean_to_binary(true) ->
|
||||
<<"true">>;
|
||||
boolean_to_binary(false) ->
|
||||
<<"false">>.
|
||||
|
||||
-spec format_local_time() -> binary().
|
||||
format_local_time() ->
|
||||
MilliSeconds = erlang:system_time(millisecond) rem 1000,
|
||||
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
|
||||
iolist_to_binary(io_lib:format(
|
||||
"~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B.~3..0B",
|
||||
[Year, Month, Day, Hour, Minute, Second, MilliSeconds]
|
||||
)).
|
||||
|
||||
-spec maybe_truncate(binary()) -> {binary(), boolean()}.
|
||||
maybe_truncate(Content) when byte_size(Content) =< ?MAX_LOG_CONTENT_BYTES ->
|
||||
{Content, false};
|
||||
maybe_truncate(Content) ->
|
||||
MaxBytes = ?MAX_LOG_CONTENT_BYTES,
|
||||
<<LoggedContent:MaxBytes/binary, _/binary>> = Content,
|
||||
{LoggedContent, true}.
|
||||
285
apps/endpoint/src/endpoint_mqtt.erl
Normal file
285
apps/endpoint/src/endpoint_mqtt.erl
Normal file
@ -0,0 +1,285 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_mqtt).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([callback_mode/0, init/1, terminate/3, code_change/4]).
|
||||
-export([disconnected/3, connected/3]).
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 15000).
|
||||
|
||||
-record(state, {
|
||||
endpoint :: #endpoint{},
|
||||
buffer :: endpoint_buffer:buffer(),
|
||||
conn_pid :: undefined | pid(),
|
||||
%% 待确认的数据, #{PacketId :: integer() => Id :: integer()}
|
||||
inflight = #{}
|
||||
}).
|
||||
|
||||
-type mqtt_state() :: disconnected | connected.
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(LocalName :: term(), Endpoint :: #endpoint{}) ->
|
||||
{ok, pid()} | ignore | {error, term()}.
|
||||
start_link(LocalName, Endpoint = #endpoint{}) ->
|
||||
gen_statem:start_link({via, gproc, {n, l, LocalName}}, ?MODULE, [Endpoint], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec callback_mode() -> state_functions.
|
||||
callback_mode() ->
|
||||
state_functions.
|
||||
|
||||
-spec init(term()) -> gen_statem:init_result(mqtt_state(), #state{}).
|
||||
init([Endpoint = #endpoint{matcher = Matcher}]) ->
|
||||
ok = endpoint_util:set_metadata(),
|
||||
erlang:process_flag(trap_exit, true),
|
||||
ok = endpoint_subscription:subscribe(Matcher, self()),
|
||||
Buffer = endpoint_buffer:new(Endpoint, 10),
|
||||
{ok, disconnected, #state{endpoint = Endpoint, buffer = Buffer}, [{next_event, internal, connect}]}.
|
||||
|
||||
-spec disconnected(gen_statem:event_type(), term(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
disconnected({call, From}, get_stat, State = #state{buffer = Buffer}) ->
|
||||
reply_stat(From, Buffer, State);
|
||||
disconnected(cast, {forward, Metric}, State = #state{buffer = Buffer}) ->
|
||||
store_metric(Metric, Buffer, State);
|
||||
disconnected(cast, cleanup, State = #state{buffer = Buffer}) ->
|
||||
cleanup_buffer(Buffer, State);
|
||||
disconnected(cast, {reload, NEndpoint = #endpoint{matcher = NMatcher}},
|
||||
State = #state{endpoint = #endpoint{matcher = Matcher}, conn_pid = ConnPid}) ->
|
||||
reload_endpoint(Matcher, NMatcher, ConnPid, NEndpoint, State);
|
||||
disconnected(internal, connect, State) ->
|
||||
{keep_state, State, [{next_event, internal, do_connect}]};
|
||||
disconnected(state_timeout, connect, State) ->
|
||||
{keep_state, State, [{next_event, internal, do_connect}]};
|
||||
disconnected(internal, do_connect,
|
||||
State = #state{
|
||||
buffer = Buffer,
|
||||
endpoint = #endpoint{
|
||||
title = Title,
|
||||
config = #mqtt_endpoint{
|
||||
host = Host,
|
||||
port = Port,
|
||||
username = Username,
|
||||
password = Password,
|
||||
client_id = ClientId
|
||||
}
|
||||
}
|
||||
}) ->
|
||||
logger:debug("[endpoint_mqtt] endpoint: ~ts, create postman", [Title]),
|
||||
Opts = [
|
||||
{owner, self()},
|
||||
{clientid, ClientId},
|
||||
{host, binary_to_list(Host)},
|
||||
{port, Port},
|
||||
{tcp_opts, []},
|
||||
{username, binary_to_list(Username)},
|
||||
{password, binary_to_list(Password)},
|
||||
{keepalive, 86400},
|
||||
{auto_ack, true},
|
||||
{connect_timeout, 5000},
|
||||
{proto_ver, v5},
|
||||
{retry_interval, 5000}
|
||||
],
|
||||
case connect_mqtt(Opts) of
|
||||
{ok, ConnPid} ->
|
||||
logger:debug("[endpoint_mqtt] connect success, pid: ~p", [ConnPid]),
|
||||
NBuffer = endpoint_buffer:trigger_n(Buffer),
|
||||
{next_state, connected, State#state{conn_pid = ConnPid, buffer = NBuffer}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_mqtt] connect get error: ~p", [Reason]),
|
||||
{keep_state, State#state{conn_pid = undefined, inflight = #{}},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]}
|
||||
end;
|
||||
disconnected(info, {timeout, TimerRef, {endpoint_buffer_retry, Id, Payload}}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:handle_timeout(TimerRef, Id, Payload, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}};
|
||||
disconnected(info, {next_data, _Id, _Tuple}, State) ->
|
||||
{keep_state, State};
|
||||
disconnected(info, {'EXIT', ConnPid, Reason},
|
||||
State = #state{endpoint = #endpoint{title = Title}, conn_pid = ConnPid}) ->
|
||||
logger:warning("[endpoint_mqtt] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
|
||||
{keep_state, State#state{conn_pid = undefined, inflight = #{}},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]};
|
||||
disconnected(info, Info, State) ->
|
||||
unknown_info(Info, disconnected, State);
|
||||
disconnected(EventType, EventContent, State) ->
|
||||
unknown_event(EventType, EventContent, disconnected, State).
|
||||
|
||||
-spec connected(gen_statem:event_type(), term(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
connected({call, From}, get_stat, State = #state{buffer = Buffer}) ->
|
||||
reply_stat(From, Buffer, State);
|
||||
connected(cast, {forward, Metric}, State = #state{buffer = Buffer}) ->
|
||||
forward_metric(Metric, Buffer, State);
|
||||
connected(cast, cleanup, State = #state{buffer = Buffer}) ->
|
||||
cleanup_buffer(Buffer, State);
|
||||
connected(cast, {reload, NEndpoint = #endpoint{matcher = NMatcher}},
|
||||
State = #state{endpoint = #endpoint{matcher = Matcher}, conn_pid = ConnPid}) ->
|
||||
reload_endpoint(Matcher, NMatcher, ConnPid, NEndpoint, State);
|
||||
connected(internal, connect, State) ->
|
||||
{keep_state, State};
|
||||
connected(info, {next_data, Id, Metric},
|
||||
State = #state{
|
||||
conn_pid = ConnPid,
|
||||
buffer = Buffer,
|
||||
inflight = InFlight,
|
||||
endpoint = #endpoint{config = #mqtt_endpoint{topic = Topic, qos = Qos}}
|
||||
}) ->
|
||||
logger:debug("[endpoint_mqtt] will publish topic: ~p, metric: ~p, qos: ~p", [Topic, Metric, Qos]),
|
||||
case emqtt:publish(ConnPid, Topic, #{}, Metric, [{qos, Qos}, {retain, true}]) of
|
||||
ok ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}};
|
||||
{ok, PacketId} ->
|
||||
{keep_state, State#state{inflight = maps:put(PacketId, Id, InFlight)}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_mqtt] send message to topic: ~p, get error: ~p", [Topic, Reason]),
|
||||
stop_mqtt_conn(ConnPid),
|
||||
NBuffer = endpoint_buffer:recover_inflight(Buffer),
|
||||
{next_state, disconnected, State#state{conn_pid = undefined, inflight = #{}, buffer = NBuffer},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]}
|
||||
end;
|
||||
connected(info, {timeout, TimerRef, {endpoint_buffer_retry, Id, Payload}}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:handle_timeout(TimerRef, Id, Payload, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}};
|
||||
connected(info, {disconnected, ReasonCode, Properties},
|
||||
State = #state{conn_pid = ConnPid, buffer = Buffer}) ->
|
||||
logger:debug("[endpoint_mqtt] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||
stop_mqtt_conn(ConnPid),
|
||||
NBuffer = endpoint_buffer:recover_inflight(Buffer),
|
||||
{next_state, disconnected, State#state{conn_pid = undefined, inflight = #{}, buffer = NBuffer},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]};
|
||||
connected(info, {publish, Message = #{packet_id := _PacketId, payload := Payload}}, State) ->
|
||||
logger:debug("[endpoint_mqtt] Recv a publish packet: ~p, payload: ~p", [Message, Payload]),
|
||||
{keep_state, State};
|
||||
connected(info, {puback, #{packet_id := PacketId}},
|
||||
State = #state{inflight = Inflight, buffer = Buffer}) ->
|
||||
case maps:take(PacketId, Inflight) of
|
||||
{Id, RestInflight} ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer, inflight = RestInflight}};
|
||||
error ->
|
||||
{keep_state, State}
|
||||
end;
|
||||
connected(info, {'EXIT', ConnPid, Reason},
|
||||
State = #state{endpoint = #endpoint{title = Title}, conn_pid = ConnPid, buffer = Buffer}) ->
|
||||
logger:warning("[endpoint_mqtt] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
|
||||
NBuffer = endpoint_buffer:recover_inflight(Buffer),
|
||||
{next_state, disconnected, State#state{conn_pid = undefined, inflight = #{}, buffer = NBuffer},
|
||||
[{state_timeout, ?RETRY_INTERVAL, connect}]};
|
||||
connected(info, Info, State) ->
|
||||
unknown_info(Info, connected, State);
|
||||
connected(EventType, EventContent, State) ->
|
||||
unknown_event(EventType, EventContent, connected, State).
|
||||
|
||||
-spec terminate(term(), mqtt_state(), #state{}) -> term().
|
||||
terminate(Reason, _StateName, #state{endpoint = #endpoint{title = Title}, buffer = Buffer, conn_pid = ConnPid}) ->
|
||||
logger:debug("[endpoint_mqtt] endpoint: ~p, terminate with reason: ~p", [Title, Reason]),
|
||||
stop_mqtt_conn(ConnPid),
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
ok.
|
||||
|
||||
-spec code_change(term() | {down, term()}, mqtt_state(), #state{}, term()) ->
|
||||
{ok, mqtt_state(), #state{}} | {error, term()}.
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec reply_stat(gen_statem:from(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
reply_stat(From, Buffer, State) ->
|
||||
Stat = endpoint_buffer:stat(Buffer),
|
||||
{keep_state, State, [{reply, From, {ok, Stat}}]}.
|
||||
|
||||
-spec forward_metric(binary(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
forward_metric(Metric, Buffer, State) ->
|
||||
NBuffer = endpoint_buffer:append(Metric, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec store_metric(binary(), endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
store_metric(Metric, Buffer, State) ->
|
||||
NBuffer = endpoint_buffer:append_only(Metric, Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec cleanup_buffer(endpoint_buffer:buffer(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
cleanup_buffer(Buffer, State) ->
|
||||
NBuffer = endpoint_buffer:cleanup(Buffer),
|
||||
{keep_state, State#state{buffer = NBuffer}}.
|
||||
|
||||
-spec reload_endpoint(binary(), binary(), undefined | pid(), #endpoint{}, #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
reload_endpoint(Matcher, NMatcher, ConnPid, NEndpoint, State = #state{}) ->
|
||||
ensure_subscription(Matcher, NMatcher),
|
||||
stop_mqtt_conn(ConnPid),
|
||||
NBuffer = endpoint_buffer:recover_inflight(State#state.buffer),
|
||||
{next_state, disconnected, State#state{endpoint = NEndpoint, conn_pid = undefined, inflight = #{}, buffer = NBuffer},
|
||||
[{next_event, internal, do_connect}]}.
|
||||
|
||||
-spec unknown_info(term(), mqtt_state(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
unknown_info(Info, StateName, State) ->
|
||||
logger:warning("[endpoint_mqtt] unknown message: ~p, status: ~p", [Info, StateName]),
|
||||
{keep_state, State}.
|
||||
|
||||
-spec unknown_event(gen_statem:event_type(), term(), mqtt_state(), #state{}) ->
|
||||
gen_statem:event_handler_result(mqtt_state(), #state{}).
|
||||
unknown_event(EventType, EventContent, StateName, State) ->
|
||||
logger:warning("[endpoint_mqtt] unknown event: ~p, content: ~p, status: ~p", [EventType, EventContent, StateName]),
|
||||
{keep_state, State}.
|
||||
|
||||
-spec ensure_subscription(binary(), binary()) -> ok.
|
||||
ensure_subscription(Matcher, Matcher) ->
|
||||
ok;
|
||||
ensure_subscription(Matcher, NMatcher) ->
|
||||
ok = endpoint_subscription:unsubscribe(Matcher, self()),
|
||||
endpoint_subscription:subscribe(NMatcher, self()).
|
||||
|
||||
-spec connect_mqtt(list()) -> {ok, pid()} | {error, term()}.
|
||||
connect_mqtt(Opts) ->
|
||||
try
|
||||
{ok, ConnPid} = emqtt:start_link(Opts),
|
||||
logger:debug("[endpoint_mqtt] start connect, options: ~p", [Opts]),
|
||||
case emqtt:connect(ConnPid, 5000) of
|
||||
{ok, _} ->
|
||||
{ok, ConnPid};
|
||||
{error, Reason} ->
|
||||
stop_mqtt_conn(ConnPid),
|
||||
{error, Reason}
|
||||
end
|
||||
catch
|
||||
_:Error ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
-spec stop_mqtt_conn(undefined | pid()) -> ok.
|
||||
stop_mqtt_conn(undefined) ->
|
||||
ok;
|
||||
stop_mqtt_conn(ConnPid) when is_pid(ConnPid) ->
|
||||
_ = catch emqtt:stop(ConnPid),
|
||||
ok.
|
||||
953
apps/endpoint/src/endpoint_outbox.erl
Normal file
953
apps/endpoint/src/endpoint_outbox.erl
Normal file
@ -0,0 +1,953 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Codex
|
||||
%%% @doc
|
||||
%%% endpoint 持久化 outbox 的第一阶段实现:
|
||||
%%% - 一个 endpoint 一个目录
|
||||
%%% - segment 按递增编号命名
|
||||
%%% - metadata 只记录连续 ack 指针
|
||||
%%% - inflight 不落盘,崩溃后从 acked_seq + 1 重放
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_outbox).
|
||||
|
||||
-export([open/2, close/1, append/2, ack/2, next/1, reset_reader/1, stat/1]).
|
||||
-export_type([outbox/0]).
|
||||
|
||||
-record(segment, {
|
||||
id :: pos_integer(),
|
||||
path :: file:filename_all(),
|
||||
start_seq = 0 :: non_neg_integer(),
|
||||
end_seq = 0 :: non_neg_integer(),
|
||||
records = 0 :: non_neg_integer(),
|
||||
bytes = 0 :: non_neg_integer()
|
||||
}).
|
||||
|
||||
-record(outbox_writer, {
|
||||
segment = 1 :: pos_integer(),
|
||||
next_seq = 1 :: pos_integer(),
|
||||
fd = undefined :: undefined | file:fd()
|
||||
}).
|
||||
|
||||
-record(outbox_reader, {
|
||||
segment :: undefined | pos_integer(),
|
||||
offset = 0 :: non_neg_integer(),
|
||||
fd = undefined :: undefined | file:fd()
|
||||
}).
|
||||
|
||||
-record(outbox_checkpoint, {
|
||||
acked_segment = 1 :: pos_integer(),
|
||||
acked_seq = 0 :: non_neg_integer(),
|
||||
pending_acks = [] :: [pos_integer()]
|
||||
}).
|
||||
|
||||
-record(outbox, {
|
||||
dir :: file:filename_all(),
|
||||
metadata_path :: file:filename_all(),
|
||||
max_records = 500000 :: pos_integer(),
|
||||
max_bytes = 134217728 :: pos_integer(),
|
||||
max_segments = 10 :: pos_integer(),
|
||||
segments = [] :: [#segment{}],
|
||||
writer = #outbox_writer{} :: #outbox_writer{},
|
||||
reader = #outbox_reader{} :: #outbox_reader{},
|
||||
checkpoint = #outbox_checkpoint{} :: #outbox_checkpoint{}
|
||||
}).
|
||||
|
||||
-type outbox() :: #outbox{}.
|
||||
|
||||
-define(METADATA_FILE, "metadata.json").
|
||||
-define(SEGMENT_EXT, ".log").
|
||||
-define(MAX_PAYLOAD_BYTES, 32 * 1024).
|
||||
-define(RECORD_MAGIC, <<"EPOBOX01">>).
|
||||
-define(RECORD_VERSION, 1).
|
||||
-define(RECORD_HEADER_BYTES, 21).
|
||||
-define(RECORD_FOOTER_BYTES, 4).
|
||||
-define(RECORD_OVERHEAD_BYTES, ?RECORD_HEADER_BYTES + ?RECORD_FOOTER_BYTES).
|
||||
-define(RESYNC_READ_BYTES, 4096).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% Opts 支持的参数:
|
||||
%% - max_records => pos_integer()
|
||||
%% 单个 segment 允许的最大记录数,默认 500000
|
||||
%% - max_bytes => pos_integer()
|
||||
%% 单个 segment 允许的最大字节数,默认 134217728
|
||||
%% - max_segments => pos_integer()
|
||||
%% 未确认 segment 的最大数量,默认 10
|
||||
-spec open(file:filename_all(), map()) -> {ok, outbox()} | {error, term()}.
|
||||
open(Dir, Opts) when is_map(Opts) ->
|
||||
ok = ensure_dir(Dir),
|
||||
MetadataPath = filename:join(Dir, ?METADATA_FILE),
|
||||
Segments = load_segments(Dir),
|
||||
Meta = read_metadata(MetadataPath),
|
||||
LastSegmentId = last_segment_id(Segments, maps:get(write_segment, Meta, 1)),
|
||||
LastSeq = last_seq(Segments, maps:get(write_seq, Meta, 0)),
|
||||
AckedSeq = min(maps:get(acked_seq, Meta, 0), LastSeq),
|
||||
AckedSegment = segment_id_for_seq(Segments, AckedSeq, maps:get(acked_segment, Meta, 1)),
|
||||
Outbox0 = #outbox{
|
||||
dir = Dir,
|
||||
metadata_path = MetadataPath,
|
||||
max_records = maps:get(max_records, Opts, 500000),
|
||||
max_bytes = maps:get(max_bytes, Opts, 134217728),
|
||||
max_segments = maps:get(max_segments, Opts, 10),
|
||||
writer = #outbox_writer{
|
||||
segment = LastSegmentId,
|
||||
next_seq = LastSeq + 1
|
||||
},
|
||||
checkpoint = #outbox_checkpoint{
|
||||
acked_segment = AckedSegment,
|
||||
acked_seq = AckedSeq
|
||||
},
|
||||
segments = Segments
|
||||
},
|
||||
{ReadSegment, ReadOffset} = locate_reader(Outbox0),
|
||||
Outbox1 = Outbox0#outbox{
|
||||
reader = #outbox_reader{
|
||||
segment = ReadSegment,
|
||||
offset = ReadOffset
|
||||
}
|
||||
},
|
||||
maybe
|
||||
{ok, Outbox} ?= open_fds(Outbox1),
|
||||
ok ?= persist_metadata(Outbox),
|
||||
{ok, Outbox}
|
||||
else
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec close(outbox()) -> ok.
|
||||
close(Outbox = #outbox{}) ->
|
||||
_ = close_open_fds(Outbox),
|
||||
ok.
|
||||
|
||||
-spec append(binary(), outbox()) ->
|
||||
{ok, Seq :: pos_integer(), outbox()} | {dropped, capacity_reached, outbox()} | {error, term()}.
|
||||
append(Payload, Outbox = #outbox{}) when is_binary(Payload) ->
|
||||
case validate_payload_size(Payload) of
|
||||
ok ->
|
||||
RecordBytes = encoded_record_size(Payload),
|
||||
case ensure_writable_segment(RecordBytes, Outbox) of
|
||||
{drop, NOutbox} ->
|
||||
{dropped, capacity_reached, NOutbox};
|
||||
{ok, SegmentId, NOutbox0} ->
|
||||
maybe
|
||||
{ok, NOutbox1} ?= ensure_write_fd(SegmentId, NOutbox0),
|
||||
Writer1 = NOutbox1#outbox.writer,
|
||||
Seq = Writer1#outbox_writer.next_seq,
|
||||
SegmentPath = segment_path(NOutbox1#outbox.dir, SegmentId),
|
||||
ok ?= append_record(Writer1#outbox_writer.fd, Seq, Payload),
|
||||
Segments = update_written_segment(
|
||||
NOutbox1#outbox.segments, SegmentId, SegmentPath, Seq, RecordBytes),
|
||||
NOutbox2 = NOutbox1#outbox{
|
||||
writer = Writer1#outbox_writer{
|
||||
segment = SegmentId,
|
||||
next_seq = Seq + 1
|
||||
},
|
||||
segments = Segments
|
||||
},
|
||||
{ok, NOutbox} ?= maybe_reset_reader_after_append(Outbox, NOutbox2),
|
||||
ok ?= persist_metadata(NOutbox),
|
||||
{ok, Seq, NOutbox}
|
||||
else
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec ack(pos_integer(), outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
ack(Seq, Outbox = #outbox{checkpoint = Checkpoint}) when is_integer(Seq), Seq > 0 ->
|
||||
AckedSeq = Checkpoint#outbox_checkpoint.acked_seq,
|
||||
case Seq =< AckedSeq of
|
||||
true ->
|
||||
{ok, Outbox};
|
||||
false ->
|
||||
PendingAcks0 = ordsets:add_element(Seq, Checkpoint#outbox_checkpoint.pending_acks),
|
||||
{NackedSeq, PendingAcks} = advance_acked_seq(AckedSeq, PendingAcks0),
|
||||
NackedSegment = segment_id_for_seq(
|
||||
Outbox#outbox.segments,
|
||||
NackedSeq,
|
||||
Checkpoint#outbox_checkpoint.acked_segment),
|
||||
Outbox0 = Outbox#outbox{
|
||||
checkpoint = Checkpoint#outbox_checkpoint{
|
||||
acked_seq = NackedSeq,
|
||||
acked_segment = NackedSegment,
|
||||
pending_acks = PendingAcks
|
||||
}
|
||||
},
|
||||
maybe
|
||||
{ok, Outbox1} ?= prune_acked_segments(Outbox0),
|
||||
ok ?= maybe_persist_metadata(NackedSeq =/= AckedSeq, Outbox1),
|
||||
{ok, Outbox1}
|
||||
else
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec next(outbox()) ->
|
||||
eof | {ok, Seq :: pos_integer(), Payload :: binary(), outbox()} | {error, term()}.
|
||||
next(#outbox{reader = #outbox_reader{segment = undefined}}) ->
|
||||
eof;
|
||||
next(Outbox = #outbox{
|
||||
reader = Reader,
|
||||
segments = Segments
|
||||
}) ->
|
||||
SegmentId = Reader#outbox_reader.segment,
|
||||
Offset = Reader#outbox_reader.offset,
|
||||
case ensure_read_fd(Outbox) of
|
||||
{ok, NOutbox0} ->
|
||||
Reader0 = NOutbox0#outbox.reader,
|
||||
case read_next_record(Reader0#outbox_reader.fd, Offset) of
|
||||
eof ->
|
||||
case next_segment_id(Segments, SegmentId) of
|
||||
undefined ->
|
||||
eof;
|
||||
NextSegmentId ->
|
||||
case switch_read_segment(NextSegmentId, 0, NOutbox0) of
|
||||
{ok, NOutbox1} ->
|
||||
next(NOutbox1);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end
|
||||
end;
|
||||
{ok, _RecordOffset, Seq, Payload, NextOffset} ->
|
||||
{ok, Seq, Payload, NOutbox0#outbox{
|
||||
reader = Reader0#outbox_reader{offset = NextOffset}
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec reset_reader(outbox()) -> {ok, outbox()}.
|
||||
reset_reader(Outbox = #outbox{}) ->
|
||||
{ReadSegment, ReadOffset} = locate_reader(Outbox),
|
||||
switch_read_segment(ReadSegment, ReadOffset, Outbox).
|
||||
|
||||
-spec stat(outbox()) -> map().
|
||||
stat(#outbox{
|
||||
dir = Dir,
|
||||
writer = #outbox_writer{
|
||||
segment = WriteSegment,
|
||||
next_seq = NextSeq
|
||||
},
|
||||
checkpoint = #outbox_checkpoint{
|
||||
acked_segment = AckedSegment,
|
||||
acked_seq = AckedSeq
|
||||
},
|
||||
segments = Segments
|
||||
}) ->
|
||||
#{
|
||||
dir => Dir,
|
||||
write_segment => WriteSegment,
|
||||
write_seq => max(NextSeq - 1, 0),
|
||||
acked_segment => AckedSegment,
|
||||
acked_seq => AckedSeq,
|
||||
segment_num => length(Segments),
|
||||
pending_segment_num => pending_segment_count(Segments, AckedSeq)
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec ensure_dir(file:filename_all()) -> ok.
|
||||
ensure_dir(Dir) ->
|
||||
ok = filelib:ensure_dir(filename:join(Dir, "dummy")),
|
||||
ok.
|
||||
|
||||
-spec read_metadata(file:filename_all()) -> map().
|
||||
read_metadata(Path) ->
|
||||
case file:read_file(Path) of
|
||||
{ok, Bin} ->
|
||||
case catch json:decode(Bin) of
|
||||
#{<<"write_segment">> := WriteSegment,
|
||||
<<"write_seq">> := WriteSeq,
|
||||
<<"acked_segment">> := AckedSegment,
|
||||
<<"acked_seq">> := AckedSeq}
|
||||
when is_integer(WriteSegment), is_integer(WriteSeq),
|
||||
is_integer(AckedSegment), is_integer(AckedSeq) ->
|
||||
#{
|
||||
write_segment => WriteSegment,
|
||||
write_seq => WriteSeq,
|
||||
acked_segment => AckedSegment,
|
||||
acked_seq => AckedSeq
|
||||
};
|
||||
_ ->
|
||||
default_metadata()
|
||||
end;
|
||||
{error, enoent} ->
|
||||
default_metadata();
|
||||
{error, _} ->
|
||||
default_metadata()
|
||||
end.
|
||||
|
||||
-spec default_metadata() -> map().
|
||||
default_metadata() ->
|
||||
#{
|
||||
write_segment => 1,
|
||||
write_seq => 0,
|
||||
acked_segment => 1,
|
||||
acked_seq => 0
|
||||
}.
|
||||
|
||||
-spec persist_metadata(outbox()) -> ok | {error, term()}.
|
||||
persist_metadata(#outbox{
|
||||
metadata_path = MetadataPath,
|
||||
writer = #outbox_writer{
|
||||
segment = WriteSegment,
|
||||
next_seq = NextSeq
|
||||
},
|
||||
checkpoint = #outbox_checkpoint{
|
||||
acked_segment = AckedSegment,
|
||||
acked_seq = AckedSeq
|
||||
}
|
||||
}) ->
|
||||
TmpPath = MetadataPath ++ ".tmp",
|
||||
Metadata = #{
|
||||
<<"write_segment">> => WriteSegment,
|
||||
<<"write_seq">> => max(NextSeq - 1, 0),
|
||||
<<"acked_segment">> => AckedSegment,
|
||||
<<"acked_seq">> => AckedSeq
|
||||
},
|
||||
Bin = iolist_to_binary(json:encode(Metadata)),
|
||||
ok = file:write_file(TmpPath, Bin),
|
||||
ok = file:rename(TmpPath, MetadataPath),
|
||||
ok.
|
||||
|
||||
-spec load_segments(file:filename_all()) -> [#segment{}].
|
||||
load_segments(Dir) ->
|
||||
Pattern = filename:join(Dir, "*" ++ ?SEGMENT_EXT),
|
||||
SegmentFiles = lists:sort(filelib:wildcard(Pattern)),
|
||||
lists:filtermap(fun(Path) ->
|
||||
case parse_segment_id(filename:basename(Path)) of
|
||||
{ok, Id} ->
|
||||
case scan_segment(Path, Id) of
|
||||
{ok, Segment = #segment{records = Records}} when Records > 0 ->
|
||||
{true, Segment};
|
||||
_ ->
|
||||
false
|
||||
end;
|
||||
error ->
|
||||
false
|
||||
end
|
||||
end, SegmentFiles).
|
||||
|
||||
-spec parse_segment_id(string()) -> {ok, pos_integer()} | error.
|
||||
parse_segment_id(Filename) ->
|
||||
case filename:extension(Filename) of
|
||||
?SEGMENT_EXT ->
|
||||
Base = filename:rootname(Filename, ?SEGMENT_EXT),
|
||||
try
|
||||
Id = list_to_integer(Base),
|
||||
case Id > 0 of
|
||||
true ->
|
||||
{ok, Id};
|
||||
false ->
|
||||
error
|
||||
end
|
||||
catch
|
||||
_:_ ->
|
||||
error
|
||||
end;
|
||||
_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec scan_segment(file:filename_all(), pos_integer()) -> {ok, #segment{}} | {error, term()}.
|
||||
scan_segment(Path, Id) ->
|
||||
case file:open(Path, [read, raw, binary]) of
|
||||
{ok, Fd} ->
|
||||
try
|
||||
scan_segment_records(Fd, 0, #segment{id = Id, path = Path})
|
||||
after
|
||||
ok = file:close(Fd)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec scan_segment_records(file:fd(), non_neg_integer(), #segment{}) -> {ok, #segment{}} | {error, term()}.
|
||||
scan_segment_records(Fd, Offset, Segment = #segment{}) ->
|
||||
case read_next_record(Fd, Offset) of
|
||||
eof ->
|
||||
{ok, Segment};
|
||||
{ok, RecordOffset, Seq, _Payload, NextOffset} ->
|
||||
RecordBytes = NextOffset - RecordOffset,
|
||||
NextSegment = Segment#segment{
|
||||
start_seq = case Segment#segment.records of
|
||||
0 -> Seq;
|
||||
_ -> Segment#segment.start_seq
|
||||
end,
|
||||
end_seq = Seq,
|
||||
records = Segment#segment.records + 1,
|
||||
bytes = Segment#segment.bytes + RecordBytes
|
||||
},
|
||||
scan_segment_records(Fd, NextOffset, NextSegment);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec encoded_record_size(binary()) -> pos_integer().
|
||||
encoded_record_size(Payload) ->
|
||||
?RECORD_OVERHEAD_BYTES + byte_size(Payload).
|
||||
|
||||
-spec validate_payload_size(binary()) -> ok | {error, payload_too_large}.
|
||||
validate_payload_size(Payload) when byte_size(Payload) =< ?MAX_PAYLOAD_BYTES ->
|
||||
ok;
|
||||
validate_payload_size(_Payload) ->
|
||||
{error, payload_too_large}.
|
||||
|
||||
-spec ensure_writable_segment(pos_integer(), outbox()) ->
|
||||
{ok, pos_integer(), outbox()} | {drop, outbox()}.
|
||||
ensure_writable_segment(RecordBytes, Outbox = #outbox{
|
||||
max_records = MaxRecords,
|
||||
max_bytes = MaxBytes,
|
||||
max_segments = MaxSegments,
|
||||
segments = Segments,
|
||||
writer = #outbox_writer{segment = WriteSegment},
|
||||
checkpoint = #outbox_checkpoint{acked_seq = AckedSeq}
|
||||
}) ->
|
||||
case find_segment(Segments, WriteSegment) of
|
||||
false ->
|
||||
{ok, WriteSegment, Outbox};
|
||||
#segment{records = Records, bytes = Bytes} ->
|
||||
NeedRoll = Records > 0 andalso (Records >= MaxRecords orelse Bytes + RecordBytes > MaxBytes),
|
||||
case NeedRoll of
|
||||
false ->
|
||||
{ok, WriteSegment, Outbox};
|
||||
true ->
|
||||
case pending_segment_count(Segments, AckedSeq) >= MaxSegments of
|
||||
true ->
|
||||
{drop, Outbox};
|
||||
false ->
|
||||
{ok, WriteSegment + 1, Outbox#outbox{
|
||||
writer = ((Outbox#outbox.writer)#outbox_writer{
|
||||
segment = WriteSegment + 1
|
||||
})
|
||||
}}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
-spec append_record(file:fd(), pos_integer(), binary()) -> ok | {error, term()}.
|
||||
append_record(Fd, Seq, Payload) when Fd =/= undefined ->
|
||||
PayloadSize = byte_size(Payload),
|
||||
Crc = record_crc(?RECORD_VERSION, Seq, PayloadSize, Payload),
|
||||
Bin = <<
|
||||
?RECORD_MAGIC/binary,
|
||||
?RECORD_VERSION:8,
|
||||
Seq:64/unsigned-big-integer,
|
||||
PayloadSize:32/unsigned-big-integer,
|
||||
Payload/binary,
|
||||
Crc:32/unsigned-big-integer
|
||||
>>,
|
||||
case file:write(Fd, Bin) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec update_written_segment([#segment{}], pos_integer(), file:filename_all(), pos_integer(), pos_integer()) -> [#segment{}].
|
||||
update_written_segment(Segments, SegmentId, Path, Seq, RecordBytes) ->
|
||||
case take_segment(SegmentId, Segments) of
|
||||
{undefined, Rest} ->
|
||||
lists:sort(fun compare_segment/2, [
|
||||
#segment{
|
||||
id = SegmentId,
|
||||
path = Path,
|
||||
start_seq = Seq,
|
||||
end_seq = Seq,
|
||||
records = 1,
|
||||
bytes = RecordBytes
|
||||
} | Rest
|
||||
]);
|
||||
{Segment, Rest} ->
|
||||
lists:sort(fun compare_segment/2, [
|
||||
Segment#segment{
|
||||
end_seq = Seq,
|
||||
records = Segment#segment.records + 1,
|
||||
bytes = Segment#segment.bytes + RecordBytes
|
||||
} | Rest
|
||||
])
|
||||
end.
|
||||
|
||||
-spec compare_segment(#segment{}, #segment{}) -> boolean().
|
||||
compare_segment(#segment{id = Id0}, #segment{id = Id1}) ->
|
||||
Id0 =< Id1.
|
||||
|
||||
-spec find_segment([#segment{}], pos_integer()) -> false | #segment{}.
|
||||
find_segment(Segments, SegmentId) ->
|
||||
lists:keyfind(SegmentId, #segment.id, Segments).
|
||||
|
||||
-spec take_segment(pos_integer(), [#segment{}]) -> {undefined | #segment{}, [#segment{}]}.
|
||||
take_segment(SegmentId, Segments) ->
|
||||
case lists:partition(fun(#segment{id = Id}) -> Id =:= SegmentId end, Segments) of
|
||||
{[Segment], Rest} ->
|
||||
{Segment, Rest};
|
||||
{[], Rest} ->
|
||||
{undefined, Rest}
|
||||
end.
|
||||
|
||||
-spec last_segment_id([#segment{}], pos_integer()) -> pos_integer().
|
||||
last_segment_id([], Default) ->
|
||||
Default;
|
||||
last_segment_id(Segments, _Default) ->
|
||||
(lists:last(Segments))#segment.id.
|
||||
|
||||
-spec last_seq([#segment{}], non_neg_integer()) -> non_neg_integer().
|
||||
last_seq([], Default) ->
|
||||
Default;
|
||||
last_seq(Segments, _Default) ->
|
||||
(lists:last(Segments))#segment.end_seq.
|
||||
|
||||
-spec segment_id_for_seq([#segment{}], non_neg_integer(), pos_integer()) -> pos_integer().
|
||||
segment_id_for_seq(_Segments, 0, Default) ->
|
||||
Default;
|
||||
segment_id_for_seq(Segments, Seq, Default) ->
|
||||
case lists:dropwhile(fun(#segment{end_seq = EndSeq}) -> EndSeq < Seq end, Segments) of
|
||||
[#segment{id = SegmentId, start_seq = StartSeq, end_seq = EndSeq} | _]
|
||||
when Seq >= StartSeq, Seq =< EndSeq ->
|
||||
SegmentId;
|
||||
_ ->
|
||||
Default
|
||||
end.
|
||||
|
||||
-spec pending_segment_count([#segment{}], non_neg_integer()) -> non_neg_integer().
|
||||
pending_segment_count(Segments, AckedSeq) ->
|
||||
length([Segment || Segment = #segment{end_seq = EndSeq} <- Segments, EndSeq > AckedSeq]).
|
||||
|
||||
-spec advance_acked_seq(non_neg_integer(), [pos_integer()]) -> {non_neg_integer(), [pos_integer()]}.
|
||||
advance_acked_seq(AckedSeq, PendingAcks) ->
|
||||
NextSeq = AckedSeq + 1,
|
||||
case ordsets:is_element(NextSeq, PendingAcks) of
|
||||
true ->
|
||||
advance_acked_seq(NextSeq, ordsets:del_element(NextSeq, PendingAcks));
|
||||
false ->
|
||||
{AckedSeq, PendingAcks}
|
||||
end.
|
||||
|
||||
-spec prune_acked_segments(outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
prune_acked_segments(Outbox = #outbox{
|
||||
segments = Segments,
|
||||
checkpoint = #outbox_checkpoint{acked_seq = AckedSeq}
|
||||
}) ->
|
||||
{DeleteSegments, KeepSegments} = lists:partition(fun(#segment{end_seq = EndSeq}) ->
|
||||
EndSeq =< AckedSeq
|
||||
end, Segments),
|
||||
Outbox0 = detach_deleted_fds(DeleteSegments, Outbox#outbox{segments = KeepSegments}),
|
||||
ok = lists:foldl(fun(#segment{path = Path}, ok) ->
|
||||
case file:delete(Path) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, enoent} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
throw({delete_segment_failed, Path, Reason})
|
||||
end
|
||||
end, ok, DeleteSegments),
|
||||
{ok, reset_reader_after_prune(Outbox0)}.
|
||||
|
||||
-spec reset_reader_after_prune(outbox()) -> outbox().
|
||||
reset_reader_after_prune(Outbox = #outbox{
|
||||
reader = #outbox_reader{segment = ReadSegment},
|
||||
segments = Segments
|
||||
}) ->
|
||||
case ReadSegment =:= undefined orelse lists:keymember(ReadSegment, #segment.id, Segments) of
|
||||
true ->
|
||||
Outbox;
|
||||
false ->
|
||||
case reset_reader(Outbox) of
|
||||
{ok, NOutbox} ->
|
||||
NOutbox
|
||||
end
|
||||
end.
|
||||
|
||||
-spec locate_reader(outbox()) -> {undefined | pos_integer(), non_neg_integer()}.
|
||||
locate_reader(#outbox{
|
||||
segments = Segments,
|
||||
checkpoint = #outbox_checkpoint{acked_seq = AckedSeq}
|
||||
}) ->
|
||||
NextSeq = AckedSeq + 1,
|
||||
case lists:dropwhile(fun(#segment{end_seq = EndSeq}) -> EndSeq < NextSeq end, Segments) of
|
||||
[] ->
|
||||
{undefined, 0};
|
||||
[#segment{id = SegmentId, start_seq = StartSeq} = Segment | _] ->
|
||||
case NextSeq =< StartSeq of
|
||||
true ->
|
||||
{SegmentId, 0};
|
||||
false ->
|
||||
case locate_offset_for_seq(Segment, NextSeq) of
|
||||
{ok, Offset} ->
|
||||
{SegmentId, Offset};
|
||||
{error, _} ->
|
||||
{SegmentId, 0}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
-spec locate_offset_for_seq(#segment{}, pos_integer()) -> {ok, non_neg_integer()} | {error, term()}.
|
||||
locate_offset_for_seq(#segment{path = Path}, TargetSeq) ->
|
||||
case file:open(Path, [read, raw, binary]) of
|
||||
{ok, Fd} ->
|
||||
try
|
||||
locate_offset_loop(Fd, TargetSeq, 0)
|
||||
after
|
||||
ok = file:close(Fd)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec locate_offset_loop(file:fd(), pos_integer(), non_neg_integer()) ->
|
||||
{ok, non_neg_integer()} | {error, term()}.
|
||||
locate_offset_loop(Fd, TargetSeq, Offset) ->
|
||||
case read_next_record(Fd, Offset) of
|
||||
eof ->
|
||||
{ok, Offset};
|
||||
{ok, RecordOffset, Seq, _Payload, _NextOffset} when Seq >= TargetSeq ->
|
||||
{ok, RecordOffset};
|
||||
{ok, _RecordOffset, _Seq, _Payload, NextOffset} ->
|
||||
locate_offset_loop(Fd, TargetSeq, NextOffset);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec read_next_record(file:fd(), non_neg_integer()) ->
|
||||
eof | {ok, non_neg_integer(), pos_integer(), binary(), non_neg_integer()} | {error, term()}.
|
||||
read_next_record(Fd, Offset) ->
|
||||
case read_record_at(Fd, Offset) of
|
||||
eof ->
|
||||
eof;
|
||||
{ok, Seq, Payload, NextOffset} ->
|
||||
{ok, Offset, Seq, Payload, NextOffset};
|
||||
{error, _Reason} ->
|
||||
resync_next_record(Fd, Offset + 1)
|
||||
end.
|
||||
|
||||
-spec read_record_at(file:fd(), non_neg_integer()) ->
|
||||
eof | {ok, pos_integer(), binary(), non_neg_integer()} | {error, term()}.
|
||||
read_record_at(Fd, Offset) ->
|
||||
maybe
|
||||
{ok, _} ?= file:position(Fd, Offset),
|
||||
read_record(Fd, Offset)
|
||||
else
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec read_record(file:fd(), non_neg_integer()) ->
|
||||
eof | {ok, pos_integer(), binary(), non_neg_integer()} | {error, term()}.
|
||||
read_record(Fd, Offset) ->
|
||||
case file:read(Fd, ?RECORD_HEADER_BYTES) of
|
||||
eof ->
|
||||
eof;
|
||||
{ok, <<
|
||||
Magic:8/binary,
|
||||
Version:8,
|
||||
Seq:64/unsigned-big-integer,
|
||||
PayloadSize:32/unsigned-big-integer
|
||||
>>} when Magic =:= ?RECORD_MAGIC, Version =:= ?RECORD_VERSION ->
|
||||
maybe
|
||||
ok ?= validate_read_payload_size(PayloadSize),
|
||||
read_record_body(Fd, Offset, Seq, PayloadSize)
|
||||
else
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{ok, Header} when byte_size(Header) < ?RECORD_HEADER_BYTES ->
|
||||
{error, truncated_header};
|
||||
{ok, _} ->
|
||||
{error, invalid_record_header};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec validate_read_payload_size(non_neg_integer()) -> ok | {error, payload_too_large}.
|
||||
validate_read_payload_size(PayloadSize) when PayloadSize =< ?MAX_PAYLOAD_BYTES ->
|
||||
ok;
|
||||
validate_read_payload_size(_PayloadSize) ->
|
||||
{error, payload_too_large}.
|
||||
|
||||
-spec read_record_body(file:fd(), non_neg_integer(), pos_integer(), non_neg_integer()) ->
|
||||
{ok, pos_integer(), binary(), non_neg_integer()} | {error, term()}.
|
||||
read_record_body(Fd, Offset, Seq, PayloadSize) ->
|
||||
case file:read(Fd, PayloadSize + ?RECORD_FOOTER_BYTES) of
|
||||
{ok, <<Payload:PayloadSize/binary, StoredCrc:32/unsigned-big-integer>>} ->
|
||||
ExpectedCrc = record_crc(?RECORD_VERSION, Seq, PayloadSize, Payload),
|
||||
case StoredCrc =:= ExpectedCrc of
|
||||
true ->
|
||||
{ok, Seq, Payload, Offset + ?RECORD_OVERHEAD_BYTES + PayloadSize};
|
||||
false ->
|
||||
{error, invalid_record_crc}
|
||||
end;
|
||||
eof ->
|
||||
{error, truncated_payload};
|
||||
{ok, _} ->
|
||||
{error, truncated_payload};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec record_crc(byte(), pos_integer(), non_neg_integer(), binary()) -> non_neg_integer().
|
||||
record_crc(Version, Seq, PayloadSize, Payload) ->
|
||||
erlang:crc32(<<
|
||||
Version:8,
|
||||
Seq:64/unsigned-big-integer,
|
||||
PayloadSize:32/unsigned-big-integer,
|
||||
Payload/binary
|
||||
>>).
|
||||
|
||||
-spec resync_next_record(file:fd(), non_neg_integer()) ->
|
||||
eof | {ok, non_neg_integer(), pos_integer(), binary(), non_neg_integer()} | {error, term()}.
|
||||
resync_next_record(Fd, SearchOffset) ->
|
||||
case find_next_magic(Fd, SearchOffset) of
|
||||
eof ->
|
||||
eof;
|
||||
{ok, RecordOffset} ->
|
||||
case read_record_at(Fd, RecordOffset) of
|
||||
eof ->
|
||||
eof;
|
||||
{ok, Seq, Payload, NextOffset} ->
|
||||
{ok, RecordOffset, Seq, Payload, NextOffset};
|
||||
{error, _Reason} ->
|
||||
resync_next_record(Fd, RecordOffset + 1)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec find_next_magic(file:fd(), non_neg_integer()) ->
|
||||
eof | {ok, non_neg_integer()} | {error, term()}.
|
||||
find_next_magic(Fd, Offset) ->
|
||||
maybe
|
||||
{ok, _} ?= file:position(Fd, Offset),
|
||||
find_next_magic_loop(Fd, Offset, <<>>)
|
||||
else
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec find_next_magic_loop(file:fd(), non_neg_integer(), binary()) ->
|
||||
eof | {ok, non_neg_integer()} | {error, term()}.
|
||||
find_next_magic_loop(Fd, Offset, Tail) ->
|
||||
case file:read(Fd, ?RESYNC_READ_BYTES) of
|
||||
eof ->
|
||||
eof;
|
||||
{ok, Bin} ->
|
||||
SearchBin = <<Tail/binary, Bin/binary>>,
|
||||
BaseOffset = Offset - byte_size(Tail),
|
||||
case binary:match(SearchBin, ?RECORD_MAGIC) of
|
||||
{Pos, _Len} ->
|
||||
{ok, BaseOffset + Pos};
|
||||
nomatch ->
|
||||
NextTail = trailing_bytes(SearchBin, byte_size(?RECORD_MAGIC) - 1),
|
||||
find_next_magic_loop(Fd, Offset + byte_size(Bin), NextTail)
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec trailing_bytes(binary(), non_neg_integer()) -> binary().
|
||||
trailing_bytes(Bin, KeepBytes) ->
|
||||
Size = byte_size(Bin),
|
||||
case Size =< KeepBytes of
|
||||
true ->
|
||||
Bin;
|
||||
false ->
|
||||
binary:part(Bin, Size - KeepBytes, KeepBytes)
|
||||
end.
|
||||
|
||||
-spec next_segment_id([#segment{}], pos_integer()) -> undefined | pos_integer().
|
||||
next_segment_id(Segments, SegmentId) ->
|
||||
case lists:dropwhile(fun(#segment{id = Id}) -> Id =< SegmentId end, Segments) of
|
||||
[#segment{id = NextSegmentId} | _] ->
|
||||
NextSegmentId;
|
||||
[] ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec segment_path(file:filename_all(), pos_integer()) -> file:filename_all().
|
||||
segment_path(Dir, SegmentId) ->
|
||||
filename:join(Dir, io_lib:format("~6..0B~s", [SegmentId, ?SEGMENT_EXT])).
|
||||
|
||||
-spec open_fds(outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
open_fds(Outbox) ->
|
||||
case maybe_open_write_fd(Outbox) of
|
||||
{ok, Outbox0} ->
|
||||
maybe
|
||||
{ok, NOutbox} ?= maybe_open_read_fd(Outbox0),
|
||||
{ok, NOutbox}
|
||||
else
|
||||
{error, Reason} ->
|
||||
_ = close_open_fds(Outbox0),
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec maybe_open_write_fd(outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
maybe_open_write_fd(Outbox = #outbox{
|
||||
segments = Segments,
|
||||
writer = #outbox_writer{
|
||||
segment = WriteSegment,
|
||||
fd = undefined
|
||||
}
|
||||
}) ->
|
||||
case find_segment(Segments, WriteSegment) of
|
||||
false ->
|
||||
{ok, Outbox};
|
||||
_ ->
|
||||
ensure_write_fd(WriteSegment, Outbox)
|
||||
end.
|
||||
|
||||
-spec maybe_open_read_fd(outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
maybe_open_read_fd(Outbox = #outbox{reader = #outbox_reader{segment = undefined}}) ->
|
||||
{ok, Outbox};
|
||||
maybe_open_read_fd(Outbox = #outbox{reader = #outbox_reader{fd = undefined}}) ->
|
||||
ensure_read_fd(Outbox);
|
||||
maybe_open_read_fd(Outbox = #outbox{}) ->
|
||||
{ok, Outbox}.
|
||||
|
||||
-spec ensure_write_fd(pos_integer(), outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
ensure_write_fd(SegmentId, Outbox = #outbox{
|
||||
writer = #outbox_writer{
|
||||
segment = SegmentId,
|
||||
fd = Fd
|
||||
}
|
||||
})
|
||||
when Fd =/= undefined ->
|
||||
{ok, Outbox};
|
||||
ensure_write_fd(SegmentId, Outbox = #outbox{dir = Dir}) ->
|
||||
Writer0 = (Outbox#outbox.writer)#outbox_writer{segment = SegmentId},
|
||||
Outbox0 = close_write_fd(Outbox#outbox{writer = Writer0}),
|
||||
Path = segment_path(Dir, SegmentId),
|
||||
case file:open(Path, [append, raw, binary]) of
|
||||
{ok, Fd} ->
|
||||
Writer1 = (Outbox0#outbox.writer)#outbox_writer{fd = Fd},
|
||||
{ok, Outbox0#outbox{writer = Writer1}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec ensure_read_fd(outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
ensure_read_fd(Outbox = #outbox{reader = #outbox_reader{segment = undefined}}) ->
|
||||
{ok, Outbox};
|
||||
ensure_read_fd(Outbox = #outbox{reader = #outbox_reader{fd = Fd}}) when Fd =/= undefined ->
|
||||
{ok, Outbox};
|
||||
ensure_read_fd(Outbox = #outbox{
|
||||
reader = Reader,
|
||||
dir = Dir
|
||||
}) ->
|
||||
SegmentId = Reader#outbox_reader.segment,
|
||||
Offset = Reader#outbox_reader.offset,
|
||||
Path = segment_path(Dir, SegmentId),
|
||||
case open_read_fd(Path, Offset) of
|
||||
{ok, Fd} ->
|
||||
{ok, Outbox#outbox{
|
||||
reader = Reader#outbox_reader{fd = Fd}
|
||||
}};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec switch_read_segment(undefined | pos_integer(), non_neg_integer(), outbox()) ->
|
||||
{ok, outbox()} | {error, term()}.
|
||||
switch_read_segment(undefined, _Offset, Outbox) ->
|
||||
Reader0 = #outbox_reader{segment = undefined, offset = 0},
|
||||
{ok, close_read_fd(Outbox#outbox{reader = Reader0})};
|
||||
switch_read_segment(SegmentId, Offset, Outbox = #outbox{reader = Reader}) ->
|
||||
ensure_read_fd(close_read_fd(Outbox#outbox{
|
||||
reader = Reader#outbox_reader{
|
||||
segment = SegmentId,
|
||||
offset = Offset
|
||||
}
|
||||
})).
|
||||
|
||||
-spec maybe_reset_reader_after_append(outbox(), outbox()) -> {ok, outbox()} | {error, term()}.
|
||||
maybe_reset_reader_after_append(#outbox{reader = #outbox_reader{segment = undefined}}, Outbox) ->
|
||||
reset_reader(Outbox);
|
||||
maybe_reset_reader_after_append(_PrevOutbox, Outbox) ->
|
||||
{ok, Outbox}.
|
||||
|
||||
-spec detach_deleted_fds([#segment{}], outbox()) -> outbox().
|
||||
detach_deleted_fds(DeleteSegments, Outbox = #outbox{
|
||||
reader = #outbox_reader{segment = ReadSegment},
|
||||
writer = #outbox_writer{segment = WriteSegment}
|
||||
}) ->
|
||||
DeleteIds = [SegmentId || #segment{id = SegmentId} <- DeleteSegments],
|
||||
Outbox0 = case lists:member(ReadSegment, DeleteIds) of
|
||||
true ->
|
||||
close_read_fd(Outbox);
|
||||
false ->
|
||||
Outbox
|
||||
end,
|
||||
case lists:member(WriteSegment, DeleteIds) of
|
||||
true ->
|
||||
close_write_fd(Outbox0);
|
||||
false ->
|
||||
Outbox0
|
||||
end.
|
||||
|
||||
-spec close_open_fds(outbox()) -> outbox().
|
||||
close_open_fds(Outbox) ->
|
||||
close_write_fd(close_read_fd(Outbox)).
|
||||
|
||||
-spec close_read_fd(outbox()) -> outbox().
|
||||
close_read_fd(Outbox = #outbox{reader = #outbox_reader{fd = undefined}}) ->
|
||||
Outbox;
|
||||
close_read_fd(Outbox = #outbox{reader = Reader}) ->
|
||||
Fd = Reader#outbox_reader.fd,
|
||||
_ = maybe_close_fd(Fd),
|
||||
Outbox#outbox{reader = Reader#outbox_reader{fd = undefined}}.
|
||||
|
||||
-spec close_write_fd(outbox()) -> outbox().
|
||||
close_write_fd(Outbox = #outbox{writer = #outbox_writer{fd = undefined}}) ->
|
||||
Outbox;
|
||||
close_write_fd(Outbox = #outbox{writer = Writer}) ->
|
||||
Fd = Writer#outbox_writer.fd,
|
||||
_ = maybe_close_fd(Fd),
|
||||
Outbox#outbox{writer = Writer#outbox_writer{fd = undefined}}.
|
||||
|
||||
-spec maybe_close_fd(file:fd()) -> ok.
|
||||
maybe_close_fd(Fd) ->
|
||||
case file:close(Fd) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, badarg} ->
|
||||
ok;
|
||||
{error, terminated} ->
|
||||
ok;
|
||||
{error, _Reason} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec maybe_persist_metadata(boolean(), outbox()) -> ok | {error, term()}.
|
||||
maybe_persist_metadata(true, Outbox) ->
|
||||
persist_metadata(Outbox);
|
||||
maybe_persist_metadata(false, _Outbox) ->
|
||||
ok.
|
||||
|
||||
-spec open_read_fd(file:filename_all(), non_neg_integer()) -> {ok, file:fd()} | {error, term()}.
|
||||
open_read_fd(Path, Offset) ->
|
||||
case file:open(Path, [read, raw, binary]) of
|
||||
{ok, Fd} ->
|
||||
maybe
|
||||
{ok, _} ?= file:position(Fd, Offset),
|
||||
{ok, Fd}
|
||||
else
|
||||
{error, Reason} ->
|
||||
ok = file:close(Fd),
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
457
apps/endpoint/src/endpoint_subscription.erl
Normal file
457
apps/endpoint/src/endpoint_subscription.erl
Normal file
@ -0,0 +1,457 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%% Endpoint 数据分发订阅索引。
|
||||
%%%
|
||||
%%% 当前实现面向较大订阅量场景,publish 热路径不再全量遍历订阅表,而是把
|
||||
%%% 订阅拆成 exact 和 wildcard 两类:
|
||||
%%%
|
||||
%%% <ul>
|
||||
%%% <li>精确订阅写入 endpoint_subscription_exact,publish 时按 RouteKey
|
||||
%%% 直接 ets:lookup/2。</li>
|
||||
%%% <li>包含通配符的订阅写入 ETS trie。trie 边存放在
|
||||
%%% endpoint_subscription_trie_edge,节点订阅存放在
|
||||
%%% endpoint_subscription_trie_sub。</li>
|
||||
%%% <li>endpoint_subscription_reverse 保存 SubscriberPid 到订阅对象的反向索引,
|
||||
%%% 用于 unsubscribe 和进程 DOWN 清理。</li>
|
||||
%%% <li>endpoint_subscription_pid 记录每个 SubscriberPid 的 monitor 和订阅计数,
|
||||
%%% 同一个 pid 只 monitor 一次。</li>
|
||||
%%% </ul>
|
||||
%%%
|
||||
%%% 所有 matcher 共用同一个 root node,root node id 固定为 0。新 trie 节点使用
|
||||
%%% 单调递增的 integer node id。subscribe/unsubscribe 通过本 gen_server 串行写 ETS;
|
||||
%%% publish/2 直接并发读取 ETS,避免中心 gen_server 成为数据分发瓶颈。
|
||||
%%%
|
||||
%%% 匹配规则:
|
||||
%%%
|
||||
%%% <ul>
|
||||
%%% <li>普通 segment 必须完全相等,例如 <<"device/a/temp">> 只精确匹配
|
||||
%%% 同名订阅。</li>
|
||||
%%% <li><<"*">> 是单级通配,只匹配一个 segment。例如
|
||||
%%% <<"device/*/temp">> 匹配 <<"device/a/temp">>,不匹配
|
||||
%%% <<"device/a/b/temp">>。</li>
|
||||
%%% <li><<"+">> 是末尾多级通配,只允许出现在 matcher 最后一段,并且至少
|
||||
%%% 匹配一个剩余 segment。例如 <<"device/+">> 匹配 <<"device/a">>
|
||||
%%% 和 <<"device/a/temp">>,不匹配 <<"device">>。</li>
|
||||
%%% </ul>
|
||||
%%%
|
||||
%%% publish/2 的匹配流程:
|
||||
%%%
|
||||
%%% <ol>
|
||||
%%% <li>从 exact 表直接查找 RouteKey 对应的订阅。</li>
|
||||
%%% <li>把 RouteKey 按 "/" 拆分后,从 trie root 开始逐层匹配。每层最多查找
|
||||
%%% 当前 segment 分支和 <<"*">> 分支。</li>
|
||||
%%% <li>每消费一层前,收集当前候选节点上的 plus 订阅;全部 segment 消费完后,
|
||||
%%% 收集候选节点上的 exact 订阅。</li>
|
||||
%%% <li>合并 exact 和 trie 命中结果,按原有优先级排序,再按 SubscriberPid 去重。
|
||||
%%% 同一个 pid 同时被多个 matcher 命中时,保留优先级最高的订阅。</li>
|
||||
%%% <li>对最终订阅者执行 endpoint:forward/2。</li>
|
||||
%%% </ol>
|
||||
%%% @end
|
||||
%%% Created : 07. 11月 2025 16:27
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_subscription).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([subscribe/2, unsubscribe/2, publish/2, get_subscribers/0]).
|
||||
-export([is_valid_components/1, of_components/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(EXACT_TAB, endpoint_subscription_exact).
|
||||
-define(TRIE_EDGE_TAB, endpoint_subscription_trie_edge).
|
||||
-define(TRIE_SUB_TAB, endpoint_subscription_trie_sub).
|
||||
-define(REVERSE_TAB, endpoint_subscription_reverse).
|
||||
-define(PID_TAB, endpoint_subscription_pid).
|
||||
-define(ROOT_NODE, 0).
|
||||
|
||||
%% 定义订阅者
|
||||
-record(subscriber, {
|
||||
topic :: binary(),
|
||||
subscriber_pid :: pid(),
|
||||
components = [],
|
||||
monitor_ref :: undefined | reference(),
|
||||
%% 优先级
|
||||
%% 1. 完全匹配的topic优先级别最高
|
||||
%% 2. 带 * 的订阅
|
||||
%% 3. 带 + 的订阅
|
||||
order :: integer()
|
||||
}).
|
||||
|
||||
-record(state, {
|
||||
exact_tid :: ets:tid(),
|
||||
edge_tid :: ets:tid(),
|
||||
trie_sub_tid :: ets:tid(),
|
||||
reverse_tid :: ets:tid(),
|
||||
pid_tid :: ets:tid(),
|
||||
next_node_id = 1 :: pos_integer()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec subscribe(Topic :: binary(), SubscriberPid :: pid()) -> ok | {error, Reason :: binary()}.
|
||||
subscribe(Topic, SubscriberPid) when is_binary(Topic), is_pid(SubscriberPid) ->
|
||||
gen_server:call(?SERVER, {subscribe, Topic, SubscriberPid}).
|
||||
|
||||
-spec unsubscribe(Topic :: binary(), SubscriberPid :: pid()) -> ok.
|
||||
unsubscribe(Topic, SubscriberPid) when is_binary(Topic), is_pid(SubscriberPid) ->
|
||||
gen_server:call(?SERVER, {unsubscribe, Topic, SubscriberPid}).
|
||||
|
||||
-spec get_subscribers() -> {ok, Subscribers :: list()}.
|
||||
get_subscribers() ->
|
||||
gen_server:call(?SERVER, get_subscribers).
|
||||
|
||||
-spec publish(RouteKey :: binary(), Content :: binary()) -> ok.
|
||||
publish(RouteKey, Content) when is_binary(RouteKey), is_binary(Content) ->
|
||||
case ets:info(?EXACT_TAB) of
|
||||
undefined ->
|
||||
ok;
|
||||
_ ->
|
||||
MatchedSubscribers = match_route_key(RouteKey),
|
||||
lists:foreach(fun(#subscriber{subscriber_pid = SubscriberPid}) ->
|
||||
endpoint:forward(SubscriberPid, Content)
|
||||
end, MatchedSubscribers),
|
||||
maybe_log_unmatched_publish(RouteKey, Content, MatchedSubscribers),
|
||||
logger:debug("[endpoint_subscription] route_key: ~p, match_count: ~p", [RouteKey, length(MatchedSubscribers)]),
|
||||
ok
|
||||
end.
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
ok = endpoint_util:set_metadata(),
|
||||
ExactTid = ets:new(?EXACT_TAB, [named_table, protected, bag, {read_concurrency, true}]),
|
||||
EdgeTid = ets:new(?TRIE_EDGE_TAB, [named_table, protected, set, {read_concurrency, true}]),
|
||||
TrieSubTid = ets:new(?TRIE_SUB_TAB, [named_table, protected, bag, {read_concurrency, true}]),
|
||||
ReverseTid = ets:new(?REVERSE_TAB, [named_table, protected, bag]),
|
||||
PidTid = ets:new(?PID_TAB, [named_table, protected, set]),
|
||||
{ok, #state{
|
||||
exact_tid = ExactTid,
|
||||
edge_tid = EdgeTid,
|
||||
trie_sub_tid = TrieSubTid,
|
||||
reverse_tid = ReverseTid,
|
||||
pid_tid = PidTid
|
||||
}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
%% 同一个SubscriberPid只能订阅同一个topic一次
|
||||
handle_call(get_subscribers, _From, State = #state{exact_tid = ExactTid, trie_sub_tid = TrieSubTid}) ->
|
||||
Subscribers = exact_subscribers(ExactTid) ++ trie_subscribers(TrieSubTid),
|
||||
{reply, {ok, Subscribers}, State};
|
||||
|
||||
handle_call({subscribe, Topic, SubscriberPid}, _From, State = #state{reverse_tid = ReverseTid}) ->
|
||||
Components = of_components(Topic),
|
||||
case is_valid_components(Components) of
|
||||
true ->
|
||||
case has_subscription(ReverseTid, Topic, SubscriberPid) of
|
||||
true ->
|
||||
{reply, ok, State};
|
||||
false ->
|
||||
{MonitorRef, State1} = ensure_pid_monitor(SubscriberPid, State),
|
||||
Sub = #subscriber{
|
||||
topic = Topic,
|
||||
subscriber_pid = SubscriberPid,
|
||||
components = Components,
|
||||
monitor_ref = MonitorRef,
|
||||
order = order_num(Components)
|
||||
},
|
||||
State2 = insert_subscription(Topic, Components, SubscriberPid, Sub, State1),
|
||||
{reply, ok, State2}
|
||||
end;
|
||||
false ->
|
||||
{reply, {error, <<"invalid topic name">>}, State}
|
||||
end;
|
||||
|
||||
handle_call({unsubscribe, Topic, SubscriberPid}, _From, State = #state{reverse_tid = ReverseTid}) ->
|
||||
Removed = [Reverse || Reverse = {SubscriberPid0, Topic0, _Kind, _NodeId, _Sub} <- ets:lookup(ReverseTid, SubscriberPid),
|
||||
SubscriberPid =:= SubscriberPid0, Topic =:= Topic0],
|
||||
lists:foreach(fun(Reverse) -> delete_subscription(Reverse, State) end, Removed),
|
||||
State1 = release_pid_monitor(SubscriberPid, length(Removed), State),
|
||||
{reply, ok, State1}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({'DOWN', MonitorRef, process, SubscriberPid, Reason}, State = #state{reverse_tid = ReverseTid, pid_tid = PidTid}) ->
|
||||
logger:debug("[endpoint_subscription] subscriber: ~p, down with reason: ~p", [SubscriberPid, Reason]),
|
||||
Removed = ets:lookup(ReverseTid, SubscriberPid),
|
||||
lists:foreach(fun(Reverse) -> delete_subscription(Reverse, State) end, Removed),
|
||||
case ets:lookup(PidTid, SubscriberPid) of
|
||||
[{SubscriberPid, MonitorRef, _Count}] ->
|
||||
ets:delete(PidTid, SubscriberPid);
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State = #state{}) ->
|
||||
logger:debug("[endpoint_subscription] get unknown info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec match_route_key(binary()) -> [#subscriber{}].
|
||||
match_route_key(RouteKey) when is_binary(RouteKey) ->
|
||||
ExactSubs = [Sub || {_Topic, Sub} <- ets:lookup(?EXACT_TAB, RouteKey)],
|
||||
TrieSubs = match_trie(of_components(RouteKey)),
|
||||
Sorted = lists:sort(fun compare_subscriber/2, ExactSubs ++ TrieSubs),
|
||||
dedupe_subscribers(Sorted).
|
||||
|
||||
-spec maybe_log_unmatched_publish(binary(), binary(), [#subscriber{}]) -> ok.
|
||||
maybe_log_unmatched_publish(RouteKey, Content, []) ->
|
||||
endpoint_log:unmatched_publish(RouteKey, Content);
|
||||
maybe_log_unmatched_publish(_RouteKey, _Content, _MatchedSubscribers) ->
|
||||
ok.
|
||||
|
||||
-spec of_components(Topic :: binary()) -> [binary()].
|
||||
of_components(Topic) when is_binary(Topic) ->
|
||||
binary:split(Topic, <<$/>>, [global]).
|
||||
|
||||
is_valid_components([]) ->
|
||||
true;
|
||||
is_valid_components([<<$+>>|T]) ->
|
||||
length(T) =:= 0;
|
||||
is_valid_components([<<$*>>|T]) ->
|
||||
is_valid_components(T);
|
||||
is_valid_components([_|T]) ->
|
||||
is_valid_components(T).
|
||||
|
||||
-spec order_num(Components :: list()) -> integer().
|
||||
order_num([]) ->
|
||||
1;
|
||||
order_num([<<$*>>|_]) ->
|
||||
2;
|
||||
order_num([<<$+>>|_]) ->
|
||||
3;
|
||||
order_num([_|Tail]) ->
|
||||
order_num(Tail).
|
||||
|
||||
-spec has_subscription(ets:tid(), binary(), pid()) -> boolean().
|
||||
has_subscription(ReverseTid, Topic, SubscriberPid) ->
|
||||
Subscriptions = ets:lookup(ReverseTid, SubscriberPid),
|
||||
lists:any(fun({SubscriberPid0, Topic0, _Kind, _NodeId, _Sub}) ->
|
||||
Topic =:= Topic0 andalso SubscriberPid =:= SubscriberPid0
|
||||
end, Subscriptions).
|
||||
|
||||
-spec compare_subscriber(#subscriber{}, #subscriber{}) -> boolean().
|
||||
compare_subscriber(#subscriber{order = Order0, topic = Topic0}, #subscriber{order = Order1, topic = Topic1}) ->
|
||||
case Order0 =:= Order1 of
|
||||
true ->
|
||||
Topic0 =< Topic1;
|
||||
false ->
|
||||
Order0 < Order1
|
||||
end.
|
||||
|
||||
-spec dedupe_subscribers([#subscriber{}]) -> [#subscriber{}].
|
||||
dedupe_subscribers(Subscribers) ->
|
||||
{_, Result} = lists:foldl(fun(S = #subscriber{subscriber_pid = SubscriberPid}, {Seen, Acc}) ->
|
||||
case sets:is_element(SubscriberPid, Seen) of
|
||||
true ->
|
||||
{Seen, Acc};
|
||||
false ->
|
||||
{sets:add_element(SubscriberPid, Seen), [S | Acc]}
|
||||
end
|
||||
end, {sets:new(), []}, Subscribers),
|
||||
lists:reverse(Result).
|
||||
|
||||
-spec insert_subscription(binary(), [binary()], pid(), #subscriber{}, #state{}) -> #state{}.
|
||||
insert_subscription(Topic, Components, SubscriberPid, Sub, State = #state{exact_tid = ExactTid, trie_sub_tid = TrieSubTid, reverse_tid = ReverseTid}) ->
|
||||
case has_wildcard(Components) of
|
||||
false ->
|
||||
true = ets:insert(ExactTid, {Topic, Sub}),
|
||||
true = ets:insert(ReverseTid, {SubscriberPid, Topic, exact, undefined, Sub}),
|
||||
State;
|
||||
true ->
|
||||
{Kind, NodeId, State1} = insert_trie_subscription(Components, State),
|
||||
true = ets:insert(TrieSubTid, {{NodeId, subscription_match_type(Kind)}, Sub}),
|
||||
true = ets:insert(ReverseTid, {SubscriberPid, Topic, Kind, NodeId, Sub}),
|
||||
State1
|
||||
end.
|
||||
|
||||
-spec insert_trie_subscription([binary()], #state{}) -> {trie_exact | trie_plus, non_neg_integer(), #state{}}.
|
||||
insert_trie_subscription(Components, State) ->
|
||||
case Components of
|
||||
[] ->
|
||||
{trie_exact, ?ROOT_NODE, State};
|
||||
_ ->
|
||||
case lists:last(Components) of
|
||||
<<"+">> ->
|
||||
Prefix = lists:sublist(Components, length(Components) - 1),
|
||||
{NodeId, State1} = ensure_path(Prefix, State),
|
||||
{trie_plus, NodeId, State1};
|
||||
_ ->
|
||||
{NodeId, State1} = ensure_path(Components, State),
|
||||
{trie_exact, NodeId, State1}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec subscription_match_type(trie_exact | trie_plus) -> exact | plus.
|
||||
subscription_match_type(trie_exact) ->
|
||||
exact;
|
||||
subscription_match_type(trie_plus) ->
|
||||
plus.
|
||||
|
||||
-spec ensure_path([binary()], #state{}) -> {non_neg_integer(), #state{}}.
|
||||
ensure_path(Components, State) ->
|
||||
ensure_path(?ROOT_NODE, Components, State).
|
||||
|
||||
-spec ensure_path(non_neg_integer(), [binary()], #state{}) -> {non_neg_integer(), #state{}}.
|
||||
ensure_path(NodeId, [], State) ->
|
||||
{NodeId, State};
|
||||
ensure_path(NodeId, [Segment | Rest], State = #state{edge_tid = EdgeTid, next_node_id = NextNodeId}) ->
|
||||
EdgeKey = {NodeId, Segment},
|
||||
case ets:lookup(EdgeTid, EdgeKey) of
|
||||
[{EdgeKey, ChildNodeId}] ->
|
||||
ensure_path(ChildNodeId, Rest, State);
|
||||
[] ->
|
||||
true = ets:insert(EdgeTid, {EdgeKey, NextNodeId}),
|
||||
ensure_path(NextNodeId, Rest, State#state{next_node_id = NextNodeId + 1})
|
||||
end.
|
||||
|
||||
-spec delete_subscription(tuple(), #state{}) -> ok.
|
||||
delete_subscription(Reverse = {_SubscriberPid, Topic, exact, undefined, Sub}, #state{exact_tid = ExactTid, reverse_tid = ReverseTid}) ->
|
||||
true = ets:delete_object(ExactTid, {Topic, Sub}),
|
||||
true = ets:delete_object(ReverseTid, Reverse),
|
||||
ok;
|
||||
delete_subscription(Reverse = {_SubscriberPid, _Topic, Kind, NodeId, Sub}, #state{trie_sub_tid = TrieSubTid, reverse_tid = ReverseTid}) ->
|
||||
true = ets:delete_object(TrieSubTid, {{NodeId, subscription_match_type(Kind)}, Sub}),
|
||||
true = ets:delete_object(ReverseTid, Reverse),
|
||||
ok.
|
||||
|
||||
-spec ensure_pid_monitor(pid(), #state{}) -> {reference(), #state{}}.
|
||||
ensure_pid_monitor(SubscriberPid, State = #state{pid_tid = PidTid}) ->
|
||||
case ets:lookup(PidTid, SubscriberPid) of
|
||||
[{SubscriberPid, MonitorRef, Count}] ->
|
||||
true = ets:insert(PidTid, {SubscriberPid, MonitorRef, Count + 1}),
|
||||
{MonitorRef, State};
|
||||
[] ->
|
||||
MonitorRef = erlang:monitor(process, SubscriberPid),
|
||||
true = ets:insert(PidTid, {SubscriberPid, MonitorRef, 1}),
|
||||
{MonitorRef, State}
|
||||
end.
|
||||
|
||||
-spec release_pid_monitor(pid(), non_neg_integer(), #state{}) -> #state{}.
|
||||
release_pid_monitor(_SubscriberPid, 0, State) ->
|
||||
State;
|
||||
release_pid_monitor(SubscriberPid, RemovedCount, State = #state{pid_tid = PidTid}) ->
|
||||
case ets:lookup(PidTid, SubscriberPid) of
|
||||
[{SubscriberPid, MonitorRef, Count}] when Count =< RemovedCount ->
|
||||
erlang:demonitor(MonitorRef, [flush]),
|
||||
ets:delete(PidTid, SubscriberPid),
|
||||
State;
|
||||
[{SubscriberPid, MonitorRef, Count}] ->
|
||||
true = ets:insert(PidTid, {SubscriberPid, MonitorRef, Count - RemovedCount}),
|
||||
State;
|
||||
[] ->
|
||||
State
|
||||
end.
|
||||
|
||||
-spec match_trie([binary()]) -> [#subscriber{}].
|
||||
match_trie(Components) ->
|
||||
match_trie([?ROOT_NODE], Components, []).
|
||||
|
||||
-spec match_trie([non_neg_integer()], [binary()], [#subscriber{}]) -> [#subscriber{}].
|
||||
match_trie(NodeIds, [], Acc) ->
|
||||
collect_trie_subscribers(NodeIds, exact) ++ Acc;
|
||||
match_trie(NodeIds, [Segment | Rest], Acc) ->
|
||||
PlusSubs = collect_trie_subscribers(NodeIds, plus),
|
||||
NextNodeIds = next_trie_nodes(NodeIds, Segment),
|
||||
match_trie(NextNodeIds, Rest, PlusSubs ++ Acc).
|
||||
|
||||
-spec collect_trie_subscribers([non_neg_integer()], exact | plus) -> [#subscriber{}].
|
||||
collect_trie_subscribers(NodeIds, MatchType) ->
|
||||
lists:flatmap(fun(NodeId) ->
|
||||
[Sub || {{NodeId0, MatchType0}, Sub} <- ets:lookup(?TRIE_SUB_TAB, {NodeId, MatchType}),
|
||||
NodeId0 =:= NodeId, MatchType0 =:= MatchType]
|
||||
end, NodeIds).
|
||||
|
||||
-spec next_trie_nodes([non_neg_integer()], binary()) -> [non_neg_integer()].
|
||||
next_trie_nodes(NodeIds, Segment) ->
|
||||
lists:usort(lists:flatmap(fun(NodeId) ->
|
||||
lookup_child(NodeId, Segment) ++ lookup_child(NodeId, <<"*">>)
|
||||
end, NodeIds)).
|
||||
|
||||
-spec lookup_child(non_neg_integer(), binary()) -> [non_neg_integer()].
|
||||
lookup_child(NodeId, Segment) ->
|
||||
EdgeKey = {NodeId, Segment},
|
||||
case ets:lookup(?TRIE_EDGE_TAB, EdgeKey) of
|
||||
[{EdgeKey, ChildNodeId}] ->
|
||||
[ChildNodeId];
|
||||
[] ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec has_wildcard([binary()]) -> boolean().
|
||||
has_wildcard(Components) ->
|
||||
lists:any(fun(Component) -> Component =:= <<"*">> orelse Component =:= <<"+">> end, Components).
|
||||
|
||||
-spec exact_subscribers(ets:tid()) -> [#subscriber{}].
|
||||
exact_subscribers(ExactTid) ->
|
||||
[Sub || {_Topic, Sub} <- ets:tab2list(ExactTid)].
|
||||
|
||||
-spec trie_subscribers(ets:tid()) -> [#subscriber{}].
|
||||
trie_subscribers(TrieSubTid) ->
|
||||
[Sub || {_Key, Sub} <- ets:tab2list(TrieSubTid)].
|
||||
58
apps/endpoint/src/endpoint_sup.erl
Normal file
58
apps/endpoint/src/endpoint_sup.erl
Normal file
@ -0,0 +1,58 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc endpoint top level supervisor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(endpoint_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
-include("endpoint.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%% sup_flags() = #{strategy => strategy(), % optional
|
||||
%% intensity => non_neg_integer(), % optional
|
||||
%% period => pos_integer()} % optional
|
||||
%% child_spec() = #{id => child_id(), % mandatory
|
||||
%% start => mfargs(), % mandatory
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_all, intensity => 1000, period => 3600},
|
||||
ChildSpecs = [
|
||||
#{
|
||||
id => endpoint_log,
|
||||
start => {'endpoint_log', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['endpoint_log']
|
||||
},
|
||||
|
||||
#{
|
||||
id => endpoint_subscription,
|
||||
start => {'endpoint_subscription', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['endpoint_subscription']
|
||||
},
|
||||
|
||||
#{
|
||||
id => 'endpoint_adapter_sup',
|
||||
start => {'endpoint_adapter_sup', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['endpoint_adapter_sup']
|
||||
}
|
||||
],
|
||||
{ok, {SupFlags, ChildSpecs}}.
|
||||
93
apps/endpoint/src/endpoint_tester.erl
Normal file
93
apps/endpoint/src/endpoint_tester.erl
Normal file
@ -0,0 +1,93 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 5月 2026 20:16
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_tester).
|
||||
-author("anlicheng").
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([test/1]).
|
||||
|
||||
%% http接口的测试
|
||||
test(#http_endpoint{url = Url, pool_size = PoolSize}) when is_integer(PoolSize), PoolSize > 0 ->
|
||||
Body = <<"">>,
|
||||
ContentType = "application/json",
|
||||
case httpc:request(post, {Url, [], ContentType, Body}, [], []) of
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
|
||||
%% 测试mqtt
|
||||
test(#mqtt_endpoint{host = Host, port = Port, username = Username, password = Password}) ->
|
||||
%% 用户测试的client
|
||||
ClientId = "mqtt_client_test:" ++ iot_util:rand_bytes(16),
|
||||
Opts = [
|
||||
{owner, self()},
|
||||
{clientid, ClientId},
|
||||
{host, binary_to_list(Host)},
|
||||
{port, Port},
|
||||
{tcp_opts, []},
|
||||
{username, binary_to_list(Username)},
|
||||
{password, binary_to_list(Password)},
|
||||
{keepalive, 86400},
|
||||
{auto_ack, true},
|
||||
{connect_timeout, 5000},
|
||||
{proto_ver, v5},
|
||||
{retry_interval, 5000}
|
||||
],
|
||||
|
||||
case emqtt:start_link(Opts) of
|
||||
{ok, ConnPid} ->
|
||||
case catch emqtt:connect(ConnPid, 5000) of
|
||||
{ok, _} ->
|
||||
emqtt:stop(ConnPid),
|
||||
ok;
|
||||
{error, _Reason} ->
|
||||
{error, <<"connect mqtt server failed">>};
|
||||
_Error ->
|
||||
emqtt:stop(ConnPid),
|
||||
{error, <<"connect mqtt server failed">>}
|
||||
end;
|
||||
Other ->
|
||||
logger:warning("[endpint_handler] test connect mqtt with options: ~p, get error: ~p", [Opts, Other]),
|
||||
{error, <<"connect mqtt server failed">>}
|
||||
end;
|
||||
|
||||
%% 测试kafka
|
||||
test(#kafka_endpoint{sasl_config = SaslConfig, bootstrap_servers = BootstrapServers, topic = Topic}) ->
|
||||
BaseConfig = [
|
||||
{reconnect_cool_down_seconds, 5},
|
||||
{socket_options, [{keepalive, true}]}
|
||||
],
|
||||
|
||||
ClientConfig = case SaslConfig of
|
||||
{Mechanism, Username, Password} ->
|
||||
[{sasl, {Mechanism, Username, Password}}|BaseConfig];
|
||||
undefined ->
|
||||
BaseConfig
|
||||
end,
|
||||
ClientId = brod_client_test,
|
||||
_ = catch brod:stop_client(ClientId),
|
||||
|
||||
case catch brod:start_link_client(BootstrapServers, ClientId, ClientConfig) of
|
||||
{ok, _ClientPid} ->
|
||||
case brod:start_producer(ClientId, Topic, _ProducerConfig = []) of
|
||||
ok ->
|
||||
ok = brod:stop_client(ClientId),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
logger:debug("[endpint_handler] start_producer: ~p, get error: ~p", [ClientId, Reason]),
|
||||
_ = catch brod:stop_client(ClientId),
|
||||
{error, <<"config kafka server failed">>}
|
||||
end;
|
||||
Error ->
|
||||
logger:debug("[endpint_handler] start_client: ~p, get error: ~p", [ClientId, Error]),
|
||||
{error, <<"config kafka server failed">>}
|
||||
end.
|
||||
18
apps/endpoint/src/endpoint_util.erl
Normal file
18
apps/endpoint/src/endpoint_util.erl
Normal file
@ -0,0 +1,18 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Endpoint-local utility functions.
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_util).
|
||||
|
||||
-export([set_metadata/0, sha256/1]).
|
||||
|
||||
-spec set_metadata() -> ok.
|
||||
set_metadata() ->
|
||||
logger:set_process_metadata(#{domain => [endpoint]}).
|
||||
|
||||
-spec sha256(string() | binary()) -> binary().
|
||||
sha256(Str) when is_list(Str) ->
|
||||
sha256(unicode:characters_to_binary(Str));
|
||||
sha256(Bin) when is_binary(Bin) ->
|
||||
HashBin = crypto:hash(sha256, Bin),
|
||||
HexStr = lists:flatten([io_lib:format("~2.16.0B", [B]) || B <- binary:bin_to_list(HashBin)]),
|
||||
list_to_binary(string:lowercase(HexStr)).
|
||||
20
apps/iot/include/domain_model.hrl
Normal file
20
apps/iot/include/domain_model.hrl
Normal file
@ -0,0 +1,20 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Codex
|
||||
%%% @doc
|
||||
%%% 领域模型定义
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-record(host_info, {
|
||||
id :: integer(),
|
||||
uuid :: binary(),
|
||||
authorize_status :: integer(),
|
||||
status :: integer()
|
||||
}).
|
||||
|
||||
-record(device_info, {
|
||||
id :: integer(),
|
||||
host_id :: integer(),
|
||||
device_uuid :: binary(),
|
||||
status :: integer()
|
||||
}).
|
||||
@ -13,6 +13,10 @@
|
||||
-define(HOST_ONLINE, 1).
|
||||
-define(HOST_NOT_JOINED, -1).
|
||||
|
||||
%% 主机是否授权处理上报数据
|
||||
-define(HOST_DENIED, 0).
|
||||
-define(HOST_AUTHORIZED, 1).
|
||||
|
||||
%% 设备是否在线状态
|
||||
-define(DEVICE_OFFLINE, 0).
|
||||
-define(DEVICE_ONLINE, 1).
|
||||
@ -22,93 +26,3 @@
|
||||
-define(TASK_STATUS_INIT, -1). %% 未接入
|
||||
-define(TASK_STATUS_FAILED, 0). %% 离线
|
||||
-define(TASK_STATUS_OK, 1). %% 在线
|
||||
|
||||
%% efka主动发起的消息体类型, 消息大类
|
||||
-define(PACKET_REQUEST, 16#01).
|
||||
-define(PACKET_RESPONSE, 16#02).
|
||||
|
||||
%% 服务器基于pub/sub的消息, 消息大类
|
||||
-define(PACKET_PUB, 16#03).
|
||||
|
||||
%% push调用不需要返回, 消息大类
|
||||
-define(PACKET_COMMAND, 16#04).
|
||||
|
||||
%% 服务器端推送消息
|
||||
-define(PACKET_ASYNC_CALL, 16#05).
|
||||
-define(PACKET_ASYNC_CALL_REPLY, 16#06).
|
||||
|
||||
%% ping包,客户端主动发起
|
||||
-define(PACKET_PING, 16#FF).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%%%% 二级分类定义
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 主机端上报数据类型标识
|
||||
-define(METHOD_AUTH, 16#01).
|
||||
-define(METHOD_DATA, 16#02).
|
||||
-define(METHOD_PING, 16#03).
|
||||
-define(METHOD_INFORM, 16#04).
|
||||
-define(METHOD_EVENT, 16#05).
|
||||
-define(METHOD_PHASE, 16#06).
|
||||
-define(METHOD_REQUEST_SERVICE_CONFIG, 16#07).
|
||||
|
||||
%%%% 命令类型子分类, 不需要返回值
|
||||
|
||||
%% 授权
|
||||
-define(COMMAND_AUTH, 16#08).
|
||||
|
||||
%%%% 主动推送的消息类型子分类, 需要返回值
|
||||
|
||||
-define(PUSH_DEPLOY, 16#01).
|
||||
-define(PUSH_START_SERVICE, 16#02).
|
||||
-define(PUSH_STOP_SERVICE, 16#03).
|
||||
|
||||
-define(PUSH_SERVICE_CONFIG, 16#04).
|
||||
-define(PUSH_INVOKE, 16#05).
|
||||
-define(PUSH_TASK_LOG, 16#06).
|
||||
|
||||
%% 缓存数据库表
|
||||
-record(kv, {
|
||||
key :: binary(),
|
||||
val :: binary() | list() | map() | sets:set(),
|
||||
expire_at = 0 :: integer(),
|
||||
type :: atom()
|
||||
}).
|
||||
|
||||
%% id生成器
|
||||
-record(id_generator, {
|
||||
tab :: atom(),
|
||||
increment_id = 0 :: integer()
|
||||
}).
|
||||
|
||||
%% 统计项
|
||||
-record(option, {
|
||||
success_num = 0,
|
||||
fail_num = 0
|
||||
}).
|
||||
|
||||
%% 北向数据
|
||||
-record(north_data, {
|
||||
id = 0 :: integer(),
|
||||
location_code :: binary(),
|
||||
%% 数据库类型的endpoint, 可以返回list: [{K, V}, {K1, V1}]
|
||||
fields :: [{K :: binary(), V :: any()}],
|
||||
timestamp = 0 :: integer()
|
||||
}).
|
||||
|
||||
%% 事件相关的数据
|
||||
-record(event_data, {
|
||||
id = 0 :: integer(),
|
||||
location_code :: binary(),
|
||||
event_type :: integer(),
|
||||
params :: map()
|
||||
}).
|
||||
|
||||
%% 发送数据
|
||||
-record(post_data, {
|
||||
id = 0 :: integer(),
|
||||
location_code :: binary(),
|
||||
%% 数据库类型的endpoint, 可以返回list: [{K, V}, {K1, V1}]
|
||||
body :: binary() | list()
|
||||
}).
|
||||
@ -1,23 +1,17 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 08. 5月 2025 12:08
|
||||
%%% Created : 24. 4月 2026 16:54
|
||||
%%%-------------------------------------------------------------------
|
||||
-author("anlicheng").
|
||||
|
||||
%% 用来保存微服务的配置
|
||||
-record(service_config, {
|
||||
service_id :: binary(),
|
||||
host_uuid :: binary(),
|
||||
config_json = <<>> :: binary(),
|
||||
%% 保持上一个版本的内容,错误时回滚
|
||||
last_config_json = <<>> :: binary(),
|
||||
%% 最后一次修改的用户id
|
||||
last_edit_user :: integer(),
|
||||
%% 状态: 0: 停止, 1: 运行中
|
||||
update_ts = 0,
|
||||
create_ts = 0
|
||||
-record(efka_client, {
|
||||
uuid :: binary(),
|
||||
token_hash :: binary(),
|
||||
salt :: binary(),
|
||||
heartbeat_secret :: binary(),
|
||||
timestamp = 0 :: integer()
|
||||
}).
|
||||
@ -1,133 +0,0 @@
|
||||
%% -*- coding: utf-8 -*-
|
||||
%% Automatically generated, do not edit
|
||||
%% Generated by gpb_compile version 4.21.1
|
||||
|
||||
-ifndef(message_pb).
|
||||
-define(message_pb, true).
|
||||
|
||||
-define(message_pb_gpb_version, "4.21.1").
|
||||
|
||||
|
||||
-ifndef('AUTH_REQUEST_PB_H').
|
||||
-define('AUTH_REQUEST_PB_H', true).
|
||||
-record(auth_request,
|
||||
{uuid = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
username = <<>> :: unicode:chardata() | undefined, % = 2, optional
|
||||
salt = <<>> :: unicode:chardata() | undefined, % = 4, optional
|
||||
token = <<>> :: unicode:chardata() | undefined, % = 5, optional
|
||||
timestamp = 0 :: non_neg_integer() | undefined % = 6, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('AUTH_REPLY_PB_H').
|
||||
-define('AUTH_REPLY_PB_H', true).
|
||||
-record(auth_reply,
|
||||
{code = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
message = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('PUB_PB_H').
|
||||
-define('PUB_PB_H', true).
|
||||
-record(pub,
|
||||
{topic = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
content = <<>> :: iodata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('COMMAND_PB_H').
|
||||
-define('COMMAND_PB_H', true).
|
||||
-record(command,
|
||||
{command_type = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
command = <<>> :: iodata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('RPC_DEPLOY_PB_H').
|
||||
-define('RPC_DEPLOY_PB_H', true).
|
||||
-record(rpc_deploy,
|
||||
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
task_id = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
config = <<>> :: unicode:chardata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('RPC_START_CONTAINER_PB_H').
|
||||
-define('RPC_START_CONTAINER_PB_H', true).
|
||||
-record(rpc_start_container,
|
||||
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
container_name = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('RPC_STOP_CONTAINER_PB_H').
|
||||
-define('RPC_STOP_CONTAINER_PB_H', true).
|
||||
-record(rpc_stop_container,
|
||||
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
container_name = <<>> :: unicode:chardata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('RPC_CONFIG_CONTAINER_PB_H').
|
||||
-define('RPC_CONFIG_CONTAINER_PB_H', true).
|
||||
-record(rpc_config_container,
|
||||
{packet_id = 0 :: non_neg_integer() | undefined, % = 1, optional, 32 bits
|
||||
container_name = <<>> :: unicode:chardata() | undefined, % = 2, optional
|
||||
config = <<>> :: iodata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('FETCH_TASK_LOG_PB_H').
|
||||
-define('FETCH_TASK_LOG_PB_H', true).
|
||||
-record(fetch_task_log,
|
||||
{task_id = 0 :: non_neg_integer() | undefined % = 1, optional, 32 bits
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('CONTAINER_CONFIG_PB_H').
|
||||
-define('CONTAINER_CONFIG_PB_H', true).
|
||||
-record(container_config,
|
||||
{container_name = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
config = <<>> :: iodata() | undefined % = 2, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('DATA_PB_H').
|
||||
-define('DATA_PB_H', true).
|
||||
-record(data,
|
||||
{service_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
device_uuid = <<>> :: unicode:chardata() | undefined, % = 2, optional
|
||||
route_key = <<>> :: unicode:chardata() | undefined, % = 3, optional
|
||||
metric = <<>> :: iodata() | undefined % = 4, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('EVENT_PB_H').
|
||||
-define('EVENT_PB_H', true).
|
||||
-record(event,
|
||||
{service_id = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
event_type = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
params = <<>> :: unicode:chardata() | undefined % = 3, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-ifndef('PING_PB_H').
|
||||
-define('PING_PB_H', true).
|
||||
-record(ping,
|
||||
{adcode = <<>> :: unicode:chardata() | undefined, % = 1, optional
|
||||
boot_time = 0 :: non_neg_integer() | undefined, % = 2, optional, 32 bits
|
||||
province = <<>> :: unicode:chardata() | undefined, % = 3, optional
|
||||
city = <<>> :: unicode:chardata() | undefined, % = 4, optional
|
||||
efka_version = <<>> :: unicode:chardata() | undefined, % = 5, optional
|
||||
kernel_arch = <<>> :: unicode:chardata() | undefined, % = 6, optional
|
||||
ips = [] :: [unicode:chardata()] | undefined, % = 7, repeated
|
||||
cpu_core = 0 :: non_neg_integer() | undefined, % = 8, optional, 32 bits
|
||||
cpu_load = 0 :: non_neg_integer() | undefined, % = 9, optional, 32 bits
|
||||
cpu_temperature = 0.0 :: float() | integer() | infinity | '-infinity' | nan | undefined, % = 10, optional
|
||||
disk = [] :: [integer()] | undefined, % = 11, repeated, 32 bits
|
||||
memory = [] :: [integer()] | undefined, % = 12, repeated, 32 bits
|
||||
interfaces = <<>> :: unicode:chardata() | undefined % = 13, optional
|
||||
}).
|
||||
-endif.
|
||||
|
||||
-endif.
|
||||
16
apps/iot/include/protocol.hrl
Normal file
16
apps/iot/include/protocol.hrl
Normal file
@ -0,0 +1,16 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%% 传输层协议常量定义
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-author("anlicheng").
|
||||
|
||||
%% 一级帧类型
|
||||
%% REQUEST: 需要响应的请求帧
|
||||
%% REPLY: 对 REQUEST 的响应帧
|
||||
%% CAST: 单向消息帧,不需要响应
|
||||
-define(FRAME_REQUEST, 16#01).
|
||||
-define(FRAME_REPLY, 16#02).
|
||||
-define(FRAME_CAST, 16#03).
|
||||
@ -1,20 +0,0 @@
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"epi", "label":"kWh", "name":"正向总有功电能", "timestamp":1705576081, "type":"AI", "unit":5, "value":314498}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"a_voltage", "label":"V", "name":"A相电压", "timestamp":1705576081, "type":"AI", "unit":0, "value":224.1}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"a_current", "label":"A", "name":"A相电流", "timestamp":1705576081, "type":"AI", "unit":3, "value":0.394}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"b_voltage", "label":"V", "name":"B相电压", "timestamp":1705576081, "type":"AI", "unit":0, "value":225.3}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"b_current", "label":"A", "name":"B相电流", "timestamp":1705576081, "type":"AI", "unit":3, "value":0.323}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"c_voltage", "label":"V", "name":"C相电压", "timestamp":1705576081, "type":"AI", "unit":0, "value":225}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"c_current", "label":"A", "name":"C相电流", "timestamp":1705576081, "type":"AI", "unit":3, "value":0.269}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"active_power", "label":"kW", "name":"瞬时总有功功率", "timestamp":1705576081, "type":"AI", "unit":23, "value":20.91}
|
||||
{"device_uuid":"20118448129232486417014256831677", "key":"power_factor", "label":"", "name":"总功率因数", "timestamp":1705576081, "type":"AI", "unit":16, "value":0.994}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"epi", "label":"kWh", "name":"正向总有功电能", "timestamp":1705576038, "type":"AI", "unit":5, "value":299493.6}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"a_voltage", "label":"V", "name":"A相电压", "timestamp":1705576038, "type":"AI", "unit":0, "value":225.9}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"a_current", "label":"A", "name":"A相电流", "timestamp":1705576038, "type":"AI", "unit":3, "value":0.318}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"b_voltage", "label":"V", "name":"B相电压", "timestamp":1705576038, "type":"AI", "unit":0, "value":225.7}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"b_current", "label":"A", "name":"B相电流", "timestamp":1705576038, "type":"AI", "unit":3, "value":0.305}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"c_voltage", "label":"V", "name":"C相电压", "timestamp":1705576038, "type":"AI", "unit":0, "value":225}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"c_current", "label":"A", "name":"C相电流", "timestamp":1705576038, "type":"AI", "unit":3, "value":0.3}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"active_power", "label":"kW", "name":"瞬时总有功功率", "timestamp":1705576038, "type":"AI", "unit":23, "value":15.056}
|
||||
{"device_uuid":"20118448549921177617014256841701", "key":"power_factor", "label":"", "name":"总功率因数", "timestamp":1705576038, "type":"AI", "unit":16, "value":1}
|
||||
{"device_uuid":"20118448970190438417014256851724", "key":"epi", "label":"kWh", "name":"正向总有功电能", "timestamp":1705576117, "type":"AI", "unit":5, "value":285011}
|
||||
{"device_uuid":"20118448970190438417014256851724", "key":"a_voltage", "label":"V", "name":"A相电压", "timestamp":1705576117, "type":"AI", "unit":0, "value":225.1}
|
||||
23
apps/iot/priv/ssl/server.crt
Normal file
23
apps/iot/priv/ssl/server.crt
Normal file
@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDwzCCAqugAwIBAgIUMhNyuVo6MieWs2I9LsyTTiDKtYgwDQYJKoZIhvcNAQEL
|
||||
BQAwgYkxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdC
|
||||
ZWlqaW5nMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
|
||||
BAMMCWFubGljaGVuZzEfMB0GCSqGSIb3DQEJARYQMjQ0MTA4NzE1QHFxLmNvbTAe
|
||||
Fw0yNTA0MjEwMzUyNTlaFw0yNjA0MjEwMzUyNTlaMIGJMQswCQYDVQQGEwJDTjEQ
|
||||
MA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEhMB8GA1UECgwYSW50
|
||||
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlhbmxpY2hlbmcxHzAdBgkq
|
||||
hkiG9w0BCQEWEDI0NDEwODcxNUBxcS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDdxxyYG0zV2KzeiVH5AGj1X1h7vLAHVS8FGkPL2QBBqW5/PrJ/
|
||||
z/sv3QJz6dB3ElOnk14GsY8lWk8uX/fjgNIPawN1G2/bCdMrlPOCEJEIOqZvzQWg
|
||||
PLRPGjoZx+wjCM/H7h47KVr4GNbo8MakLJeg6QB3rEEIiPrhQIoq9N7AhcjbEvWR
|
||||
NIxIQrpSqMCE2A5RoKUxAMum9rEoe/6PBw2GgEsQl5E6suRZw3wavg3aUU+6MMyx
|
||||
iiKEA6fCD5gDqgSo/xjgDUWJIOTTrmW6RBTcWP9iBUjWnCovQQ9zc6CzDXRgfsz9
|
||||
7uXQt9fuQEi++lcGnrsePjC99PnirCvmj/C/AgMBAAGjITAfMB0GA1UdDgQWBBT6
|
||||
n4jUTWQNcastC5jPHuU7CDzLFjANBgkqhkiG9w0BAQsFAAOCAQEAZJ9fY2Z+4vKr
|
||||
bBqwHmfBjEnGgS7L2mC7uS2x/x2meBRKlAlw5+nKdaUBccyI2baI3P1mh/iV72Wr
|
||||
OTcwUwSS6gIOB7JeWSB0UT7KbEOJ1tM/1HYs5F9tOT94P4Adm2gcKY81UlJMfSNQ
|
||||
WyWFLjWOk/5fUP42BmgbUIafTT9p+LeP6NOyEs6b4hGpF3q5L1QDwMUfASpOWtHn
|
||||
O7VyBvFyCGkVchnorWJ3ZXPaa7hy+2ULOK/d9zH3xxq4LKRclAS5XAMWHuw6/+tu
|
||||
CoxA+RPnaqHexPSzYlEElOT286FDyZHadjUDD0q/0Um92NUM3r+UrNEs/OHp7/nU
|
||||
M3BVM7H6LQ==
|
||||
-----END CERTIFICATE-----
|
||||
18
apps/iot/priv/ssl/server.csr
Normal file
18
apps/iot/priv/ssl/server.csr
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICzzCCAbcCAQAwgYkxCzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAw
|
||||
DgYDVQQHDAdCZWlqaW5nMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBM
|
||||
dGQxEjAQBgNVBAMMCWFubGljaGVuZzEfMB0GCSqGSIb3DQEJARYQMjQ0MTA4NzE1
|
||||
QHFxLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3HHJgbTNXY
|
||||
rN6JUfkAaPVfWHu8sAdVLwUaQ8vZAEGpbn8+sn/P+y/dAnPp0HcSU6eTXgaxjyVa
|
||||
Ty5f9+OA0g9rA3Ubb9sJ0yuU84IQkQg6pm/NBaA8tE8aOhnH7CMIz8fuHjspWvgY
|
||||
1ujwxqQsl6DpAHesQQiI+uFAiir03sCFyNsS9ZE0jEhCulKowITYDlGgpTEAy6b2
|
||||
sSh7/o8HDYaASxCXkTqy5FnDfBq+DdpRT7owzLGKIoQDp8IPmAOqBKj/GOANRYkg
|
||||
5NOuZbpEFNxY/2IFSNacKi9BD3NzoLMNdGB+zP3u5dC31+5ASL76Vwaeux4+ML30
|
||||
+eKsK+aP8L8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBHdTrwcS0Ip5XO/zJ/
|
||||
wFZvxe8wcdMBaHjSRx09Lgy3V1L9d0DPOfuUP4LP8UCxniucA2JLugJO1wr/5nZA
|
||||
FS9GcyCgbMKX/EskFkpuz72EA11WhyqFp/9nWZsxZB0t1cs3bUFeoFd2SE6QIo4N
|
||||
UY+guZCcF8hVwppceOxyaUQ9cudAClH9JSMR1XoIwvv3X7FJkPhM12DvcsLr062S
|
||||
fwG7/1h+VoSpAM/UYtBHBkU0MIVn1Gw6lJvkabUV0oYYBzl9ejsq1Qo2nUMdOEQY
|
||||
SS/VhVLONMyMCIERt3/GJTXdrJ12VqawJACFwv8g2i6NxbQvbZpF7TpS8amduO8+
|
||||
aRE6
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
28
apps/iot/priv/ssl/server.key
Normal file
28
apps/iot/priv/ssl/server.key
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdxxyYG0zV2Kze
|
||||
iVH5AGj1X1h7vLAHVS8FGkPL2QBBqW5/PrJ/z/sv3QJz6dB3ElOnk14GsY8lWk8u
|
||||
X/fjgNIPawN1G2/bCdMrlPOCEJEIOqZvzQWgPLRPGjoZx+wjCM/H7h47KVr4GNbo
|
||||
8MakLJeg6QB3rEEIiPrhQIoq9N7AhcjbEvWRNIxIQrpSqMCE2A5RoKUxAMum9rEo
|
||||
e/6PBw2GgEsQl5E6suRZw3wavg3aUU+6MMyxiiKEA6fCD5gDqgSo/xjgDUWJIOTT
|
||||
rmW6RBTcWP9iBUjWnCovQQ9zc6CzDXRgfsz97uXQt9fuQEi++lcGnrsePjC99Pni
|
||||
rCvmj/C/AgMBAAECggEAM4j6RwpI/4RbH1cvmjoTKbmfORmumfWceIYS7QKfAaMa
|
||||
jy0Fk5fD2ep0kHTrwU+b6tvexJVsGxTyQ2d/lfkwVu7aHdNjWbXdwUnakAXDffML
|
||||
C/3LaeHRUHRavfTsFXQNvHrDwaGphu9WuUiCEFJgZb7fIfAAKLiT+9XghXzjaj79
|
||||
nGANBB++kfBGacfj2NMedCxsbZ++pD7DbTG4utnHmzgwB3WEEm2kIbt0XpuDSZqe
|
||||
/tCA1gCOADjV17C31booT6xdoj+s/Se0YxY91JaZFkI5wTSDIhB9scZzh8piy8YQ
|
||||
QaEaRUUYNbzdI1MyvgewT6wQLONNlytOOc9hYnPgAQKBgQDyzaublY2q+yzqCQ68
|
||||
JSKNTCSFRwumJ1kmdfz/0QwzS363l7LFnfIb55h27q3vuiagPfRbi0GK/tVXhOe+
|
||||
8mntNMoJJQJzowpy4ANqKfGndojXejE1s1ExpuGyqWV4MyFGiwAnnzyWRENw4laS
|
||||
roY0OcynNuq++isgrVMPlDLAvwKBgQDp1OTh2wcSfhsaVbMjNVCOGD82OFK057JB
|
||||
3C62Li2lCybFwcooypCyZthUdskY2Gf/mDdnpLSLvXr/7bO3IyLopA3WpnYqMBt+
|
||||
7sP/VQJMoyobLFMnJh+N+CxwkhRNAib64054WKwiJcccJ5fplUvNjOLBgGx3a/3N
|
||||
OIxx+L7QAQKBgQC+EK3zPuEFJVYFZk24jkE75oz4H6NIz6iD6PzBrH0mckShpwh0
|
||||
la1+lo7NGw3hiRDPg3ATcTE/gziyKAHZgZ3V5+r3uZbvuoNlZWKG6oqWkr2QH8EB
|
||||
znsSqRYoa15Y931m4Uyft5EP+CPq6+LlM+UuYMiJZP3hvaehDszkdxg7tQKBgGjv
|
||||
vMPLCpJ2+2zHFvxu+ba7FOsdPain7ix2RpRFhwBxT7Yh8Lp7pZIaa20EXd0DiTCA
|
||||
PCUMGmY345IlN6ixYQIsVXWGALOQIVbGijj1CnIgK05EhxCjoDeTL0ZZmDizBZFE
|
||||
HzwM9zrf30o4TolqEbmuRzj1jDfPw/esMAMZ0XABAoGBANpjvUN0Qvy9WmLyPe6U
|
||||
DBzIAUyyIL3mnCuHLCQ+dE6nBf5l20sVjTdIl/eLlvmD7dwT/D9Zsg32/c2NBCdY
|
||||
DltKKZl2u9nuSJ1g8DWTZUQHkYSTeKPOoAl/5mbCgyTxzmmmtZF6MySKuif0rzwS
|
||||
X2Rwou9zTpwgsEEmQ/RH4G94
|
||||
-----END PRIVATE KEY-----
|
||||
259
apps/iot/src/adapters/control_api/iot_api_client.erl
Normal file
259
apps/iot/src/adapters/control_api/iot_api_client.erl
Normal file
@ -0,0 +1,259 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 24. 12月 2023 15:42
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_api_client).
|
||||
-author("anlicheng").
|
||||
-include("domain_model.hrl").
|
||||
|
||||
%% API
|
||||
-export([ai_event/1]).
|
||||
|
||||
-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).
|
||||
|
||||
-export([get_all_hosts/0, get_host_by_id/1, get_host_by_uuid/1, change_host_status/2]).
|
||||
-export([get_host_devices/1, get_device_by_uuid/1, change_device_status/2]).
|
||||
-export([get_all_endpoints/0, get_endpoint/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec get_all_hosts() -> [HostUUID :: binary()].
|
||||
get_all_hosts() ->
|
||||
case do_get("/get_all_hosts", []) of
|
||||
{ok, Ids} ->
|
||||
Ids;
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec get_host_by_uuid(UUID :: binary()) -> undefined | {ok, HostInfo :: #host_info{}}.
|
||||
get_host_by_uuid(UUID) when is_binary(UUID) ->
|
||||
case do_get("/get_host_by_uuid", [{<<"uuid">>, UUID}]) of
|
||||
{ok, HostInfo} ->
|
||||
case host_info_record(HostInfo) of
|
||||
{ok, Record} ->
|
||||
{ok, Record};
|
||||
error ->
|
||||
undefined
|
||||
end;
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec get_host_by_id(HostId :: integer()) -> undefined | {ok, HostInfo :: #host_info{}}.
|
||||
get_host_by_id(HostId) when is_integer(HostId) ->
|
||||
case do_get("/get_host_by_id", [{<<"host_id">>, integer_to_binary(HostId)}]) of
|
||||
{ok, HostInfo} ->
|
||||
case host_info_record(HostInfo) of
|
||||
{ok, Record} ->
|
||||
{ok, Record};
|
||||
error ->
|
||||
undefined
|
||||
end;
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%% 修改主机的状态
|
||||
-spec change_host_status(UUID :: binary(), Status :: integer()) -> {ok, Result :: any()} | {error, Reason :: any()}.
|
||||
change_host_status(UUID, NStatus) when is_binary(UUID), is_integer(NStatus) ->
|
||||
do_post("/change_host_status", #{<<"uuid">> => UUID, <<"new_status">> => NStatus}).
|
||||
|
||||
-spec get_host_devices(HostId :: integer()) -> {ok, Devices :: [#device_info{}]} | {error, Reason::any()}.
|
||||
get_host_devices(HostId) when is_integer(HostId) ->
|
||||
case do_get("/get_host_devices", [{<<"host_id">>, integer_to_binary(HostId)}]) of
|
||||
{ok, DeviceInfos} when is_list(DeviceInfos) ->
|
||||
device_info_records(DeviceInfos);
|
||||
{ok, _Other} ->
|
||||
{error, invalid_device_infos};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec get_device_by_uuid(DeviceUUID :: binary()) -> {ok, DeviceInfo :: #device_info{}} | undefined.
|
||||
get_device_by_uuid(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||
case do_get("/get_device_by_uuid", [{<<"device_uuid">>, DeviceUUID}]) of
|
||||
{ok, DeviceInfo} ->
|
||||
case device_info_record(DeviceInfo) of
|
||||
{ok, Record} ->
|
||||
{ok, Record};
|
||||
error ->
|
||||
undefined
|
||||
end;
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
%% 修改主机的状态
|
||||
-spec change_device_status(DeviceUUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
change_device_status(DeviceUUID, NStatus) when is_binary(DeviceUUID), is_integer(NStatus) ->
|
||||
do_post("/change_device_status", #{<<"device_uuid">> => DeviceUUID, <<"new_status">> => NStatus}).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%% endpoint相关的api
|
||||
%%%-------------------------------------------------------------------
|
||||
%% API
|
||||
|
||||
-spec get_all_endpoints() -> [Endpoint :: map()].
|
||||
get_all_endpoints() ->
|
||||
case do_get("/get_all_endpoints", []) of
|
||||
{ok, Endpoints} ->
|
||||
Endpoints;
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec get_endpoint(Id :: integer()) -> undefined | {ok, EndpointInfo :: map()}.
|
||||
get_endpoint(Id) when is_integer(Id) ->
|
||||
case do_get("/get_endpoint", [{<<"id">>, integer_to_binary(Id)}]) of
|
||||
{ok, EndpointInfo} when is_map(EndpointInfo) ->
|
||||
{ok, EndpointInfo};
|
||||
_ ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
ai_event(Id) when is_integer(Id) ->
|
||||
Token = iot_util:md5(<<?API_TOKEN/binary, (integer_to_binary(Id))/binary, ?API_TOKEN/binary>>),
|
||||
{ok, Url} = application:get_env(iot, api_url),
|
||||
|
||||
Headers = [
|
||||
{<<"content-type">>, <<"application/json">>}
|
||||
],
|
||||
ReqData = #{
|
||||
<<"token">> => Token,
|
||||
<<"id">> => Id
|
||||
},
|
||||
Body = iolist_to_binary(json:encode(ReqData)),
|
||||
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
logger:debug("[iot_api_client] send body: ~p, get error is: ~p", [Body, RespBody]),
|
||||
hackney:close(ClientRef);
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
logger:warning("[iot_api_client] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]);
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_api_client] send body: ~p, get error is: ~p", [Body, Reason])
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%% helper methods
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-spec do_post(Path :: string(), Params :: map()) -> {ok, Resp :: any()} | {error, Reason :: any()}.
|
||||
do_post(Path, Params) when is_list(Path), is_map(Params) ->
|
||||
{ok, BaseUrl} = application:get_env(iot, api_url),
|
||||
Headers = [
|
||||
{<<"content-type">>, <<"application/json">>},
|
||||
{<<"Accept">>, <<"application/json">>}
|
||||
],
|
||||
Url = BaseUrl ++ Path,
|
||||
Body = iolist_to_binary(json:encode(Params)),
|
||||
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
logger:debug("[iot_api_client] request url: ~p, send body: ~p, get response is: ~p", [Url, Body, RespBody]),
|
||||
hackney:close(ClientRef),
|
||||
case catch json:decode(RespBody) of
|
||||
#{<<"result">> := Result} ->
|
||||
{ok, Result};
|
||||
#{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
|
||||
{error, {Code, Message}};
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
Other ->
|
||||
{error, Other}
|
||||
end;
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
logger:warning("[iot_api_client] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, {HttpCode, RespBody}]),
|
||||
{error, {HttpCode, RespBody}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_api_client] request url: ~p, send body: ~p, get error is: ~p", [Url, Body, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec do_get(Path :: string(), Params :: [{Key :: binary(), Val :: binary()}]) -> {ok, Resp :: any()} | {error, Reason :: any()}.
|
||||
do_get(Path, Params) when is_list(Path), is_list(Params) ->
|
||||
{ok, BaseUrl} = application:get_env(iot, api_url),
|
||||
Headers = [
|
||||
{<<"Accept">>, <<"application/json">>}
|
||||
],
|
||||
|
||||
Url = case length(Params) > 0 of
|
||||
true ->
|
||||
QS = binary_to_list(uri_string:compose_query(Params)),
|
||||
BaseUrl ++ Path ++ "?" ++ QS;
|
||||
false ->
|
||||
BaseUrl ++ Path
|
||||
end,
|
||||
|
||||
case hackney:request(get, Url, Headers, <<>>, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
logger:debug("[iot_api_client] url: ~p, get response is: ~p", [Url, RespBody]),
|
||||
case catch json:decode(RespBody) of
|
||||
#{<<"result">> := Result} ->
|
||||
{ok, Result};
|
||||
#{<<"error">> := #{<<"code">> := Code, <<"message">> := Message}} ->
|
||||
{error, {Code, Message}};
|
||||
{error, Reason} ->
|
||||
{error, Reason};
|
||||
Other ->
|
||||
{error, Other}
|
||||
end;
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
logger:warning("[iot_api_client] request url: ~p, get error is: ~p", [Url, {HttpCode, RespBody}]),
|
||||
{error, {HttpCode, RespBody}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_api_client] request url: ~p, get error is: ~p", [Url, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec host_info_record(map()) -> {ok, #host_info{}} | error.
|
||||
host_info_record(#{<<"id">> := Id, <<"uuid">> := UUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status})
|
||||
when is_integer(Id), is_binary(UUID), is_integer(AuthorizeStatus), is_integer(Status) ->
|
||||
{ok, #host_info{
|
||||
id = Id,
|
||||
uuid = UUID,
|
||||
authorize_status = AuthorizeStatus,
|
||||
status = Status
|
||||
}};
|
||||
host_info_record(_) ->
|
||||
error.
|
||||
|
||||
-spec device_info_record(map()) -> {ok, #device_info{}} | error.
|
||||
device_info_record(#{<<"id">> := Id, <<"host_id">> := HostId, <<"device_uuid">> := DeviceUUID, <<"status">> := Status})
|
||||
when is_integer(Id), is_integer(HostId), is_binary(DeviceUUID), is_integer(Status) ->
|
||||
{ok, #device_info{
|
||||
id = Id,
|
||||
host_id = HostId,
|
||||
device_uuid = DeviceUUID,
|
||||
status = Status
|
||||
}};
|
||||
device_info_record(_) ->
|
||||
error.
|
||||
|
||||
-spec device_info_records([map()]) -> {ok, [#device_info{}]} | {error, invalid_device_info}.
|
||||
device_info_records(DeviceInfos) ->
|
||||
lists:foldr(fun(DeviceInfo, Acc) ->
|
||||
case {device_info_record(DeviceInfo), Acc} of
|
||||
{{ok, Record}, {ok, Records}} ->
|
||||
{ok, [Record | Records]};
|
||||
{error, _} ->
|
||||
{error, invalid_device_info};
|
||||
{_, Error = {error, _}} ->
|
||||
Error
|
||||
end
|
||||
end, {ok, []}, DeviceInfos).
|
||||
@ -57,7 +57,7 @@ get_precision(Timestamp) when is_integer(Timestamp) ->
|
||||
<<"ms">>
|
||||
end.
|
||||
|
||||
-spec write_data(Measurement :: binary(), Tags :: map(), FieldsList :: list(), Timestamp :: integer()) -> no_return().
|
||||
-spec write_data(Measurement :: binary(), Tags :: map(), FieldsList :: list(), Timestamp :: integer()) -> term().
|
||||
write_data(Measurement, Tags, FieldsList, Timestamp) when is_binary(Measurement), is_map(Tags), is_list(FieldsList), is_integer(Timestamp) ->
|
||||
%% 过来掉没有key的选项
|
||||
NFieldsList = lists:filter(fun data_filter/1, FieldsList),
|
||||
@ -76,12 +76,12 @@ write_data(Measurement, Tags, FieldsList, Timestamp) when is_binary(Measurement)
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Points :: list()) -> no_return().
|
||||
-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Points :: list()) -> ok.
|
||||
write(Pid, Bucket, Org, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_list(Points) ->
|
||||
write(Pid, Bucket, Org, <<"ms">>, Points).
|
||||
|
||||
%% Precision的值为: ms|ns|s; 表示时间的精度,默认为毫秒(ms)
|
||||
-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Precision :: binary(), Points :: list()) -> no_return().
|
||||
-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Precision :: binary(), Points :: list()) -> ok.
|
||||
write(Pid, Bucket, Org, Precision, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_binary(Precision), is_list(Points) ->
|
||||
gen_server:cast(Pid, {write, Bucket, Org, Precision, Points}).
|
||||
|
||||
@ -101,6 +101,7 @@ start_link(Opts) when is_list(Opts) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([InfluxProps]) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
Token = proplists:get_value(token, InfluxProps),
|
||||
Host = proplists:get_value(host, InfluxProps),
|
||||
Port = proplists:get_value(port, InfluxProps),
|
||||
@ -151,18 +152,18 @@ handle_cast({write, Bucket, Org, Precision, Points}, State = #state{host = Host,
|
||||
query => Query
|
||||
}),
|
||||
|
||||
lager:debug("[influx_client] url is: ~p, headers: ~p, body: ~ts", [Url, Headers, Body]),
|
||||
logger:debug("[influx_client] url is: ~p, headers: ~p, body: ~ts", [Url, Headers, Body]),
|
||||
case hackney:request(post, Url, Headers, GZipBody, [{pool, false}]) of
|
||||
{ok, StatusCode, _RespHeaders, ClientRef} ->
|
||||
case hackney:body(ClientRef) of
|
||||
{ok, RespBody} ->
|
||||
lager:debug("[influx_client] status_code: ~p, response body is: ~p", [StatusCode, RespBody]);
|
||||
logger:debug("[influx_client] status_code: ~p, response body is: ~p", [StatusCode, RespBody]);
|
||||
{error, Error} ->
|
||||
lager:warning("[influx_client] status_code: ~p, error is: ~p", [Error])
|
||||
logger:warning("[influx_client] status_code: ~p, error is: ~p", [Error])
|
||||
end,
|
||||
hackney:close(ClientRef);
|
||||
{error, Reason} ->
|
||||
lager:warning("[influx_client] request result is: ~p", [Reason])
|
||||
logger:warning("[influx_client] request result is: ~p", [Reason])
|
||||
end,
|
||||
|
||||
{noreply, State}.
|
||||
@ -45,7 +45,7 @@ field_val(V) when is_float(V) ->
|
||||
field_val(V) when is_binary(V) ->
|
||||
<<$", V/binary, $">>;
|
||||
field_val(V) when is_list(V); is_map(V) ->
|
||||
S = base64:encode(iolist_to_binary(jiffy:encode(V, [force_utf8]))),
|
||||
S = base64:encode(iolist_to_binary(json:encode(V))),
|
||||
<<$", "base64:", S/binary, $">>;
|
||||
field_val(true) ->
|
||||
<<"true">>;
|
||||
@ -56,4 +56,3 @@ as_list(L) when is_list(L) ->
|
||||
L;
|
||||
as_list(L) when is_map(L) ->
|
||||
maps:to_list(L).
|
||||
|
||||
41
apps/iot/src/ctrl/ctrl_acceptor.erl
Normal file
41
apps/iot/src/ctrl/ctrl_acceptor.erl
Normal file
@ -0,0 +1,41 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc Control socket acceptor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(ctrl_acceptor).
|
||||
|
||||
-export([start_link/1, init/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(gen_tcp:socket()) -> {ok, pid()} | {error, term()}.
|
||||
start_link(ListenSocket) ->
|
||||
proc_lib:start_link(?MODULE, init, [ListenSocket]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec init(gen_tcp:socket()) -> ok.
|
||||
init(ListenSocket) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
proc_lib:init_ack({ok, self()}),
|
||||
accept_loop(ListenSocket).
|
||||
|
||||
-spec accept_loop(gen_tcp:socket()) -> ok.
|
||||
accept_loop(ListenSocket) ->
|
||||
case gen_tcp:accept(ListenSocket) of
|
||||
{ok, Socket} ->
|
||||
{ok, Pid} = ctrl_channel:start(),
|
||||
ok = gen_tcp:controlling_process(Socket, Pid),
|
||||
ok = ctrl_channel:set_socket(Pid, Socket),
|
||||
accept_loop(ListenSocket);
|
||||
{error, closed} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
logger:error("[ctrl_acceptor] accept failed: ~p", [Reason]),
|
||||
exit({accept_failed, Reason})
|
||||
end.
|
||||
116
apps/iot/src/ctrl/ctrl_channel.erl
Normal file
116
apps/iot/src/ctrl/ctrl_channel.erl
Normal file
@ -0,0 +1,116 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc Single control socket channel.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(ctrl_channel).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-export([start/0, set_socket/2]).
|
||||
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-record(state, {
|
||||
socket = undefined :: gen_tcp:socket() | undefined
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start() -> {ok, pid()} | ignore | {error, term()}.
|
||||
start() ->
|
||||
gen_server:start(?MODULE, [], []).
|
||||
|
||||
-spec set_socket(pid(), gen_tcp:socket()) -> ok.
|
||||
set_socket(Pid, Socket) ->
|
||||
gen_server:call(Pid, {set_socket, Socket}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec init([]) -> {ok, #state{}}.
|
||||
init([]) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
{ok, #state{}}.
|
||||
|
||||
-spec handle_call(term(), {pid(), term()}, #state{}) ->
|
||||
{reply, term(), #state{}} | {stop, term(), term(), #state{}}.
|
||||
handle_call({set_socket, Socket}, _From, State = #state{socket = undefined}) ->
|
||||
case inet:setopts(Socket, [{packet, 2}, {active, once}]) of
|
||||
ok ->
|
||||
{reply, ok, State#state{socket = Socket}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[ctrl_channel] failed to activate socket: ~p", [Reason]),
|
||||
gen_tcp:close(Socket),
|
||||
{stop, Reason, {error, Reason}, State}
|
||||
end;
|
||||
handle_call(_Request, _From, State = #state{}) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(term(), #state{}) -> {noreply, #state{}} | {stop, term(), #state{}}.
|
||||
handle_info({tcp, Socket, Data}, State = #state{socket = Socket}) ->
|
||||
ok = send_response(Socket, dispatch(Data)),
|
||||
ok = inet:setopts(Socket, [{active, once}]),
|
||||
{noreply, State};
|
||||
handle_info({tcp_closed, Socket}, State = #state{socket = Socket}) ->
|
||||
{stop, normal, State};
|
||||
handle_info({tcp_error, Socket, Reason}, State = #state{socket = Socket}) ->
|
||||
logger:warning("[ctrl_channel] socket error: ~p", [Reason]),
|
||||
{stop, Reason, State};
|
||||
handle_info(Info, State = #state{}) ->
|
||||
logger:warning("[ctrl_channel] ignore unknown info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(term(), #state{}) -> ok.
|
||||
terminate(_Reason, #state{socket = undefined}) ->
|
||||
ok;
|
||||
terminate(_Reason, #state{socket = Socket}) ->
|
||||
gen_tcp:close(Socket),
|
||||
ok.
|
||||
|
||||
-spec code_change(term(), #state{}, term()) -> {ok, #state{}}.
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec send_response(gen_tcp:socket(), {ok, iodata()} | {error, iodata()}) -> ok.
|
||||
send_response(Socket, {ok, Response}) ->
|
||||
Reply = json:encode(#{<<"result">> => Response}),
|
||||
gen_tcp:send(Socket, Reply);
|
||||
send_response(Socket, {error, Response}) ->
|
||||
Reply = json:encode(#{<<"error">> => #{<<"message">> => Response}}),
|
||||
gen_tcp:send(Socket, Reply).
|
||||
|
||||
-spec dispatch(binary()) -> {ok, iodata()} | {error, iodata()}.
|
||||
dispatch(Data) when is_binary(Data) ->
|
||||
Request = json:decode(Data),
|
||||
handle_request(Request).
|
||||
|
||||
-spec handle_request(binary()) -> {ok, iodata()} | {error, iodata()}.
|
||||
handle_request(#{<<"method">> := <<"ping">>}) ->
|
||||
{ok, <<"pong">>};
|
||||
|
||||
handle_request(#{<<"method">> := <<"add_client">>, <<"params">> := #{<<"uuid">> := UUID, <<"token">> := Token}}) ->
|
||||
case efka_client_store:register(UUID, Token) of
|
||||
ok ->
|
||||
{ok, <<"OK">>};
|
||||
{error, Reason} ->
|
||||
ReadableReason = readable_binary(Reason),
|
||||
{error, <<"add failed: ", ReadableReason/binary>>}
|
||||
end;
|
||||
handle_request(Command) ->
|
||||
logger:warning("[ctrl_channel] unsupported command: ~p", [Command]),
|
||||
{error, <<"error unsupported_command\n">>}.
|
||||
|
||||
-spec readable_binary(Term :: any()) -> binary().
|
||||
readable_binary(Term) ->
|
||||
iolist_to_binary(io_lib:format("~p", [Term])).
|
||||
111
apps/iot/src/ctrl/ctrl_server.erl
Normal file
111
apps/iot/src/ctrl/ctrl_server.erl
Normal file
@ -0,0 +1,111 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc Unix domain socket control service.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(ctrl_server).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(DEFAULT_SOCKET_PATH, "/var/lib/iot/ctl.sock").
|
||||
-define(DEFAULT_BACKLOG, 128).
|
||||
|
||||
-record(state, {
|
||||
listen_socket :: gen_tcp:socket(),
|
||||
socket_path :: file:filename_all(),
|
||||
acceptor :: pid()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec init([]) -> {ok, #state{}} | {stop, term()}.
|
||||
init([]) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
Props = application:get_env(iot, ctrl_server, []),
|
||||
SocketPath = proplists:get_value(socket_path, Props, ?DEFAULT_SOCKET_PATH),
|
||||
Backlog = proplists:get_value(backlog, Props, ?DEFAULT_BACKLOG),
|
||||
|
||||
ok = ensure_socket_dir(SocketPath),
|
||||
ok = delete_stale_socket(SocketPath),
|
||||
|
||||
ListenOpts = [
|
||||
binary,
|
||||
{packet, line},
|
||||
{active, false},
|
||||
{backlog, Backlog},
|
||||
{ifaddr, {local, SocketPath}}
|
||||
],
|
||||
case gen_tcp:listen(0, ListenOpts) of
|
||||
{ok, ListenSocket} ->
|
||||
{ok, Acceptor} = ctrl_acceptor:start_link(ListenSocket),
|
||||
logger:debug("[ctrl_server] start at socket: ~ts", [SocketPath]),
|
||||
{ok, #state{listen_socket = ListenSocket, socket_path = SocketPath, acceptor = Acceptor}};
|
||||
{error, Reason} ->
|
||||
logger:error("[ctrl_server] failed to listen on socket ~ts: ~p", [SocketPath, Reason]),
|
||||
{stop, Reason}
|
||||
end.
|
||||
|
||||
-spec handle_call(term(), {pid(), term()}, #state{}) -> {reply, ok, #state{}}.
|
||||
handle_call(_Request, _From, State = #state{}) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(term(), #state{}) -> {noreply, #state{}}.
|
||||
handle_info(Info, State = #state{}) ->
|
||||
logger:warning("[ctrl_server] ignore unknown info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(term(), #state{}) -> ok.
|
||||
terminate(_Reason, #state{listen_socket = ListenSocket, socket_path = SocketPath}) ->
|
||||
gen_tcp:close(ListenSocket),
|
||||
ok = delete_stale_socket(SocketPath),
|
||||
ok.
|
||||
|
||||
-spec code_change(term(), #state{}, term()) -> {ok, #state{}}.
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec ensure_socket_dir(file:filename_all()) -> ok | {error, term()}.
|
||||
ensure_socket_dir(SocketPath) ->
|
||||
case filelib:ensure_dir(SocketPath) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
logger:error("[ctrl_server] failed to ensure socket dir for ~ts: ~p",
|
||||
[SocketPath, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec delete_stale_socket(file:filename_all()) -> ok.
|
||||
delete_stale_socket(SocketPath) ->
|
||||
case file:delete(SocketPath) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, enoent} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
logger:warning("[ctrl_server] failed to delete stale socket ~ts: ~p",
|
||||
[SocketPath, Reason]),
|
||||
ok
|
||||
end.
|
||||
@ -1,30 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ai_event_logs_bo).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
-export([insert/6]).
|
||||
|
||||
%% API
|
||||
|
||||
-spec insert(HostUUID :: binary(), DeviceUUID :: binary(), SceneId :: integer(), MicroId :: integer(), EventType :: integer(), Content :: binary()) ->
|
||||
ok | {ok, InsertId :: integer()} | {error, Reason :: any()}.
|
||||
insert(HostUUID, DeviceUUID, SceneId, MicroId, EventType, Content)
|
||||
when is_integer(EventType), is_binary(HostUUID), is_binary(DeviceUUID), is_integer(SceneId), is_integer(MicroId), is_binary(Content) ->
|
||||
|
||||
mysql_pool:insert(mysql_iot, <<"ai_event_logs">>, #{
|
||||
<<"event_type">> => EventType,
|
||||
<<"host_uuid">> => HostUUID,
|
||||
<<"device_uuid">> => DeviceUUID,
|
||||
<<"scene_id">> => SceneId,
|
||||
<<"micro_id">> => MicroId,
|
||||
<<"content">> => Content,
|
||||
<<"created_at">> => calendar:local_time()
|
||||
}, true).
|
||||
@ -1,43 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(device_bo).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([get_all_devices/0, get_host_devices/1, get_device_by_uuid/1, change_status/2]).
|
||||
|
||||
-spec get_all_devices() -> {ok, Devices :: [map()]} | {error, Reason :: any()}.
|
||||
get_all_devices() ->
|
||||
mysql_pool:get_all(mysql_iot, <<"SELECT * FROM device WHERE device_uuid != ''">>).
|
||||
|
||||
-spec get_host_devices(HostId :: integer()) -> {ok, Devices :: [map()]} | {error, Reason::any()}.
|
||||
get_host_devices(HostId) when is_integer(HostId) ->
|
||||
mysql_pool:get_all(mysql_iot, <<"SELECT device_uuid FROM device WHERE host_id = ? AND device_uuid != ''">>, [HostId]).
|
||||
|
||||
-spec get_device_by_uuid(DeviceUUID :: binary()) -> {ok, DeviceInfo :: map()} | undefined.
|
||||
get_device_by_uuid(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM device WHERE device_uuid = ? LIMIT 1">>, [DeviceUUID]).
|
||||
|
||||
%% 修改主机的状态
|
||||
-spec change_status(DeviceUUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
change_status(DeviceUUID, NStatus) when is_binary(DeviceUUID), is_integer(NStatus) ->
|
||||
change_status0(DeviceUUID, NStatus).
|
||||
change_status0(DeviceUUID, ?DEVICE_ONLINE) when is_binary(DeviceUUID) ->
|
||||
Timestamp = calendar:local_time(),
|
||||
case mysql_pool:get_row(mysql_iot, <<"SELECT status FROM device WHERE device_uuid = ? LIMIT 1">>, [DeviceUUID]) of
|
||||
{ok, #{<<"status">> := -1}} ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE device SET status = ?, access_at = ?, updated_at = ? WHERE device_uuid = ? LIMIT 1">>, [?DEVICE_ONLINE, Timestamp, Timestamp, DeviceUUID]);
|
||||
{ok, _} ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE device SET status = ?, updated_at = ? WHERE device_uuid = ? LIMIT 1">>, [?DEVICE_ONLINE, Timestamp, DeviceUUID]);
|
||||
undefined ->
|
||||
{error, <<"device not found">>}
|
||||
end;
|
||||
change_status0(DeviceUUID, ?DEVICE_OFFLINE) when is_binary(DeviceUUID) ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE device SET status = ? WHERE device_uuid = ? LIMIT 1">>, [?DEVICE_OFFLINE, DeviceUUID]).
|
||||
@ -1,97 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_bo).
|
||||
-author("aresei").
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([get_all_endpoints/0, get_endpoint/1]).
|
||||
-export([endpoint_record/1]).
|
||||
|
||||
-spec get_all_endpoints() -> [Endpoint :: map()].
|
||||
get_all_endpoints() ->
|
||||
case mysql_pool:get_all(mysql_iot, <<"SELECT * FROM endpoint where status = 1">>) of
|
||||
{ok, Endpoints} ->
|
||||
Endpoints;
|
||||
{error, _} ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec get_endpoint(Id :: integer()) -> undefined | {ok, EndpointInfo :: map()}.
|
||||
get_endpoint(Id) when is_integer(Id) ->
|
||||
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM endpoint WHERE id = ? and status = 1 LIMIT 1">>, [Id]).
|
||||
|
||||
-spec endpoint_record(EndpointInfo :: #{}) -> error | {ok, Endpoint :: #endpoint{}}.
|
||||
endpoint_record(#{<<"id">> := Id, <<"name">> := Name, <<"title">> := Title, <<"type">> := Type, <<"config_json">> := ConfigJson,
|
||||
<<"status">> := Status, <<"updated_at">> := UpdatedAt, <<"created_at">> := CreatedAt}) ->
|
||||
try
|
||||
Config = parse_config(Type, catch jiffy:decode(ConfigJson, [return_maps])),
|
||||
{ok, #endpoint {
|
||||
id = Id,
|
||||
name = Name,
|
||||
title = Title,
|
||||
config = Config,
|
||||
status = Status,
|
||||
updated_at = UpdatedAt,
|
||||
created_at = CreatedAt
|
||||
}}
|
||||
catch throw:_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
parse_config(<<"mqtt">>, #{<<"host">> := Host, <<"port">> := Port, <<"client_id">> := ClientId, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}) ->
|
||||
#mqtt_endpoint{
|
||||
host = Host,
|
||||
port = Port,
|
||||
client_id = ClientId,
|
||||
username = Username,
|
||||
password = Password,
|
||||
topic = Topic,
|
||||
qos = Qos
|
||||
};
|
||||
parse_config(<<"http">>, #{<<"url">> := Url, <<"pool_size">> := PoolSize}) ->
|
||||
#http_endpoint{
|
||||
url = Url,
|
||||
pool_size = PoolSize
|
||||
};
|
||||
parse_config(<<"kafka">>, #{<<"sasl_config">> := #{<<"username">> := Username, <<"password">> := Password, <<"mechanism">> := Mechanism0}, <<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
|
||||
Mechanism = case Mechanism0 of
|
||||
<<"sha_256">> ->
|
||||
scram_sha_256;
|
||||
<<"sha_512">> ->
|
||||
scram_sha_512;
|
||||
<<"plain">> ->
|
||||
plain;
|
||||
_ ->
|
||||
plain
|
||||
end,
|
||||
|
||||
#kafka_endpoint{
|
||||
sasl_config = {Mechanism, Username, Password},
|
||||
bootstrap_servers = parse_bootstrap_servers(BootstrapServers),
|
||||
topic = Topic
|
||||
};
|
||||
parse_config(<<"kafka">>, #{<<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}) ->
|
||||
#kafka_endpoint{
|
||||
sasl_config = undefined,
|
||||
bootstrap_servers = parse_bootstrap_servers(BootstrapServers),
|
||||
topic = Topic
|
||||
};
|
||||
parse_config(_, _) ->
|
||||
throw(invalid_config).
|
||||
|
||||
parse_bootstrap_servers(BootstrapServers) when is_list(BootstrapServers) ->
|
||||
lists:filtermap(fun(S) ->
|
||||
case binary:split(S, <<":">>) of
|
||||
[Host0, Port0] ->
|
||||
{true, {binary_to_list(Host0), binary_to_integer(Port0)}};
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end, BootstrapServers).
|
||||
@ -1,25 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(event_logs_bo).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
-export([insert/3]).
|
||||
|
||||
%% API
|
||||
|
||||
-spec insert(EventType :: integer(), AssocUUID :: binary(), Status :: integer()) ->
|
||||
{ok, InsertId :: integer()} | {error, Reason :: any()}.
|
||||
insert(EventType, AssocUUID, Status) when is_integer(EventType), is_binary(AssocUUID), is_integer(Status) ->
|
||||
mysql_pool:insert(mysql_iot, <<"event_logs">>, #{
|
||||
<<"event_type">> => EventType,
|
||||
<<"assoc_uuid">> => AssocUUID,
|
||||
<<"status">> => Status,
|
||||
<<"created_at">> => calendar:local_time()
|
||||
}, true).
|
||||
@ -1,49 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(host_bo).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([get_all_hosts/0, change_status/2, get_host_by_uuid/1, get_host_by_id/1]).
|
||||
|
||||
-spec get_all_hosts() -> UUIDList :: [binary()].
|
||||
get_all_hosts() ->
|
||||
case mysql_pool:get_all(mysql_iot, <<"SELECT uuid FROM host where uuid != '' limit 10">>) of
|
||||
{ok, Hosts} ->
|
||||
lists:map(fun(#{<<"uuid">> := UUID}) -> UUID end, Hosts);
|
||||
{error, _} ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec get_host_by_uuid(UUID :: binary()) -> undefined | {ok, HostInfo :: map()}.
|
||||
get_host_by_uuid(UUID) when is_binary(UUID) ->
|
||||
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM host WHERE uuid = ? LIMIT 1">>, [UUID]).
|
||||
|
||||
-spec get_host_by_id(HostId :: integer()) -> undefined | {ok, HostInfo :: map()}.
|
||||
get_host_by_id(HostId) when is_integer(HostId) ->
|
||||
mysql_pool:get_row(mysql_iot, <<"SELECT * FROM host WHERE id = ? LIMIT 1">>, [HostId]).
|
||||
|
||||
%% 修改主机的状态
|
||||
-spec change_status(UUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
change_status(UUID, NStatus) when is_binary(UUID), is_integer(NStatus) ->
|
||||
change_status0(UUID, NStatus).
|
||||
change_status0(UUID, ?HOST_ONLINE) when is_binary(UUID) ->
|
||||
Timestamp = calendar:local_time(),
|
||||
case mysql_pool:get_row(mysql_iot, <<"SELECT status FROM host WHERE uuid = ? LIMIT 1">>, [UUID]) of
|
||||
%% 第一次更新激活
|
||||
{ok, #{<<"status">> := -1}} ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE host SET status = ?, access_at = ?, updated_at = ? WHERE uuid = ? LIMIT 1">>, [?HOST_ONLINE, Timestamp, Timestamp, UUID]);
|
||||
{ok, _} ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE host SET status = ?, updated_at = ? WHERE uuid = ? LIMIT 1">>, [?HOST_ONLINE, Timestamp, UUID]);
|
||||
undefined ->
|
||||
{error, <<"host not found">>}
|
||||
end;
|
||||
change_status0(UUID, ?HOST_OFFLINE) when is_binary(UUID) ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE host SET status = ? WHERE uuid = ? LIMIT 1">>, [?HOST_OFFLINE, UUID]).
|
||||
@ -1,17 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(micro_inform_log).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([insert/1]).
|
||||
|
||||
insert(Fields) when is_map(Fields) ->
|
||||
mysql_pool:insert(mysql_iot, <<"micro_inform_log">>, Fields, true).
|
||||
@ -1,23 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(micro_service_bo).
|
||||
-author("aresei").
|
||||
-export([get_service_config/1]).
|
||||
|
||||
%% API
|
||||
|
||||
%% TODO
|
||||
-spec get_service_config(ServiceId :: binary()) -> {ok, ConfigJson :: binary()} | error.
|
||||
get_service_config(ServiceId) when is_binary(ServiceId) ->
|
||||
case mysql_pool:get_row(mysql_iot, <<"SELECT * FROM micro_service WHERE id = ? LIMIT 1">>, [ServiceId]) of
|
||||
undefined ->
|
||||
error;
|
||||
{ok, #{<<"config">> := Config}} ->
|
||||
{ok, Config}
|
||||
end.
|
||||
@ -1,19 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(micro_set_bo).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([change_status/4]).
|
||||
|
||||
%% 修改主机的状态
|
||||
-spec change_status(HostId :: integer(), SceneId :: integer(), MircoId :: integer(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
change_status(HostId, SceneId, MircoId, Status) when is_integer(HostId), is_integer(SceneId), is_integer(MircoId), is_integer(Status) ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE micro_set SET status = ? WHERE host_id = ? AND scene_id = ? AND micro_id = ? LIMIT 1">>, [Status, HostId, SceneId, MircoId]).
|
||||
@ -1,19 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 16. 5月 2023 12:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(task_logs_bo).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([change_status/2]).
|
||||
|
||||
%% 修改主机的状态
|
||||
-spec change_status(TaskId :: integer(), Status :: integer()) -> {ok, AffectedRow :: integer()} | {error, Reason :: any()}.
|
||||
change_status(TaskId, Status) when is_integer(TaskId), is_integer(Status) ->
|
||||
mysql_pool:update_by(mysql_iot, <<"UPDATE task_logs SET status = ? WHERE id = ? LIMIT 1">>, [Status, TaskId]).
|
||||
60
apps/iot/src/devtools/endpoint_kafka_test.erl
Normal file
60
apps/iot/src/devtools/endpoint_kafka_test.erl
Normal file
@ -0,0 +1,60 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 14. 11月 2025 23:29
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_kafka_test).
|
||||
-author("anlicheng").
|
||||
-include_lib("endpoint/include/endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_manual/0, test_consumer/0]).
|
||||
|
||||
start_manual() ->
|
||||
Name = endpoint:get_name(100),
|
||||
{ok, Pid} = endpoint_kafka:start_link(Name, #endpoint{
|
||||
id = 100,
|
||||
%% 全局唯一,在路由规则中通过名称来指定
|
||||
matcher = <<"/dhlr/device/*/*">>,
|
||||
%% 标题描述
|
||||
title = <<"test_kafka_title">>,
|
||||
%% 配置项, 格式: #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}
|
||||
config = #kafka_endpoint{
|
||||
sasl_config = {plain, <<"test">>, <<"test123">>},
|
||||
bootstrap_servers = [{"118.178.229.213", 9092}],
|
||||
topic = <<"dhlr_data">>
|
||||
}
|
||||
}),
|
||||
|
||||
Json = iolist_to_binary(json:encode(#{
|
||||
<<"name">> => <<"anlicheng">>,
|
||||
<<"age">> => 30
|
||||
})),
|
||||
|
||||
endpoint:forward(Pid, Json),
|
||||
ok.
|
||||
|
||||
test_consumer() ->
|
||||
KafkaBootstrapEndpoints = [{"118.178.229.213", 9092}],
|
||||
Topic = <<"dhlr_data">>,
|
||||
|
||||
ClientConfig = [
|
||||
{sasl, {plain, <<"test">>, <<"test123">>}},
|
||||
{reconnect_cool_down_seconds, 5},
|
||||
{socket_options, [{keepalive, true}]}
|
||||
],
|
||||
|
||||
ok = brod:start_client(KafkaBootstrapEndpoints, client1, ClientConfig),
|
||||
SubscriberCallbackFun = fun(_Partition, Msg, ShellPid = CallbackState) ->
|
||||
logger:debug("call here msg: ~p", [Msg]),
|
||||
ShellPid ! Msg, {ok, ack, CallbackState}
|
||||
end,
|
||||
Res = brod_topic_subscriber:start_link(client1, Topic, all,
|
||||
_ConsumerConfig=[{begin_offset, 0}],
|
||||
_CommittedOffsets=[], message, SubscriberCallbackFun,
|
||||
_CallbackState=self()),
|
||||
|
||||
logger:debug("start subscriber res: ~p", [Res]).
|
||||
@ -4,56 +4,42 @@
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 17. 8月 2025 00:26
|
||||
%%% Created : 17. 11月 2025 16:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_name_server).
|
||||
-module(endpoint_mqtt_subscriber).
|
||||
-author("anlicheng").
|
||||
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([whereis_alias/1, register/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(TAB, iot_name_server).
|
||||
|
||||
%% 需要订阅的主题信息
|
||||
-define(Topics,[
|
||||
{<<"/dhlr/data">>, 0}
|
||||
]).
|
||||
|
||||
-record(state, {
|
||||
%% #{Pid => Name}
|
||||
pid_names = #{},
|
||||
refs = []
|
||||
conn_pid :: pid()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec register(Name :: atom(), Pid :: pid()) -> ok.
|
||||
register(Name, Pid) when is_atom(Name), is_pid(Pid) ->
|
||||
gen_server:call(?SERVER, {register, Name, Pid}).
|
||||
|
||||
-spec whereis_alias(Name :: atom()) -> undefined | pid().
|
||||
whereis_alias(Name) when is_atom(Name) ->
|
||||
case ets:lookup(?TAB, Name) of
|
||||
[] ->
|
||||
undefined;
|
||||
[{Name, Pid}|_] ->
|
||||
case is_process_alive(Pid) of
|
||||
true ->
|
||||
Pid;
|
||||
false ->
|
||||
undefined
|
||||
end
|
||||
end.
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
@ -65,9 +51,41 @@ start_link() ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
%% 初始化存储
|
||||
ets:new(?TAB, [named_table, ordered_set, public, {keypos, 1}]),
|
||||
{ok, #state{}}.
|
||||
%% 建立到emqx服务器的连接
|
||||
ClientId = <<"mqtt-client-test-host-subscriber">>,
|
||||
Opts = [
|
||||
{clientid, ClientId},
|
||||
{host, "118.178.229.213"},
|
||||
{port, 1883},
|
||||
{owner, self()},
|
||||
{tcp_opts, []},
|
||||
{username, "admin"},
|
||||
{password, "admin"},
|
||||
{keepalive, 86400},
|
||||
{auto_ack, true},
|
||||
{proto_ver, v5},
|
||||
{retry_interval, 5}
|
||||
],
|
||||
|
||||
logger:debug("[opts] is: ~p", [Opts]),
|
||||
case emqtt:start_link(Opts) of
|
||||
{ok, ConnPid} ->
|
||||
%% 监听和host相关的全部事件
|
||||
logger:debug("[iot_mqtt_subscriber] start conntecting, pid: ~p", [ConnPid]),
|
||||
{ok, _} = emqtt:connect(ConnPid),
|
||||
logger:debug("[iot_mqtt_subscriber] connect success, pid: ~p", [ConnPid]),
|
||||
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
|
||||
|
||||
logger:debug("[iot_mqtt_subscriber] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
|
||||
|
||||
{ok, #state{conn_pid = ConnPid}};
|
||||
ignore ->
|
||||
logger:debug("[iot_mqtt_subscriber] connect emqx get ignore"),
|
||||
{stop, ignore};
|
||||
{error, Reason} ->
|
||||
logger:debug("[iot_mqtt_subscriber] connect emqx get error: ~p", [Reason]),
|
||||
{stop, Reason}
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
@ -79,10 +97,8 @@ init([]) ->
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call({register, Name, Pid}, _From, State = #state{refs = Refs, pid_names = PidNames}) ->
|
||||
true = ets:insert(?TAB, {Name, Pid}),
|
||||
MRef = erlang:monitor(process, Pid),
|
||||
{reply, ok, State#state{refs = [MRef|Refs], pid_names = maps:put(Pid, Name, PidNames)}}.
|
||||
handle_call(_Info, _From, State = #state{conn_pid = _ConnPid}) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
@ -99,20 +115,21 @@ handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({'DOWN', MRef, process, Pid, Reason}, State = #state{refs = Refs, pid_names = PidNames}) ->
|
||||
% lager:debug("[iot_name_server] pid: ~p, down with reason: ~p", [Reason]),
|
||||
case lists:member(MRef, Refs) of
|
||||
true ->
|
||||
case maps:take(Pid, PidNames) of
|
||||
error ->
|
||||
{noreply, State#state{refs = lists:delete(MRef, Refs)}};
|
||||
{Name, NPidNames} ->
|
||||
true = ets:delete(?TAB, Name),
|
||||
{noreply, State#state{pid_names = NPidNames, refs = lists:delete(MRef, Refs)}}
|
||||
end;
|
||||
false ->
|
||||
{noreply, State}
|
||||
end.
|
||||
handle_info({disconnect, ReasonCode, Properties}, State = #state{}) ->
|
||||
logger:debug("[iot_mqtt_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||
{stop, disconnected, State};
|
||||
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
||||
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) ->
|
||||
logger:debug("[iot_mqtt_subscriber] Recv a topic: ~p, publish packet: ~p, qos: ~p", [Topic, Payload, Qos]),
|
||||
%% 将消息分发到对应的host进程去处理
|
||||
{noreply, State};
|
||||
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
||||
logger:debug("[iot_mqtt_subscriber] receive puback packet: ~p", [Packet]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State = #state{}) ->
|
||||
logger:debug("[iot_mqtt_subscriber] get info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
@ -121,7 +138,16 @@ handle_info({'DOWN', MRef, process, Pid, Reason}, State = #state{refs = Refs, pi
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
||||
%% 取消topic的订阅
|
||||
TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics),
|
||||
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames),
|
||||
|
||||
ok = emqtt:disconnect(ConnPid),
|
||||
logger:debug("[iot_mqtt_subscriber] terminate with reason: ~p", [Reason]),
|
||||
ok;
|
||||
terminate(Reason, _State) ->
|
||||
logger:debug("[iot_mqtt_subscriber] terminate with reason: ~p", [Reason]),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
@ -13,13 +13,5 @@
|
||||
-export([test/0]).
|
||||
|
||||
test() ->
|
||||
|
||||
{ok, Content} = file:read_file("/tmp/test.erl"),
|
||||
|
||||
{ok, Tokens, _} = erl_scan:string(binary_to_list(Content)),
|
||||
{ok, ExprList} = erl_parse:parse_exprs(Tokens),
|
||||
|
||||
{value, F, _NewBindings} = erl_eval:exprs(ExprList, []),
|
||||
F(#{name => <<"test">>}).
|
||||
|
||||
{error, disabled}.
|
||||
|
||||
@ -12,8 +12,7 @@
|
||||
|
||||
%% API
|
||||
-export([rsa_encode/1]).
|
||||
-export([insert_services/1]).
|
||||
-export([test_mqtt/0, test_influxdb/0]).
|
||||
-export([test_influxdb/0]).
|
||||
|
||||
test_influxdb() ->
|
||||
UUID = <<"device123123">>,
|
||||
@ -29,41 +28,20 @@ test_influxdb() ->
|
||||
end)
|
||||
end, lists:seq(1, 100)).
|
||||
|
||||
test_mqtt() ->
|
||||
iot_zd_endpoint:forward(<<"location_code_test123">>, [
|
||||
#{<<"key">> => <<"name">>, <<"value">> => <<"anlicheng">>},
|
||||
#{<<"key">> => <<"age">>, <<"value">> => 30},
|
||||
#{<<"key">> => <<"flow">>, <<"value">> => 30}
|
||||
], iot_util:timestamp_of_seconds()).
|
||||
|
||||
insert_services(Num) ->
|
||||
lists:foreach(fun(Id) ->
|
||||
Res = mysql_pool:insert(mysql_iot, <<"micro_service">>,
|
||||
#{
|
||||
<<"name">> => <<"微服务"/utf8, (integer_to_binary(Id))/binary>>,
|
||||
<<"code">> => <<"1223423423423423"/utf8>>,
|
||||
<<"type">> => 1,
|
||||
<<"version">> => <<"v1.0">>,
|
||||
<<"url">> => <<"https://www.baidu.com">>,
|
||||
<<"detail">> => <<"这是一个关于测试的微服务"/utf8>>
|
||||
}, false),
|
||||
lager:debug("insert service result is: ~p", [Res])
|
||||
end, lists:seq(1, Num)).
|
||||
|
||||
rsa_encode(Data) when is_binary(Data) ->
|
||||
%% 读取相关配置
|
||||
PublicPemFile = "/tmp/keys/public.pem",
|
||||
|
||||
%% 私钥保存解析后的
|
||||
{ok, PubBin} = file:read_file(PublicPemFile),
|
||||
lager:debug("pub bin is: ~p", [PubBin]),
|
||||
logger:debug("pub bin is: ~p", [PubBin]),
|
||||
[Pub] = public_key:pem_decode(PubBin),
|
||||
lager:debug("pub pem bin is: ~p", [Pub]),
|
||||
logger:debug("pub pem bin is: ~p", [Pub]),
|
||||
PubKey = public_key:pem_entry_decode(Pub),
|
||||
lager:debug("the public key is: ~p", [PubKey]),
|
||||
logger:debug("the public key is: ~p", [PubKey]),
|
||||
|
||||
EncData = public_key:encrypt_public(Data, PubKey),
|
||||
lager:debug("enc data is: ~p", [EncData]),
|
||||
logger:debug("enc data is: ~p", [EncData]),
|
||||
|
||||
rsa_decode(EncData),
|
||||
|
||||
@ -75,13 +53,13 @@ rsa_decode(EncData) when is_binary(EncData) ->
|
||||
|
||||
%% 私钥保存解析后的
|
||||
{ok, PubBin} = file:read_file(PublicPemFile),
|
||||
lager:debug("pub bin is: ~p", [PubBin]),
|
||||
logger:debug("pub bin is: ~p", [PubBin]),
|
||||
[Pub] = public_key:pem_decode(PubBin),
|
||||
lager:debug("pub pem bin is: ~p", [Pub]),
|
||||
logger:debug("pub pem bin is: ~p", [Pub]),
|
||||
PubKey = public_key:pem_entry_decode(Pub),
|
||||
lager:debug("the public key is: ~p", [PubKey]),
|
||||
logger:debug("the public key is: ~p", [PubKey]),
|
||||
|
||||
PlainData = public_key:decrypt_private(EncData, PubKey),
|
||||
lager:debug("plain data is: ~p", [PlainData]),
|
||||
logger:debug("plain data is: ~p", [PlainData]),
|
||||
|
||||
ok.
|
||||
588
apps/iot/src/docker/docker_container_builder.erl
Normal file
588
apps/iot/src/docker/docker_container_builder.erl
Normal file
@ -0,0 +1,588 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%% ContainerRequest wire builder helpers.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(docker_container_builder).
|
||||
|
||||
-export([list_request/0, config_request/2, deploy_request/2, start_request/1, stop_request/1, kill_request/1, remove_request/1]).
|
||||
|
||||
-spec list_request() -> map().
|
||||
list_request() ->
|
||||
#{action => list, all => true}.
|
||||
|
||||
-spec config_request(binary(), binary()) -> map().
|
||||
config_request(ContainerName, ConfigJson) when is_binary(ContainerName), is_binary(ConfigJson) ->
|
||||
#{
|
||||
action => config,
|
||||
target => container_ref(ContainerName),
|
||||
config => ConfigJson
|
||||
}.
|
||||
|
||||
-spec deploy_request(integer(), map()) -> {ok, map()} | {error, binary()}.
|
||||
deploy_request(TaskId, Config) when is_integer(TaskId), is_map(Config), TaskId >= 0 ->
|
||||
try
|
||||
ensure_supported_deploy_config(Config),
|
||||
validate_deploy_config(Config),
|
||||
Params = build_container_deploy_params(Config),
|
||||
{ok, #{action => deploy, task_id => TaskId, params => Params}}
|
||||
catch
|
||||
throw:{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
deploy_request(TaskId, _Config) when is_integer(TaskId), TaskId < 0 ->
|
||||
{error, <<"task_id must be non-negative">>};
|
||||
deploy_request(_TaskId, _Config) ->
|
||||
{error, <<"invalid deploy request">>}.
|
||||
|
||||
-spec start_request(binary()) -> map().
|
||||
start_request(ContainerName) when is_binary(ContainerName) ->
|
||||
#{action => start, target => container_ref(ContainerName)}.
|
||||
|
||||
-spec stop_request(binary()) -> map().
|
||||
stop_request(ContainerName) when is_binary(ContainerName) ->
|
||||
#{
|
||||
action => stop,
|
||||
target => container_ref(ContainerName),
|
||||
timeout_seconds => 0
|
||||
}.
|
||||
|
||||
-spec kill_request(binary()) -> map().
|
||||
kill_request(ContainerName) when is_binary(ContainerName) ->
|
||||
#{
|
||||
action => kill,
|
||||
target => container_ref(ContainerName),
|
||||
signal => <<>>
|
||||
}.
|
||||
|
||||
-spec remove_request(binary()) -> map().
|
||||
remove_request(ContainerName) when is_binary(ContainerName) ->
|
||||
#{
|
||||
action => remove,
|
||||
target => container_ref(ContainerName),
|
||||
force => false,
|
||||
remove_volumes => false
|
||||
}.
|
||||
|
||||
-spec container_ref(binary()) -> map().
|
||||
container_ref(ContainerName) when is_binary(ContainerName) ->
|
||||
#{name => ContainerName, id => <<>>}.
|
||||
|
||||
-spec ensure_supported_deploy_config(map()) -> ok.
|
||||
ensure_supported_deploy_config(Config) when is_map(Config) ->
|
||||
UnsupportedKeys = [Key || Key <- [<<"container_dir">>], maps:is_key(Key, Config)],
|
||||
case UnsupportedKeys of
|
||||
[] ->
|
||||
ok;
|
||||
_ ->
|
||||
Unsupported = iolist_to_binary(lists:join(<<", ">>, UnsupportedKeys)),
|
||||
throw({error, <<"unsupported container config keys: ", Unsupported/binary>>})
|
||||
end.
|
||||
|
||||
-spec validate_deploy_config(map()) -> ok.
|
||||
validate_deploy_config(Config) when is_map(Config) ->
|
||||
Required = [
|
||||
{<<"image">>, binary},
|
||||
{<<"container_name">>, binary},
|
||||
{<<"command">>, {list, binary}},
|
||||
{<<"restart">>, binary}
|
||||
],
|
||||
Optional = [
|
||||
{<<"privileged">>, boolean},
|
||||
{<<"entrypoint">>, {list, binary}},
|
||||
{<<"envs">>, {list, binary}},
|
||||
{<<"ports">>, {list, binary}},
|
||||
{<<"expose">>, {list, binary}},
|
||||
{<<"volumes">>, {list, binary}},
|
||||
{<<"networks">>, {list, binary}},
|
||||
{<<"labels">>, {map, {binary, binary}}},
|
||||
{<<"user">>, binary},
|
||||
{<<"working_dir">>, binary},
|
||||
{<<"hostname">>, binary},
|
||||
{<<"network_mode">>, binary},
|
||||
{<<"cap_add">>, {list, binary}},
|
||||
{<<"cap_drop">>, {list, binary}},
|
||||
{<<"devices">>, {list, binary}},
|
||||
{<<"mem_limit">>, binary},
|
||||
{<<"mem_reservation">>, binary},
|
||||
{<<"cpu_shares">>, non_neg_integer},
|
||||
{<<"cpus">>, non_neg_number},
|
||||
{<<"ulimits">>, {map, {binary, binary}}},
|
||||
{<<"sysctls">>, {map, {binary, binary}}},
|
||||
{<<"tmpfs">>, {list, binary}},
|
||||
{<<"extra_hosts">>, {list, binary}},
|
||||
{<<"healthcheck">>, map}
|
||||
],
|
||||
Errors = check_required(Config, Required) ++ check_optional(Config, Optional) ++ check_healthcheck(Config),
|
||||
case Errors of
|
||||
[] ->
|
||||
ok;
|
||||
_ ->
|
||||
throw({error, iolist_to_binary(lists:join(<<"|||">>, Errors))})
|
||||
end.
|
||||
|
||||
-spec check_required(map(), list()) -> [binary()].
|
||||
check_required(Config, Fields) ->
|
||||
lists:foldl(
|
||||
fun({Key, Type}, ErrAcc) ->
|
||||
case maps:get(Key, Config, undefined) of
|
||||
undefined ->
|
||||
[iolist_to_binary(io_lib:format("miss requied parameter: ~p", [Key])) | ErrAcc];
|
||||
Value ->
|
||||
case check_type(Value, Type) of
|
||||
true ->
|
||||
ErrAcc;
|
||||
false ->
|
||||
[iolist_to_binary(io_lib:format("required parameter: ~p, type must be: ~ts", [Key, type_name(Type)])) | ErrAcc]
|
||||
end
|
||||
end
|
||||
end,
|
||||
[], Fields).
|
||||
|
||||
-spec check_optional(map(), list()) -> [binary()].
|
||||
check_optional(Config, Fields) ->
|
||||
lists:foldl(
|
||||
fun({Key, Type}, ErrAcc) ->
|
||||
case maps:get(Key, Config, undefined) of
|
||||
undefined ->
|
||||
ErrAcc;
|
||||
Value ->
|
||||
case check_type(Value, Type) of
|
||||
true ->
|
||||
ErrAcc;
|
||||
false ->
|
||||
[iolist_to_binary(io_lib:format("optional parameter: ~p, type must be: ~ts", [Key, type_name(Type)])) | ErrAcc]
|
||||
end
|
||||
end
|
||||
end,
|
||||
[], Fields).
|
||||
|
||||
-spec type_name(tuple() | atom()) -> binary().
|
||||
type_name(binary) ->
|
||||
<<"string">>;
|
||||
type_name(integer) ->
|
||||
<<"integer">>;
|
||||
type_name(non_neg_integer) ->
|
||||
<<"non-negative integer">>;
|
||||
type_name(number) ->
|
||||
<<"number">>;
|
||||
type_name(non_neg_number) ->
|
||||
<<"non-negative number">>;
|
||||
type_name(duration) ->
|
||||
<<"duration string or non-negative integer">>;
|
||||
type_name(list) ->
|
||||
<<"list">>;
|
||||
type_name({list, binary}) ->
|
||||
<<"list of string">>;
|
||||
type_name({list, number}) ->
|
||||
<<"list of number">>;
|
||||
type_name({list, integer}) ->
|
||||
<<"list of integer">>;
|
||||
type_name(map) ->
|
||||
<<"map">>;
|
||||
type_name({map, {binary, binary}}) ->
|
||||
<<"map of string:string">>;
|
||||
type_name({map, {binary, any}}) ->
|
||||
<<"map of string:any">>;
|
||||
type_name(boolean) ->
|
||||
<<"boolean">>.
|
||||
|
||||
-spec check_type(any(), any()) -> boolean().
|
||||
check_type(Value, binary) ->
|
||||
is_binary(Value);
|
||||
check_type(Value, integer) ->
|
||||
is_integer(Value);
|
||||
check_type(Value, non_neg_integer) ->
|
||||
is_integer(Value) andalso Value >= 0;
|
||||
check_type(Value, number) ->
|
||||
is_number(Value);
|
||||
check_type(Value, non_neg_number) ->
|
||||
is_number(Value) andalso Value >= 0;
|
||||
check_type(Value, duration) ->
|
||||
is_binary(Value) orelse (is_integer(Value) andalso Value >= 0);
|
||||
check_type(Value, list) when is_list(Value) ->
|
||||
true;
|
||||
check_type(Value, {list, binary}) when is_list(Value) ->
|
||||
lists:all(fun(E) -> is_binary(E) end, Value);
|
||||
check_type(Value, {list, number}) when is_list(Value) ->
|
||||
lists:all(fun(E) -> is_number(E) end, Value);
|
||||
check_type(Value, {list, integer}) when is_list(Value) ->
|
||||
lists:all(fun(E) -> is_integer(E) end, Value);
|
||||
check_type(Value, map) when is_map(Value) ->
|
||||
true;
|
||||
check_type(Value, {map, {binary, binary}}) when is_map(Value) ->
|
||||
lists:all(fun({K, V}) -> is_binary(K) andalso is_binary(V) end, maps:to_list(Value));
|
||||
check_type(Value, {map, {binary, any}}) when is_map(Value) ->
|
||||
lists:all(fun({K, _}) -> is_binary(K) end, maps:to_list(Value));
|
||||
check_type(Value, boolean) ->
|
||||
is_boolean(Value);
|
||||
check_type(_, _) ->
|
||||
false.
|
||||
|
||||
-spec check_healthcheck(map()) -> [binary()].
|
||||
check_healthcheck(Config) ->
|
||||
case maps:get(<<"healthcheck">>, Config, undefined) of
|
||||
undefined ->
|
||||
[];
|
||||
Healthcheck when is_map(Healthcheck) ->
|
||||
Fields = [
|
||||
{<<"test">>, {list, binary}},
|
||||
{<<"interval">>, duration},
|
||||
{<<"timeout">>, duration},
|
||||
{<<"retries">>, non_neg_integer}
|
||||
],
|
||||
KeyErrors = [<<"optional parameter: <<\"healthcheck\">>, type must be: map of string:any">> ||
|
||||
{Key, _Value} <- maps:to_list(Healthcheck), not is_binary(Key)],
|
||||
KeyErrors ++ check_optional(Healthcheck, Fields);
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
-spec build_container_deploy_params(map()) -> map().
|
||||
build_container_deploy_params(Config) when is_map(Config) ->
|
||||
#{
|
||||
container_name => maps:get(<<"container_name">>, Config),
|
||||
create => build_docker_create_options(Config)
|
||||
}.
|
||||
|
||||
-spec build_docker_create_options(map()) -> map().
|
||||
build_docker_create_options(Config) when is_map(Config) ->
|
||||
#{
|
||||
config => build_docker_container_config(Config),
|
||||
host_config => build_docker_host_config(Config),
|
||||
networking_config => build_docker_networking_config(Config)
|
||||
}.
|
||||
|
||||
-spec build_docker_container_config(map()) -> map().
|
||||
build_docker_container_config(Config) when is_map(Config) ->
|
||||
#{
|
||||
image => maps:get(<<"image">>, Config),
|
||||
cmd => maps:get(<<"command">>, Config),
|
||||
entrypoint => maps:get(<<"entrypoint">>, Config, []),
|
||||
env => maps:get(<<"envs">>, Config, []),
|
||||
labels => maps:get(<<"labels">>, Config, #{}),
|
||||
volumes => build_container_volumes(maps:get(<<"volumes">>, Config, [])),
|
||||
user => maps:get(<<"user">>, Config, <<>>),
|
||||
working_dir => maps:get(<<"working_dir">>, Config, <<>>),
|
||||
hostname => maps:get(<<"hostname">>, Config, <<>>),
|
||||
exposed_ports => build_exposed_ports(Config),
|
||||
healthcheck => build_healthcheck(maps:get(<<"healthcheck">>, Config, undefined))
|
||||
}.
|
||||
|
||||
-spec build_docker_host_config(map()) -> map().
|
||||
build_docker_host_config(Config) when is_map(Config) ->
|
||||
#{
|
||||
binds => build_host_binds(maps:get(<<"volumes">>, Config, [])),
|
||||
network_mode => maps:get(<<"network_mode">>, Config, <<>>),
|
||||
restart_policy => build_restart_policy(maps:get(<<"restart">>, Config)),
|
||||
privileged => maps:get(<<"privileged">>, Config, false),
|
||||
cap_add => maps:get(<<"cap_add">>, Config, []),
|
||||
cap_drop => maps:get(<<"cap_drop">>, Config, []),
|
||||
devices => build_device_mappings(maps:get(<<"devices">>, Config, [])),
|
||||
memory => default_uint64(parse_optional_size_bytes(maps:get(<<"mem_limit">>, Config, undefined), <<"mem_limit">>)),
|
||||
memory_reservation => default_uint64(parse_optional_size_bytes(maps:get(<<"mem_reservation">>, Config, undefined), <<"mem_reservation">>)),
|
||||
nano_cpus => default_uint64(parse_optional_nano_cpus(maps:get(<<"cpus">>, Config, undefined))),
|
||||
cpu_shares => default_uint64(maps:get(<<"cpu_shares">>, Config, undefined)),
|
||||
port_bindings => build_port_bindings(maps:get(<<"ports">>, Config, [])),
|
||||
ulimits => build_ulimits(maps:get(<<"ulimits">>, Config, #{})),
|
||||
tmpfs => maps:from_list(build_tmpfs_options(maps:get(<<"tmpfs">>, Config, []))),
|
||||
sysctls => maps:get(<<"sysctls">>, Config, #{}),
|
||||
extra_hosts => maps:get(<<"extra_hosts">>, Config, [])
|
||||
}.
|
||||
|
||||
-spec build_docker_networking_config(map()) -> map().
|
||||
build_docker_networking_config(Config) when is_map(Config) ->
|
||||
Networks = maps:get(<<"networks">>, Config, []),
|
||||
#{endpoints => [#{name => Network} || Network <- Networks]}.
|
||||
|
||||
-spec build_restart_policy(binary()) -> map().
|
||||
build_restart_policy(Restart0) when is_binary(Restart0) ->
|
||||
case binary:split(Restart0, <<":">>) of
|
||||
[Name, RetryCountBin] ->
|
||||
#{name => Name, maximum_retry_count => parse_uint32(RetryCountBin, <<"restart">>)};
|
||||
[Name] ->
|
||||
#{name => Name, maximum_retry_count => 0}
|
||||
end.
|
||||
|
||||
-spec build_healthcheck(undefined | map()) -> undefined | map().
|
||||
build_healthcheck(undefined) ->
|
||||
undefined;
|
||||
build_healthcheck(Healthcheck) when is_map(Healthcheck) ->
|
||||
#{
|
||||
test => maps:get(<<"test">>, Healthcheck, []),
|
||||
interval_ns => parse_duration_ns(maps:get(<<"interval">>, Healthcheck, <<"0s">>), <<"healthcheck.interval">>),
|
||||
timeout_ns => parse_duration_ns(maps:get(<<"timeout">>, Healthcheck, <<"0s">>), <<"healthcheck.timeout">>),
|
||||
retries => maps:get(<<"retries">>, Healthcheck, 0)
|
||||
}.
|
||||
|
||||
-spec default_uint64(undefined | non_neg_integer()) -> non_neg_integer().
|
||||
default_uint64(undefined) ->
|
||||
0;
|
||||
default_uint64(Value) when is_integer(Value), Value >= 0 ->
|
||||
Value.
|
||||
|
||||
-spec parse_optional_nano_cpus(undefined | number()) -> undefined | non_neg_integer().
|
||||
parse_optional_nano_cpus(undefined) ->
|
||||
undefined;
|
||||
parse_optional_nano_cpus(Cpus) when is_integer(Cpus), Cpus >= 0 ->
|
||||
Cpus * 1000000000;
|
||||
parse_optional_nano_cpus(Cpus) when is_float(Cpus), Cpus >= 0 ->
|
||||
trunc(Cpus * 1000000000).
|
||||
|
||||
-spec build_container_volumes([binary()]) -> [binary()].
|
||||
build_container_volumes(VolumeSpecs) when is_list(VolumeSpecs) ->
|
||||
[ContainerPath || VolumeSpec <- VolumeSpecs, {_HostPath, ContainerPath, _ReadOnly} <- [parse_volume_spec(VolumeSpec)]].
|
||||
|
||||
-spec build_host_binds([binary()]) -> [binary()].
|
||||
build_host_binds(VolumeSpecs) when is_list(VolumeSpecs) ->
|
||||
[volume_bind(HostPath, ContainerPath, ReadOnly) ||
|
||||
VolumeSpec <- VolumeSpecs,
|
||||
{HostPath, ContainerPath, ReadOnly} <- [parse_volume_spec(VolumeSpec)]].
|
||||
|
||||
-spec parse_volume_spec(binary()) -> {binary(), binary(), boolean()}.
|
||||
parse_volume_spec(VolumeSpec) when is_binary(VolumeSpec) ->
|
||||
case binary:split(VolumeSpec, <<":">>, [global]) of
|
||||
[HostPath, ContainerPath] when HostPath =/= <<>>, ContainerPath =/= <<>> ->
|
||||
{HostPath, ContainerPath, false};
|
||||
[HostPath, ContainerPath | Modes] when HostPath =/= <<>>, ContainerPath =/= <<>> ->
|
||||
{HostPath, ContainerPath, lists:member(<<"ro">>, Modes)};
|
||||
_ ->
|
||||
throw({error, <<"invalid volume binding">>})
|
||||
end.
|
||||
|
||||
-spec volume_bind(binary(), binary(), boolean()) -> binary().
|
||||
volume_bind(HostPath, ContainerPath, true) when is_binary(HostPath), is_binary(ContainerPath) ->
|
||||
<<HostPath/binary, ":", ContainerPath/binary, ":ro">>;
|
||||
volume_bind(HostPath, ContainerPath, false) when is_binary(HostPath), is_binary(ContainerPath) ->
|
||||
<<HostPath/binary, ":", ContainerPath/binary>>.
|
||||
|
||||
-spec build_exposed_ports(map()) -> [map()].
|
||||
build_exposed_ports(Config) when is_map(Config) ->
|
||||
ExposePorts = [build_exposed_port(ExposeSpec) || ExposeSpec <- maps:get(<<"expose">>, Config, [])],
|
||||
BoundPorts = [
|
||||
#{container_port => ContainerPort, protocol => Protocol} ||
|
||||
#{container_port := ContainerPort, protocol := Protocol} <- build_port_bindings(maps:get(<<"ports">>, Config, []))
|
||||
],
|
||||
unique_ports(ExposePorts ++ BoundPorts).
|
||||
|
||||
-spec build_exposed_port(binary()) -> map().
|
||||
build_exposed_port(ExposeSpec) when is_binary(ExposeSpec) ->
|
||||
case binary:split(ExposeSpec, <<"/">>) of
|
||||
[PortBin] ->
|
||||
#{container_port => parse_uint32(PortBin, <<"expose">>), protocol => <<"tcp">>};
|
||||
[PortBin, Protocol] ->
|
||||
#{container_port => parse_uint32(PortBin, <<"expose">>), protocol => Protocol}
|
||||
end.
|
||||
|
||||
-spec build_port_bindings([binary()]) -> [map()].
|
||||
build_port_bindings(PortSpecs) when is_list(PortSpecs) ->
|
||||
[build_port_binding(PortSpec) || PortSpec <- PortSpecs].
|
||||
|
||||
-spec build_port_binding(binary()) -> map().
|
||||
build_port_binding(PortSpec) when is_binary(PortSpec) ->
|
||||
case binary:split(PortSpec, <<":">>, [global]) of
|
||||
[HostPortBin, ContainerPortSpec] when HostPortBin =/= <<>>, ContainerPortSpec =/= <<>> ->
|
||||
{ContainerPort, Protocol} = parse_container_port_spec(ContainerPortSpec, <<"ports">>),
|
||||
#{
|
||||
host_ip => <<>>,
|
||||
host_port => parse_tcp_port(HostPortBin, <<"ports.host_port">>),
|
||||
container_port => ContainerPort,
|
||||
protocol => Protocol
|
||||
};
|
||||
_ ->
|
||||
throw({error, <<"invalid port binding">>})
|
||||
end.
|
||||
|
||||
-spec parse_container_port_spec(binary(), binary()) -> {non_neg_integer(), binary()}.
|
||||
parse_container_port_spec(PortSpec, Field) when is_binary(PortSpec), is_binary(Field) ->
|
||||
case binary:split(PortSpec, <<"/">>) of
|
||||
[PortBin] ->
|
||||
{parse_tcp_port(PortBin, Field), <<"tcp">>};
|
||||
[PortBin, Protocol] when Protocol =/= <<>> ->
|
||||
{parse_tcp_port(PortBin, Field), Protocol};
|
||||
_ ->
|
||||
throw({error, <<"invalid port binding">>})
|
||||
end.
|
||||
|
||||
-spec unique_ports([map()]) -> [map()].
|
||||
unique_ports(Ports) when is_list(Ports) ->
|
||||
{_Seen, Result} = lists:foldl(
|
||||
fun(Port = #{container_port := ContainerPort, protocol := Protocol}, {Seen, Acc}) ->
|
||||
Key = {ContainerPort, Protocol},
|
||||
case maps:is_key(Key, Seen) of
|
||||
true ->
|
||||
{Seen, Acc};
|
||||
false ->
|
||||
{Seen#{Key => true}, [Port | Acc]}
|
||||
end
|
||||
end,
|
||||
{#{}, []},
|
||||
Ports),
|
||||
lists:reverse(Result).
|
||||
|
||||
-spec build_device_mappings([binary()]) -> [map()].
|
||||
build_device_mappings(DeviceSpecs) when is_list(DeviceSpecs) ->
|
||||
[build_device_mapping(DeviceSpec) || DeviceSpec <- DeviceSpecs].
|
||||
|
||||
-spec build_device_mapping(binary()) -> map().
|
||||
build_device_mapping(DeviceSpec) when is_binary(DeviceSpec) ->
|
||||
case binary:split(DeviceSpec, <<":">>, [global]) of
|
||||
[HostPath, ContainerPath] when HostPath =/= <<>>, ContainerPath =/= <<>> ->
|
||||
#{path_on_host => HostPath, path_in_container => ContainerPath, cgroup_permissions => <<"rwm">>};
|
||||
[HostPath, ContainerPath, Permissions] when HostPath =/= <<>>, ContainerPath =/= <<>>, Permissions =/= <<>> ->
|
||||
#{path_on_host => HostPath, path_in_container => ContainerPath, cgroup_permissions => Permissions};
|
||||
_ ->
|
||||
throw({error, <<"invalid device mapping">>})
|
||||
end.
|
||||
|
||||
-spec build_ulimits(map()) -> [map()].
|
||||
build_ulimits(Ulimits) when is_map(Ulimits) ->
|
||||
[build_ulimit(Name, Value) || {Name, Value} <- maps:to_list(Ulimits)].
|
||||
|
||||
-spec build_ulimit(binary(), binary()) -> map().
|
||||
build_ulimit(Name, Value) when is_binary(Name), is_binary(Value) ->
|
||||
case binary:split(Value, <<":">>) of
|
||||
[SoftBin, HardBin] ->
|
||||
#{name => Name, soft => parse_uint64(SoftBin, <<"ulimits.soft">>), hard => parse_uint64(HardBin, <<"ulimits.hard">>)};
|
||||
[LimitBin] ->
|
||||
Limit = parse_uint64(LimitBin, <<"ulimits.limit">>),
|
||||
#{name => Name, soft => Limit, hard => Limit}
|
||||
end.
|
||||
|
||||
-spec build_tmpfs_options([binary()]) -> [{binary(), binary()}].
|
||||
build_tmpfs_options(TmpfsSpecs) when is_list(TmpfsSpecs) ->
|
||||
[build_tmpfs_option(TmpfsSpec) || TmpfsSpec <- TmpfsSpecs].
|
||||
|
||||
-spec build_tmpfs_option(binary()) -> {binary(), binary()}.
|
||||
build_tmpfs_option(TmpfsSpec) when is_binary(TmpfsSpec) ->
|
||||
case binary:split(TmpfsSpec, <<":">>) of
|
||||
[Path] when Path =/= <<>> ->
|
||||
{Path, <<>>};
|
||||
[Path, Options] when Path =/= <<>> ->
|
||||
{Path, Options};
|
||||
_ ->
|
||||
throw({error, <<"invalid tmpfs mount">>})
|
||||
end.
|
||||
|
||||
-spec parse_optional_size_bytes(undefined | binary(), binary()) -> undefined | non_neg_integer().
|
||||
parse_optional_size_bytes(undefined, _Field) ->
|
||||
undefined;
|
||||
parse_optional_size_bytes(Value, Field) when is_binary(Value) ->
|
||||
parse_size_bytes(Value, Field).
|
||||
|
||||
-spec parse_duration_ns(binary() | integer(), binary()) -> non_neg_integer().
|
||||
parse_duration_ns(Value, _Field) when is_integer(Value), Value >= 0 ->
|
||||
Value;
|
||||
parse_duration_ns(Value, Field) when is_binary(Value) ->
|
||||
parse_scaled_uint64(Value, Field, #{
|
||||
<<"ns">> => 1,
|
||||
<<"us">> => 1000,
|
||||
<<"ms">> => 1000000,
|
||||
<<"s">> => 1000000000,
|
||||
<<"m">> => 60000000000,
|
||||
<<"h">> => 3600000000000,
|
||||
<<>> => 1000000000
|
||||
}).
|
||||
|
||||
-spec parse_size_bytes(binary(), binary()) -> non_neg_integer().
|
||||
parse_size_bytes(Value, Field) when is_binary(Value) ->
|
||||
parse_scaled_uint64(Value, Field, #{
|
||||
<<"b">> => 1,
|
||||
<<"k">> => 1024,
|
||||
<<"kb">> => 1024,
|
||||
<<"ki">> => 1024,
|
||||
<<"kib">> => 1024,
|
||||
<<"m">> => 1048576,
|
||||
<<"mb">> => 1048576,
|
||||
<<"mi">> => 1048576,
|
||||
<<"mib">> => 1048576,
|
||||
<<"g">> => 1073741824,
|
||||
<<"gb">> => 1073741824,
|
||||
<<"gi">> => 1073741824,
|
||||
<<"gib">> => 1073741824,
|
||||
<<"t">> => 1099511627776,
|
||||
<<"tb">> => 1099511627776,
|
||||
<<"ti">> => 1099511627776,
|
||||
<<"tib">> => 1099511627776,
|
||||
<<>> => 1
|
||||
}).
|
||||
|
||||
-spec parse_scaled_uint64(binary(), binary(), map()) -> non_neg_integer().
|
||||
parse_scaled_uint64(Value0, Field, Multipliers) when is_binary(Value0), is_binary(Field), is_map(Multipliers) ->
|
||||
Value = trim_binary(Value0),
|
||||
LowerValue = lower_binary(Value),
|
||||
{NumberBin, Unit} = split_numeric_suffix(LowerValue),
|
||||
case maps:get(Unit, Multipliers, undefined) of
|
||||
undefined ->
|
||||
throw({error, <<"invalid value for ", Field/binary, ": ", Value0/binary>>});
|
||||
Multiplier ->
|
||||
trunc(parse_decimal(NumberBin, Field) * Multiplier)
|
||||
end.
|
||||
|
||||
-spec parse_uint32(binary(), binary()) -> non_neg_integer().
|
||||
parse_uint32(Value, Field) when is_binary(Value), is_binary(Field) ->
|
||||
Parsed = parse_uint64(Value, Field),
|
||||
case Parsed =< 16#FFFFFFFF of
|
||||
true ->
|
||||
Parsed;
|
||||
false ->
|
||||
throw({error, <<"value overflow for ", Field/binary>>})
|
||||
end.
|
||||
|
||||
-spec parse_tcp_port(binary(), binary()) -> non_neg_integer().
|
||||
parse_tcp_port(Value, Field) when is_binary(Value), is_binary(Field) ->
|
||||
Parsed = parse_uint32(Value, Field),
|
||||
case Parsed =< 65535 of
|
||||
true ->
|
||||
Parsed;
|
||||
false ->
|
||||
throw({error, <<"port out of range for ", Field/binary>>})
|
||||
end.
|
||||
|
||||
-spec parse_uint64(binary(), binary()) -> non_neg_integer().
|
||||
parse_uint64(Value0, Field) when is_binary(Value0), is_binary(Field) ->
|
||||
Value = trim_binary(Value0),
|
||||
case catch binary_to_integer(Value) of
|
||||
Parsed when is_integer(Parsed), Parsed >= 0 ->
|
||||
Parsed;
|
||||
_ ->
|
||||
throw({error, <<"invalid unsigned integer for ", Field/binary, ": ", Value0/binary>>})
|
||||
end.
|
||||
|
||||
-spec parse_decimal(binary(), binary()) -> float().
|
||||
parse_decimal(Value, Field) when is_binary(Value), is_binary(Field) ->
|
||||
case catch binary_to_integer(Value) of
|
||||
ParsedInt when is_integer(ParsedInt), ParsedInt >= 0 ->
|
||||
float(ParsedInt);
|
||||
_ ->
|
||||
case catch binary_to_float(Value) of
|
||||
ParsedFloat when is_float(ParsedFloat), ParsedFloat >= 0 ->
|
||||
ParsedFloat;
|
||||
_ ->
|
||||
throw({error, <<"invalid number for ", Field/binary, ": ", Value/binary>>})
|
||||
end
|
||||
end.
|
||||
|
||||
-spec split_numeric_suffix(binary()) -> {binary(), binary()}.
|
||||
split_numeric_suffix(Value) when is_binary(Value) ->
|
||||
split_numeric_suffix(Value, <<>>).
|
||||
|
||||
-spec split_numeric_suffix(binary(), binary()) -> {binary(), binary()}.
|
||||
split_numeric_suffix(<<Char, Rest/binary>>, Acc)
|
||||
when (Char >= $0 andalso Char =< $9) orelse Char =:= $. ->
|
||||
split_numeric_suffix(Rest, <<Acc/binary, Char>>);
|
||||
split_numeric_suffix(Rest, <<>>) ->
|
||||
throw({error, <<"invalid numeric value: ", Rest/binary>>});
|
||||
split_numeric_suffix(Rest, Acc) ->
|
||||
{Acc, Rest}.
|
||||
|
||||
-spec trim_binary(binary()) -> binary().
|
||||
trim_binary(Value) when is_binary(Value) ->
|
||||
Trimmed = string:trim(binary_to_list(Value)),
|
||||
list_to_binary(Trimmed).
|
||||
|
||||
-spec lower_binary(binary()) -> binary().
|
||||
lower_binary(Value) when is_binary(Value) ->
|
||||
list_to_binary(string:lowercase(binary_to_list(Value))).
|
||||
@ -1,61 +0,0 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint).
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
-export([get_name/1, get_pid/1, forward/3, reload/2, clean_up/1]).
|
||||
-export([get_alias_pid/1]).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(Endpoint :: #endpoint{}) -> {'ok', pid()} | 'ignore' | {'error', term()}.
|
||||
start_link(Endpoint = #endpoint{id = Id, name = Name, config = #http_endpoint{}}) ->
|
||||
LocalName = get_name(Id),
|
||||
AliasName = get_alias_name(Name),
|
||||
endpoint_http:start_link(LocalName, AliasName, Endpoint);
|
||||
start_link(Endpoint = #endpoint{id = Id, name = Name, config = #mqtt_endpoint{}}) ->
|
||||
LocalName = get_name(Id),
|
||||
AliasName = get_alias_name(Name),
|
||||
endpoint_mqtt:start_link(LocalName, AliasName, Endpoint);
|
||||
start_link(Endpoint = #endpoint{id = Id, name = Name, config = #kafka_endpoint{}}) ->
|
||||
LocalName = get_name(Id),
|
||||
AliasName = get_alias_name(Name),
|
||||
endpoint_kafka:start_link(LocalName, AliasName, Endpoint).
|
||||
|
||||
-spec get_name(Id :: integer()) -> atom().
|
||||
get_name(Id) when is_integer(Id) ->
|
||||
list_to_atom("endpoint:" ++ integer_to_list(Id)).
|
||||
|
||||
-spec get_pid(Id :: integer()) -> undefined | pid().
|
||||
get_pid(Id) when is_integer(Id) ->
|
||||
whereis(get_name(Id)).
|
||||
|
||||
-spec get_alias_name(Name :: binary()) -> atom().
|
||||
get_alias_name(Name) when is_binary(Name) ->
|
||||
list_to_atom("endpoint:" ++ binary_to_list(Name)).
|
||||
|
||||
-spec get_alias_pid(Name :: binary()) -> undefined | pid().
|
||||
get_alias_pid(Name) when is_binary(Name) ->
|
||||
gproc:whereis_name({n, l, get_alias_name(Name)}).
|
||||
|
||||
-spec forward(Pid :: pid(), ServiceId :: binary(), Metric :: binary()) -> no_return().
|
||||
forward(Pid, ServiceId, Metric) when is_pid(Pid), is_binary(ServiceId), is_binary(Metric) ->
|
||||
gen_server:cast(Pid, {forward, ServiceId, Metric}).
|
||||
|
||||
reload(Pid, NEndpoint = #endpoint{}) when is_pid(Pid) ->
|
||||
gen_statem:cast(Pid, {reload, NEndpoint}).
|
||||
|
||||
-spec clean_up(Pid :: pid()) -> ok.
|
||||
clean_up(Pid) when is_pid(Pid) ->
|
||||
gen_server:call(Pid, clean_up, 5000).
|
||||
@ -1,105 +0,0 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_buffer).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
|
||||
-export([new/2, append/2, trigger_next/1, trigger_n/1, cleanup/1, ack/2, stat/1]).
|
||||
-export_type([buffer/0]).
|
||||
|
||||
-record(buffer, {
|
||||
endpoint :: #endpoint{},
|
||||
next_id = 1 :: integer(),
|
||||
%% 当前数据所在的游标
|
||||
cursor = 0 :: integer(),
|
||||
%% ets存储的引用
|
||||
tid :: ets:tid(),
|
||||
%% 定时器
|
||||
timer_pid :: pid(),
|
||||
%% 窗口大小,允许最大的未确认消息数
|
||||
window_size = 10,
|
||||
%% 未确认的消息数
|
||||
flight_num = 0,
|
||||
%% 记录成功处理的消息数
|
||||
acc_num = 0
|
||||
}).
|
||||
|
||||
-record(north_data, {
|
||||
id :: integer(),
|
||||
tuple :: any()
|
||||
}).
|
||||
|
||||
-type buffer() :: #buffer{}.
|
||||
|
||||
-spec new(Endpoint :: #endpoint{}, WindowSize :: integer()) -> Buffer :: #buffer{}.
|
||||
new(Endpoint = #endpoint{id = Id}, WindowSize) when is_integer(WindowSize), WindowSize > 0 ->
|
||||
%% 初始化存储
|
||||
EtsName = list_to_atom("endpoint_buffer_ets:" ++ integer_to_list(Id)),
|
||||
Tid = ets:new(EtsName, [ordered_set, private, {keypos, 2}]),
|
||||
%% 定义重发器
|
||||
{ok, TimerPid} = endpoint_timer:start_link(?RETRY_INTERVAL),
|
||||
|
||||
#buffer{cursor = 0, tid = Tid, timer_pid = TimerPid, endpoint = Endpoint, window_size = WindowSize}.
|
||||
|
||||
-spec append(tuple(), Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
append(Tuple, Buffer = #buffer{tid = Tid, next_id = NextId, window_size = WindowSize, flight_num = FlightNum}) ->
|
||||
NorthData = #north_data{id = NextId, tuple = Tuple},
|
||||
true = ets:insert(Tid, NorthData),
|
||||
NBuffer = Buffer#buffer{next_id = NextId + 1},
|
||||
case FlightNum < WindowSize of
|
||||
true ->
|
||||
trigger_next(NBuffer);
|
||||
false ->
|
||||
NBuffer
|
||||
end.
|
||||
|
||||
-spec trigger_n(Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
trigger_n(Buffer = #buffer{window_size = WindowSize}) ->
|
||||
%% 最多允许window_size
|
||||
lists:foldl(fun(_, Buffer0) -> trigger_next(Buffer0) end, Buffer, lists:seq(1, WindowSize)).
|
||||
|
||||
%% 触发读取下一条数据
|
||||
-spec trigger_next(Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
trigger_next(Buffer = #buffer{tid = Tid, cursor = Cursor, timer_pid = TimerPid, flight_num = FlightNum}) ->
|
||||
case ets:first(Tid) of
|
||||
'$end_of_table' ->
|
||||
Buffer;
|
||||
Key ->
|
||||
[#north_data{id = Id, tuple = Tuple}|_] = ets:take(Tid, Key),
|
||||
ReceiverPid = self(),
|
||||
ReceiverPid ! {next_data, Id, Tuple},
|
||||
endpoint_timer:task(TimerPid, Id, fun() -> ReceiverPid ! {next_data, Id, Tuple} end),
|
||||
Buffer#buffer{flight_num = FlightNum + 1, cursor = Cursor + 1}
|
||||
end.
|
||||
|
||||
-spec ack(Id :: integer(), Buffer :: #buffer{}) -> NBuffer :: #buffer{}.
|
||||
ack(Id, Buffer = #buffer{timer_pid = TimerPid, acc_num = AccNum, flight_num = FlightNum}) when is_integer(Id) ->
|
||||
endpoint_timer:ack(TimerPid, Id),
|
||||
trigger_next(Buffer#buffer{acc_num = AccNum + 1, flight_num = FlightNum - 1}).
|
||||
|
||||
%% 获取当前统计信息
|
||||
-spec stat(Buffer :: #buffer{}) -> map().
|
||||
stat(#buffer{acc_num = AccNum, tid = Tid}) ->
|
||||
#{
|
||||
<<"acc_num">> => AccNum,
|
||||
<<"queue_num">> => ets:info(Tid, size)
|
||||
}.
|
||||
|
||||
-spec cleanup(Buffer :: #buffer{}) -> ok.
|
||||
cleanup(#buffer{timer_pid = TimerPid}) ->
|
||||
endpoint_timer:cleanup(TimerPid),
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
@ -1,185 +0,0 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_kafka).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/3]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
|
||||
-define(DISCONNECTED, disconnected).
|
||||
-define(CONNECTED, connected).
|
||||
|
||||
-record(state, {
|
||||
endpoint :: #endpoint{},
|
||||
buffer :: endpoint_buffer:buffer(),
|
||||
client_id :: atom(),
|
||||
client_pid :: undefined | pid(),
|
||||
status = ?DISCONNECTED
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName), is_atom(AliasName) ->
|
||||
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
init([AliasName, Endpoint = #endpoint{id = Id}]) ->
|
||||
erlang:process_flag(trap_exit, true),
|
||||
true = gproc:reg({n, l, AliasName}),
|
||||
|
||||
%% 创建转发器, 避免阻塞当前进程的创建,因此采用了延时初始化的机制
|
||||
erlang:start_timer(0, self(), connect),
|
||||
%% 初始化存储
|
||||
Buffer = endpoint_buffer:new(Endpoint, 10),
|
||||
ClientId = list_to_atom("brod_client:" ++ integer_to_list(Id)),
|
||||
|
||||
{ok, #state{endpoint = Endpoint, buffer = Buffer, status = ?DISCONNECTED, client_id = ClientId}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(get_stat, _From, State = #state{buffer = Buffer}) ->
|
||||
Stat = endpoint_buffer:stat(Buffer),
|
||||
{reply, {ok, Stat}, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({forward, ServiceId, Metric}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:append({ServiceId, Metric}, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, connect}, State = #state{buffer = Buffer, status = ?DISCONNECTED, client_id = ClientId,
|
||||
endpoint = #endpoint{title = Title, config = #kafka_endpoint{sasl_config = SaslConfig, bootstrap_servers = BootstrapServers, topic = Topic}}}) ->
|
||||
lager:debug("[endpoint_kafka] endpoint: ~p, create postman", [Title]),
|
||||
|
||||
BaseConfig = [
|
||||
{reconnect_cool_down_seconds, 5},
|
||||
{socket_options, [{keepalive, true}]}
|
||||
],
|
||||
|
||||
ClientConfig = case SaslConfig of
|
||||
{Mechanism, Username, Password} ->
|
||||
[{sasl, {Mechanism, Username, Password}}|BaseConfig];
|
||||
undefined ->
|
||||
BaseConfig
|
||||
end,
|
||||
|
||||
case brod:start_link_client(BootstrapServers, ClientId, ClientConfig) of
|
||||
{ok, ClientPid} ->
|
||||
ok = brod:start_producer(ClientId, Topic, _ProducerConfig = []),
|
||||
NBuffer = endpoint_buffer:trigger_next(Buffer),
|
||||
{noreply, State#state{buffer = NBuffer, client_pid = ClientPid, status = ?CONNECTED}};
|
||||
{error, Reason} ->
|
||||
lager:debug("[endpoint_kafka] start_client: ~p, get error: ~p", [ClientId, Reason]),
|
||||
retry_connect(),
|
||||
{noreply, State#state{status = ?DISCONNECTED, client_pid = undefined}}
|
||||
end;
|
||||
|
||||
%% 离线时,忽略数据发送逻辑
|
||||
handle_info({next_data, _Id, _Tuple}, State = #state{status = ?DISCONNECTED}) ->
|
||||
{noreply, State};
|
||||
%% 发送数据到mqtt服务器
|
||||
handle_info({next_data, Id, {_ServiceId, Metric}}, State = #state{status = ?CONNECTED, client_pid = ClientPid,
|
||||
endpoint = #endpoint{config = #kafka_endpoint{topic = Topic}}}) ->
|
||||
|
||||
ReceiverPid = self(),
|
||||
AckCb = fun(Partition, BaseOffset) ->
|
||||
lager:debug("[endpoint_kafka] ack partion: ~p, offset: ~p", [Partition, BaseOffset]),
|
||||
ReceiverPid ! {ack, Id}
|
||||
end,
|
||||
_ = brod:produce_cb(ClientPid, Topic, random, <<>>, Metric, AckCb),
|
||||
|
||||
{noreply, State};
|
||||
|
||||
handle_info({ack, Id}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}};
|
||||
|
||||
%% postman进程挂掉时,重新建立新的
|
||||
handle_info({'EXIT', ClientPid, Reason}, State = #state{client_pid = ClientPid, endpoint = #endpoint{title = Title}}) ->
|
||||
lager:warning("[endpoint_kafka] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
|
||||
retry_connect(),
|
||||
{noreply, State#state{client_pid = undefined, status = ?DISCONNECTED}};
|
||||
|
||||
handle_info(Info, State = #state{status = Status}) ->
|
||||
lager:warning("[endpoint_kafka] unknown message: ~p, status: ~p", [Info, Status]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(Reason, #state{endpoint = #endpoint{title = Title}, buffer = Buffer}) ->
|
||||
lager:debug("[endpoint_kafka] endpoint: ~p, terminate with reason: ~p", [Title, Reason]),
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
retry_connect() ->
|
||||
erlang:start_timer(?RETRY_INTERVAL, self(), connect).
|
||||
|
||||
check_produce_result(ok) ->
|
||||
true;
|
||||
check_produce_result({ok, _}) ->
|
||||
true;
|
||||
check_produce_result({ok, _}) ->
|
||||
false.
|
||||
|
||||
@ -1,197 +0,0 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_mqtt).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/3]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
|
||||
-define(DISCONNECTED, disconnected).
|
||||
-define(CONNECTED, connected).
|
||||
|
||||
-record(state, {
|
||||
endpoint :: #endpoint{},
|
||||
buffer :: endpoint_buffer:buffer(),
|
||||
conn_pid :: undefined | pid(),
|
||||
%% 待确认的数据, #{PacketId :: integer() => Id :: integer()}
|
||||
inflight = #{},
|
||||
|
||||
status = disconnected
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName), is_atom(AliasName) ->
|
||||
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
init([AliasName, Endpoint]) ->
|
||||
erlang:process_flag(trap_exit, true),
|
||||
true = gproc:reg({n, l, AliasName}),
|
||||
%% 创建转发器, 避免阻塞当前进程的创建,因此采用了延时初始化的机制
|
||||
erlang:start_timer(0, self(), create_postman),
|
||||
%% 初始化存储
|
||||
Buffer = endpoint_buffer:new(Endpoint, 10),
|
||||
|
||||
{ok, #state{endpoint = Endpoint, buffer = Buffer, status = ?DISCONNECTED}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(get_stat, _From, State = #state{buffer = Buffer}) ->
|
||||
Stat = endpoint_buffer:stat(Buffer),
|
||||
{reply, {ok, Stat}, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({forward, ServiceId, Metric}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:append({ServiceId, Metric}, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, create_postman}, State = #state{buffer = Buffer, status = ?DISCONNECTED,
|
||||
endpoint = #endpoint{title = Title, config = #mqtt_endpoint{host = Host, port = Port, username = Username, password = Password, client_id = ClientId}}}) ->
|
||||
lager:debug("[endpoint_mqtt] endpoint: ~p, create postman", [Title]),
|
||||
Opts = [
|
||||
{owner, self()},
|
||||
{clientid, ClientId},
|
||||
{host, binary_to_list(Host)},
|
||||
{port, Port},
|
||||
{tcp_opts, []},
|
||||
{username, binary_to_list(Username)},
|
||||
{password, binary_to_list(Password)},
|
||||
{keepalive, 86400},
|
||||
{auto_ack, true},
|
||||
{connect_timeout, 5000},
|
||||
{proto_ver, v5},
|
||||
{retry_interval, 5000}
|
||||
],
|
||||
|
||||
{ok, ConnPid} = emqtt:start_link(Opts),
|
||||
lager:debug("[endpoint_mqtt] start connect, options: ~p", [Opts]),
|
||||
case emqtt:connect(ConnPid, 5000) of
|
||||
{ok, _} ->
|
||||
lager:debug("[endpoint_mqtt] connect success, pid: ~p", [ConnPid]),
|
||||
NBuffer = endpoint_buffer:trigger_n(Buffer),
|
||||
{noreply, State#state{conn_pid = ConnPid, buffer = NBuffer, status = ?CONNECTED}};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_mqtt] connect get error: ~p", [Reason]),
|
||||
erlang:start_timer(5000, self(), create_postman),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 离线时,忽略数据发送逻辑
|
||||
handle_info({next_data, _Id, _Tuple}, State = #state{status = ?DISCONNECTED}) ->
|
||||
{keep_state, State};
|
||||
%% 发送数据到mqtt服务器
|
||||
handle_info({next_data, Id, {ServiceId, Metric}}, State = #state{status = ?CONNECTED, conn_pid = ConnPid, buffer = Buffer, inflight = InFlight,
|
||||
endpoint = #endpoint{config = #mqtt_endpoint{topic = Topic0, qos = Qos}}}) ->
|
||||
|
||||
Topic = re:replace(Topic0, <<"\\${service_id}">>, ServiceId, [global, {return, binary}]),
|
||||
lager:debug("[endpoint_mqtt] will publish topic: ~p, metric: ~p, qos: ~p", [Topic, Metric, Qos]),
|
||||
case emqtt:publish(ConnPid, Topic, #{}, Metric, [{qos, Qos}, {retain, true}]) of
|
||||
ok ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}};
|
||||
{ok, PacketId} ->
|
||||
{noreply, State#state{inflight = maps:put(PacketId, Id, InFlight)}};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_mqtt] send message to topic: ~p, get error: ~p", [Topic, Reason]),
|
||||
{stop, Reason, State}
|
||||
end;
|
||||
|
||||
handle_info({disconnected, ReasonCode, Properties}, State = #state{status = ?CONNECTED}) ->
|
||||
lager:debug("[endpoint_mqtt] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||
erlang:start_timer(?RETRY_INTERVAL, self(), create_postman),
|
||||
{noreply, State#state{conn_pid = undefined, status = ?DISCONNECTED}};
|
||||
|
||||
handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload}}, State = #state{status = ?CONNECTED}) ->
|
||||
lager:debug("[endpoint_mqtt] Recv a publish packet: ~p, payload: ~p", [Message, Payload]),
|
||||
{noreply, State};
|
||||
|
||||
%% 收到确认的消息
|
||||
handle_info({puback, #{packet_id := PacketId}}, State = #state{status = ?CONNECTED, inflight = Inflight, buffer = Buffer}) ->
|
||||
case maps:take(PacketId, Inflight) of
|
||||
{Id, RestInflight} ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer, inflight = RestInflight}};
|
||||
error ->
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% postman进程挂掉时,重新建立新的
|
||||
handle_info({'EXIT', ConnPid, Reason}, State = #state{endpoint = #endpoint{title = Title}, conn_pid = ConnPid}) ->
|
||||
lager:warning("[endpoint_mqtt] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
|
||||
erlang:start_timer(?RETRY_INTERVAL, self(), create_postman),
|
||||
{noreply, State#state{conn_pid = undefined, status = ?DISCONNECTED}};
|
||||
|
||||
handle_info(Info, State = #state{status = Status}) ->
|
||||
lager:warning("[endpoint_mqtt] unknown message: ~p, status: ~p", [Info, Status]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(Reason, #state{endpoint = #endpoint{title = Title}, buffer = Buffer}) ->
|
||||
lager:debug("[endpoint_mqtt] endpoint: ~p, terminate with reason: ~p", [Title, Reason]),
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
@ -1,110 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%% @doc endpoint top level supervisor.
|
||||
%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-module(endpoint_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
-include("endpoint.hrl").
|
||||
|
||||
-export([start_link/0]).
|
||||
-export([ensured_endpoint_started/1, delete_endpoint/1]).
|
||||
|
||||
-export([init/1]).
|
||||
-export([kafka_test/0]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
%% sup_flags() = #{strategy => strategy(), % optional
|
||||
%% intensity => non_neg_integer(), % optional
|
||||
%% period => pos_integer()} % optional
|
||||
%% child_spec() = #{id => child_id(), % mandatory
|
||||
%% start => mfargs(), % mandatory
|
||||
%% restart => restart(), % optional
|
||||
%% shutdown => shutdown(), % optional
|
||||
%% type => worker(), % optional
|
||||
%% modules => modules()} % optional
|
||||
init([]) ->
|
||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||
Endpoints = endpoint_bo:get_all_endpoints(),
|
||||
ChildSpecs = lists:flatmap(fun(EndpointInfo) ->
|
||||
case endpoint_bo:endpoint_record(EndpointInfo) of
|
||||
error ->
|
||||
[];
|
||||
{ok, Endpoint} ->
|
||||
[Endpoint]
|
||||
end
|
||||
end, Endpoints),
|
||||
{ok, {SupFlags, ChildSpecs}}.
|
||||
|
||||
%% internal functions
|
||||
|
||||
kafka_test() ->
|
||||
Endpoint = #endpoint{
|
||||
id = 1,
|
||||
%% 全局唯一,在路由规则中通过名称来指定
|
||||
name = <<"kafka_test">>,
|
||||
%% 标题描述
|
||||
title = <<"kafka测试"/utf8>>,
|
||||
%% 配置项, 格式: #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}
|
||||
config = #kafka_endpoint{
|
||||
%sasl_config = {
|
||||
% scram_sha_256,
|
||||
% <<"admin">>,
|
||||
% <<"lz4rP5UavRTiGZEZK8G51mxHcM5iPC">>
|
||||
%},
|
||||
|
||||
sasl_config = undefined,
|
||||
|
||||
bootstrap_servers = [
|
||||
{"127.0.0.1", 19092}
|
||||
],
|
||||
topic = <<"metric">>
|
||||
},
|
||||
status = 0,
|
||||
updated_at = 0,
|
||||
created_at = 0
|
||||
},
|
||||
{ok, Pid} = ensured_endpoint_started(Endpoint),
|
||||
ServiceId = <<"service_id_123">>,
|
||||
Metric = <<"this is a test">>,
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric),
|
||||
endpoint:forward(Pid, ServiceId, Metric).
|
||||
|
||||
-spec ensured_endpoint_started(Endpoint :: #endpoint{}) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensured_endpoint_started(Endpoint = #endpoint{}) ->
|
||||
case supervisor:start_child(?MODULE, child_spec(Endpoint)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
-spec delete_endpoint(Id :: integer()) -> ok | {error, Reason :: any()}.
|
||||
delete_endpoint(Id) when is_integer(Id) ->
|
||||
Name = endpoint:get_name(Id),
|
||||
supervisor:terminate_child(?MODULE, Name),
|
||||
supervisor:delete_child(?MODULE, Name).
|
||||
|
||||
child_spec(Endpoint = #endpoint{id = Id}) ->
|
||||
Name = endpoint:get_name(Id),
|
||||
#{id => Name,
|
||||
start => {endpoint, start_link, [Endpoint]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['endpoint']}.
|
||||
@ -1,128 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2024, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 07. 5月 2024 10:30
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_timer).
|
||||
-author("anlicheng").
|
||||
-include("endpoint.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
-export([task/3, ack/2, cleanup/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-record(state, {
|
||||
retry_interval = 0,
|
||||
%% 定时器
|
||||
timer_map = #{}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
task(Pid, Id, Task) when is_pid(Pid), is_integer(Id), is_function(Task, 0) ->
|
||||
gen_server:cast(Pid, {task, Id, Task}).
|
||||
|
||||
ack(Pid, Id) when is_pid(Pid), is_integer(Id) ->
|
||||
gen_server:cast(Pid, {ack, Id}).
|
||||
|
||||
cleanup(Pid) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, cleanup).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(RetryInterval :: integer()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(RetryInterval) when is_integer(RetryInterval) ->
|
||||
gen_server:start_link(?MODULE, [RetryInterval], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([RetryInterval]) ->
|
||||
{ok, #state{retry_interval = RetryInterval}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(_Request, _From, State = #state{}) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({task, Id, Task}, State = #state{retry_interval = RetryInterval, timer_map = TimerMap}) ->
|
||||
TimerRef = erlang:start_timer(RetryInterval, self(), {repost_ticker, {Id, Task}}),
|
||||
{noreply, State#state{timer_map = maps:put(Id, TimerRef, TimerMap)}};
|
||||
|
||||
%% 取消
|
||||
handle_cast({ack, Id}, State = #state{timer_map = TimerMap}) ->
|
||||
case maps:take(Id, TimerMap) of
|
||||
error ->
|
||||
{noreply, State};
|
||||
{TimerRef, NTimerMap} ->
|
||||
is_reference(TimerRef) andalso erlang:cancel_timer(TimerRef),
|
||||
{noreply, State#state{timer_map = NTimerMap}}
|
||||
end;
|
||||
|
||||
handle_cast(cleanup, State = #state{timer_map = TimerMap}) ->
|
||||
lists:foreach(fun({_, TimerRef}) -> catch erlang:cancel_timer(TimerRef) end, maps:to_list(TimerMap)),
|
||||
{noreply, State#state{timer_map = #{}}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, {repost_ticker, {Id, Task}}}, State = #state{retry_interval = RetryInterval, timer_map = TimerMap}) ->
|
||||
Task(),
|
||||
TimerRef = erlang:start_timer(RetryInterval, self(), {repost_ticker, {Id, Task}}),
|
||||
|
||||
{noreply, State#state{timer_map = maps:put(Id, TimerRef, TimerMap)}}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
147
apps/iot/src/host/iot_container_task.erl
Normal file
147
apps/iot/src/host/iot_container_task.erl
Normal file
@ -0,0 +1,147 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Owns one container deployment task stream.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_container_task).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/2]).
|
||||
-export([get_pid/2, subscribe/3, stream/3, close/2]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(MAX_EVENTS, 200).
|
||||
-define(STOP_AFTER_CLOSE, 300000).
|
||||
|
||||
-record(state, {
|
||||
uuid :: binary(),
|
||||
task_id :: integer(),
|
||||
status = pending :: pending | running | success | fail,
|
||||
events = [] :: [{binary(), binary()}],
|
||||
listeners = #{} :: #{pid() => reference()},
|
||||
close_reason = undefined :: undefined | binary(),
|
||||
stop_ref = undefined :: undefined | reference()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link(binary(), integer()) -> {ok, pid()} | ignore | {error, term()}.
|
||||
start_link(UUID, TaskId) when is_binary(UUID), is_integer(TaskId) ->
|
||||
gen_server:start_link({via, gproc, {n, l, name(UUID, TaskId)}}, ?MODULE, [UUID, TaskId], []).
|
||||
|
||||
-spec get_pid(binary(), integer()) -> undefined | pid().
|
||||
get_pid(UUID, TaskId) when is_binary(UUID), is_integer(TaskId) ->
|
||||
gproc:whereis_name({n, l, name(UUID, TaskId)}).
|
||||
|
||||
-spec subscribe(binary(), integer(), pid()) -> ok | {error, term()}.
|
||||
subscribe(UUID, TaskId, ListenerPid) when is_binary(UUID), is_integer(TaskId), is_pid(ListenerPid) ->
|
||||
case iot_container_task_sup:ensure_started(UUID, TaskId) of
|
||||
{ok, Pid} ->
|
||||
gen_server:call(Pid, {subscribe, ListenerPid});
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec stream(pid(), binary(), binary()) -> ok.
|
||||
stream(Pid, Type, Stream) when is_pid(Pid), is_binary(Type), is_binary(Stream) ->
|
||||
gen_server:cast(Pid, {stream, Type, Stream}).
|
||||
|
||||
-spec close(pid(), binary()) -> ok.
|
||||
close(Pid, Reason) when is_pid(Pid), is_binary(Reason) ->
|
||||
gen_server:cast(Pid, {close, Reason}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec init([binary() | integer()]) -> {ok, #state{}}.
|
||||
init([UUID, TaskId]) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
{ok, #state{uuid = UUID, task_id = TaskId}}.
|
||||
|
||||
-spec handle_call(term(), {pid(), term()}, #state{}) ->
|
||||
{reply, term(), #state{}}.
|
||||
handle_call({subscribe, ListenerPid}, _From, State0 = #state{task_id = TaskId, events = Events, listeners = Listeners, close_reason = CloseReason}) ->
|
||||
MonRef = erlang:monitor(process, ListenerPid),
|
||||
maps:get(ListenerPid, Listeners, undefined) =/= undefined andalso erlang:demonitor(maps:get(ListenerPid, Listeners), [flush]),
|
||||
lists:foreach(fun({Type, Stream}) ->
|
||||
ListenerPid ! {stream_data, TaskId, Type, Stream}
|
||||
end, Events),
|
||||
case CloseReason of
|
||||
undefined ->
|
||||
ok;
|
||||
Reason ->
|
||||
ListenerPid ! {stream_close, TaskId, Reason}
|
||||
end,
|
||||
{reply, ok, State0#state{listeners = maps:put(ListenerPid, MonRef, Listeners)}};
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
|
||||
handle_cast({stream, Type, Stream}, State0 = #state{task_id = TaskId, events = Events0, listeners = Listeners}) ->
|
||||
Event = {Type, Stream},
|
||||
Events = trim_events(Events0 ++ [Event]),
|
||||
maps:foreach(fun(ListenerPid, _MonRef) ->
|
||||
ListenerPid ! {stream_data, TaskId, Type, Stream}
|
||||
end, Listeners),
|
||||
{noreply, State0#state{status = running, events = Events}};
|
||||
handle_cast({close, Reason}, State0 = #state{task_id = TaskId, listeners = Listeners}) ->
|
||||
maps:foreach(fun(ListenerPid, _MonRef) ->
|
||||
ListenerPid ! {stream_close, TaskId, Reason}
|
||||
end, Listeners),
|
||||
Status = close_status(Reason),
|
||||
{noreply, ensure_stop_timer(State0#state{status = Status, close_reason = Reason})};
|
||||
handle_cast(_Request, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(term(), #state{}) -> {noreply, #state{}} | {stop, normal, #state{}}.
|
||||
handle_info({'DOWN', _Ref, process, ListenerPid, _Reason}, State = #state{listeners = Listeners}) ->
|
||||
{noreply, State#state{listeners = maps:remove(ListenerPid, Listeners)}};
|
||||
handle_info(stop_after_close, State) ->
|
||||
{stop, normal, State};
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(term(), #state{}) -> ok.
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
-spec code_change(term(), #state{}, term()) -> {ok, #state{}}.
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec name(binary(), integer()) -> term().
|
||||
name(UUID, TaskId) ->
|
||||
{iot_container_task, UUID, TaskId}.
|
||||
|
||||
-spec trim_events([{binary(), binary()}]) -> [{binary(), binary()}].
|
||||
trim_events(Events) ->
|
||||
Len = length(Events),
|
||||
case Len > ?MAX_EVENTS of
|
||||
true ->
|
||||
lists:nthtail(Len - ?MAX_EVENTS, Events);
|
||||
false ->
|
||||
Events
|
||||
end.
|
||||
|
||||
-spec close_status(binary()) -> success | fail.
|
||||
close_status(<<"success">>) ->
|
||||
success;
|
||||
close_status(_) ->
|
||||
fail.
|
||||
|
||||
-spec ensure_stop_timer(#state{}) -> #state{}.
|
||||
ensure_stop_timer(State = #state{stop_ref = undefined}) ->
|
||||
Ref = erlang:send_after(?STOP_AFTER_CLOSE, self(), stop_after_close),
|
||||
State#state{stop_ref = Ref};
|
||||
ensure_stop_timer(State) ->
|
||||
State.
|
||||
100
apps/iot/src/host/iot_container_task_sup.erl
Normal file
100
apps/iot/src/host/iot_container_task_sup.erl
Normal file
@ -0,0 +1,100 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Dynamic supervisor for container deployment tasks.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_container_task_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([ensure_started/2, stream/4, close/3, fail/3]).
|
||||
|
||||
%% supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||
|
||||
-spec ensure_started(binary(), integer()) -> {ok, pid()} | {error, term()}.
|
||||
ensure_started(UUID, TaskId) when is_binary(UUID), is_integer(TaskId) ->
|
||||
case iot_container_task:get_pid(UUID, TaskId) of
|
||||
undefined ->
|
||||
case supervisor:start_child(?SERVER, [UUID, TaskId]) of
|
||||
{ok, Pid} ->
|
||||
{ok, Pid};
|
||||
{ok, Pid, _Info} ->
|
||||
{ok, Pid};
|
||||
{error, {already_started, Pid}} ->
|
||||
{ok, Pid};
|
||||
{error, {shutdown, {failed_to_start_child, _Id, {already_started, Pid}}}} ->
|
||||
{ok, Pid};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
Pid when is_pid(Pid) ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
-spec stream(binary(), integer(), binary(), binary()) -> ok.
|
||||
stream(UUID, TaskId, Type, Stream)
|
||||
when is_binary(UUID), is_integer(TaskId), is_binary(Type), is_binary(Stream) ->
|
||||
case ensure_started(UUID, TaskId) of
|
||||
{ok, Pid} ->
|
||||
iot_container_task:stream(Pid, Type, Stream);
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_container_task_sup] start task failed, uuid: ~p, task_id: ~p, reason: ~p", [UUID, TaskId, Reason]),
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec close(binary(), integer(), binary()) -> ok.
|
||||
close(UUID, TaskId, Reason) when is_binary(UUID), is_integer(TaskId), is_binary(Reason) ->
|
||||
case ensure_started(UUID, TaskId) of
|
||||
{ok, Pid} ->
|
||||
iot_container_task:close(Pid, Reason);
|
||||
{error, StartReason} ->
|
||||
logger:warning("[iot_container_task_sup] start task before close failed, uuid: ~p, task_id: ~p, reason: ~p", [UUID, TaskId, StartReason]),
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec fail(binary(), integer(), term()) -> ok.
|
||||
fail(UUID, TaskId, Reason) when is_binary(UUID), is_integer(TaskId) ->
|
||||
ReasonBin = reason_to_binary(Reason),
|
||||
ok = stream(UUID, TaskId, <<"error">>, ReasonBin),
|
||||
close(UUID, TaskId, <<"fail">>).
|
||||
|
||||
%%%===================================================================
|
||||
%%% supervisor callbacks
|
||||
%%%===================================================================
|
||||
|
||||
-spec init([]) -> {ok, {{simple_one_for_one, non_neg_integer(), pos_integer()}, [supervisor:child_spec()]}}.
|
||||
init([]) ->
|
||||
SupFlags = {simple_one_for_one, 10, 60},
|
||||
ChildSpec = #{
|
||||
id => iot_container_task,
|
||||
start => {iot_container_task, start_link, []},
|
||||
restart => temporary,
|
||||
shutdown => 5000,
|
||||
type => worker,
|
||||
modules => [iot_container_task]
|
||||
},
|
||||
{ok, {SupFlags, [ChildSpec]}}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec reason_to_binary(term()) -> binary().
|
||||
reason_to_binary(Reason) when is_binary(Reason) ->
|
||||
Reason;
|
||||
reason_to_binary(timeout) ->
|
||||
<<"timeout">>;
|
||||
reason_to_binary(Reason) ->
|
||||
unicode:characters_to_binary(io_lib:format("~p", [Reason])).
|
||||
458
apps/iot/src/host/iot_host.erl
Normal file
458
apps/iot/src/host/iot_host.erl
Normal file
@ -0,0 +1,458 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 22. 9月 2023 16:38
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_host).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
-include("domain_model.hrl").
|
||||
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% 心跳包检测时间间隔, 15分钟检测一次
|
||||
-define(HEARTBEAT_INTERVAL, 900 * 1000).
|
||||
|
||||
%% 状态
|
||||
-define(STATE_DENIED, denied).
|
||||
-define(STATE_ACTIVATED, activated).
|
||||
|
||||
%% API
|
||||
-export([start_link/2, get_name/1, get_alias_name/1, get_pid/1, handle/2, activate/2]).
|
||||
-export([lookup_pid/1]).
|
||||
-export([get_metric/1, get_status/1, kill/1]).
|
||||
%% 通讯相关
|
||||
-export([pub/4, attach_channel/2]).
|
||||
-export([deploy_container/3, start_container/2, stop_container/2, remove_container/2, kill_container/2, config_container/3, get_containers/1, await_reply/3]).
|
||||
-export([heartbeat/1]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||
|
||||
-record(state, {
|
||||
host_id :: integer(),
|
||||
%% 从数据库里面读取到的数据
|
||||
uuid :: binary(),
|
||||
%% 主机状态
|
||||
host_status :: integer(),
|
||||
|
||||
has_session = false :: boolean(),
|
||||
%% 心跳计数器
|
||||
heartbeat_counter = 0 :: integer(),
|
||||
%% websocket相关
|
||||
channel_pid :: undefined | pid(),
|
||||
%% 主机的相关信息
|
||||
metrics = #{} :: map()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
-spec get_pid(UUID :: binary()) -> undefined | pid().
|
||||
get_pid(UUID) when is_binary(UUID) ->
|
||||
Name = get_name(UUID),
|
||||
gproc:whereis_name({n, l, Name}).
|
||||
|
||||
-spec lookup_pid(UUID :: binary()) -> {ok, pid()} | {error, Reason :: any()}.
|
||||
lookup_pid(UUID) when is_binary(UUID) ->
|
||||
case get_pid(UUID) of
|
||||
undefined ->
|
||||
{error, <<"host not found">>};
|
||||
HostPid ->
|
||||
{ok, HostPid}
|
||||
end.
|
||||
|
||||
-spec get_name(UUID :: binary()) -> term().
|
||||
get_name(UUID) when is_binary(UUID) ->
|
||||
{iot_host, UUID}.
|
||||
|
||||
-spec get_alias_name(HostId :: integer()) -> term().
|
||||
get_alias_name(HostId0) when is_integer(HostId0) ->
|
||||
{iot_host_id, HostId0}.
|
||||
|
||||
-spec kill(UUID :: binary()) -> ok | true.
|
||||
kill(UUID) when is_binary(UUID) ->
|
||||
case get_pid(UUID) of
|
||||
undefined ->
|
||||
ok;
|
||||
Pid ->
|
||||
exit(Pid, kill)
|
||||
end.
|
||||
|
||||
%% 处理消息
|
||||
-spec handle(Pid :: pid(), Packet :: tuple()) -> ok.
|
||||
handle(Pid, Packet) when is_pid(Pid) ->
|
||||
gen_statem:cast(Pid, {handle, Packet}).
|
||||
|
||||
-spec get_status(Pid :: pid()) -> {ok, Status :: map()}.
|
||||
get_status(Pid) when is_pid(Pid) ->
|
||||
gen_statem:call(Pid, get_status).
|
||||
|
||||
%% 激活主机, true 表示激活; false表示关闭激活
|
||||
-spec activate(Pid :: pid(), Auth :: boolean()) -> ok | {error, term()}.
|
||||
activate(Pid, Auth) when is_pid(Pid), is_boolean(Auth) ->
|
||||
gen_statem:call(Pid, {activate, Auth}).
|
||||
|
||||
-spec get_metric(Pid :: pid()) -> {ok, MetricInfo :: map()}.
|
||||
get_metric(Pid) when is_pid(Pid) ->
|
||||
gen_statem:call(Pid, get_metric).
|
||||
|
||||
-spec attach_channel(pid(), pid()) -> ok | {error, Reason :: binary()}.
|
||||
attach_channel(Pid, ChannelPid) when is_pid(Pid), is_pid(ChannelPid) ->
|
||||
gen_statem:call(Pid, {attach_channel, ChannelPid}).
|
||||
|
||||
-type request_ref() :: binary().
|
||||
|
||||
-spec get_containers(Pid :: pid()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
get_containers(Pid) when is_pid(Pid) ->
|
||||
container_call(Pid, docker_container_builder:list_request()).
|
||||
|
||||
-spec config_container(Pid :: pid(), ContainerName :: binary(), ConfigJson :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
config_container(Pid, ContainerName, ConfigJson) when is_pid(Pid), is_binary(ContainerName), is_binary(ConfigJson) ->
|
||||
container_call(Pid, docker_container_builder:config_request(ContainerName, ConfigJson)).
|
||||
|
||||
-spec deploy_container(Pid :: pid(), TaskId :: integer(), Config :: map()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
deploy_container(Pid, TaskId, Config) when is_pid(Pid), is_integer(TaskId), is_map(Config) ->
|
||||
case docker_container_builder:deploy_request(TaskId, Config) of
|
||||
{ok, Request} ->
|
||||
container_call(Pid, Request);
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec start_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
start_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
|
||||
container_call(Pid, docker_container_builder:start_request(ContainerName)).
|
||||
|
||||
-spec stop_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
stop_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
|
||||
container_call(Pid, docker_container_builder:stop_request(ContainerName)).
|
||||
|
||||
-spec kill_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
kill_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
|
||||
container_call(Pid, docker_container_builder:kill_request(ContainerName)).
|
||||
|
||||
-spec remove_container(Pid :: pid(), ContainerName :: binary()) -> {ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
remove_container(Pid, ContainerName) when is_pid(Pid), is_binary(ContainerName) ->
|
||||
container_call(Pid, docker_container_builder:remove_request(ContainerName)).
|
||||
|
||||
-spec await_reply(Pid :: pid(), Ref :: request_ref(), Timeout :: integer()) ->
|
||||
ok | {ok, Result :: term()} | {error, Reason :: term()}.
|
||||
await_reply(Pid, Ref, Timeout) when is_pid(Pid), is_binary(Ref), is_integer(Timeout) ->
|
||||
receive
|
||||
{command_reply, Ref, ok} ->
|
||||
ok;
|
||||
{command_reply, Ref, {ok, Result}} ->
|
||||
{ok, Result};
|
||||
{command_reply, Ref, {error, Reason}} ->
|
||||
{error, Reason}
|
||||
after Timeout ->
|
||||
ok = gen_statem:call(Pid, {cancel_command_call, Ref}),
|
||||
flush_reply(Ref),
|
||||
{error, timeout}
|
||||
end.
|
||||
|
||||
-spec pub(Pid :: pid(), Topic :: binary(), Qos :: integer(), Content :: binary()) -> ok | {error, Reason :: any()}.
|
||||
pub(Pid, Topic, Qos, Content) when is_pid(Pid), is_binary(Topic), is_integer(Qos), is_binary(Content) ->
|
||||
gen_statem:call(Pid, {pub, Topic, Qos, Content}).
|
||||
|
||||
-spec heartbeat(Pid :: undefined | pid()) -> ok.
|
||||
heartbeat(undefined) ->
|
||||
ok;
|
||||
heartbeat(Pid) when is_pid(Pid) ->
|
||||
gen_statem:cast(Pid, heartbeat).
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
start_link(Name, UUID) when is_binary(UUID) ->
|
||||
gen_statem:start_link({via, gproc, {n, l, Name}}, ?MODULE, [UUID], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
init([UUID]) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
case iot_api_client:get_host_by_uuid(UUID) of
|
||||
{ok, #host_info{id = HostId, authorize_status = AuthorizeStatus, status = HostStatus}} ->
|
||||
%% 通过host_id注册别名, 可以避免通过查询数据库获取HostPid
|
||||
AliasName = get_alias_name(HostId),
|
||||
global:register_name(AliasName, self()),
|
||||
|
||||
%% 心跳检测机制
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
|
||||
StateName = case AuthorizeStatus =:= ?HOST_AUTHORIZED of
|
||||
true -> ?STATE_ACTIVATED;
|
||||
false -> ?STATE_DENIED
|
||||
end,
|
||||
{ok, StateName, #state{host_id = HostId, uuid = UUID, host_status = HostStatus, has_session = false}};
|
||||
undefined ->
|
||||
logger:warning("[iot_host] host uuid: ~p, load host info failed", [UUID]),
|
||||
ignore
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem when it needs to find out
|
||||
%% the callback mode of the callback module.
|
||||
callback_mode() ->
|
||||
handle_event_function.
|
||||
|
||||
%% @private
|
||||
%% @doc If callback_mode is handle_event_function, then whenever a
|
||||
%% gen_statem receives an event from call/2, cast/2, or as a normal
|
||||
%% process message, this function is called.
|
||||
|
||||
handle_event({call, From}, get_metric, _, State = #state{metrics = Metrics}) ->
|
||||
{keep_state, State, [{reply, From, {ok, Metrics}}]};
|
||||
|
||||
%% 获取主机的状态
|
||||
handle_event({call, From}, get_status, _, State = #state{channel_pid = ChannelPid, heartbeat_counter = HeartbeatCounter, metrics = Metrics, has_session = HasSession, host_status = HostStatus}) ->
|
||||
HasChannel = (ChannelPid /= undefined),
|
||||
Reply = #{
|
||||
<<"has_channel">> => HasChannel,
|
||||
<<"has_session">> => HasSession,
|
||||
<<"host_status">> => HostStatus,
|
||||
<<"heartbeat_counter">> => HeartbeatCounter,
|
||||
<<"metrics">> => Metrics
|
||||
},
|
||||
{keep_state, State, [{reply, From, {ok, Reply}}]};
|
||||
|
||||
handle_event({call, From}, {container_call, ReceiverPid, Request}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
case HasSession andalso is_pid(ChannelPid) of
|
||||
true ->
|
||||
Ref = request_ref(),
|
||||
ok = ssl_channel:container_call(ChannelPid, ReceiverPid, Ref, Request),
|
||||
{keep_state, State, [{reply, From, {ok, Ref}}]};
|
||||
false ->
|
||||
logger:debug("[iot_host] uuid: ~p, invalid state: ~p", [UUID, state_map(State)]),
|
||||
{keep_state, State, [{reply, From, {error, <<"主机离线,发送命令失败"/utf8>>}}]}
|
||||
end;
|
||||
|
||||
handle_event({call, From}, {cancel_command_call, Ref}, _, State = #state{channel_pid = ChannelPid}) ->
|
||||
case is_pid(ChannelPid) of
|
||||
true ->
|
||||
ok = ssl_channel:cancel_command_call(ChannelPid, Ref),
|
||||
{keep_state, State, [{reply, From, ok}]};
|
||||
false ->
|
||||
{keep_state, State, [{reply, From, ok}]}
|
||||
end;
|
||||
|
||||
%% 发送指令时, pub/sub
|
||||
handle_event({call, From}, {pub, Topic, Qos, Content}, StateName, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
case StateName of
|
||||
?STATE_ACTIVATED ->
|
||||
case HasSession andalso is_pid(ChannelPid) of
|
||||
true ->
|
||||
logger:debug("[iot_host] host: ~p, publish to topic: ~p, content: ~p", [UUID, Topic, Content]),
|
||||
%% 通过websocket发送消息
|
||||
ssl_channel:pub(ChannelPid, Topic, Qos, Content),
|
||||
|
||||
{keep_state, State, [{reply, From, ok}]};
|
||||
false ->
|
||||
logger:debug("[iot_host] uuid: ~p, publish to topic: ~p, content: ~p, invalid state: ~p", [UUID, Topic, Content, state_map(State)]),
|
||||
{keep_state, State, [{reply, From, {error, <<"主机离线,发送失败"/utf8>>}}]}
|
||||
end;
|
||||
?STATE_DENIED ->
|
||||
{keep_state, State, [{reply, From, {error, <<"主机未激活,发送失败"/utf8>>}}]}
|
||||
end;
|
||||
|
||||
%% 激活/关闭授权只修改 iot 本地授权状态;efka 连接保持在线,数据是否处理由 host 状态决定。
|
||||
handle_event({call, From}, {activate, Auth}, _, State = #state{uuid = UUID}) ->
|
||||
NStateName = case Auth of
|
||||
true ->
|
||||
?STATE_ACTIVATED;
|
||||
false ->
|
||||
?STATE_DENIED
|
||||
end,
|
||||
logger:debug("[iot_host] uuid: ~p, state_name change to ~p", [UUID, NStateName]),
|
||||
{next_state, NStateName, State, [{reply, From, ok}]};
|
||||
|
||||
%% 绑定channel
|
||||
handle_event({call, From}, {attach_channel, ChannelPid}, StateName, State = #state{uuid = UUID, channel_pid = OldChannelPid, host_status = HostStatus}) ->
|
||||
case OldChannelPid == undefined orelse OldChannelPid =:= ChannelPid of
|
||||
true ->
|
||||
case StateName of
|
||||
?STATE_ACTIVATED ->
|
||||
erlang:monitor(process, ChannelPid),
|
||||
%% 更新主机为在线状态
|
||||
NState = mark_host_online(UUID, HostStatus, State#state{channel_pid = ChannelPid, has_session = true}),
|
||||
{keep_state, NState, [{reply, From, ok}]};
|
||||
%% 主机未激活
|
||||
?STATE_DENIED ->
|
||||
logger:notice("[iot_host] attach_channel host_id uuid: ~p, channel: ~p, host denied locally", [UUID, ChannelPid]),
|
||||
erlang:monitor(process, ChannelPid),
|
||||
NState = mark_host_online(UUID, HostStatus, State#state{channel_pid = ChannelPid, has_session = true}),
|
||||
{keep_state, NState, [{reply, From, ok}]}
|
||||
end;
|
||||
false ->
|
||||
logger:notice("[iot_host] attach_channel host_id uuid: ~p, old channel exists: ~p", [UUID, OldChannelPid]),
|
||||
{keep_state, State, [{reply, From, {error, <<"channel existed">>}}]}
|
||||
end;
|
||||
|
||||
%% 数据分发
|
||||
handle_event(cast, {handle, {data, RouteKey, MetricBin}}, ?STATE_ACTIVATED,
|
||||
State = #state{uuid = UUID, has_session = true}) ->
|
||||
logger:debug("[iot_host] metric_data host: ~p, route_key: ~p, metric: ~p", [UUID, RouteKey, MetricBin]),
|
||||
endpoint_subscription:publish(get_route_key(RouteKey), MetricBin),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(cast, {handle, {data, RouteKey, MetricBin}}, StateName,
|
||||
State = #state{uuid = UUID, has_session = true}) ->
|
||||
logger:notice("[iot_host] host_uuid: ~p, state_name: ~p, metric_data route_key: ~p, metric: ~p, discard", [UUID, StateName, RouteKey, MetricBin]),
|
||||
{keep_state, State};
|
||||
|
||||
%% ping的数据是通过aes加密后的,因此需要在有会话的情况下才行
|
||||
handle_event(cast, {handle, {ping, Metrics}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
|
||||
logger:debug("[iot_host] ping host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
|
||||
{keep_state, State#state{metrics = Metrics}};
|
||||
|
||||
%% 心跳机制
|
||||
handle_event(cast, heartbeat, _, State = #state{uuid = UUID, heartbeat_counter = HeartbeatCounter, host_status = HostStatus}) ->
|
||||
NState = mark_host_online(UUID, HostStatus, State#state{heartbeat_counter = HeartbeatCounter + 1}),
|
||||
{keep_state, NState};
|
||||
|
||||
%% UDP 心跳丢失但 SSL channel 仍在时,不能把 host 标记为离线。
|
||||
handle_event(info, {timeout, _, heartbeat_ticker}, _,
|
||||
State = #state{uuid = UUID, heartbeat_counter = 0, host_status = HostStatus, channel_pid = ChannelPid}) when is_pid(ChannelPid) ->
|
||||
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
logger:warning("[iot_host] uuid: ~p, udp heartbeat lost but ssl channel is alive: ~p", [UUID, ChannelPid]),
|
||||
NState = mark_host_online(UUID, HostStatus, State#state{heartbeat_counter = 0}),
|
||||
{keep_state, NState};
|
||||
|
||||
%% 没有收到 UDP 心跳且没有 SSL channel,主机下线, 设备状态不变
|
||||
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{uuid = UUID, heartbeat_counter = 0, host_status = HostStatus}) ->
|
||||
logger:warning("[iot_host] uuid: ~p, heartbeat lost, devices will unknown", [UUID]),
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
NState = mark_host_offline(UUID, HostStatus, State#state{channel_pid = undefined, has_session = false, heartbeat_counter = 0}),
|
||||
{keep_state, NState};
|
||||
|
||||
%% 其他情况下需要重置系统计数器
|
||||
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{}) ->
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
{keep_state, State#state{heartbeat_counter = 0}};
|
||||
|
||||
%% 当websocket断开的时候,主机的状态不一定改变;主机的状态改变通过心跳机制,会话状态需要改变
|
||||
handle_event(info, {'DOWN', _Ref, process, ChannelPid, Reason}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
logger:warning("[iot_host] uuid: ~p, channel: ~p, down with reason: ~p, has_session: ~p, state: ~p", [UUID, ChannelPid, Reason, HasSession, State]),
|
||||
{keep_state, State#state{channel_pid = undefined, has_session = false}};
|
||||
|
||||
handle_event(info, {'DOWN', _Ref, process, Pid, Reason}, _, State = #state{uuid = UUID}) ->
|
||||
logger:debug("[iot_host] uuid: ~p, process_pid: ~p, down with reason: ~p, state: ~p", [UUID, Pid, Reason, State]),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(Event, Info, StateName, State = #state{uuid = UUID}) ->
|
||||
logger:warning("[iot_host] host: ~p, event: ~p, unknown message: ~p, state_name: ~p, state: ~p", [UUID, Event, Info, StateName, state_map(State)]),
|
||||
{keep_state, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_statem terminates with
|
||||
%% Reason. The return value is ignored.
|
||||
terminate(Reason, _StateName, _State = #state{uuid = UUID, has_session = HasSession}) ->
|
||||
logger:debug("[iot_host] host: ~p, terminate with reason: ~p, has_session: ~p", [UUID, Reason, HasSession]),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec container_call(Pid :: pid(), Request :: term()) ->
|
||||
{ok, Ref :: request_ref()} | {error, Reason :: any()}.
|
||||
container_call(Pid, Request) when is_pid(Pid) ->
|
||||
gen_statem:call(Pid, {container_call, self(), Request}).
|
||||
|
||||
-spec get_route_key(binary()) -> binary().
|
||||
get_route_key(<<"">>) ->
|
||||
<<"/">>;
|
||||
get_route_key(RouteKey) when is_binary(RouteKey) ->
|
||||
RouteKey.
|
||||
|
||||
%% 将当前的state转换成map
|
||||
state_map(#state{host_id = HostId, uuid = UUID, host_status = HostStatus, has_session = HasSession, heartbeat_counter = HeartbeatCounter, channel_pid = ChannelPid, metrics = Metrics}) ->
|
||||
#{
|
||||
host_id => HostId,
|
||||
uuid => UUID,
|
||||
host_status => HostStatus,
|
||||
has_session => HasSession,
|
||||
heartbeat_counter => HeartbeatCounter,
|
||||
channel_pid => ChannelPid,
|
||||
metrics => Metrics
|
||||
}.
|
||||
|
||||
flush_reply(Ref) ->
|
||||
receive
|
||||
{command_reply, Ref, _Reply} ->
|
||||
ok
|
||||
after 0 ->
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec request_ref() -> request_ref().
|
||||
request_ref() ->
|
||||
crypto:strong_rand_bytes(16).
|
||||
|
||||
-spec mark_host_online(binary(), integer(), #state{}) -> #state{}.
|
||||
mark_host_online(UUID, HostStatus, State) ->
|
||||
case maybe_mark_host_online(UUID, HostStatus) of
|
||||
keep_status ->
|
||||
State;
|
||||
{next_status, NStatus} ->
|
||||
State#state{host_status = NStatus}
|
||||
end.
|
||||
|
||||
-spec mark_host_offline(binary(), integer(), #state{}) -> #state{}.
|
||||
mark_host_offline(UUID, HostStatus, State) ->
|
||||
case maybe_mark_host_offline(UUID, HostStatus) of
|
||||
keep_status ->
|
||||
State;
|
||||
{next_status, NStatus} ->
|
||||
State#state{host_status = NStatus}
|
||||
end.
|
||||
|
||||
-spec maybe_mark_host_online(binary(), integer()) -> {next_status, NStatus :: integer()} | keep_status.
|
||||
maybe_mark_host_online(UUID, HostStatus) ->
|
||||
case HostStatus of
|
||||
?HOST_OFFLINE ->
|
||||
case iot_api_client:change_host_status(UUID, ?HOST_ONLINE) of
|
||||
{ok, Result} ->
|
||||
logger:debug("[iot_host] uuid: ~p, change host status online success: ~p", [UUID, Result]),
|
||||
{next_status, ?HOST_ONLINE};
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_host] uuid: ~p, change host status online failed: ~p", [UUID, Reason]),
|
||||
keep_status
|
||||
end;
|
||||
_ ->
|
||||
keep_status
|
||||
end.
|
||||
|
||||
-spec maybe_mark_host_offline(binary(), integer()) -> {next_status, NStatus :: integer()} | keep_status.
|
||||
maybe_mark_host_offline(UUID, HostStatus) ->
|
||||
case HostStatus of
|
||||
?HOST_ONLINE ->
|
||||
case iot_api_client:change_host_status(UUID, ?HOST_OFFLINE) of
|
||||
{ok, Result} ->
|
||||
logger:debug("[iot_host] uuid: ~p, change host status offline success: ~p", [UUID, Result]),
|
||||
{next_status, ?HOST_OFFLINE};
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_host] uuid: ~p, change host status offline failed: ~p", [UUID, Reason]),
|
||||
keep_status
|
||||
end;
|
||||
_ ->
|
||||
keep_status
|
||||
end.
|
||||
85
apps/iot/src/host/iot_host_sup.erl
Normal file
85
apps/iot/src/host/iot_host_sup.erl
Normal file
@ -0,0 +1,85 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_host_sup).
|
||||
-include("iot.hrl").
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0, init/1, delete_host/1, ensured_host_started/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
Specs = lists:map(fun child_spec/1, iot_api_client:get_all_hosts()),
|
||||
|
||||
{ok, {#{strategy => one_for_one, intensity => 1000, period => 3600}, Specs}}.
|
||||
|
||||
-spec ensured_host_started(UUID :: binary()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensured_host_started(UUID) when is_binary(UUID) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
%% 尝试删除下host对应的信息
|
||||
case delete_host(UUID) of
|
||||
ok ->
|
||||
case supervisor:start_child(?MODULE, child_spec(UUID)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
Pid when is_pid(Pid) ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
delete_host(UUID) when is_binary(UUID) ->
|
||||
Id = iot_host:get_name(UUID),
|
||||
case supervisor:terminate_child(?MODULE, Id) of
|
||||
ok ->
|
||||
delete_host_child(Id, UUID);
|
||||
{error, not_found} ->
|
||||
delete_host_child(Id, UUID);
|
||||
{error, Reason} ->
|
||||
logger:warning("[iot_host_sup] terminate host: ~p failed: ~p", [UUID, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
delete_host_child(Id, UUID) ->
|
||||
case supervisor:delete_child(?MODULE, Id) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, not_found} ->
|
||||
ok;
|
||||
{error, running} ->
|
||||
%% ensure killed then delete again
|
||||
iot_host:kill(UUID),
|
||||
case supervisor:delete_child(?MODULE, Id) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, not_found} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec child_spec(UUID :: binary()) -> map().
|
||||
child_spec(UUID) when is_binary(UUID) ->
|
||||
Id = iot_host:get_name(UUID),
|
||||
#{id => Id,
|
||||
start => {iot_host, start_link, [Id, UUID]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['iot_host']}.
|
||||
239
apps/iot/src/http/container_handler.erl
Normal file
239
apps/iot/src/http/container_handler.erl
Normal file
@ -0,0 +1,239 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(container_handler).
|
||||
-author("licheng5").
|
||||
-include("iot.hrl").
|
||||
|
||||
-define(REQ_TIMEOUT, 10000).
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
handle_request("GET", "/container/get_all", #{<<"uuid">> := UUID}, _) when is_binary(UUID) ->
|
||||
%% 检查ConfigJson是否是合法的json字符串
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:get_containers(Pid) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 下发config.json, 微服务接受后,保存服务配置
|
||||
handle_request("POST", "/container/push_config", _,
|
||||
#{<<"uuid">> := UUID, <<"container_name">> := ContainerName, <<"config">> := Config, <<"timeout">> := Timeout0})
|
||||
when is_binary(UUID), is_binary(ContainerName), is_binary(Config), is_integer(Timeout0) ->
|
||||
|
||||
%% 检查ConfigJson是否是合法的json字符串
|
||||
true = iot_util:is_json(Config),
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
Timeout = Timeout0 * 1000,
|
||||
case iot_host:config_container(Pid, ContainerName, Config) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, Timeout) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 部署微服务
|
||||
handle_request("POST", "/container/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"config">> := Config})
|
||||
when is_binary(UUID), is_integer(TaskId), is_map(Config) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_container_task_sup:ensure_started(UUID, TaskId) of
|
||||
{ok, _TaskPid} ->
|
||||
handle_deploy_container(Pid, UUID, TaskId, Config);
|
||||
{error, Reason} ->
|
||||
{ok, 500, iot_util:json_error(500, reason_to_binary(Reason))}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 启动服务
|
||||
handle_request("POST", "/container/start", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:start_container(Pid, ContainerName) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 停止服务
|
||||
handle_request("POST", "/container/stop", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:stop_container(Pid, ContainerName) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/container/kill", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:kill_container(Pid, ContainerName) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 删除容器
|
||||
handle_request("POST", "/container/remove", _, #{<<"uuid">> := UUID, <<"container_name">> := ContainerName}) when is_binary(UUID), is_binary(ContainerName) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:remove_container(Pid, ContainerName) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
|
||||
request_success_response(Result) when is_binary(Result) ->
|
||||
case decode_json_bytes(Result) of
|
||||
{ok, Data} ->
|
||||
iot_util:json_data(Data);
|
||||
error ->
|
||||
iot_util:json_data(Result)
|
||||
end;
|
||||
request_success_response(Result) ->
|
||||
iot_util:json_data(Result).
|
||||
|
||||
request_error_response(Code, Reason) when is_integer(Code), is_binary(Reason) ->
|
||||
case decode_json_bytes(Reason) of
|
||||
{ok, #{<<"message">> := Message}} when is_binary(Message) ->
|
||||
iot_util:json_error(Code, Message);
|
||||
{ok, Message} when is_binary(Message) ->
|
||||
iot_util:json_error(Code, Message);
|
||||
_ ->
|
||||
iot_util:json_error(Code, Reason)
|
||||
end.
|
||||
|
||||
-spec request_error_http_response(Reason :: term()) ->
|
||||
{ok, HttpStatus :: 400 | 504, Body :: binary()}.
|
||||
request_error_http_response(Reason) ->
|
||||
HttpStatus = request_error_status(Reason),
|
||||
{ok, HttpStatus, request_error_response(HttpStatus, reason_to_binary(Reason))}.
|
||||
|
||||
-spec request_error_status(Reason :: term()) -> integer().
|
||||
request_error_status(timeout) ->
|
||||
504;
|
||||
request_error_status(<<"timeout">>) ->
|
||||
504;
|
||||
request_error_status(_) ->
|
||||
400.
|
||||
|
||||
-spec reason_to_binary(term()) -> binary().
|
||||
reason_to_binary(Reason) when is_binary(Reason) ->
|
||||
Reason;
|
||||
reason_to_binary(timeout) ->
|
||||
<<"timeout">>;
|
||||
reason_to_binary(invalid_response) ->
|
||||
<<"invalid response">>;
|
||||
reason_to_binary(Reason) ->
|
||||
unicode:characters_to_binary(io_lib:format("~p", [Reason])).
|
||||
|
||||
decode_json_bytes(Data) when is_binary(Data) ->
|
||||
case catch json:decode(Data) of
|
||||
{'EXIT', _} ->
|
||||
error;
|
||||
{error, _} ->
|
||||
error;
|
||||
Decoded ->
|
||||
{ok, Decoded}
|
||||
end.
|
||||
|
||||
-spec handle_deploy_container(pid(), binary(), integer(), map()) ->
|
||||
{ok, 200 | 400 | 504, binary()}.
|
||||
handle_deploy_container(Pid, UUID, TaskId, Config) ->
|
||||
case iot_host:deploy_container(Pid, TaskId, Config) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Pid, Ref, ?REQ_TIMEOUT) of
|
||||
ok ->
|
||||
{ok, 200, request_success_response(<<"ok">>)};
|
||||
{ok, Result} ->
|
||||
{ok, 200, request_success_response(Result)};
|
||||
{error, Reason} ->
|
||||
ok = iot_container_task_sup:fail(UUID, TaskId, Reason),
|
||||
request_error_http_response(Reason)
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
ok = iot_container_task_sup:fail(UUID, TaskId, Reason),
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end.
|
||||
155
apps/iot/src/http/endpoint_handler.erl
Normal file
155
apps/iot/src/http/endpoint_handler.erl
Normal file
@ -0,0 +1,155 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_handler).
|
||||
-author("licheng5").
|
||||
-include_lib("endpoint/include/endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 获取endpoint的运行状态
|
||||
handle_request("POST", "/endpoint/run_statuses", _, Ids) when is_list(Ids) ->
|
||||
Statuses = lists:map(fun(Id) ->
|
||||
case endpoint:get_pid(Id) of
|
||||
undefined ->
|
||||
0;
|
||||
Pid when is_pid(Pid) ->
|
||||
1
|
||||
end
|
||||
end, Ids),
|
||||
{ok, 200, iot_util:json_data(Statuses)};
|
||||
|
||||
handle_request("POST", "/endpoint/start", _, #{<<"id">> := Id}) when is_integer(Id) ->
|
||||
case iot_api_client:get_endpoint(Id) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||
{ok, EndpointInfo} ->
|
||||
case endpoint:endpoint_record(EndpointInfo) of
|
||||
{ok, Endpoint = #endpoint{title = Title}} ->
|
||||
case endpoint_adapter_sup:ensured_endpoint_started(Endpoint) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"start endpoint error">>)}
|
||||
end;
|
||||
error ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint invalid">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/endpoint/stop", _, #{<<"id">> := Id}) when is_integer(Id) ->
|
||||
case iot_api_client:get_endpoint(Id) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||
{ok, _} ->
|
||||
case endpoint_adapter_sup:delete_endpoint(Id) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_handler] stop endpoint id: ~p, get error: ~p", [Id, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"stop endpoint error">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/endpoint/restart", _, #{<<"id">> := Id}) when is_integer(Id) ->
|
||||
case iot_api_client:get_endpoint(Id) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||
{ok, EndpointInfo} ->
|
||||
case endpoint:endpoint_record(EndpointInfo) of
|
||||
{ok, Endpoint = #endpoint{title = Title}} ->
|
||||
case endpoint:get_pid(Id) of
|
||||
undefined ->
|
||||
case endpoint_adapter_sup:ensured_endpoint_started(Endpoint) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"restart endpoint error">>)}
|
||||
end;
|
||||
Pid when is_pid(Pid) ->
|
||||
case endpoint_adapter_sup:delete_endpoint(Id) of
|
||||
ok ->
|
||||
case endpoint_adapter_sup:ensured_endpoint_started(Endpoint) of
|
||||
{ok, Pid0} when is_pid(Pid0) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"restart endpoint error">>)}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
logger:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Title, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"stop endpoint error">>)}
|
||||
end
|
||||
end;
|
||||
error ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint invalid">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% http接口的测试
|
||||
handle_request("POST", "/endpoint/test", _, #{<<"protocol">> := <<"http">>, <<"config">> := #{<<"url">> := Url, <<"pool_size">> := PoolSize}}) when is_integer(PoolSize), PoolSize > 0 ->
|
||||
case endpoint_tester:test(#http_endpoint{url = Url, pool_size = PoolSize}) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"ok">>)};
|
||||
{error, Reason} ->
|
||||
logger:debug("[endpint_handler] test http: ~p, error: ~p", [Url, Reason]),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url failed">>)}
|
||||
end;
|
||||
|
||||
%% 测试mqtt
|
||||
handle_request("POST", "/endpoint/test", _, #{<<"protocol">> := <<"mqtt">>, <<"config">> := Config}) ->
|
||||
case endpoint:parse_config(<<"mqtt">>, Config) of
|
||||
{ok, MqttEndpoint = #mqtt_endpoint{}} ->
|
||||
case endpoint_tester:test(MqttEndpoint) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"ok">>)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end;
|
||||
{error, Errors} ->
|
||||
{ok, 200, iot_util:json_error(-1, Errors)}
|
||||
end;
|
||||
|
||||
%% 测试kafka
|
||||
handle_request("POST", "/endpoint/test", _, #{<<"protocol">> := <<"kafka">>, <<"config">> := Config}) ->
|
||||
case endpoint:parse_config(<<"kafka">>, Config) of
|
||||
{ok, KafkaEndpoint = #kafka_endpoint{}} ->
|
||||
case endpoint_tester:test(KafkaEndpoint) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"ok">>)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end;
|
||||
{error, Errors} ->
|
||||
{ok, 200, iot_util:json_error(-1, Errors)}
|
||||
end;
|
||||
|
||||
%% 发布消息
|
||||
handle_request("POST", "/endpoint/publish_metric", _, #{<<"route_key">> := RouteKey, <<"metric">> := Metric0}) when is_binary(RouteKey) ->
|
||||
if
|
||||
is_map(Metric0) orelse is_list(Metric0) ->
|
||||
Metric = iolist_to_binary(json:encode(Metric0)),
|
||||
endpoint_subscription:publish(RouteKey, Metric),
|
||||
{ok, 200, iot_util:json_data(<<"ok">>)};
|
||||
is_binary(Metric0) ->
|
||||
endpoint_subscription:publish(RouteKey, Metric0),
|
||||
{ok, 200, iot_util:json_data(<<"ok">>)};
|
||||
true ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"invalid metric">>)}
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
73
apps/iot/src/http/event_stream_handler.erl
Normal file
73
apps/iot/src/http/event_stream_handler.erl
Normal file
@ -0,0 +1,73 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 08. 5月 2025 13:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(event_stream_handler).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([init/2]).
|
||||
|
||||
init(Req0, Opts) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
Method = binary_to_list(cowboy_req:method(Req0)),
|
||||
Path = binary_to_list(cowboy_req:path(Req0)),
|
||||
GetParams0 = cowboy_req:parse_qs(Req0),
|
||||
GetParams = maps:from_list(GetParams0),
|
||||
|
||||
logger:debug("method: ~p, path: ~p, get: ~p", [Method, Path, GetParams]),
|
||||
case parse_stream_params(GetParams) of
|
||||
{ok, UUID, TaskId} ->
|
||||
Req1 = cowboy_req:stream_reply(200, #{
|
||||
<<"Content-Type">> => <<"text/event-stream">>,
|
||||
<<"Cache-Control">> => <<"no-cache">>,
|
||||
<<"Connection">> => <<"keep-alive">>
|
||||
}, Req0),
|
||||
|
||||
ok = iot_container_task:subscribe(UUID, TaskId, self()),
|
||||
receiver_events(TaskId, Req1),
|
||||
|
||||
{ok, Req1, Opts};
|
||||
{error, Reason} ->
|
||||
Req1 = cowboy_req:reply(400, #{
|
||||
<<"Content-Type">> => <<"application/json">>
|
||||
}, iot_util:json_error(400, Reason), Req0),
|
||||
|
||||
{ok, Req1, Opts}
|
||||
end.
|
||||
|
||||
receiver_events(TaskId, Req) ->
|
||||
receive
|
||||
{stream_data, TaskId, Type, Stream} ->
|
||||
Data = iolist_to_binary(json:encode(#{<<"type">> => Type, <<"stream">> => Stream})),
|
||||
Body = iolist_to_binary([<<"event: message\n">>, <<"data: ", Data/binary, "\n">>, <<"\n">>]),
|
||||
ok = cowboy_req:stream_body(Body, nofin, Req),
|
||||
|
||||
receiver_events(TaskId, Req);
|
||||
{stream_close, TaskId, Reason} ->
|
||||
CloseFrame = iolist_to_binary([<<"event: close\n">>, <<"data: ", Reason/binary, "\n">>, <<"\n">>]),
|
||||
ok = cowboy_req:stream_body(CloseFrame, fin, Req)
|
||||
after 30000 ->
|
||||
ok = cowboy_req:stream_body(<<": heartbeat\n\n">>, nofin, Req),
|
||||
receiver_events(TaskId, Req)
|
||||
end.
|
||||
|
||||
-spec parse_stream_params(map()) -> {ok, binary(), integer()} | {error, binary()}.
|
||||
parse_stream_params(#{<<"uuid">> := UUID, <<"task_id">> := TaskId0}) when is_binary(UUID), is_binary(TaskId0) ->
|
||||
try binary_to_integer(TaskId0) of
|
||||
TaskId when TaskId >= 0 ->
|
||||
{ok, UUID, TaskId};
|
||||
_ ->
|
||||
{error, <<"task_id must be non-negative">>}
|
||||
catch
|
||||
error:badarg ->
|
||||
{error, <<"task_id must be integer">>}
|
||||
end;
|
||||
parse_stream_params(#{<<"task_id">> := _TaskId0}) ->
|
||||
{error, <<"uuid required">>};
|
||||
parse_stream_params(_) ->
|
||||
{error, <<"uuid and task_id required">>}.
|
||||
@ -18,7 +18,7 @@
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
handle_request("GET", "/host/metric", #{<<"uuid">> := UUID}, _) ->
|
||||
lager:debug("[host_handler] get host metric uuid is: ~p", [UUID]),
|
||||
logger:debug("[host_handler] get host metric uuid is: ~p", [UUID]),
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
@ -42,28 +42,13 @@ handle_request("GET", "/host/status", #{<<"uuid">> := UUID}, _) when is_binary(U
|
||||
{ok, 200, iot_util:json_data(StatusInfo)}
|
||||
end;
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
handle_request("POST", "/host/reload", _, #{<<"uuid">> := UUID}) when is_binary(UUID) ->
|
||||
lager:debug("[host_handler] will reload host uuid: ~p", [UUID]),
|
||||
case iot_host_sup:ensured_host_started(UUID) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, #{<<"authorize_status">> := AuthorizeStatus}} = host_bo:get_host_by_uuid(UUID),
|
||||
ok = iot_host:activate(Pid, AuthorizeStatus =:= 1),
|
||||
lager:debug("[host_handler] already_started reload host uuid: ~p, success", [UUID]),
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
Error ->
|
||||
lager:debug("[host_handler] reload host uuid: ~p, error: ~p", [UUID, Error]),
|
||||
{ok, 200, iot_util:json_error(404, <<"reload error">>)}
|
||||
end;
|
||||
|
||||
%% 删除对应的主机信息
|
||||
handle_request("POST", "/host/delete", _, #{<<"uuid">> := UUID}) when is_binary(UUID) ->
|
||||
case iot_host_sup:delete_host(UUID) of
|
||||
ok ->
|
||||
lager:debug("[host_handler] will delete host uuid: ~p", [UUID]),
|
||||
case iot_host:get_pid(UUID) of
|
||||
Pid when is_pid(Pid) ->
|
||||
ok = iot_host_sup:delete_host(UUID),
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[host_handler] delete host uuid: ~p, get error is: ~p", [UUID, Reason]),
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"error">>)}
|
||||
end;
|
||||
|
||||
@ -71,39 +56,40 @@ handle_request("POST", "/host/delete", _, #{<<"uuid">> := UUID}) when is_binary(
|
||||
handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> := true}) when is_binary(UUID) ->
|
||||
case iot_host_sup:ensured_host_started(UUID) of
|
||||
{error, Reason} ->
|
||||
lager:debug("[host_handler] activate host_id: ~p, failed with reason: ~p", [UUID, Reason]),
|
||||
logger:debug("[host_handler] activate host_id: ~p, failed with reason: ~p", [UUID, Reason]),
|
||||
{ok, 200, iot_util:json_error(400, <<"host not found">>)};
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
||||
ok = iot_host:activate(Pid, true),
|
||||
|
||||
{ok, 200, iot_util:json_data(<<"success">>)}
|
||||
logger:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
||||
auth_response(iot_host:activate(Pid, true))
|
||||
end;
|
||||
|
||||
%% 处理主机的授权的 取消激活
|
||||
handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> := false}) when is_binary(UUID) ->
|
||||
case iot_host_sup:ensured_host_started(UUID) of
|
||||
{error, Reason} ->
|
||||
lager:debug("[host_handler] activate host_id: ~p, failed with reason: ~p", [UUID, Reason]),
|
||||
logger:debug("[host_handler] activate host_id: ~p, failed with reason: ~p", [UUID, Reason]),
|
||||
{ok, 200, iot_util:json_error(400, <<"host not found">>)};
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
||||
ok = iot_host:activate(Pid, false),
|
||||
|
||||
{ok, 200, iot_util:json_data(<<"success">>)}
|
||||
logger:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
||||
auth_response(iot_host:activate(Pid, false))
|
||||
end;
|
||||
|
||||
%% 主机事件发布
|
||||
handle_request("POST", "/host/pub", _, #{<<"uuid">> := UUID, <<"topic">> := Topic, <<"content">> := Content})
|
||||
when is_binary(UUID), is_binary(Topic), is_binary(Content) ->
|
||||
handle_request("POST", "/host/pub", _, #{<<"uuid">> := UUID, <<"topic">> := Topic, <<"qos">> := Qos0, <<"content">> := Content})
|
||||
when is_binary(UUID), is_binary(Topic), is_binary(Content), is_integer(Qos0) ->
|
||||
|
||||
Qos = case Qos0 > 0 of true -> 1; false -> 0 end,
|
||||
case iot_host_sup:ensured_host_started(UUID) of
|
||||
{error, Reason} ->
|
||||
lager:debug("[host_handler] pub host_id: ~p, topic: ~p, failed with reason: ~p", [UUID, Topic, Reason]),
|
||||
logger:debug("[host_handler] pub host_id: ~p, topic: ~p, failed with reason: ~p", [UUID, Topic, Reason]),
|
||||
{ok, 200, iot_util:json_error(400, <<"host not found">>)};
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
ok = iot_host:pub(Pid, Topic, Content),
|
||||
{ok, 200, iot_util:json_data(<<"success">>)}
|
||||
case iot_host:pub(Pid, Topic, Qos, Content) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
@ -113,3 +99,28 @@ handle_request(_, Path, _, _) ->
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
auth_response(ok) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
auth_response({error, Reason}) ->
|
||||
{ok, auth_error_status(Reason), iot_util:json_error(auth_error_status(Reason), reason_to_binary(Reason))}.
|
||||
|
||||
auth_error_status(timeout) ->
|
||||
504;
|
||||
auth_error_status(<<"timeout">>) ->
|
||||
504;
|
||||
auth_error_status(_) ->
|
||||
400.
|
||||
|
||||
reason_to_binary(Reason) when is_binary(Reason) ->
|
||||
Reason;
|
||||
reason_to_binary(timeout) ->
|
||||
<<"timeout">>;
|
||||
reason_to_binary(invalid_response) ->
|
||||
<<"invalid response">>;
|
||||
reason_to_binary({failed, Reason}) ->
|
||||
reason_to_binary(Reason);
|
||||
reason_to_binary({channel_closed, Reason}) ->
|
||||
<<"channel closed: ", (reason_to_binary(Reason))/binary>>;
|
||||
reason_to_binary(Reason) ->
|
||||
unicode:characters_to_binary(io_lib:format("~p", [Reason])).
|
||||
@ -6,49 +6,34 @@
|
||||
%%% @end
|
||||
%%% Created : 08. 5月 2025 13:00
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(http_server).
|
||||
-module(http_protocol).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([start/0]).
|
||||
-export([init/2]).
|
||||
|
||||
%% 启动http服务
|
||||
start() ->
|
||||
{ok, Props} = application:get_env(iot, http_server),
|
||||
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||
Port = proplists:get_value(port, Props),
|
||||
|
||||
Dispatcher = cowboy_router:compile([
|
||||
{'_', [
|
||||
{"/host/[...]", ?MODULE, [host_handler]},
|
||||
{"/service/[...]", ?MODULE, [service_handler]},
|
||||
{"/device/[...]", ?MODULE, [device_handler]}
|
||||
]}
|
||||
]),
|
||||
|
||||
TransOpts = [
|
||||
{port, Port},
|
||||
{num_acceptors, Acceptors},
|
||||
{backlog, Backlog},
|
||||
{max_connections, MaxConnections}
|
||||
],
|
||||
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
||||
|
||||
lager:debug("[http_server] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||
-define(MAX_BODY_BYTES, 10 * 1024 * 1024).
|
||||
|
||||
init(Req0, Opts = [Mod|_]) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
Method = binary_to_list(cowboy_req:method(Req0)),
|
||||
Path = binary_to_list(cowboy_req:path(Req0)),
|
||||
GetParams0 = cowboy_req:parse_qs(Req0),
|
||||
GetParams = maps:from_list(GetParams0),
|
||||
{ok, PostParams, Req1} = parse_body(Req0),
|
||||
case parse_body(Req0) of
|
||||
{ok, PostParams, Req1} ->
|
||||
handle_request(Mod, Method, Path, GetParams, PostParams, Req1, Opts);
|
||||
{error, StatusCode, Resp, Req1} ->
|
||||
Req2 = cowboy_req:reply(StatusCode, #{
|
||||
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||
}, Resp, Req1),
|
||||
{ok, Req2, Opts}
|
||||
end.
|
||||
|
||||
handle_request(Mod, Method, Path, GetParams, PostParams, Req1, Opts) ->
|
||||
try Mod:handle_request(Method, Path, GetParams, PostParams) of
|
||||
{ok, StatusCode, Resp} ->
|
||||
lager:debug("[http_protocol] request path: ~p, get_params: ~p, post_params: ~p, response: ~ts",
|
||||
logger:debug("[http_protocol] request path: ~p, get_params: ~p, post_params: ~p, response: ~ts",
|
||||
[Path, GetParams, PostParams, Resp]),
|
||||
AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>),
|
||||
Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of
|
||||
@ -72,7 +57,7 @@ init(Req0, Opts = [Mod|_]) ->
|
||||
}, ErrResp, Req1),
|
||||
{ok, Req2, Opts};
|
||||
_:Error:Stack ->
|
||||
lager:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
|
||||
logger:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
|
||||
Req2 = cowboy_req:reply(500, #{
|
||||
<<"Content-Type">> => <<"text/html;charset=utf-8">>
|
||||
}, <<"Internal Server Error">>, Req1),
|
||||
@ -87,17 +72,18 @@ parse_body(Req0) ->
|
||||
ContentType = cowboy_req:header(<<"content-type">>, Req0),
|
||||
case ContentType of
|
||||
<<"application/json", _/binary>> ->
|
||||
{ok, Body, Req1} = read_body(Req0),
|
||||
case Body =/= <<"">> of
|
||||
true ->
|
||||
{ok, catch jiffy:decode(Body, [return_maps]), Req1};
|
||||
false ->
|
||||
{ok, #{}, Req1}
|
||||
case read_body(Req0) of
|
||||
{ok, Body, Req1} ->
|
||||
decode_json_body(Body, Req1);
|
||||
{error, payload_too_large, Req1} ->
|
||||
{error, 413, iot_util:json_error(413, <<"payload too large">>), Req1}
|
||||
end;
|
||||
<<"application/x-www-form-urlencoded">> ->
|
||||
{ok, PostParams0, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||
PostParams = maps:from_list(PostParams0),
|
||||
{ok, PostParams, Req1};
|
||||
case cowboy_req:read_urlencoded_body(Req0) of
|
||||
{ok, PostParams0, Req1} ->
|
||||
PostParams = maps:from_list(PostParams0),
|
||||
{ok, PostParams, Req1}
|
||||
end;
|
||||
_ ->
|
||||
{ok, #{}, Req0}
|
||||
end.
|
||||
@ -108,7 +94,33 @@ read_body(Req) ->
|
||||
read_body(Req, AccData) ->
|
||||
case cowboy_req:read_body(Req) of
|
||||
{ok, Data, Req1} ->
|
||||
{ok, <<AccData/binary, Data/binary>>, Req1};
|
||||
NAccData = <<AccData/binary, Data/binary>>,
|
||||
case byte_size(NAccData) =< ?MAX_BODY_BYTES of
|
||||
true ->
|
||||
{ok, NAccData, Req1};
|
||||
false ->
|
||||
{error, payload_too_large, Req1}
|
||||
end;
|
||||
{more, Data, Req1} ->
|
||||
read_body(Req1, <<AccData/binary, Data/binary>>)
|
||||
NAccData = <<AccData/binary, Data/binary>>,
|
||||
case byte_size(NAccData) =< ?MAX_BODY_BYTES of
|
||||
true ->
|
||||
read_body(Req1, NAccData);
|
||||
false ->
|
||||
{error, payload_too_large, Req1}
|
||||
end
|
||||
end.
|
||||
|
||||
decode_json_body(<<"">>, Req) ->
|
||||
{ok, #{}, Req};
|
||||
decode_json_body(Body, Req) ->
|
||||
case catch json:decode(Body) of
|
||||
{'EXIT', _} ->
|
||||
{error, 400, iot_util:json_error(400, <<"invalid json">>), Req};
|
||||
{error, _} ->
|
||||
{error, 400, iot_util:json_error(400, <<"invalid json">>), Req};
|
||||
Decoded when is_map(Decoded); is_list(Decoded) ->
|
||||
{ok, Decoded, Req};
|
||||
_ ->
|
||||
{error, 400, iot_util:json_error(400, <<"invalid json">>), Req}
|
||||
end.
|
||||
@ -1,68 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(device_handler).
|
||||
-author("licheng5").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 重新加载对应的主机信息
|
||||
handle_request("POST", "/device/reload", _, #{<<"host_id">> := HostId, <<"device_uuid">> := DeviceUUID}) when is_integer(HostId), is_binary(DeviceUUID) ->
|
||||
lager:debug("[device_handler] host_id: ~p, will reload device uuid: ~p", [HostId, DeviceUUID]),
|
||||
AliasName = iot_host:get_alias_name(HostId),
|
||||
case global:whereis_name(AliasName) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"reload device failed">>)};
|
||||
HostPid when is_pid(HostPid) ->
|
||||
case iot_host:reload_device(HostPid, DeviceUUID) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[device_handler] reload device: ~p, get error: ~p", [DeviceUUID, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"reload device failed">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 删除对应的主机信息
|
||||
handle_request("POST", "/device/delete", _, #{<<"host_id">> := HostId, <<"device_uuid">> := DeviceUUID}) when is_integer(HostId), is_binary(DeviceUUID) ->
|
||||
AliasName = iot_host:get_alias_name(HostId),
|
||||
case global:whereis_name(AliasName) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"delete device failed">>)};
|
||||
HostPid when is_pid(HostPid) ->
|
||||
ok = iot_host:delete_device(HostPid, DeviceUUID),
|
||||
{ok, 200, iot_util:json_data(<<"success">>)}
|
||||
end;
|
||||
|
||||
%% 处理主机的授权的激活
|
||||
handle_request("POST", "/device/activate", _, #{<<"host_id">> := HostId, <<"device_uuid">> := DeviceUUID, <<"auth">> := Auth})
|
||||
when is_integer(HostId), is_binary(DeviceUUID), is_boolean(Auth) ->
|
||||
|
||||
AliasName = iot_host:get_alias_name(HostId),
|
||||
case global:whereis_name(AliasName) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"activate device failed">>)};
|
||||
HostPid when is_pid(HostPid) ->
|
||||
case iot_host:activate_device(HostPid, DeviceUUID, Auth) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[device_handler] activate device: ~p, get error: ~p", [DeviceUUID, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"activate device failed">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
@ -1,103 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_handler).
|
||||
-author("licheng5").
|
||||
-include("endpoint.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
%% 获取endpoint的运行状态
|
||||
handle_request("POST", "/endpoint/run_statuses", _, Ids) when is_list(Ids) ->
|
||||
Statuses = lists:map(fun(Id) ->
|
||||
case endpoint:get_pid(Id) of
|
||||
undefined ->
|
||||
0;
|
||||
Pid when is_pid(Pid) ->
|
||||
1
|
||||
end
|
||||
end, Ids),
|
||||
{ok, 200, iot_util:json_data(Statuses)};
|
||||
|
||||
handle_request("POST", "/endpoint/start", _, #{<<"id">> := Id}) when is_integer(Id) ->
|
||||
case endpoint_bo:get_endpoint(Id) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||
{ok, EndpointInfo} ->
|
||||
case endpoint_bo:endpoint_record(EndpointInfo) of
|
||||
{ok, Endpoint = #endpoint{name = Name}} ->
|
||||
case endpoint_sup:ensured_endpoint_started(Endpoint) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"start endpoint error">>)}
|
||||
end;
|
||||
error ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint invalid">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/endpoint/stop", _, #{<<"id">> := Id}) when is_integer(Id) ->
|
||||
case endpoint_bo:get_endpoint(Id) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||
{ok, _} ->
|
||||
case endpoint_sup:delete_endpoint(Id) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_handler] stop endpoint id: ~p, get error: ~p", [Id, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"stop endpoint error">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/endpoint/restart", _, #{<<"id">> := Id}) when is_integer(Id) ->
|
||||
case endpoint_bo:get_endpoint(Id) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||
{ok, EndpointInfo} ->
|
||||
case endpoint_bo:endpoint_record(EndpointInfo) of
|
||||
{ok, Endpoint = #endpoint{name = Name}} ->
|
||||
case endpoint:get_pid(Id) of
|
||||
undefined ->
|
||||
case endpoint_sup:ensured_endpoint_started(Endpoint) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"restart endpoint error">>)}
|
||||
end;
|
||||
Pid ->
|
||||
case endpoint_sup:delete_endpoint(Id) of
|
||||
ok ->
|
||||
case endpoint_sup:ensured_endpoint_started(Endpoint) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"restart endpoint error">>)}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
lager:warning("[endpoint_handler] start endpoint: ~p, get error: ~p", [Name, Reason]),
|
||||
{ok, 200, iot_util:json_error(404, <<"stop endpoint error">>)}
|
||||
end
|
||||
end;
|
||||
error ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint invalid">>)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
@ -1,146 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(service_handler).
|
||||
-author("licheng5").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%% 下发config.json, 微服务接受后,保存服务配置
|
||||
handle_request("POST", "/service/push_config", _,
|
||||
#{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"config_json">> := ConfigJson, <<"timeout">> := Timeout0})
|
||||
when is_binary(UUID), is_binary(ServiceId), is_binary(ConfigJson), is_integer(Timeout0) ->
|
||||
|
||||
%% 检查ConfigJson是否是合法的json字符串
|
||||
true = iot_util:is_json(ConfigJson),
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
Timeout = Timeout0 * 1000,
|
||||
case iot_host:async_service_config(Pid, ServiceId, ConfigJson, Timeout) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, Timeout) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 部署微服务
|
||||
handle_request("POST", "/service/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"service_id">> := ServiceId, <<"tar_url">> := TarUrl})
|
||||
when is_binary(UUID), is_integer(TaskId), is_binary(ServiceId), is_binary(TarUrl) ->
|
||||
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:deploy_service(Pid, TaskId, ServiceId, TarUrl) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 启动服务
|
||||
handle_request("POST", "/service/start", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:start_service(Pid, ServiceId) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 停止服务
|
||||
handle_request("POST", "/service/stop", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:stop_service(Pid, ServiceId) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 远程调用微服务, 返回值的格式为json
|
||||
handle_request("POST", "/service/invoke", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"payload">> := Payload, <<"timeout">> := Timeout0})
|
||||
when is_binary(UUID), is_binary(ServiceId), is_binary(Payload), is_integer(Timeout0) ->
|
||||
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
Timeout = Timeout0 * 1000,
|
||||
case iot_host:invoke_service(Pid, ServiceId, Payload, Timeout) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, Timeout) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/service/task_log", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId}) when is_binary(UUID), is_integer(TaskId) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:task_log(Pid, TaskId) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -6,20 +6,16 @@
|
||||
{applications,
|
||||
[
|
||||
sync,
|
||||
endpoint,
|
||||
emqtt,
|
||||
eredis,
|
||||
ranch,
|
||||
cowboy,
|
||||
lager,
|
||||
jiffy,
|
||||
brod,
|
||||
parse_trans,
|
||||
hackney,
|
||||
poolboy,
|
||||
mysql,
|
||||
gproc,
|
||||
% gpb,
|
||||
esockd,
|
||||
mnesia,
|
||||
crypto,
|
||||
public_key,
|
||||
@ -27,6 +23,7 @@
|
||||
erts,
|
||||
runtime_tools,
|
||||
observer,
|
||||
inets,
|
||||
kernel,
|
||||
stdlib
|
||||
]},
|
||||
|
||||
@ -9,47 +9,100 @@
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
io:setopts([{encoding, unicode}]),
|
||||
%% 加速内存的回收
|
||||
erlang:system_flag(fullsweep_after, 16),
|
||||
%% 启动mnesia数据库
|
||||
start_mnesia(),
|
||||
|
||||
%% 启动http服务
|
||||
http_server:start(),
|
||||
iot_mnesia:safe_start(),
|
||||
|
||||
%% 启动tcp服务
|
||||
tcp_server:start(),
|
||||
%% 启动http服务。当前 supervisor 初始化依赖本机 simulator API,因此保留此顺序。
|
||||
start_http_server(),
|
||||
|
||||
%% 启动endpoints的相关依赖
|
||||
start_endpoints(),
|
||||
|
||||
%% 启动ssl服务
|
||||
start_ssl_server(),
|
||||
|
||||
iot_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
stop_started_services(),
|
||||
ok.
|
||||
|
||||
%% internal functions
|
||||
|
||||
%% 启动内存数据库
|
||||
start_mnesia() ->
|
||||
ok = ensure_mnesia_schema(),
|
||||
%% 启动数据库
|
||||
ok = mnesia:start(),
|
||||
Tables = mnesia:system_info(tables),
|
||||
lager:debug("[iot_app] tables: ~p", [Tables]),
|
||||
%% 创建数据库表
|
||||
ok.
|
||||
start_endpoints() ->
|
||||
EndpointInfos = iot_api_client:get_all_endpoints(),
|
||||
Endpoints = lists:filtermap(fun(EndpointInfo) ->
|
||||
case endpoint:endpoint_record(EndpointInfo) of
|
||||
error ->
|
||||
false;
|
||||
{ok, Endpoint} ->
|
||||
{true, Endpoint}
|
||||
end
|
||||
end, EndpointInfos),
|
||||
ok = endpoint_adapter_sup:start_endpoints(Endpoints).
|
||||
|
||||
-spec ensure_mnesia_schema() -> any().
|
||||
ensure_mnesia_schema() ->
|
||||
case mnesia:system_info(use_dir) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
mnesia:stop(),
|
||||
case mnesia:create_schema([node()]) of
|
||||
ok -> ok;
|
||||
{error, {_, {already_exists, _}}} -> ok;
|
||||
Error ->
|
||||
lager:debug("[iot_app] create mnesia schema failed with error: ~p", [Error]),
|
||||
throw({init_schema, Error})
|
||||
end
|
||||
end.
|
||||
start_http_server() ->
|
||||
{ok, Props} = application:get_env(iot, http_server),
|
||||
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||
Port = proplists:get_value(port, Props),
|
||||
|
||||
Dispatcher = cowboy_router:compile([
|
||||
{'_', [
|
||||
{"/host/[...]", http_protocol, [host_handler]},
|
||||
{"/container/[...]", http_protocol, [container_handler]},
|
||||
{"/endpoint/[...]", http_protocol, [endpoint_handler]},
|
||||
{"/simulator/[...]", http_protocol, [simulator_api_handler]},
|
||||
{"/event_stream", event_stream_handler, []}
|
||||
]}
|
||||
]),
|
||||
|
||||
TransOpts = #{
|
||||
max_connections => MaxConnections,
|
||||
num_acceptors => Acceptors,
|
||||
shutdown => brutal_kill,
|
||||
socket_opts => [
|
||||
{backlog, Backlog},
|
||||
{port, Port}
|
||||
]
|
||||
},
|
||||
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
||||
|
||||
logger:debug("[http_server] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||
|
||||
%% 启动ssl服务
|
||||
start_ssl_server() ->
|
||||
{ok, Props} = application:get_env(iot, ssl_server),
|
||||
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||
Port = proplists:get_value(port, Props),
|
||||
PrivDir = code:priv_dir(iot),
|
||||
CertFile = filename:join([PrivDir, "ssl", "server.crt"]),
|
||||
KeyFile = filename:join([PrivDir, "ssl", "server.key"]),
|
||||
|
||||
TransOpts = #{
|
||||
max_connections => MaxConnections,
|
||||
num_acceptors => Acceptors,
|
||||
shutdown => brutal_kill,
|
||||
socket_opts => [
|
||||
{nodelay, true},
|
||||
{backlog, Backlog},
|
||||
{port, Port},
|
||||
{certfile, CertFile},
|
||||
{keyfile, KeyFile}
|
||||
]
|
||||
},
|
||||
{ok, _} = ranch:start_listener(ssl_server, ranch_ssl, TransOpts, ssl_channel, []),
|
||||
logger:debug("[iot_app] the ssl server start at: ~p", [Port]).
|
||||
|
||||
stop_started_services() ->
|
||||
_ = cowboy:stop_listener(http_listener),
|
||||
_ = ranch:stop_listener(ssl_server),
|
||||
_ = iot_mnesia:stop(),
|
||||
ok.
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%% 授权校验模块
|
||||
%%% @end
|
||||
%%% Created : 27. 6月 2023 09:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_auth).
|
||||
-author("aresei").
|
||||
|
||||
%% API
|
||||
-export([check/5]).
|
||||
|
||||
%% 检测token是否是合法值
|
||||
-spec check(Username :: binary(), Token :: binary(), UUID :: binary(), Salt :: binary(), Timestamp :: integer()) -> boolean().
|
||||
check(Username, Token, UUID, Salt, Timestamp) when is_binary(Username), is_binary(Token), is_binary(UUID), is_binary(Salt), is_integer(Timestamp) ->
|
||||
true;
|
||||
check(Username, Token, UUID, Salt, Timestamp) when is_binary(Username), is_binary(Token), is_binary(UUID), is_binary(Salt), is_integer(Timestamp) ->
|
||||
BinTimestamp = integer_to_binary(Timestamp),
|
||||
%% 1分钟内有效
|
||||
case iot_util:current_time() - Timestamp =< 60 of
|
||||
true ->
|
||||
{ok, PreTokens} = application:get_env(iot, pre_tokens),
|
||||
case proplists:get_value(Username, PreTokens) of
|
||||
undefined ->
|
||||
false;
|
||||
PreToken when is_binary(PreToken) ->
|
||||
iot_util:md5(<<Salt/binary, "!", PreToken/binary, "!", UUID/binary, "!", BinTimestamp/binary>>) =:= Token
|
||||
end;
|
||||
false ->
|
||||
false
|
||||
end.
|
||||
@ -1,126 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 14. 8月 2023 11:40
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_device).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([new/1, is_activated/1, change_status/2, reload/1, auth/2]).
|
||||
|
||||
%% 终端是否授权
|
||||
-define(DEVICE_AUTH_DENIED, 0).
|
||||
-define(DEVICE_AUTH_AUTHED, 1).
|
||||
|
||||
%% 状态
|
||||
-define(STATE_DENIED, denied).
|
||||
-define(STATE_ACTIVATED, activated).
|
||||
|
||||
-record(device, {
|
||||
device_uuid :: binary(),
|
||||
auth_state = ?STATE_DENIED,
|
||||
status = ?DEVICE_OFFLINE
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec new(DeviceInfo :: binary() | map()) -> error | {ok, Device :: #device{}}.
|
||||
new(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||
case device_bo:get_device_by_uuid(DeviceUUID) of
|
||||
{ok, #{<<"device_uuid">> := DeviceUUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}} ->
|
||||
{ok, #device{device_uuid = DeviceUUID, status = Status, auth_state = auth_state(AuthorizeStatus)}};
|
||||
undefined ->
|
||||
lager:warning("[iot_device] device uuid: ~p, loaded from mysql failed", [DeviceUUID]),
|
||||
error
|
||||
end;
|
||||
new(#{<<"device_uuid">> := DeviceUUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}) ->
|
||||
{ok, #device{device_uuid = DeviceUUID, status = Status, auth_state = auth_state(AuthorizeStatus)}}.
|
||||
|
||||
-spec is_activated(Device :: #device{}) -> boolean().
|
||||
is_activated(#device{auth_state = AuthState}) ->
|
||||
AuthState =:= ?STATE_ACTIVATED.
|
||||
|
||||
-spec change_status(Device :: #device{}, NewStatus :: integer()) -> NDevice :: #device{}.
|
||||
change_status(Device = #device{status = Status}, NewStatus) when is_integer(NewStatus), Status =:= NewStatus ->
|
||||
Device;
|
||||
change_status(Device = #device{device_uuid = DeviceUUID}, ?DEVICE_ONLINE) ->
|
||||
{ok, _} = device_bo:change_status(DeviceUUID, ?DEVICE_ONLINE),
|
||||
report_event(DeviceUUID, ?DEVICE_ONLINE),
|
||||
Device#device{status = ?DEVICE_ONLINE};
|
||||
change_status(Device = #device{device_uuid = DeviceUUID}, ?DEVICE_OFFLINE) ->
|
||||
{ok, #{<<"status">> := Status}} = device_bo:get_device_by_uuid(DeviceUUID),
|
||||
case Status of
|
||||
?DEVICE_NOT_JOINED ->
|
||||
lager:debug("[iot_device] device: ~p, device_maybe_offline, not joined, can not change to offline", [DeviceUUID]),
|
||||
Device#device{status = ?DEVICE_NOT_JOINED};
|
||||
?DEVICE_OFFLINE ->
|
||||
lager:debug("[iot_device] device: ~p, device_maybe_offline, is offline, do nothing", [DeviceUUID]),
|
||||
Device#device{status = ?DEVICE_OFFLINE};
|
||||
?DEVICE_ONLINE ->
|
||||
{ok, _} = device_bo:change_status(DeviceUUID, ?DEVICE_OFFLINE),
|
||||
report_event(DeviceUUID, ?DEVICE_OFFLINE),
|
||||
Device#device{status = ?DEVICE_OFFLINE}
|
||||
end.
|
||||
|
||||
-spec reload(Device :: #device{}) -> error | {ok, NDevice :: #device{}}.
|
||||
reload(Device = #device{device_uuid = DeviceUUID}) ->
|
||||
lager:debug("[iot_device] will reload: ~p", [DeviceUUID]),
|
||||
case device_bo:get_device_by_uuid(DeviceUUID) of
|
||||
{ok, #{<<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}} ->
|
||||
{ok, Device#device{device_uuid = DeviceUUID, status = Status, auth_state = auth_state(AuthorizeStatus)}};
|
||||
undefined ->
|
||||
lager:warning("[iot_device] device uuid: ~p, loaded from mysql failed", [DeviceUUID]),
|
||||
error
|
||||
end.
|
||||
|
||||
-spec auth(Device :: #device{}, Auth :: boolean()) -> NDevice :: #device{}.
|
||||
auth(Device = #device{auth_state = StateName, device_uuid = DeviceUUID}, Auth) when is_boolean(Auth) ->
|
||||
case {StateName, Auth} of
|
||||
{?STATE_DENIED, false} ->
|
||||
lager:debug("[iot_device] device_uuid: ~p, auth: false, will keep state_name: ~p", [DeviceUUID, ?STATE_DENIED]),
|
||||
Device;
|
||||
{?STATE_DENIED, true} ->
|
||||
Device#device{auth_state = ?STATE_ACTIVATED};
|
||||
{?STATE_ACTIVATED, false} ->
|
||||
lager:debug("[iot_device] device_uuid: ~p, auth: false, state_name from: ~p, to: ~p", [DeviceUUID, ?STATE_ACTIVATED, ?STATE_DENIED]),
|
||||
Device#device{auth_state = ?STATE_DENIED};
|
||||
{?STATE_ACTIVATED, true} ->
|
||||
lager:debug("[iot_device] device_uuid: ~p, auth: true, will keep state_name: ~p", [DeviceUUID, ?STATE_ACTIVATED]),
|
||||
Device
|
||||
end.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec auth_state(integer()) -> atom().
|
||||
auth_state(?DEVICE_AUTH_AUTHED) ->
|
||||
?STATE_ACTIVATED;
|
||||
auth_state(?DEVICE_AUTH_DENIED) ->
|
||||
?STATE_DENIED.
|
||||
|
||||
-spec report_event(DeviceUUID :: binary(), NewStatus :: integer()) -> no_return().
|
||||
report_event(DeviceUUID, NewStatus) when is_binary(DeviceUUID), is_integer(NewStatus) ->
|
||||
TextMap = #{
|
||||
0 => <<"离线"/utf8>>,
|
||||
1 => <<"在线"/utf8>>
|
||||
},
|
||||
%% 设备的状态信息上报给中电
|
||||
Timestamp = iot_util:timestamp_of_seconds(),
|
||||
FieldsList = [#{
|
||||
<<"key">> => <<"device_status">>,
|
||||
<<"value">> => NewStatus,
|
||||
<<"value_text">> => maps:get(NewStatus, TextMap),
|
||||
<<"unit">> => 0,
|
||||
<<"type">> => <<"DI">>,
|
||||
<<"name">> => <<"设备状态"/utf8>>,
|
||||
<<"timestamp">> => Timestamp
|
||||
}],
|
||||
iot_router:route_uuid(DeviceUUID, FieldsList, Timestamp),
|
||||
lager:debug("[iot_device] device_uuid: ~p, route fields: ~p", [DeviceUUID, FieldsList]).
|
||||
@ -1,482 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 22. 9月 2023 16:38
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_host).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
-include("message_pb.hrl").
|
||||
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% 心跳包检测时间间隔, 15分钟检测一次
|
||||
-define(HEARTBEAT_INTERVAL, 900 * 1000).
|
||||
|
||||
%% 状态
|
||||
-define(STATE_DENIED, denied).
|
||||
-define(STATE_ACTIVATED, activated).
|
||||
|
||||
%% API
|
||||
-export([start_link/2, get_name/1, get_alias_name/1, get_pid/1, handle/2, activate/2]).
|
||||
-export([get_metric/1, get_status/1]).
|
||||
%% 通讯相关
|
||||
-export([pub/3, attach_channel/2, command/3]).
|
||||
-export([deploy_service/4, start_service/2, stop_service/2, invoke_service/4, async_service_config/4, task_log/2, await_reply/2]).
|
||||
%% 设备管理
|
||||
-export([reload_device/2, delete_device/2, activate_device/3]).
|
||||
-export([heartbeat/1]).
|
||||
|
||||
%% gen_statem callbacks
|
||||
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||
|
||||
-record(state, {
|
||||
host_id :: integer(),
|
||||
%% 从数据库里面读取到的数据
|
||||
uuid :: binary(),
|
||||
has_session = false :: boolean(),
|
||||
%% 心跳计数器
|
||||
heartbeat_counter = 0 :: integer(),
|
||||
%% websocket相关
|
||||
channel_pid :: undefined | pid(),
|
||||
|
||||
%% 设备的关系, #{device_uuid => Device}
|
||||
device_map = #{},
|
||||
|
||||
%% 主机的相关信息
|
||||
metrics = #{} :: map()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
-spec get_pid(UUID :: binary()) -> undefined | pid().
|
||||
get_pid(UUID) when is_binary(UUID) ->
|
||||
Name = get_name(UUID),
|
||||
whereis(Name).
|
||||
|
||||
-spec get_name(UUID :: binary()) -> atom().
|
||||
get_name(UUID) when is_binary(UUID) ->
|
||||
binary_to_atom(<<"iot_host:", UUID/binary>>).
|
||||
|
||||
-spec get_alias_name(HostId :: integer()) -> atom().
|
||||
get_alias_name(HostId0) when is_integer(HostId0) ->
|
||||
HostId = integer_to_binary(HostId0),
|
||||
binary_to_atom(<<"iot_host_id:", HostId/binary>>).
|
||||
|
||||
%% 处理消息
|
||||
-spec handle(Pid :: pid(), Packet :: {atom(), binary()} | {atom(), {binary(), binary()}}) -> no_return().
|
||||
handle(Pid, Packet) when is_pid(Pid) ->
|
||||
gen_statem:cast(Pid, {handle, Packet}).
|
||||
|
||||
-spec get_status(Pid :: pid()) -> {ok, Status :: map()}.
|
||||
get_status(Pid) when is_pid(Pid) ->
|
||||
gen_statem:call(Pid, get_status).
|
||||
|
||||
%% 激活主机, true 表示激活; false表示关闭激活
|
||||
-spec activate(Pid :: pid(), Auth :: boolean()) -> ok.
|
||||
activate(Pid, Auth) when is_pid(Pid), is_boolean(Auth) ->
|
||||
gen_statem:call(Pid, {activate, Auth}).
|
||||
|
||||
-spec get_metric(Pid :: pid()) -> {ok, MetricInfo :: map()}.
|
||||
get_metric(Pid) when is_pid(Pid) ->
|
||||
gen_statem:call(Pid, get_metric).
|
||||
|
||||
-spec attach_channel(pid(), pid()) -> ok | {error, Reason :: binary()} | {denied, Reason :: binary()}.
|
||||
attach_channel(Pid, ChannelPid) when is_pid(Pid), is_pid(ChannelPid) ->
|
||||
gen_statem:call(Pid, {attach_channel, ChannelPid}).
|
||||
|
||||
-spec async_service_config(Pid :: pid(), ServiceId :: binary(), ConfigJson :: binary(), Timeout :: integer()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
|
||||
async_service_config(Pid, ServiceId, ConfigJson, Timeout) when is_pid(Pid), is_binary(ServiceId), is_binary(ConfigJson), is_integer(Timeout) ->
|
||||
ConfigBin = message_pb:encode_msg(#push_service_config{service_id = ServiceId, config_json = ConfigJson, timeout = Timeout}),
|
||||
gen_statem:call(Pid, {async_call, self(), ?PUSH_SERVICE_CONFIG, ConfigBin}).
|
||||
|
||||
-spec deploy_service(Pid :: pid(), TaskId :: integer(), ServiceId :: binary(), TarUrl :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
|
||||
deploy_service(Pid, TaskId, ServiceId, TarUrl) when is_pid(Pid), is_integer(TaskId), is_binary(ServiceId), is_binary(TarUrl) ->
|
||||
PushBin = message_pb:encode_msg(#deploy{task_id = TaskId, service_id = ServiceId, tar_url = TarUrl}),
|
||||
gen_statem:call(Pid, {async_call, self(), ?PUSH_DEPLOY, PushBin}).
|
||||
|
||||
-spec start_service(Pid :: pid(), ServiceId :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
|
||||
start_service(Pid, ServiceId) when is_pid(Pid), is_binary(ServiceId) ->
|
||||
gen_statem:call(Pid, {async_call, self(), ?PUSH_START_SERVICE, ServiceId}).
|
||||
|
||||
-spec stop_service(Pid :: pid(), ServiceId :: binary()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
|
||||
stop_service(Pid, ServiceId) when is_pid(Pid), is_binary(ServiceId) ->
|
||||
gen_statem:call(Pid, {async_call, self(), ?PUSH_STOP_SERVICE, ServiceId}).
|
||||
|
||||
-spec invoke_service(Pid :: pid(), ServiceId :: binary(), Payload :: binary(), Timeout :: integer()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
|
||||
invoke_service(Pid, ServiceId, Payload, Timeout) when is_pid(Pid), is_binary(ServiceId), is_binary(Payload), is_integer(Timeout) ->
|
||||
InvokeBin = message_pb:encode_msg(#invoke{service_id = ServiceId, payload = Payload, timeout = Timeout}),
|
||||
gen_statem:call(Pid, {async_call, self(), ?PUSH_INVOKE, InvokeBin}).
|
||||
|
||||
-spec task_log(Pid :: pid(), TaskId :: integer()) -> {ok, Ref :: reference()} | {error, Reason :: any()}.
|
||||
task_log(Pid, TaskId) when is_pid(Pid), is_integer(TaskId) ->
|
||||
TaskLogBin = message_pb:encode_msg(#fetch_task_log{task_id = TaskId}),
|
||||
gen_statem:call(Pid, {async_call, self(), ?PUSH_TASK_LOG, TaskLogBin}).
|
||||
|
||||
-spec await_reply(Ref :: reference(), Timeout :: integer()) -> {ok, Result :: binary()} | {error, Reason :: binary()}.
|
||||
await_reply(Ref, Timeout) when is_reference(Ref), is_integer(Timeout) ->
|
||||
receive
|
||||
{async_call_reply, Ref, #async_call_reply{code = 1, result = Result}} ->
|
||||
{ok, Result};
|
||||
{async_call_reply, Ref, #async_call_reply{code = 0, message = Message}} ->
|
||||
{error, Message}
|
||||
after Timeout ->
|
||||
{error, <<"timeout">>}
|
||||
end.
|
||||
|
||||
-spec pub(Pid :: pid(), Topic :: binary(), Content :: binary()) -> ok | {error, Reason :: any()}.
|
||||
pub(Pid, Topic, Content) when is_pid(Pid), is_binary(Topic), is_binary(Content) ->
|
||||
gen_statem:call(Pid, {pub, Topic, Content}).
|
||||
|
||||
-spec command(Pid :: pid(), CommandType :: integer(), Command :: binary()) -> ok | {error, Reason :: any()}.
|
||||
command(Pid, CommandType, Command) when is_pid(Pid), is_integer(CommandType), is_binary(Command) ->
|
||||
gen_statem:call(Pid, {command, CommandType, Command}).
|
||||
|
||||
%% 设备管理相关
|
||||
-spec reload_device(Pid :: pid(), DeviceUUID :: binary()) -> ok | {error, Reason :: any()}.
|
||||
reload_device(Pid, DeviceUUID) when is_pid(Pid), is_binary(DeviceUUID) ->
|
||||
gen_statem:call(Pid, {reload_device, DeviceUUID}).
|
||||
|
||||
-spec delete_device(Pid :: pid(), DeviceUUID :: binary()) -> ok.
|
||||
delete_device(Pid, DeviceUUID) when is_pid(Pid), is_binary(DeviceUUID) ->
|
||||
gen_statem:call(Pid, {delete_device, DeviceUUID}).
|
||||
|
||||
-spec activate_device(Pid :: pid(), DeviceUUID :: binary(), Auth :: boolean()) -> ok | {error, Reason :: any()}.
|
||||
activate_device(Pid, DeviceUUID, Auth) when is_pid(Pid), is_binary(DeviceUUID), is_boolean(Auth) ->
|
||||
gen_statem:call(Pid, {activate_device, DeviceUUID, Auth}).
|
||||
|
||||
-spec heartbeat(Pid :: pid()) -> no_return().
|
||||
heartbeat(undefined) ->
|
||||
ok;
|
||||
heartbeat(Pid) when is_pid(Pid) ->
|
||||
gen_statem:cast(Pid, heartbeat).
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
start_link(Name, UUID) when is_atom(Name), is_binary(UUID) ->
|
||||
gen_statem:start_link({local, Name}, ?MODULE, [UUID], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
init([UUID]) ->
|
||||
case host_bo:get_host_by_uuid(UUID) of
|
||||
{ok, #{<<"id">> := HostId, <<"authorize_status">> := AuthorizeStatus}} ->
|
||||
%% 通过host_id注册别名, 可以避免通过查询数据库获取HostPid
|
||||
AliasName = get_alias_name(HostId),
|
||||
global:register_name(AliasName, self()),
|
||||
|
||||
%% 心跳检测机制
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
|
||||
StateName = case AuthorizeStatus =:= 1 of
|
||||
true -> ?STATE_ACTIVATED;
|
||||
false -> ?STATE_DENIED
|
||||
end,
|
||||
|
||||
%% 加载设备信息
|
||||
{ok, DeviceInfos} = device_bo:get_host_devices(HostId),
|
||||
Devices = lists:filtermap(fun(DeviceInfo = #{<<"device_uuid">> := DeviceUUID}) ->
|
||||
case iot_device:new(DeviceInfo) of
|
||||
error ->
|
||||
false;
|
||||
{ok, Device} ->
|
||||
{true, {DeviceUUID, Device}}
|
||||
end
|
||||
end, DeviceInfos),
|
||||
|
||||
{ok, StateName, #state{host_id = HostId, uuid = UUID, device_map = maps:from_list(Devices), has_session = false}};
|
||||
undefined ->
|
||||
lager:warning("[iot_host] host uuid: ~p, loaded from mysql failed", [UUID]),
|
||||
ignore
|
||||
end.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem when it needs to find out
|
||||
%% the callback mode of the callback module.
|
||||
callback_mode() ->
|
||||
handle_event_function.
|
||||
|
||||
%% @private
|
||||
%% @doc If callback_mode is handle_event_function, then whenever a
|
||||
%% gen_statem receives an event from call/2, cast/2, or as a normal
|
||||
%% process message, this function is called.
|
||||
|
||||
handle_event({call, From}, get_metric, _, State = #state{metrics = Metrics}) ->
|
||||
{keep_state, State, [{reply, From, {ok, Metrics}}]};
|
||||
|
||||
%% 获取主机的状态
|
||||
handle_event({call, From}, get_status, _, State = #state{channel_pid = ChannelPid, heartbeat_counter = HeartbeatCounter, metrics = Metrics, has_session = HasSession}) ->
|
||||
HasChannel = (ChannelPid /= undefined),
|
||||
Reply = #{
|
||||
<<"has_channel">> => HasChannel,
|
||||
<<"has_session">> => HasSession,
|
||||
<<"heartbeat_counter">> => HeartbeatCounter,
|
||||
<<"metrics">> => Metrics
|
||||
},
|
||||
{keep_state, State, [{reply, From, {ok, Reply}}]};
|
||||
|
||||
%% 只要channel存在,就负责将消息推送到边缘端主机
|
||||
handle_event({call, From}, {async_call, ReceiverPid, PushType, PushBin}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
case HasSession andalso is_pid(ChannelPid) of
|
||||
true ->
|
||||
%% 通过websocket发送请求
|
||||
Ref = tcp_channel:async_call(ChannelPid, ReceiverPid, PushType, PushBin),
|
||||
{keep_state, State, [{reply, From, {ok, Ref}}]};
|
||||
false ->
|
||||
lager:debug("[iot_host] uuid: ~p, publish_type: ~p, invalid state: ~p", [UUID, PushType, state_map(State)]),
|
||||
{keep_state, State, [{reply, From, {error, <<"主机离线,发送请求失败"/utf8>>}}]}
|
||||
end;
|
||||
|
||||
%% 发送指令时, pub/sub
|
||||
handle_event({call, From}, {pub, Topic, Content}, ?STATE_ACTIVATED, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
case HasSession andalso is_pid(ChannelPid) of
|
||||
true ->
|
||||
lager:debug("[iot_host] host: ~p, publish to topic: ~p, content: ~p", [UUID, Topic, Content]),
|
||||
%% 通过websocket发送请求
|
||||
tcp_channel:pub(ChannelPid, Topic, Content),
|
||||
|
||||
{keep_state, State, [{reply, From, ok}]};
|
||||
false ->
|
||||
lager:debug("[iot_host] uuid: ~p, publish to topic: ~p, content: ~p, invalid state: ~p", [UUID, Topic, Content, state_map(State)]),
|
||||
{keep_state, State, [{reply, From, {error, <<"主机离线,发送失败"/utf8>>}}]}
|
||||
end;
|
||||
|
||||
%% 发送指令时
|
||||
handle_event({call, From}, {command, CommandType, Command}, ?STATE_ACTIVATED, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
case HasSession andalso is_pid(ChannelPid) of
|
||||
true ->
|
||||
lager:debug("[iot_host] host: ~p, command_type: ~p, command: ~p", [UUID, CommandType, Command]),
|
||||
%% 通过websocket发送请求
|
||||
tcp_channel:command(ChannelPid, CommandType, Command),
|
||||
{keep_state, State, [{reply, From, ok}]};
|
||||
false ->
|
||||
lager:debug("[iot_host] host: ~p, command_type: ~p, command: ~p, invalid state: ~p", [UUID, CommandType, Command, state_map(State)]),
|
||||
{keep_state, State, [{reply, From, {error, <<"主机离线,发送指令失败"/utf8>>}}]}
|
||||
end;
|
||||
|
||||
%% 激活主机
|
||||
handle_event({call, From}, {activate, true}, _, State = #state{uuid = UUID, channel_pid = ChannelPid}) ->
|
||||
case is_pid(ChannelPid) of
|
||||
true ->
|
||||
lager:debug("[iot_host] uuid: ~p, activate: true", [UUID]),
|
||||
tcp_channel:command(ChannelPid, ?COMMAND_AUTH, <<1:8>>);
|
||||
false ->
|
||||
lager:debug("[iot_host] uuid: ~p, activate: true, no channel", [UUID])
|
||||
end,
|
||||
{next_state, ?STATE_ACTIVATED, State, [{reply, From, ok}]};
|
||||
|
||||
%% 关闭授权
|
||||
handle_event({call, From}, {activate, false}, _, State = #state{uuid = UUID, channel_pid = ChannelPid}) ->
|
||||
case is_pid(ChannelPid) of
|
||||
true ->
|
||||
tcp_channel:command(ChannelPid, ?COMMAND_AUTH, <<0:8>>),
|
||||
lager:debug("[iot_host] uuid: ~p, activate: false", [UUID]),
|
||||
tcp_channel:stop(ChannelPid, closed);
|
||||
false ->
|
||||
lager:debug("[iot_host] uuid: ~p, activate: false, no channel", [UUID])
|
||||
end,
|
||||
{next_state, ?STATE_DENIED, State#state{channel_pid = undefined, has_session = false}, [{reply, From, ok}]};
|
||||
|
||||
%% 绑定channel
|
||||
handle_event({call, From}, {attach_channel, ChannelPid}, StateName, State = #state{uuid = UUID, channel_pid = undefined}) ->
|
||||
case StateName of
|
||||
?STATE_ACTIVATED ->
|
||||
erlang:monitor(process, ChannelPid),
|
||||
%% 更新主机为在线状态
|
||||
{ok, AffectedRow} = host_bo:change_status(UUID, ?HOST_ONLINE),
|
||||
report_event(UUID, ?HOST_ONLINE),
|
||||
lager:debug("[iot_host] host_id(attach_channel) uuid: ~p, will change status, affected_row: ~p", [UUID, AffectedRow]),
|
||||
{keep_state, State#state{channel_pid = ChannelPid, has_session = true}, [{reply, From, ok}]};
|
||||
%% 主机未激活
|
||||
?STATE_DENIED ->
|
||||
lager:notice("[iot_host] attach_channel host_id uuid: ~p, channel: ~p, host inactivated", [UUID, ChannelPid]),
|
||||
erlang:monitor(process, ChannelPid),
|
||||
{keep_state, State#state{channel_pid = ChannelPid}, [{reply, From, {denied, <<"host inactivated">>}}]}
|
||||
end;
|
||||
%% 已经绑定了channel
|
||||
handle_event({call, From}, {attach_channel, _}, _, State = #state{uuid = UUID, channel_pid = OldChannelPid}) ->
|
||||
lager:notice("[iot_host] attach_channel host_id uuid: ~p, old channel exists: ~p", [UUID, OldChannelPid]),
|
||||
{keep_state, State, [{reply, From, {error, <<"channel existed">>}}]};
|
||||
|
||||
%% 重新加载设备信息
|
||||
handle_event({call, From}, {reload_device, DeviceUUID}, _, State = #state{device_map = DeviceMap}) ->
|
||||
case maps:find(DeviceUUID, DeviceMap) of
|
||||
error ->
|
||||
{keep_state, State, [{reply, From, {error, <<"device not found">>}}]};
|
||||
{ok, Device} ->
|
||||
case iot_device:reload(Device) of
|
||||
error ->
|
||||
{keep_state, State#state{device_map = maps:remove(Device, DeviceMap)}, [{reply, From, {error, <<"reload device error">>}}]};
|
||||
{ok, NDevice} ->
|
||||
{keep_state, State#state{device_map = maps:put(DeviceUUID, NDevice, DeviceMap)}, [{reply, From, ok}]}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 删除设备
|
||||
handle_event({call, From}, {delete_device, DeviceUUID}, _, State = #state{device_map = DeviceMap}) ->
|
||||
{keep_state, State#state{device_map = maps:remove(DeviceUUID, DeviceMap)}, [{reply, From, ok}]};
|
||||
|
||||
%% 激活设备
|
||||
handle_event({call, From}, {activate_device, DeviceUUID, Auth}, _, State = #state{device_map = DeviceMap}) ->
|
||||
case maps:find(DeviceUUID, DeviceMap) of
|
||||
error ->
|
||||
{keep_state, State, [{reply, From, {error, <<"device not found">>}}]};
|
||||
{ok, Device} ->
|
||||
NDevice = iot_device:auth(Device, Auth),
|
||||
{keep_state, State#state{device_map = maps:put(DeviceUUID, NDevice, DeviceMap)}, [{reply, From, ok}]}
|
||||
end;
|
||||
|
||||
%% todo
|
||||
handle_event(cast, {handle, {data, #data{service_id = ServiceId, device_uuid = DeviceUUID, route_key = RouteKey0, metric = Metric}}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true, device_map = DeviceMap}) ->
|
||||
lager:debug("[iot_host] metric_data host: ~p, service_id: ~p, device_uuid: ~p, route_key: ~p, metric: ~p", [UUID, ServiceId, DeviceUUID, RouteKey0, Metric]),
|
||||
case DeviceUUID =/= <<"">> of
|
||||
true ->
|
||||
case maps:find(DeviceUUID, DeviceMap) of
|
||||
error ->
|
||||
lager:warning("[iot_host] host uuid: ~p, device uuid: ~p not found, metric: ~p", [UUID, DeviceUUID, Metric]),
|
||||
{keep_state, State};
|
||||
{ok, Device} ->
|
||||
case iot_device:is_activated(Device) of
|
||||
true ->
|
||||
RouteKey = get_route_key(RouteKey0),
|
||||
case endpoint:get_alias_pid(RouteKey) of
|
||||
undefined ->
|
||||
ok;
|
||||
EndpointPid ->
|
||||
endpoint:forward(EndpointPid, ServiceId, Metric)
|
||||
end,
|
||||
NDevice = iot_device:change_status(Device, ?DEVICE_ONLINE),
|
||||
{keep_state, State#state{device_map = maps:put(DeviceUUID, NDevice, DeviceMap)}};
|
||||
false ->
|
||||
lager:warning("[iot_host] host uuid: ~p, device_uuid: ~p not activated, metric: ~p", [UUID, DeviceUUID, Metric]),
|
||||
{keep_state, State}
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
{keep_state, State}
|
||||
end;
|
||||
|
||||
%% ping的数据是通过aes加密后的,因此需要在有会话的情况下才行
|
||||
handle_event(cast, {handle, {ping, Metrics}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
|
||||
lager:debug("[iot_host] ping host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
|
||||
{keep_state, State#state{metrics = Metrics}};
|
||||
|
||||
handle_event(cast, {handle, {inform, #service_inform{service_id = ServiceId, status = Status, timestamp = Timestamp}}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
|
||||
lager:debug("[iot_host] inform host: ~p, service_id: ~p, status: ~p, timestamp: ~p", [UUID, ServiceId, Status, Timestamp]),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(cast, {handle, {event, #event{service_id = ServiceId, event_type = EventType, params = Params}}}, ?STATE_ACTIVATED, State = #state{uuid = UUID, has_session = true}) ->
|
||||
lager:debug("[iot_host] event uuid: ~p, service_id: ~p, event_type: ~p, params: ~p", [UUID, ServiceId, EventType, Params]),
|
||||
%DevicePid = iot_device:get_pid(DeviceUUID),
|
||||
%iot_device:change_status(DevicePid, Status),
|
||||
|
||||
{keep_state, State};
|
||||
|
||||
%% 心跳机制
|
||||
handle_event(cast, heartbeat, _, State = #state{heartbeat_counter = HeartbeatCounter}) ->
|
||||
{keep_state, State#state{heartbeat_counter = HeartbeatCounter + 1}};
|
||||
|
||||
%% 没有收到心跳包,主机下线, 设备状态不变
|
||||
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{uuid = UUID, heartbeat_counter = 0, channel_pid = ChannelPid}) ->
|
||||
lager:warning("[iot_host] uuid: ~p, heartbeat lost, devices will unknown", [UUID]),
|
||||
{ok, #{<<"status">> := Status}} = host_bo:get_host_by_uuid(UUID),
|
||||
case Status of
|
||||
?HOST_NOT_JOINED ->
|
||||
lager:debug("[iot_host] host: ~p, host_maybe_offline, host not joined, can not change to offline", [UUID]);
|
||||
?HOST_OFFLINE ->
|
||||
lager:debug("[iot_host] host: ~p, host_maybe_offline, host now is offline, do nothing", [UUID]);
|
||||
?HOST_ONLINE ->
|
||||
{ok, _} = host_bo:change_status(UUID, ?HOST_OFFLINE),
|
||||
report_event(UUID, ?HOST_OFFLINE)
|
||||
end,
|
||||
|
||||
%% 关闭channel,主机需要重新连接,才能保存状态的一致
|
||||
is_pid(ChannelPid) andalso tcp_channel:stop(ChannelPid, closed),
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
|
||||
{keep_state, State#state{channel_pid = undefined, has_session = false, heartbeat_counter = 0}};
|
||||
|
||||
%% 其他情况下需要重置系统计数器
|
||||
handle_event(info, {timeout, _, heartbeat_ticker}, _, State = #state{}) ->
|
||||
erlang:start_timer(?HEARTBEAT_INTERVAL, self(), heartbeat_ticker),
|
||||
{keep_state, State#state{heartbeat_counter = 0}};
|
||||
|
||||
%% 当websocket断开的时候,主机的状态不一定改变;主机的状态改变通过心跳机制,会话状态需要改变
|
||||
handle_event(info, {'DOWN', _Ref, process, ChannelPid, Reason}, _, State = #state{uuid = UUID, channel_pid = ChannelPid, has_session = HasSession}) ->
|
||||
lager:warning("[iot_host] uuid: ~p, channel: ~p, down with reason: ~p, has_session: ~p, state: ~p", [UUID, ChannelPid, Reason, HasSession, State]),
|
||||
{keep_state, State#state{channel_pid = undefined, has_session = false}};
|
||||
|
||||
handle_event(info, {'DOWN', _Ref, process, Pid, Reason}, _, State = #state{uuid = UUID}) ->
|
||||
lager:debug("[iot_host] uuid: ~p, process_pid: ~p, down with reason: ~p, state: ~p", [UUID, Pid, Reason, State]),
|
||||
{keep_state, State};
|
||||
|
||||
handle_event(Event, Info, StateName, State = #state{uuid = UUID}) ->
|
||||
lager:warning("[iot_host] host: ~p, event: ~p, unknown message: ~p, state_name: ~p, state: ~p", [UUID, Event, Info, StateName, state_map(State)]),
|
||||
{keep_state, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_statem when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_statem terminates with
|
||||
%% Reason. The return value is ignored.
|
||||
terminate(Reason, _StateName, _State = #state{uuid = UUID, has_session = HasSession}) ->
|
||||
lager:debug("[iot_host] host: ~p, terminate with reason: ~p, has_session: ~p", [UUID, Reason, HasSession]),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||
{ok, StateName, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
get_route_key(<<"">>) ->
|
||||
<<"default">>;
|
||||
get_route_key(RouteKey) when is_binary(RouteKey) ->
|
||||
RouteKey.
|
||||
|
||||
-spec report_event(UUID :: binary(), NewStatus :: integer()) -> no_return().
|
||||
report_event(UUID, NewStatus) when is_binary(UUID), is_integer(NewStatus) ->
|
||||
TextMap = #{
|
||||
0 => <<"离线"/utf8>>,
|
||||
1 => <<"在线"/utf8>>
|
||||
},
|
||||
|
||||
%% 设备的状态信息上报给中电
|
||||
Timestamp = iot_util:timestamp_of_seconds(),
|
||||
FieldsList = [#{
|
||||
<<"key">> => <<"host_status">>,
|
||||
<<"value">> => NewStatus,
|
||||
<<"value_text">> => maps:get(NewStatus, TextMap),
|
||||
<<"unit">> => 0,
|
||||
<<"type">> => <<"DI">>,
|
||||
<<"name">> => <<"主机状态"/utf8>>,
|
||||
<<"timestamp">> => Timestamp
|
||||
}],
|
||||
%% todo 这里需要实现新的机制
|
||||
% iot_router:route_uuid(UUID, FieldsList, Timestamp),
|
||||
lager:debug("[iot_host] host_uuid: ~p, route fields: ~p", [UUID, FieldsList]).
|
||||
|
||||
%% 将当前的state转换成map
|
||||
state_map(#state{host_id = HostId, uuid = UUID, has_session = HasSession, heartbeat_counter = HeartbeatCounter, channel_pid = ChannelPid, metrics = Metrics}) ->
|
||||
#{
|
||||
host_id => HostId,
|
||||
uuid => UUID,
|
||||
has_session => HasSession,
|
||||
heartbeat_counter => HeartbeatCounter,
|
||||
channel_pid => ChannelPid,
|
||||
metrics => Metrics
|
||||
}.
|
||||
@ -1,50 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_host_sup).
|
||||
-include("iot.hrl").
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0, init/1, delete_host/1, ensured_host_started/1]).
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
Specs = lists:map(fun child_spec/1, host_bo:get_all_hosts()),
|
||||
|
||||
{ok, {#{strategy => one_for_one, intensity => 1000, period => 3600}, Specs}}.
|
||||
|
||||
-spec ensured_host_started(UUID :: binary()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||
ensured_host_started(UUID) when is_binary(UUID) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
case supervisor:start_child(?MODULE, child_spec(UUID)) of
|
||||
{ok, Pid} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||
{ok, Pid};
|
||||
{error, Error} ->
|
||||
{error, Error}
|
||||
end;
|
||||
Pid when is_pid(Pid) ->
|
||||
{ok, Pid}
|
||||
end.
|
||||
|
||||
delete_host(UUID) when is_binary(UUID) ->
|
||||
Id = iot_host:get_name(UUID),
|
||||
ok = supervisor:terminate_child(?MODULE, Id),
|
||||
supervisor:delete_child(?MODULE, Id).
|
||||
|
||||
child_spec(UUID) ->
|
||||
Id = iot_host:get_name(UUID),
|
||||
#{id => Id,
|
||||
start => {iot_host, start_link, [Id, UUID]},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['iot_host']}.
|
||||
@ -1,40 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 03. 3月 2023 11:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_http_client).
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([post/2]).
|
||||
|
||||
post(Url, Body) when is_list(Url), is_binary(Body) ->
|
||||
case hackney:request(post, Url, [], Body) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
case hackney:body(ClientRef) of
|
||||
{ok, RespBody} ->
|
||||
lager:debug("[iot_http_client] url: ~p, response is: ~p", [Url, RespBody]),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
lager:warning("[iot_http_client] url: ~p, get error: ~p", [Url, Reason]),
|
||||
{error, Reason}
|
||||
end;
|
||||
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
case hackney:body(ClientRef) of
|
||||
{ok, RespBody} ->
|
||||
lager:debug("[iot_http_client] url: ~p, http_code: ~p, response is: ~p", [Url, HttpCode, RespBody]),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
lager:warning("[iot_http_client] url: ~p, http_code: ~p, get error: ~p", [Url, HttpCode, Reason]),
|
||||
{error, Reason}
|
||||
end;
|
||||
|
||||
{error, Reason} ->
|
||||
lager:warning("[iot_http_client] url: ~p, get error: ~p", [Url, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
10
apps/iot/src/iot_log.erl
Normal file
10
apps/iot/src/iot_log.erl
Normal file
@ -0,0 +1,10 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Logger helper for project-scoped metadata.
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_log).
|
||||
|
||||
-export([set_metadata/0]).
|
||||
|
||||
-spec set_metadata() -> ok.
|
||||
set_metadata() ->
|
||||
logger:set_process_metadata(#{domain => [iot]}).
|
||||
@ -1,26 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 04. 7月 2023 11:30
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_router).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([route_uuid/3]).
|
||||
|
||||
-spec route_uuid(RouterUUID :: binary(), Fields :: list(), Timestamp :: integer()) -> no_return().
|
||||
route_uuid(RouterUUID, Fields, Timestamp) when is_binary(RouterUUID), is_list(Fields), is_integer(Timestamp) ->
|
||||
%% 查找终端设备对应的点位信息
|
||||
case redis_client:hget(RouterUUID, <<"location_code">>) of
|
||||
{ok, undefined} ->
|
||||
lager:warning("[iot_host] the north_data hget location_code, uuid: ~p, not found, fields: ~p", [RouterUUID, Fields]);
|
||||
{ok, LocationCode} when is_binary(LocationCode) ->
|
||||
iot_zd_endpoint:forward(LocationCode, Fields, Timestamp);
|
||||
{error, Reason} ->
|
||||
lager:warning("[iot_host] the north_data hget location_code uuid: ~p, get error: ~p, fields: ~p", [RouterUUID, Reason, Fields])
|
||||
end.
|
||||
@ -29,21 +29,30 @@ init([]) ->
|
||||
|
||||
Specs = [
|
||||
#{
|
||||
id => 'iot_name_server',
|
||||
start => {'iot_name_server', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['iot_name_server']
|
||||
},
|
||||
|
||||
#{
|
||||
id => 'endpoint_sup',
|
||||
start => {'endpoint_sup', start_link, []},
|
||||
id => 'iot_container_task_sup',
|
||||
start => {'iot_container_task_sup', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => supervisor,
|
||||
modules => ['endpoint_sup']
|
||||
modules => ['iot_container_task_sup']
|
||||
},
|
||||
|
||||
#{
|
||||
id => 'udp_server',
|
||||
start => {'udp_server', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['udp_server']
|
||||
},
|
||||
|
||||
#{
|
||||
id => 'ctrl_server',
|
||||
start => {'ctrl_server', start_link, []},
|
||||
restart => permanent,
|
||||
shutdown => 2000,
|
||||
type => worker,
|
||||
modules => ['ctrl_server']
|
||||
},
|
||||
|
||||
#{
|
||||
@ -61,7 +70,11 @@ init([]) ->
|
||||
%% internal functions
|
||||
|
||||
pools() ->
|
||||
{ok, Pools} = application:get_env(iot, pools),
|
||||
lists:map(fun({Name, PoolArgs, WorkerArgs}) ->
|
||||
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs)
|
||||
end, Pools).
|
||||
case application:get_env(iot, pools) of
|
||||
undefined ->
|
||||
[];
|
||||
{ok, Pools} ->
|
||||
lists:map(fun({Name, PoolArgs, WorkerArgs}) ->
|
||||
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs)
|
||||
end, Pools)
|
||||
end.
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 31. 8月 2023 13:48
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_udp_handler).
|
||||
-author("aresei").
|
||||
|
||||
%% API
|
||||
-export([start_link/2, loop/2]).
|
||||
|
||||
start_link(Transport, Peer) ->
|
||||
{ok, spawn_link(?MODULE, loop, [Transport, Peer])}.
|
||||
|
||||
loop(Transport = {udp, Server, _Sock}, Peer) ->
|
||||
receive
|
||||
{datagram, Server, <<Len:16, HostUUID:Len/binary>>} ->
|
||||
Pid = iot_host:get_pid(HostUUID),
|
||||
iot_host:heartbeat(Pid),
|
||||
loop(Transport, Peer);
|
||||
{datagram, Server, _} ->
|
||||
exit(normal)
|
||||
end.
|
||||
@ -10,14 +10,15 @@
|
||||
-author("licheng5").
|
||||
|
||||
%% API
|
||||
-export([timestamp/0, number_format/2, current_time/0, timestamp_of_seconds/0, float_to_binary/2, int_format/2, file_uri/1]).
|
||||
-export([step/3, chunks/2, rand_bytes/1, uuid/0, md5/1, parse_mapper/1]).
|
||||
-export([timestamp/0, number_format/2, current_time/0, timestamp_of_seconds/0, float_to_binary/2, int_format/2]).
|
||||
-export([step/3, rand_bytes/1, uuid/0, md5/1, parse_mapper/1]).
|
||||
-export([json_data/1, json_error/2, is_json/1]).
|
||||
-export([queue_limited_in/3, assert_call/2, assert/2]).
|
||||
-export([sha256/1]).
|
||||
|
||||
-spec is_json(Json :: term()) -> boolean().
|
||||
is_json(Json) when is_binary(Json) ->
|
||||
case catch jiffy:decode(Json, [return_maps]) of
|
||||
case catch json:decode(Json) of
|
||||
Result when is_list(Result) orelse is_map(Result) ->
|
||||
true;
|
||||
_ ->
|
||||
@ -60,33 +61,18 @@ step(Start, End, Step, Acc) when Start < End ->
|
||||
step(_, _, _, Acc) ->
|
||||
lists:reverse(Acc).
|
||||
|
||||
%% 将数据分组
|
||||
-spec chunks(list(), integer()) -> [list()].
|
||||
chunks(List, Size) when is_list(List), is_integer(Size), Size > 0, length(List) =< Size ->
|
||||
[List];
|
||||
chunks(List, Size) when is_list(List), is_integer(Size), Size > 0 ->
|
||||
chunks0(List, Size, Size, [], []).
|
||||
chunks0([], _, _, [], AccTarget) ->
|
||||
lists:reverse(AccTarget);
|
||||
chunks0([], _, _, Target, AccTarget) ->
|
||||
lists:reverse([lists:reverse(Target) | AccTarget]);
|
||||
chunks0(List, Size, 0, Target, AccTarget) ->
|
||||
chunks0(List, Size, Size, [], [lists:reverse(Target) | AccTarget]);
|
||||
chunks0([Hd | Tail], Size, Num, Target, AccTarget) ->
|
||||
chunks0(Tail, Size, Num - 1, [Hd | Target], AccTarget).
|
||||
|
||||
json_data(Data) ->
|
||||
jiffy:encode(#{
|
||||
iolist_to_binary(json:encode(#{
|
||||
<<"result">> => Data
|
||||
}, [force_utf8]).
|
||||
})).
|
||||
|
||||
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
|
||||
jiffy:encode(#{
|
||||
json_error(ErrCode, ErrMessage) when is_integer(ErrCode) ->
|
||||
iolist_to_binary(json:encode(#{
|
||||
<<"error">> => #{
|
||||
<<"code">> => ErrCode,
|
||||
<<"message">> => ErrMessage
|
||||
}
|
||||
}, [force_utf8]).
|
||||
})).
|
||||
|
||||
uuid() ->
|
||||
rand_bytes(16).
|
||||
@ -112,6 +98,14 @@ assert_call(true, Fun) ->
|
||||
assert_call(false, _) ->
|
||||
ok.
|
||||
|
||||
-spec sha256(Str :: string() | binary()) -> binary().
|
||||
sha256(Str) when is_list(Str) ->
|
||||
sha256(unicode:characters_to_binary(Str));
|
||||
sha256(Bin) when is_binary(Bin) ->
|
||||
HashBin = crypto:hash(sha256, Bin),
|
||||
HexStr = lists:flatten([io_lib:format("~2.16.0B", [B]) || B <- binary:bin_to_list(HashBin)]),
|
||||
list_to_binary(string:lowercase(HexStr)).
|
||||
|
||||
-spec md5(Str :: binary()) -> binary().
|
||||
md5(Str) when is_binary(Str) ->
|
||||
list_to_binary(lists:flatten([hex(X) || <<X:4>> <= erlang:md5(Str)])).
|
||||
@ -123,18 +117,9 @@ hex(N) ->
|
||||
|
||||
%% 转换映射器
|
||||
-spec parse_mapper(Mapper :: binary() | string()) -> error | {ok, F :: fun((binary(), any()) -> any())}.
|
||||
parse_mapper(Mapper) when is_binary(Mapper) ->
|
||||
parse_mapper(binary_to_list(Mapper));
|
||||
parse_mapper(Mapper) when is_list(Mapper) ->
|
||||
{ok, Tokens, _} = erl_scan:string(Mapper),
|
||||
{ok, ExprList} = erl_parse:parse_exprs(Tokens),
|
||||
{value, F, _} = erl_eval:exprs(ExprList, []),
|
||||
case is_function(F, 2) orelse is_function(F, 3) of
|
||||
true ->
|
||||
{ok, F};
|
||||
false ->
|
||||
error
|
||||
end.
|
||||
parse_mapper(_Mapper) ->
|
||||
%% Dynamic Erlang eval is intentionally disabled in runtime code.
|
||||
error.
|
||||
|
||||
-spec float_to_binary(Num :: number(), integer()) -> binary().
|
||||
float_to_binary(V, _) when is_integer(V) ->
|
||||
@ -149,13 +134,3 @@ assert(false, F) when is_function(F) ->
|
||||
F();
|
||||
assert(false, Msg) ->
|
||||
throw(Msg).
|
||||
|
||||
|
||||
-spec file_uri(Filename :: binary()) -> error | {ok, FileUri :: binary()}.
|
||||
file_uri(Filename) when is_binary(Filename) ->
|
||||
case binary:split(Filename, <<"-">>, [global]) of
|
||||
[Year, Month, Day | _] ->
|
||||
{ok, <<"https://lgsiot.njau.edu.cn/upload/", Year/binary, $/, Month/binary, $/, Day/binary, $/, Filename/binary>>};
|
||||
_ ->
|
||||
error
|
||||
end.
|
||||
154
apps/iot/src/mnesia/efka_client_store.erl
Normal file
154
apps/iot/src/mnesia/efka_client_store.erl
Normal file
@ -0,0 +1,154 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc efka_client table store based on mnesia.
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(efka_client_store).
|
||||
|
||||
-include("iot_tables.hrl").
|
||||
|
||||
-export([ensure_table/0, register/2, get/1, update/1, delete/1, list/0]).
|
||||
-export([auth/2, verify_heartbeat/4]).
|
||||
|
||||
-define(TOKEN_HASH_ITERATIONS, 32).
|
||||
-define(TOKEN_HASH_BYTES, 32).
|
||||
-define(HEARTBEAT_TIMESTAMP_WINDOW, 120).
|
||||
|
||||
-spec ensure_table() -> ok | {error, term()}.
|
||||
ensure_table() ->
|
||||
case mnesia:create_table(efka_client, [
|
||||
{attributes, record_info(fields, efka_client)},
|
||||
{type, set},
|
||||
{record_name, efka_client},
|
||||
{disc_copies, [node()]}
|
||||
]) of
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{aborted, {already_exists, efka_client}} ->
|
||||
ok;
|
||||
{aborted, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec auth(UUID :: binary(), Token :: binary()) -> boolean().
|
||||
auth(UUID, Token) when is_binary(UUID), UUID =/= <<>>, is_binary(Token) ->
|
||||
case mnesia:dirty_read(efka_client, UUID) of
|
||||
[] ->
|
||||
false;
|
||||
[#efka_client{token_hash = TokenHash, salt = Salt}] ->
|
||||
hash_token(Token, Salt) =:= TokenHash
|
||||
end.
|
||||
|
||||
-spec verify_heartbeat(UUID :: binary(), Timestamp :: integer(), Payload :: binary(), Mac :: binary()) -> boolean().
|
||||
verify_heartbeat(UUID, Timestamp, Payload, Mac)
|
||||
when is_binary(UUID), UUID =/= <<>>, is_integer(Timestamp), is_binary(Payload), is_binary(Mac) ->
|
||||
case valid_heartbeat_timestamp(Timestamp) of
|
||||
true ->
|
||||
case mnesia:dirty_read(efka_client, UUID) of
|
||||
[] ->
|
||||
false;
|
||||
[#efka_client{heartbeat_secret = HeartbeatSecret}] ->
|
||||
ExpectedMac = crypto:mac(hmac, sha256, HeartbeatSecret, Payload),
|
||||
ExpectedMac =:= Mac
|
||||
end;
|
||||
false ->
|
||||
false
|
||||
end.
|
||||
|
||||
-spec register(UUID :: binary(), Token :: binary()) -> ok | {error, term()}.
|
||||
register(UUID, Token) when is_binary(UUID), UUID =/= <<>>, is_binary(Token), byte_size(Token) >= 32 ->
|
||||
case mnesia:transaction(fun() ->
|
||||
case mnesia:read(efka_client, UUID, write) of
|
||||
[] ->
|
||||
%% 1. 生成 16 字节随机盐
|
||||
Salt = crypto:strong_rand_bytes(16),
|
||||
TokenHash = hash_token(Token, Salt),
|
||||
HeartbeatSecret = heartbeat_secret(Token),
|
||||
ok = mnesia:write(#efka_client{
|
||||
uuid = UUID,
|
||||
token_hash = TokenHash,
|
||||
salt = Salt,
|
||||
heartbeat_secret = HeartbeatSecret,
|
||||
timestamp = iot_util:timestamp()
|
||||
}),
|
||||
ok;
|
||||
[_] ->
|
||||
{error, already_exists}
|
||||
end
|
||||
end) of
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{atomic, {error, Reason}} ->
|
||||
{error, Reason};
|
||||
{aborted, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec get(binary()) -> {ok, #efka_client{}} | not_found | {error, term()}.
|
||||
get(UUID) when is_binary(UUID), UUID =/= <<>> ->
|
||||
case mnesia:transaction(fun() ->
|
||||
case mnesia:read(efka_client, UUID, read) of
|
||||
[Client = #efka_client{}] ->
|
||||
{ok, Client};
|
||||
[] ->
|
||||
not_found
|
||||
end
|
||||
end) of
|
||||
{atomic, Result} ->
|
||||
Result;
|
||||
{aborted, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec update(#efka_client{}) -> ok | {error, term()}.
|
||||
update(Client = #efka_client{uuid = UUID}) when is_binary(UUID), UUID =/= <<>> ->
|
||||
case mnesia:transaction(fun() ->
|
||||
case mnesia:read(efka_client, UUID, write) of
|
||||
[_] ->
|
||||
ok = mnesia:write(Client),
|
||||
ok;
|
||||
[] ->
|
||||
{error, not_found}
|
||||
end
|
||||
end) of
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{atomic, {error, Reason}} ->
|
||||
{error, Reason};
|
||||
{aborted, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec delete(binary()) -> ok | {error, term()}.
|
||||
delete(UUID) when is_binary(UUID), UUID =/= <<>> ->
|
||||
case mnesia:transaction(fun() ->
|
||||
ok = mnesia:delete({efka_client, UUID}),
|
||||
ok
|
||||
end) of
|
||||
{atomic, ok} ->
|
||||
ok;
|
||||
{aborted, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec list() -> {ok, [#efka_client{}]} | {error, term()}.
|
||||
list() ->
|
||||
case mnesia:transaction(fun() ->
|
||||
mnesia:foldl(fun(Client = #efka_client{}, Acc) -> [Client | Acc] end, [], efka_client)
|
||||
end) of
|
||||
{atomic, Clients} ->
|
||||
{ok, lists:reverse(Clients)};
|
||||
{aborted, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec hash_token(binary(), binary()) -> binary().
|
||||
hash_token(Token, Salt) when is_binary(Token), is_binary(Salt) ->
|
||||
crypto:pbkdf2_hmac(sha256, Token, Salt, ?TOKEN_HASH_ITERATIONS, ?TOKEN_HASH_BYTES).
|
||||
|
||||
-spec heartbeat_secret(binary()) -> binary().
|
||||
heartbeat_secret(Token) when is_binary(Token) ->
|
||||
crypto:hash(sha256, Token).
|
||||
|
||||
-spec valid_heartbeat_timestamp(integer()) -> boolean().
|
||||
valid_heartbeat_timestamp(Timestamp) ->
|
||||
Now = iot_util:current_time(),
|
||||
Timestamp =< Now andalso Now - Timestamp =< ?HEARTBEAT_TIMESTAMP_WINDOW.
|
||||
74
apps/iot/src/mnesia/iot_mnesia.erl
Normal file
74
apps/iot/src/mnesia/iot_mnesia.erl
Normal file
@ -0,0 +1,74 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Mnesia bootstrap and runtime startup helpers.
|
||||
%%%
|
||||
%%% `init/0` is a manual, one-time bootstrap operation. Application
|
||||
%%% startup must call `start/0`, which only starts an existing schema and
|
||||
%%% verifies required tables.
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_mnesia).
|
||||
|
||||
-include("iot_tables.hrl").
|
||||
|
||||
-export([safe_start/0, stop/0]).
|
||||
|
||||
-define(TABLES, [efka_client]).
|
||||
-define(WAIT_TIMEOUT, 5000).
|
||||
|
||||
safe_start() ->
|
||||
case ensure_schema() of
|
||||
create_and_start ->
|
||||
ok = mnesia:start(),
|
||||
ok = create_tables(),
|
||||
ok = wait_for_tables();
|
||||
only_start ->
|
||||
ok = mnesia:start(),
|
||||
ok = wait_for_tables()
|
||||
end,
|
||||
log_runtime_info().
|
||||
|
||||
ensure_schema() ->
|
||||
stopped = mnesia:stop(),
|
||||
case mnesia:create_schema([node()]) of
|
||||
ok ->
|
||||
create_and_start;
|
||||
{error, {_, {already_exists, _}}} ->
|
||||
only_start;
|
||||
{error, {_, already_exists}} ->
|
||||
only_start;
|
||||
{error, _Reason} ->
|
||||
only_start
|
||||
end.
|
||||
|
||||
-spec stop() -> ok.
|
||||
stop() ->
|
||||
_ = mnesia:stop(),
|
||||
ok.
|
||||
|
||||
-spec create_tables() -> ok.
|
||||
create_tables() ->
|
||||
case efka_client_store:ensure_table() of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, {create_table_failed, {efka_client, Reason}}}
|
||||
end.
|
||||
|
||||
-spec wait_for_tables() -> ok.
|
||||
wait_for_tables() ->
|
||||
case mnesia:wait_for_tables(?TABLES, ?WAIT_TIMEOUT) of
|
||||
ok ->
|
||||
ok;
|
||||
{timeout, Tables} ->
|
||||
{error, {mnesia_tables_timeout, Tables}};
|
||||
{error, Reason} ->
|
||||
{error, {mnesia_tables_error, Reason}}
|
||||
end.
|
||||
|
||||
-spec log_runtime_info() -> ok.
|
||||
log_runtime_info() ->
|
||||
logger:debug("[iot_mnesia] node: ~p, dir: ~p, tables: ~p", [
|
||||
node(),
|
||||
mnesia:system_info(directory),
|
||||
mnesia:system_info(tables)
|
||||
]),
|
||||
ok.
|
||||
@ -1,281 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%% 1. 需要考虑集群部署的相关问题,上行的数据可能在集群中共享
|
||||
%%% 2. host进程不能直接去监听topic,这样涉及到新增和下线的很多问题
|
||||
%%% @end
|
||||
%%% Created : 12. 3月 2023 21:27
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_mqtt_consumer).
|
||||
-author("aresei").
|
||||
-include("iot.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([mock/5]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
|
||||
%% 执行超时时间
|
||||
-define(EXECUTE_TIMEOUT, 10 * 1000).
|
||||
|
||||
%% 需要订阅的主题信息
|
||||
-define(Topics,[
|
||||
{<<"CET/NX/download">>, 2}
|
||||
]).
|
||||
|
||||
-record(state, {
|
||||
conn_pid :: undefined | pid(),
|
||||
logger_pid :: pid(),
|
||||
mqtt_props :: list(),
|
||||
%% 执行中的任务数
|
||||
flight_num = 0
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
mock(LocationCode, Para, SType, CType, Value) when is_binary(LocationCode), is_integer(SType), is_integer(CType), is_integer(Para) ->
|
||||
Req = #{
|
||||
<<"version">> => <<"1.0">>,
|
||||
<<"ts">> => iot_util:current_time(),
|
||||
<<"properties">> => #{
|
||||
<<"type">> => <<"ctrl">>,
|
||||
<<"para">> => Para,
|
||||
<<"stype">> => SType,
|
||||
<<"ctype">> => CType,
|
||||
<<"value">> => Value,
|
||||
<<"timestamp">> => iot_util:current_time()
|
||||
},
|
||||
<<"location_code">> => LocationCode
|
||||
},
|
||||
gen_server:call(?MODULE, {mock, Req}).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
erlang:process_flag(trap_exit, true),
|
||||
|
||||
{ok, Props} = application:get_env(iot, zhongdian),
|
||||
%% 创建转发器, 避免阻塞当前进程的创建,因此采用了延时初始化的机制
|
||||
erlang:start_timer(0, self(), create_consumer),
|
||||
%% 启动日志记录器
|
||||
{ok, LoggerPid} = iot_logger:start_link("zd_directive_data"),
|
||||
|
||||
{ok, #state{mqtt_props = Props, conn_pid = undefined, logger_pid = LoggerPid}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call({mock, Request}, _From, State = #state{conn_pid = ConnPid, flight_num = FlightNum}) when is_pid(ConnPid) ->
|
||||
publish_directive(Request, jiffy:encode(Request, [force_utf8])),
|
||||
{reply, ok, State#state{flight_num = FlightNum + 1}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({disconnect, ReasonCode, Properties}, State) ->
|
||||
lager:debug("[iot_zd_consumer] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||
{stop, disconnected, State};
|
||||
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
||||
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := 2, topic := Topic}}, State = #state{flight_num = FlightNum}) ->
|
||||
lager:debug("[iot_zd_consumer] Recv a topic: ~p, publish packet: ~ts, qos: 2", [Topic, Payload]),
|
||||
|
||||
Request = catch jiffy:decode(Payload, [return_maps]),
|
||||
publish_directive(Request, Payload),
|
||||
|
||||
{noreply, State#state{flight_num = FlightNum + 1}};
|
||||
|
||||
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State) ->
|
||||
lager:notice("[iot_zd_consumer] Recv a topic: ~p, publish packet: ~ts, qos: ~p, qos is error", [Topic, Payload, Qos]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
||||
lager:debug("[iot_zd_consumer] receive puback packet: ~p", [Packet]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({timeout, _, create_consumer}, State = #state{mqtt_props = Props, conn_pid = undefined}) ->
|
||||
try
|
||||
{ok, ConnPid} = create_consumer(Props),
|
||||
{noreply, State#state{conn_pid = ConnPid}}
|
||||
catch _:Error:Stack ->
|
||||
lager:warning("[iot_zd_consumer] config: ~p, create consumer get error: ~p, stack: ~p", [Props, Error, Stack]),
|
||||
erlang:start_timer(?RETRY_INTERVAL, self(), create_consumer),
|
||||
{noreply, State#state{conn_pid = undefined}}
|
||||
end;
|
||||
|
||||
%% postman进程挂掉时,重新建立新的
|
||||
handle_info({'EXIT', ConnPid, Reason}, State = #state{conn_pid = ConnPid}) ->
|
||||
lager:warning("[iot_zd_consumer] consumer exited with reason: ~p", [Reason]),
|
||||
erlang:start_timer(?RETRY_INTERVAL, self(), create_consumer),
|
||||
|
||||
{noreply, State#state{conn_pid = undefined}};
|
||||
|
||||
handle_info({'EXIT', LoggerPid, Reason}, State = #state{logger_pid = LoggerPid}) ->
|
||||
lager:warning("[iot_zd_consumer] logger exited with reason: ~p", [Reason]),
|
||||
{ok, LoggerPid} = iot_logger:start_link("zd_directive_data"),
|
||||
|
||||
{noreply, State#state{logger_pid = LoggerPid}};
|
||||
|
||||
handle_info({directive_reply, Reply}, State = #state{logger_pid = LoggerPid, flight_num = FlightNum}) ->
|
||||
FlightInfo = <<"flight_num: ", (integer_to_binary(FlightNum - 1))/binary>>,
|
||||
case Reply of
|
||||
{ok, RawReq, DirectiveResult} ->
|
||||
case DirectiveResult of
|
||||
ok ->
|
||||
iot_logger:write(LoggerPid, [<<"[success]">>, RawReq, <<"OK">>, FlightInfo]);
|
||||
{ok, Response} when is_binary(Response) ->
|
||||
iot_logger:write(LoggerPid, [<<"[success]">>, RawReq, Response, FlightInfo]);
|
||||
{error, Reason0} ->
|
||||
Reason = if
|
||||
is_atom(Reason0) -> atom_to_binary(Reason0);
|
||||
is_binary(Reason0) -> Reason0;
|
||||
true -> <<"Unknow error">>
|
||||
end,
|
||||
iot_logger:write(LoggerPid, [<<"[error]">>, RawReq, Reason, FlightInfo])
|
||||
end;
|
||||
{error, RawReq, Error} when is_binary(Error) ->
|
||||
iot_logger:write(LoggerPid, [<<"[error]">>, RawReq, Error, FlightInfo])
|
||||
end,
|
||||
{noreply, State#state{flight_num = FlightNum - 1}};
|
||||
|
||||
handle_info(Info, State = #state{}) ->
|
||||
lager:notice("[iot_zd_consumer] get a unknown info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
||||
%% 取消topic的订阅
|
||||
TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics),
|
||||
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames),
|
||||
|
||||
ok = emqtt:disconnect(ConnPid),
|
||||
lager:debug("[iot_zd_consumer] terminate with reason: ~p", [Reason]),
|
||||
ok;
|
||||
terminate(Reason, _State) ->
|
||||
lager:debug("[iot_zd_consumer] terminate with reason: ~p", [Reason]),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
publish_directive(#{<<"version">> := Version, <<"location_code">> := LocationCode, <<"properties">> := DirectiveParams}, RawReq) ->
|
||||
%% 通过LocationCode查找到主机和Device_uuid
|
||||
ReceiverPid = self(),
|
||||
case redis_client:hgetall(LocationCode) of
|
||||
{ok, #{<<"host_uuid">> := HostUUID, <<"device_uuid">> := DeviceUUID}} ->
|
||||
case iot_host:get_pid(HostUUID) of
|
||||
undefined ->
|
||||
ReceiverPid ! {directive_reply, {error, RawReq, <<"host uuid: ", HostUUID/binary, " not found">>}};
|
||||
Pid when is_pid(Pid) ->
|
||||
ok
|
||||
end;
|
||||
{ok, Map} when is_map(Map) ->
|
||||
RedisData = iolist_to_binary(jiffy:encode(Map, [force_utf8])),
|
||||
ReceiverPid ! {directive_reply, {error, RawReq, <<"invalid redis data: ", RedisData/binary>>}};
|
||||
_ ->
|
||||
ReceiverPid ! {directive_reply, {error, RawReq, <<"location_code: ", LocationCode/binary, " not found in redis">>}}
|
||||
end;
|
||||
publish_directive(Other, RawReq) ->
|
||||
lager:warning("[iot_zd_consumer] get a error message: ~p", [Other]),
|
||||
self() ! {directive_reply, {error, RawReq, <<"unknown directive">>}}.
|
||||
|
||||
-spec create_consumer(Props :: list()) -> {ok, ConnPid :: pid()} | {error, Reason :: any()}.
|
||||
create_consumer(Props) when is_list(Props) ->
|
||||
Node = atom_to_binary(node()),
|
||||
ClientId = <<"mqtt-client-", Node/binary, "-zhongdian_mqtt_consumer">>,
|
||||
|
||||
%% 建立到emqx服务器的连接
|
||||
Host = proplists:get_value(host, Props),
|
||||
Port = proplists:get_value(port, Props, 18080),
|
||||
Username = proplists:get_value(username, Props),
|
||||
Password = proplists:get_value(password, Props),
|
||||
Keepalive = proplists:get_value(keepalive, Props, 86400),
|
||||
|
||||
Opts = [
|
||||
{clientid, ClientId},
|
||||
{host, Host},
|
||||
{port, Port},
|
||||
{owner, self()},
|
||||
{tcp_opts, []},
|
||||
{username, Username},
|
||||
{password, Password},
|
||||
{keepalive, Keepalive},
|
||||
{auto_ack, true},
|
||||
{connect_timeout, 5000},
|
||||
{proto_ver, v5},
|
||||
{retry_interval, 5000}
|
||||
],
|
||||
|
||||
%% 建立到emqx服务器的连接
|
||||
lager:debug("[iot_zd_consumer] opts is: ~p", [Opts]),
|
||||
case emqtt:start_link(Opts) of
|
||||
{ok, ConnPid} ->
|
||||
%% 监听和host相关的全部事件
|
||||
lager:debug("[iot_zd_consumer] start conntecting, pid: ~p", [ConnPid]),
|
||||
{ok, _} = emqtt:connect(ConnPid),
|
||||
lager:debug("[iot_zd_consumer] connect success, pid: ~p", [ConnPid]),
|
||||
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
|
||||
lager:debug("[iot_zd_consumer] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
|
||||
|
||||
{ok, ConnPid};
|
||||
ignore ->
|
||||
{error, ignore};
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
@ -1,48 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2018, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 29. 九月 2018 17:01
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(mysql_pool).
|
||||
-author("aresei").
|
||||
|
||||
%% API
|
||||
-export([get_row/2, get_row/3, get_all/2, get_all/3]).
|
||||
-export([update/4, update_by/2, update_by/3, insert/4]).
|
||||
|
||||
%% 从数据库中查找一行记录
|
||||
-spec get_row(Pool :: atom(), Sql::binary()) -> {ok, Record::map()} | undefined.
|
||||
get_row(Pool, Sql) when is_atom(Pool), is_binary(Sql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_row(ConnPid, Sql) end).
|
||||
|
||||
-spec get_row(Pool :: atom(), Sql::binary(), Params::list()) -> {ok, Record::map()} | undefined.
|
||||
get_row(Pool, Sql, Params) when is_atom(Pool), is_binary(Sql), is_list(Params) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_row(ConnPid, Sql, Params) end).
|
||||
|
||||
-spec get_all(Pool :: atom(), Sql::binary()) -> {ok, Rows::list()} | {error, Reason :: any()}.
|
||||
get_all(Pool, Sql) when is_atom(Pool), is_binary(Sql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_all(ConnPid, Sql) end).
|
||||
|
||||
-spec get_all(Pool :: atom(), Sql::binary(), Params::list()) -> {ok, Rows::list()} | {error, Reason::any()}.
|
||||
get_all(Pool, Sql, Params) when is_atom(Pool), is_binary(Sql), is_list(Params) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:get_all(ConnPid, Sql, Params) end).
|
||||
|
||||
-spec insert(Pool :: atom(), Table :: binary(), Fields :: map() | list(), boolean()) ->
|
||||
ok | {ok, InsertId :: integer()} | {error, Reason :: any()}.
|
||||
insert(Pool, Table, Fields, FetchInsertId) when is_atom(Pool), is_binary(Table), is_list(Fields); is_map(Fields), is_boolean(FetchInsertId) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:insert(ConnPid, Table, Fields, FetchInsertId) end).
|
||||
|
||||
-spec update_by(Pool :: atom(), UpdateSql :: binary()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(Pool, UpdateSql) when is_atom(Pool), is_binary(UpdateSql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:update_by(ConnPid, UpdateSql) end).
|
||||
|
||||
-spec update_by(Pool :: atom(), UpdateSql :: binary(), Params :: list()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(Pool, UpdateSql, Params) when is_atom(Pool), is_binary(UpdateSql) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:update_by(ConnPid, UpdateSql, Params) end).
|
||||
|
||||
-spec update(Pool :: atom(), Table :: binary(), Fields :: map(), WhereFields :: map()) -> {ok, AffectedRows::integer()} | {error, Reason::any()}.
|
||||
update(Pool, Table, Fields, WhereFields) when is_atom(Pool), is_binary(Table), is_map(Fields), is_map(WhereFields) ->
|
||||
poolboy:transaction(Pool, fun(ConnPid) -> mysql_provider:update(ConnPid, Table, Fields, WhereFields) end).
|
||||
@ -1,144 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2018, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 29. 九月 2018 17:01
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(mysql_provider).
|
||||
-author("aresei").
|
||||
|
||||
%% API
|
||||
-export([get_row/2, get_row/3, get_all/2, get_all/3]).
|
||||
-export([update/4, update_by/2, update_by/3, insert/4]).
|
||||
|
||||
%% 从数据库中查找一行记录
|
||||
-spec get_row(ConnPid :: pid(), Sql::binary()) -> {ok, Record::map()} | undefined.
|
||||
get_row(ConnPid, Sql) when is_pid(ConnPid), is_binary(Sql) ->
|
||||
lager:debug("[mysql_client] get_row sql is: ~p", [Sql]),
|
||||
case mysql:query(ConnPid, Sql) of
|
||||
{ok, Names, [Row | _]} ->
|
||||
{ok, maps:from_list(lists:zip(Names, Row))};
|
||||
{ok, _, []} ->
|
||||
undefined;
|
||||
Error ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Error]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec get_row(ConnPid :: pid(), Sql::binary(), Params::list()) -> {ok, Record::map()} | undefined.
|
||||
get_row(ConnPid, Sql, Params) when is_pid(ConnPid), is_binary(Sql), is_list(Params) ->
|
||||
lager:debug("[mysql_client] get_row sql is: ~p, params: ~p", [Sql, Params]),
|
||||
case mysql:query(ConnPid, Sql, Params) of
|
||||
{ok, Names, [Row | _]} ->
|
||||
{ok, maps:from_list(lists:zip(Names, Row))};
|
||||
{ok, _, []} ->
|
||||
undefined;
|
||||
Error ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Error]),
|
||||
undefined
|
||||
end.
|
||||
|
||||
-spec get_all(ConnPid :: pid(), Sql::binary()) -> {ok, Rows::list()} | {error, Reason :: any()}.
|
||||
get_all(ConnPid, Sql) when is_pid(ConnPid), is_binary(Sql) ->
|
||||
lager:debug("[mysql_client] get_all sql is: ~p", [Sql]),
|
||||
case mysql:query(ConnPid, Sql) of
|
||||
{ok, Names, Rows} ->
|
||||
{ok, lists:map(fun(Row) -> maps:from_list(lists:zip(Names, Row)) end, Rows)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec get_all(ConnPid :: pid(), Sql::binary(), Params::list()) -> {ok, Rows::list()} | {error, Reason::any()}.
|
||||
get_all(ConnPid, Sql, Params) when is_pid(ConnPid), is_binary(Sql), is_list(Params) ->
|
||||
lager:debug("[mysql_client] get_all sql is: ~p, params: ~p", [Sql, Params]),
|
||||
case mysql:query(ConnPid, Sql, Params) of
|
||||
{ok, Names, Rows} ->
|
||||
{ok, lists:map(fun(Row) -> maps:from_list(lists:zip(Names, Row)) end, Rows)};
|
||||
{error, Reason} ->
|
||||
lager:warning("[mysql_client] get error: ~p", [Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec insert(ConnPid :: pid(), Table :: binary(), Fields :: map() | list(), boolean()) ->
|
||||
ok | {ok, InsertId :: integer()} | {error, Reason :: any()}.
|
||||
insert(ConnPid, Table, Fields, FetchInsertId) when is_pid(ConnPid), is_binary(Table), is_map(Fields), is_boolean(FetchInsertId) ->
|
||||
insert(ConnPid, Table, maps:to_list(Fields), FetchInsertId);
|
||||
insert(ConnPid, Table, Fields, FetchInsertId) when is_pid(ConnPid), is_binary(Table), is_list(Fields), is_boolean(FetchInsertId) ->
|
||||
{Keys, Values} = kvs(Fields),
|
||||
|
||||
FieldSql = iolist_to_binary(lists:join(<<", ">>, Keys)),
|
||||
Placeholders = lists:duplicate(length(Keys), <<"?">>),
|
||||
ValuesPlaceholder = iolist_to_binary(lists:join(<<", ">>, Placeholders)),
|
||||
|
||||
Sql = <<"INSERT INTO ", Table/binary, "(", FieldSql/binary, ") VALUES(", ValuesPlaceholder/binary, ")">>,
|
||||
lager:debug("[mysql_client] insert sql is: ~p, params: ~p", [Sql, Values]),
|
||||
case mysql:query(ConnPid, Sql, Values) of
|
||||
ok ->
|
||||
case FetchInsertId of
|
||||
true ->
|
||||
InsertId = mysql:insert_id(ConnPid),
|
||||
{ok, InsertId};
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec update_by(ConnPid :: pid(), UpdateSql :: binary()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(ConnPid, UpdateSql) when is_pid(ConnPid), is_binary(UpdateSql) ->
|
||||
lager:debug("[mysql_client] updateBySql sql: ~p", [UpdateSql]),
|
||||
case mysql:query(ConnPid, UpdateSql) of
|
||||
ok ->
|
||||
AffectedRows = mysql:affected_rows(ConnPid),
|
||||
{ok, AffectedRows};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec update_by(ConnPid :: pid(), UpdateSql :: binary(), Params :: list()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}.
|
||||
update_by(ConnPid, UpdateSql, Params) when is_pid(ConnPid), is_binary(UpdateSql) ->
|
||||
lager:debug("[mysql_client] updateBySql sql: ~p, params: ~p", [UpdateSql, Params]),
|
||||
case mysql:query(ConnPid, UpdateSql, Params) of
|
||||
ok ->
|
||||
AffectedRows = mysql:affected_rows(ConnPid),
|
||||
{ok, AffectedRows};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec update(ConnPid :: pid(), Sql :: binary(), Fields :: map(), WhereFields :: map()) ->
|
||||
{ok, AffectedRows::integer()} | {error, Reason::any()}.
|
||||
update(ConnPid, Table, Fields, WhereFields) when is_pid(ConnPid), is_binary(Table), is_map(Fields), is_map(WhereFields) ->
|
||||
%% 拼接set
|
||||
{SetKeys, SetVals} = kvs(Fields),
|
||||
SetKeys1 = lists:map(fun(K) when is_binary(K) -> <<"`", K/binary, "` = ?">> end, SetKeys),
|
||||
SetSql = iolist_to_binary(lists:join(<<", ">>, SetKeys1)),
|
||||
|
||||
%% 拼接where
|
||||
{WhereKeys, WhereVals} = kvs(WhereFields),
|
||||
WhereKeys1 = lists:map(fun(K) when is_binary(K) -> <<"`", K/binary, "` = ?">> end, WhereKeys),
|
||||
WhereSql = iolist_to_binary(lists:join(<<" AND ">>, WhereKeys1)),
|
||||
|
||||
Params = SetVals ++ WhereVals,
|
||||
|
||||
Sql = <<"UPDATE ", Table/binary, " SET ", SetSql/binary, " WHERE ", WhereSql/binary>>,
|
||||
lager:debug("[mysql_client] update sql is: ~p, params: ~p", [Sql, Params]),
|
||||
case mysql:query(ConnPid, Sql, Params) of
|
||||
ok ->
|
||||
AffectedRows = mysql:affected_rows(ConnPid),
|
||||
{ok, AffectedRows};
|
||||
Error ->
|
||||
lager:error("[mysql_client] update sql: ~p, params: ~p, get a error: ~p", [Sql, Params, Error]),
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec kvs(Fields :: map() | list()) -> {Keys :: list(), Values :: list()}.
|
||||
kvs(Fields) when is_map(Fields) ->
|
||||
kvs(maps:to_list(Fields));
|
||||
kvs(Fields) when is_list(Fields) ->
|
||||
{Keys0, Values0} = lists:foldl(fun({K, V}, {Acc0, Acc1}) -> {[K|Acc0], [V|Acc1]} end, {[], []}, Fields),
|
||||
{lists:reverse(Keys0), lists:reverse(Values0)}.
|
||||
File diff suppressed because it is too large
Load Diff
156
apps/iot/src/simulator/simulator_api_handler.erl
Normal file
156
apps/iot/src/simulator/simulator_api_handler.erl
Normal file
@ -0,0 +1,156 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author Codex
|
||||
%%% @doc
|
||||
%%% 本地模拟 control api,用于替代 iot_api_client 依赖的远端接口。
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(simulator_api_handler).
|
||||
|
||||
-include_lib("endpoint/include/endpoint.hrl").
|
||||
|
||||
-export([handle_request/4]).
|
||||
|
||||
-define(HOST_UUID, <<"qbxmjyzrkpntfgswaevodhluicqzxplkm">>).
|
||||
-define(HOST_ID, 1).
|
||||
-define(DEVICE_UUID, <<"sim-device-001">>).
|
||||
-define(DEVICE_ID, 1).
|
||||
-define(ENDPOINT_ID, 1).
|
||||
-define(TABLE, ?MODULE).
|
||||
|
||||
-spec handle_request(string(), string(), map(), map()) ->
|
||||
{ok, non_neg_integer(), iodata()}.
|
||||
handle_request("GET", "/simulator/get_all_hosts", _, _) ->
|
||||
{ok, 200, iot_util:json_data([?HOST_UUID])};
|
||||
handle_request("GET", "/simulator/get_host_by_uuid", #{<<"uuid">> := UUID}, _) when is_binary(UUID) ->
|
||||
reply_host_by_uuid(UUID);
|
||||
handle_request("GET", "/simulator/get_host_by_id", #{<<"host_id">> := HostIdBin}, _) when is_binary(HostIdBin) ->
|
||||
case binary_to_integer(HostIdBin) of
|
||||
?HOST_ID ->
|
||||
{ok, 200, iot_util:json_data(host_info())};
|
||||
_ ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)}
|
||||
end;
|
||||
handle_request("POST", "/simulator/change_host_status", _, #{<<"uuid">> := UUID, <<"new_status">> := Status})
|
||||
when is_binary(UUID), is_integer(Status) ->
|
||||
reply_change_host_status(UUID, Status);
|
||||
handle_request("GET", "/simulator/get_host_devices", #{<<"host_id">> := HostIdBin}, _) when is_binary(HostIdBin) ->
|
||||
case binary_to_integer(HostIdBin) of
|
||||
?HOST_ID ->
|
||||
{ok, 200, iot_util:json_data([device_info()])};
|
||||
_ ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)}
|
||||
end;
|
||||
handle_request("GET", "/simulator/get_device_by_uuid", #{<<"device_uuid">> := DeviceUUID}, _) when is_binary(DeviceUUID) ->
|
||||
case DeviceUUID of
|
||||
?DEVICE_UUID ->
|
||||
{ok, 200, iot_util:json_data(device_info())};
|
||||
_ ->
|
||||
{ok, 200, iot_util:json_error(404, <<"device not found">>)}
|
||||
end;
|
||||
handle_request("POST", "/simulator/change_device_status", _, #{<<"device_uuid">> := DeviceUUID, <<"new_status">> := Status})
|
||||
when is_binary(DeviceUUID), is_integer(Status) ->
|
||||
reply_change_device_status(DeviceUUID, Status);
|
||||
handle_request("GET", "/simulator/get_all_endpoints", _, _) ->
|
||||
{ok, 200, iot_util:json_data([endpoint_info()])};
|
||||
handle_request("GET", "/simulator/get_endpoint", #{<<"id">> := IdBin}, _) when is_binary(IdBin) ->
|
||||
case binary_to_integer(IdBin) of
|
||||
?ENDPOINT_ID ->
|
||||
{ok, 200, iot_util:json_data(endpoint_info())};
|
||||
_ ->
|
||||
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)}
|
||||
end;
|
||||
handle_request("POST", "/simulator/endpoint_sink", _, Body) ->
|
||||
logger:debug("[simulator_api_handler] receive endpoint sink body: ~p", [Body]),
|
||||
{ok, 200, iot_util:json_data(<<"ok">>)};
|
||||
handle_request(_, Path, _, _) ->
|
||||
PathBin = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", PathBin/binary, " not found">>)}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec reply_host_by_uuid(binary()) -> {ok, non_neg_integer(), iodata()}.
|
||||
reply_host_by_uuid(?HOST_UUID) ->
|
||||
{ok, 200, iot_util:json_data(host_info())};
|
||||
reply_host_by_uuid(_) ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)}.
|
||||
|
||||
-spec reply_change_host_status(binary(), integer()) -> {ok, non_neg_integer(), iodata()}.
|
||||
reply_change_host_status(?HOST_UUID, Status) ->
|
||||
ok = put_state({host_status, ?HOST_UUID}, Status),
|
||||
{ok, 200, iot_util:json_data(1)};
|
||||
reply_change_host_status(_, _) ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)}.
|
||||
|
||||
-spec reply_change_device_status(binary(), integer()) -> {ok, non_neg_integer(), iodata()}.
|
||||
reply_change_device_status(?DEVICE_UUID, Status) ->
|
||||
ok = put_state({device_status, ?DEVICE_UUID}, Status),
|
||||
{ok, 200, iot_util:json_data(1)};
|
||||
reply_change_device_status(_, _) ->
|
||||
{ok, 200, iot_util:json_error(404, <<"device not found">>)}.
|
||||
|
||||
-spec host_info() -> map().
|
||||
host_info() ->
|
||||
#{
|
||||
<<"id">> => ?HOST_ID,
|
||||
<<"uuid">> => ?HOST_UUID,
|
||||
<<"authorize_status">> => 1,
|
||||
<<"status">> => get_state({host_status, ?HOST_UUID}, 1)
|
||||
}.
|
||||
|
||||
-spec device_info() -> map().
|
||||
device_info() ->
|
||||
#{
|
||||
<<"id">> => ?DEVICE_ID,
|
||||
<<"host_id">> => ?HOST_ID,
|
||||
<<"device_uuid">> => ?DEVICE_UUID,
|
||||
<<"status">> => get_state({device_status, ?DEVICE_UUID}, 1)
|
||||
}.
|
||||
|
||||
-spec endpoint_info() -> map().
|
||||
endpoint_info() ->
|
||||
#{
|
||||
<<"id">> => ?ENDPOINT_ID,
|
||||
<<"matcher">> => <<"simulator.metric">>,
|
||||
<<"title">> => <<"Simulator HTTP Endpoint">>,
|
||||
<<"type">> => <<"http">>,
|
||||
<<"config">> => #{
|
||||
<<"url">> => <<"http://127.0.0.1:18090/simulator/endpoint_sink">>,
|
||||
<<"pool_size">> => 4
|
||||
},
|
||||
<<"status">> => 1,
|
||||
<<"updated_at">> => iot_util:timestamp(),
|
||||
<<"created_at">> => iot_util:timestamp()
|
||||
}.
|
||||
|
||||
-spec ensure_table() -> ets:tid().
|
||||
ensure_table() ->
|
||||
case ets:info(?TABLE) of
|
||||
undefined ->
|
||||
try ets:new(?TABLE, [named_table, public, set]) of
|
||||
Tid ->
|
||||
Tid
|
||||
catch
|
||||
error:badarg ->
|
||||
?TABLE
|
||||
end;
|
||||
_ ->
|
||||
?TABLE
|
||||
end.
|
||||
|
||||
-spec get_state(term(), term()) -> term().
|
||||
get_state(Key, Default) ->
|
||||
_ = ensure_table(),
|
||||
case ets:lookup(?TABLE, Key) of
|
||||
[{_, Value}] ->
|
||||
Value;
|
||||
[] ->
|
||||
Default
|
||||
end.
|
||||
|
||||
-spec put_state(term(), term()) -> ok.
|
||||
put_state(Key, Value) ->
|
||||
_ = ensure_table(),
|
||||
true = ets:insert(?TABLE, {Key, Value}),
|
||||
ok.
|
||||
@ -1,218 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2021, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 1月 2021 上午12:17
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(tcp_channel).
|
||||
-author("licheng5").
|
||||
-include("iot.hrl").
|
||||
-include("message_pb.hrl").
|
||||
|
||||
%% API
|
||||
-export([pub/3, async_call/4, command/3]).
|
||||
-export([stop/2]).
|
||||
|
||||
-export([start_link/2]).
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
|
||||
|
||||
-record(state, {
|
||||
transport,
|
||||
socket,
|
||||
|
||||
uuid :: undefined | binary(),
|
||||
%% 用户进程id
|
||||
host_pid = undefined,
|
||||
%% 发送消息对应的id
|
||||
packet_id = 1 :: integer(),
|
||||
|
||||
%% 请求响应的对应关系
|
||||
inflight = #{}
|
||||
}).
|
||||
|
||||
%% 向通道中写入消息
|
||||
-spec pub(Pid :: pid(), Topic :: binary(), Content :: binary()) -> no_return().
|
||||
pub(Pid, Topic, Content) when is_pid(Pid), is_binary(Topic), is_binary(Content) ->
|
||||
gen_server:cast(Pid, {pub, Topic, Content}).
|
||||
|
||||
%% 向通道中写入消息
|
||||
-spec command(Pid :: pid(), CommandType :: integer(), Command :: binary()) -> no_return().
|
||||
command(Pid, CommandType, Command) when is_pid(Pid), is_integer(CommandType), is_binary(Command) ->
|
||||
gen_server:cast(Pid, {command, CommandType, Command}).
|
||||
|
||||
%% 向通道中写入消息
|
||||
-spec async_call(Pid :: pid(), ReceiverPid :: pid(), CallType :: integer(), CallBin :: binary()) -> Ref :: reference().
|
||||
async_call(Pid, ReceiverPid, CallType, CallBin) when is_pid(Pid), is_pid(ReceiverPid), is_integer(CallType), is_binary(CallBin) ->
|
||||
Ref = make_ref(),
|
||||
gen_server:cast(Pid, {async_call, ReceiverPid, Ref, CallType, CallBin}),
|
||||
Ref.
|
||||
|
||||
%% 关闭方法
|
||||
-spec stop(Pid :: pid(), Reason :: any()) -> no_return().
|
||||
stop(undefined, _Reason) ->
|
||||
ok;
|
||||
stop(Pid, Reason) when is_pid(Pid) ->
|
||||
gen_server:stop(Pid, Reason, 5000).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% 逻辑处理方法
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
start_link(Transport, Sock) ->
|
||||
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock]])}.
|
||||
|
||||
init([Transport, Sock]) ->
|
||||
lager:debug("[sdlan_channel] get a new connection: ~p", [Sock]),
|
||||
case Transport:wait(Sock) of
|
||||
{ok, NewSock} ->
|
||||
Transport:setopts(Sock, [{active, true}]),
|
||||
% erlang:start_timer(?PING_TICKER, self(), ping_ticker),
|
||||
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = NewSock});
|
||||
{error, Reason} ->
|
||||
{stop, Reason}
|
||||
end.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% 发送消息, 基于pub/sub机制
|
||||
handle_cast({pub, Topic, Content}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
PubBin = message_pb:encode_msg(#pub{topic = Topic, content = Content}),
|
||||
Transport:send(Socket, <<?PACKET_PUB, PubBin/binary>>),
|
||||
{noreply, State};
|
||||
|
||||
%% 发送Command消息
|
||||
handle_cast({command, CommandType, Command}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
Transport:send(Socket, <<?PACKET_COMMAND, CommandType:8, Command/binary>>),
|
||||
{noreply, State};
|
||||
|
||||
%% 推送消息
|
||||
handle_cast({async_call, ReceiverPid, Ref, CallType, CallBin}, State = #state{transport = Transport, socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
|
||||
Transport:send(Socket, <<?PACKET_ASYNC_CALL, PacketId:32, CallType:8, CallBin/binary>>),
|
||||
{noreply, State#state{packet_id = PacketId + 1, inflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight)}}.
|
||||
|
||||
%% auth验证
|
||||
handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, ?METHOD_AUTH:8, AuthRequestBin/binary>>}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
#auth_request{ uuid = UUID, username = Username, token = Token, salt = Salt, timestamp = Timestamp } = message_pb:decode_msg(AuthRequestBin, auth_request),
|
||||
lager:debug("[ws_channel] auth uuid: ~p", [UUID]),
|
||||
case iot_auth:check(Username, Token, UUID, Salt, Timestamp) of
|
||||
true ->
|
||||
case host_bo:get_host_by_uuid(UUID) of
|
||||
undefined ->
|
||||
lager:warning("[ws_channel] uuid: ~p, user: ~p, host not found", [UUID, Username]),
|
||||
{stop, State};
|
||||
{ok, _} ->
|
||||
%% 尝试启动主机的服务进程
|
||||
{ok, HostPid} = iot_host_sup:ensured_host_started(UUID),
|
||||
case iot_host:attach_channel(HostPid, self()) of
|
||||
ok ->
|
||||
%% 建立到host的monitor
|
||||
erlang:monitor(process, HostPid),
|
||||
AuthReplyBin = message_pb:encode_msg(#auth_reply{code = 0, message = <<"ok">>}),
|
||||
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, AuthReplyBin/binary>>),
|
||||
|
||||
{noreply, State#state{uuid = UUID, host_pid = HostPid}};
|
||||
{denied, Reason} when is_binary(Reason) ->
|
||||
erlang:monitor(process, HostPid),
|
||||
AuthReplyBin = message_pb:encode_msg(#auth_reply{code = 1, message = Reason}),
|
||||
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, AuthReplyBin/binary>>),
|
||||
lager:debug("[ws_channel] uuid: ~p, attach channel get error: ~p, stop channel", [UUID, Reason]),
|
||||
|
||||
{noreply, State#state{uuid = UUID, host_pid = HostPid}};
|
||||
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
AuthReplyBin = message_pb:encode_msg(#auth_reply{code = 2, message = Reason}),
|
||||
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, AuthReplyBin/binary>>),
|
||||
lager:debug("[ws_channel] uuid: ~p, attach channel get error: ~p, stop channel", [UUID, Reason]),
|
||||
|
||||
{stop, State}
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
lager:warning("[ws_channel] uuid: ~p, user: ~p, auth failed", [UUID, Username]),
|
||||
{stop, State}
|
||||
end;
|
||||
|
||||
%% 请求微服务配置
|
||||
handle_info({tcp, Socket, <<?PACKET_REQUEST, PacketId:32, ?METHOD_REQUEST_SERVICE_CONFIG:8, ServiceId/binary>>}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
lager:debug("[ws_channel] service_config request service_id: ~p", [ServiceId]),
|
||||
case micro_service_bo:get_service_config(ServiceId) of
|
||||
error ->
|
||||
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32>>);
|
||||
{ok, ConfigJson} when is_binary(ConfigJson) ->
|
||||
Transport:send(Socket, <<?PACKET_RESPONSE, PacketId:32, ConfigJson/binary>>)
|
||||
end,
|
||||
{noreply, State};
|
||||
|
||||
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_DATA:8, Data0/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||
Data = message_pb:decode_msg(Data0, data),
|
||||
iot_host:handle(HostPid, {data, Data}),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_PING:8, PingData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||
Ping = message_pb:decode_msg(PingData, ping),
|
||||
iot_host:handle(HostPid, {ping, Ping}),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_INFORM:8, InformData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||
ServiceInform = message_pb:decode_msg(InformData, service_inform),
|
||||
iot_host:handle(HostPid, {inform, ServiceInform}),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({tcp, Socket, <<?PACKET_REQUEST, ?METHOD_EVENT:8, EventData/binary>>}, State = #state{socket = Socket, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||
Event = message_pb:decode_msg(EventData, event),
|
||||
iot_host:handle(HostPid, {event, Event}),
|
||||
{noreply, State};
|
||||
|
||||
%% 主机端的消息响应
|
||||
handle_info({tcp, Socket, <<?PACKET_ASYNC_CALL_REPLY, PacketId:32, ResponseBin/binary>>}, State = #state{socket = Socket, uuid = UUID, inflight = Inflight}) when PacketId > 0 ->
|
||||
AsyncCallReply = message_pb:decode_msg(ResponseBin, async_call_reply),
|
||||
lager:debug("[ws_channel] uuid: ~p, get async_call_reply: ~p, packet_id: ~p", [UUID, AsyncCallReply, PacketId]),
|
||||
case maps:take(PacketId, Inflight) of
|
||||
error ->
|
||||
lager:warning("[ws_channel] get unknown async_call_reply message: ~p, packet_id: ~p", [AsyncCallReply, PacketId]),
|
||||
{noreply, State};
|
||||
{{ReceiverPid, Ref}, NInflight} ->
|
||||
case is_pid(ReceiverPid) andalso is_process_alive(ReceiverPid) of
|
||||
true ->
|
||||
ReceiverPid ! {async_call_reply, Ref, AsyncCallReply};
|
||||
false ->
|
||||
lager:warning("[ws_channel] get async_call_reply message: ~p, packet_id: ~p, but receiver_pid is deaded", [AsyncCallReply, PacketId])
|
||||
end,
|
||||
{noreply, State#state{inflight = NInflight}}
|
||||
end;
|
||||
|
||||
%% 来自efka的ping包
|
||||
handle_info({tcp, Socket, <<?PACKET_PING>>}, State = #state{socket = Socket}) ->
|
||||
{noreply, State};
|
||||
|
||||
handle_info({tcp_error, Sock, Reason}, State = #state{socket = Sock}) ->
|
||||
lager:notice("[sdlan_channel] tcp_error: ~p", [Reason]),
|
||||
{stop, normal, State};
|
||||
|
||||
handle_info({tcp_closed, Sock}, State = #state{socket = Sock}) ->
|
||||
lager:notice("[sdlan_channel] tcp_closed"),
|
||||
{stop, normal, State};
|
||||
|
||||
%% 关闭当前通道
|
||||
handle_info({stop, Reason}, State) ->
|
||||
{stop, Reason, State};
|
||||
|
||||
%% 主机进程挂掉的时候,需要关闭掉链接
|
||||
handle_info({'DOWN', _, process, HostPid, Reason}, State = #state{uuid = UUID, host_pid = HostPid}) ->
|
||||
lager:debug("[ws_channel] uuid: ~p, channel will close because host exited with reason: ~p", [UUID, Reason]),
|
||||
{stop, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
lager:warning("[sdlan_channel] get a unknown message: ~p, channel will closed", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(Reason, #state{}) ->
|
||||
lager:warning("[sdlan_channel] stop with reason: ~p", [Reason]),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
@ -1,37 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 08. 5月 2025 12:58
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(tcp_server).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([start/0]).
|
||||
|
||||
%% 启动tcp服务
|
||||
start() ->
|
||||
{ok, Props} = application:get_env(iot, tcp_server),
|
||||
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||
Port = proplists:get_value(port, Props),
|
||||
|
||||
TransOpts = [
|
||||
{tcp_options, [
|
||||
binary,
|
||||
{reuseaddr, true},
|
||||
{active, false},
|
||||
{packet, 4},
|
||||
{nodelay, false},
|
||||
{backlog, Backlog}
|
||||
]},
|
||||
{acceptors, Acceptors},
|
||||
{max_connections, MaxConnections}
|
||||
],
|
||||
{ok, _} = esockd:open('iot/tcp_server', Port, TransOpts, {tcp_channel, start_link, []}),
|
||||
|
||||
lager:debug("[iot_app] the tcp server start at: ~p", [Port]).
|
||||
369
apps/iot/src/transport/ssl_channel.erl
Normal file
369
apps/iot/src/transport/ssl_channel.erl
Normal file
@ -0,0 +1,369 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2021, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 11. 1月 2021 上午12:17
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(ssl_channel).
|
||||
-author("licheng5").
|
||||
-behaviour(ranch_protocol).
|
||||
|
||||
-define(INFLIGHT_TIMEOUT, 60000).
|
||||
-define(SSL_IDLE_TIMEOUT, 120000).
|
||||
|
||||
%% API
|
||||
-export([pub/4, container_call/4, cancel_command_call/2]).
|
||||
|
||||
-export([start_link/3, stop/2]).
|
||||
%% gen_server callbacks
|
||||
-export([init/3, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).
|
||||
|
||||
-record(state, {
|
||||
transport,
|
||||
socket,
|
||||
|
||||
uuid :: undefined | binary(),
|
||||
is_authed = false :: boolean(),
|
||||
%% 用户进程id
|
||||
host_pid = undefined,
|
||||
|
||||
%% iot 发起的 command 与 command_response 的对应关系
|
||||
inflight = #{},
|
||||
|
||||
idle_timer_ref :: undefined | reference()
|
||||
}).
|
||||
|
||||
-record(inflight_command, {
|
||||
receiver_pid :: undefined | pid(),
|
||||
timer_ref :: reference()
|
||||
}).
|
||||
|
||||
-type request_ref() :: binary().
|
||||
|
||||
%% 向通道中写入消息
|
||||
-spec pub(Pid :: pid(), Topic :: binary(), Qos :: integer(), Content :: binary()) -> ok.
|
||||
pub(Pid, Topic, Qos, Content) when is_pid(Pid), is_binary(Topic), is_integer(Qos), is_binary(Content) ->
|
||||
gen_server:cast(Pid, {pub, Topic, Qos, Content}).
|
||||
|
||||
-spec container_call(Pid :: pid(), ReceiverPid :: pid(), Ref :: request_ref(), Request :: map()) -> ok.
|
||||
container_call(Pid, ReceiverPid, Ref, Request) when is_pid(Pid), is_pid(ReceiverPid), is_binary(Ref), is_map(Request) ->
|
||||
gen_server:cast(Pid, {command_call, ReceiverPid, Ref, {container, Request}}),
|
||||
ok.
|
||||
|
||||
-spec cancel_command_call(Pid :: pid(), Ref :: request_ref()) -> ok.
|
||||
cancel_command_call(Pid, Ref) when is_pid(Pid), is_binary(Ref) ->
|
||||
gen_server:call(Pid, {cancel_command_call, Ref}).
|
||||
|
||||
%% 关闭方法
|
||||
-spec stop(Pid :: undefined | pid(), Reason :: any()) -> ok.
|
||||
stop(undefined, _Reason) ->
|
||||
ok;
|
||||
stop(Pid, Reason) when is_pid(Pid) ->
|
||||
gen_server:stop(Pid, Reason, 5000).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% 逻辑处理方法
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
start_link(Ref, Transport, Opts) ->
|
||||
{ok, proc_lib:spawn_link(?MODULE, init, [Ref, Transport, Opts])}.
|
||||
|
||||
init(Ref, Transport, _Opts = []) ->
|
||||
ok = iot_log:set_metadata(),
|
||||
{ok, Socket} = ranch:handshake(Ref),
|
||||
logger:debug("[ssl_channel] get a new connection: ~p", [Socket]),
|
||||
Transport:setopts(Socket, [binary, {active, true}, {packet, 4}]),
|
||||
IdleTimerRef = start_idle_timer(),
|
||||
gen_server:enter_loop(?MODULE, [], #state{transport = Transport, socket = Socket, idle_timer_ref = IdleTimerRef}).
|
||||
|
||||
handle_call({cancel_command_call, Ref}, _From, State = #state{inflight = Inflight}) ->
|
||||
case maps:take(Ref, Inflight) of
|
||||
{#inflight_command{timer_ref = TimerRef}, NInflight} ->
|
||||
erlang:cancel_timer(TimerRef),
|
||||
{reply, ok, State#state{inflight = NInflight}};
|
||||
error ->
|
||||
{reply, ok, State}
|
||||
end;
|
||||
handle_call(_Request, _From, State) ->
|
||||
{reply, ok, State}.
|
||||
|
||||
%% 发送消息, 基于pub/sub机制
|
||||
handle_cast({pub, Topic, Qos, Content}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
Packet = term_to_binary({<<"message">>, {<<"pub">>, #{<<"topic">> => Topic, <<"qos">> => Qos, <<"content">> => Content}}}),
|
||||
case Transport:send(Socket, Packet) of
|
||||
ok ->
|
||||
{noreply, State};
|
||||
{error, Reason} ->
|
||||
logger:warning("[ssl_channel] send pub failed, reason: ~p", [Reason]),
|
||||
{stop, {send_failed, Reason}, State}
|
||||
end;
|
||||
|
||||
%% iot 请求 efka 时使用 command/command_response 语义。
|
||||
handle_cast({command_call, ReceiverPid, Ref, Body}, State = #state{transport = Transport, socket = Socket, inflight = Inflight}) ->
|
||||
Packet = term_to_binary({<<"command">>, Ref, encode_command_body(Body)}),
|
||||
case Transport:send(Socket, Packet) of
|
||||
ok ->
|
||||
TimerRef = erlang:start_timer(?INFLIGHT_TIMEOUT, self(), {command_timeout, Ref}),
|
||||
CommandInfo = #inflight_command{receiver_pid = ReceiverPid, timer_ref = TimerRef},
|
||||
{noreply, State#state{inflight = maps:put(Ref, CommandInfo, Inflight)}};
|
||||
{error, Reason} ->
|
||||
logger:warning("[ssl_channel] send command failed, ref: ~p, reason: ~p", [Ref, Reason]),
|
||||
deliver_command_error(ReceiverPid, Ref, {send_failed, Reason}),
|
||||
{stop, {send_failed, Reason}, State}
|
||||
end.
|
||||
|
||||
handle_info({timeout, TimerRef, {command_timeout, Ref}}, State = #state{inflight = Inflight}) ->
|
||||
case maps:get(Ref, Inflight, undefined) of
|
||||
#inflight_command{receiver_pid = ReceiverPid, timer_ref = TimerRef} ->
|
||||
logger:warning("[ws_channel] command timeout, ref: ~p", [Ref]),
|
||||
reply_command_timeout(ReceiverPid, Ref),
|
||||
{noreply, State#state{inflight = maps:remove(Ref, Inflight)}};
|
||||
_ ->
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
handle_info({timeout, TimerRef, ssl_idle_timeout}, State = #state{idle_timer_ref = TimerRef}) ->
|
||||
logger:notice("[ssl_channel] ssl channel idle timeout"),
|
||||
{stop, ssl_idle_timeout, State#state{idle_timer_ref = undefined}};
|
||||
handle_info({timeout, _TimerRef, ssl_idle_timeout}, State) ->
|
||||
{noreply, State};
|
||||
|
||||
%% 关闭当前通道
|
||||
handle_info({stop, Reason}, State) ->
|
||||
{stop, Reason, State};
|
||||
|
||||
%% 主机进程挂掉的时候,需要关闭掉链接
|
||||
handle_info({'DOWN', _, process, HostPid, Reason}, State = #state{uuid = UUID, host_pid = HostPid}) ->
|
||||
logger:debug("[ws_channel] uuid: ~p, channel will close because host exited with reason: ~p", [UUID, Reason]),
|
||||
{stop, Reason, State};
|
||||
|
||||
handle_info({ssl, Socket, PacketBin}, State = #state{socket = Socket}) when is_binary(PacketBin) ->
|
||||
State1 = reset_idle_timer(State),
|
||||
try binary_to_term(PacketBin, [safe]) of
|
||||
{<<"request">>, Ref, Body} ->
|
||||
handle_request_frame(Ref, Body, State1);
|
||||
{<<"message">>, Body} ->
|
||||
handle_message_frame(Body, State1);
|
||||
{<<"command_response">>, Ref, Response} ->
|
||||
handle_command_response_frame(Ref, Response, State1);
|
||||
Other ->
|
||||
logger:warning("[ssl_channel] unsupported packet: ~p", [Other]),
|
||||
{stop, bad_packet, State1}
|
||||
catch error:Error ->
|
||||
logger:warning("[ssl_channel] binary_to_term get error: ~p, packet_size: ~p", [Error, byte_size(PacketBin)]),
|
||||
{stop, bad_packet, State1}
|
||||
end;
|
||||
|
||||
handle_info({ssl_closed, Socket}, State = #state{socket = Socket}) ->
|
||||
logger:notice("[ssl_channel] ssl socket closed"),
|
||||
{stop, normal, State};
|
||||
|
||||
handle_info({ssl_error, Socket, Reason}, State = #state{socket = Socket}) ->
|
||||
logger:notice("[ssl_channel] ssl socket error: ~p", [Reason]),
|
||||
{stop, normal, State};
|
||||
|
||||
handle_info({ssl_passive, Socket}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
ok = Transport:setopts(Socket, [{active, true}]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
logger:warning("[ssl_channel] get a unknown message: ~p, state: ~p", [Info, State]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(Reason, #state{inflight = Inflight, transport = Transport, socket = Socket, idle_timer_ref = IdleTimerRef}) ->
|
||||
cancel_timer(IdleTimerRef),
|
||||
maps:foreach(fun(Ref, CommandInfo) -> reply_command_closed(Ref, CommandInfo, Reason) end, Inflight),
|
||||
Transport:close(Socket),
|
||||
logger:warning("[ssl_channel] stop with reason: ~p", [Reason]),
|
||||
ok.
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%%%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
-spec handle_request_frame(request_ref(), tuple(), #state{}) -> {noreply, #state{}} | {stop, term(), #state{}}.
|
||||
handle_request_frame(Ref, {<<"auth_request">>, #{<<"uuid">> := UUID}}, State = #state{is_authed = true}) ->
|
||||
logger:warning("[ws_channel] repeated auth request, ref: ~p, uuid: ~p, close channel", [Ref, UUID]),
|
||||
{stop, repeated_auth, State};
|
||||
handle_request_frame(Ref, {<<"auth_request">>, #{<<"uuid">> := UUID, <<"token">> := Token, <<"timestamp">> := Timestamp}}, State = #state{transport = Transport, socket = Socket}) ->
|
||||
maybe
|
||||
ok ?= auth(Token, UUID, Timestamp),
|
||||
{ok, HostPid} ?= iot_host:lookup_pid(UUID),
|
||||
ok ?= iot_host:attach_channel(HostPid, self()),
|
||||
erlang:monitor(process, HostPid),
|
||||
ok = send_reply_frame(Transport, Socket, Ref, {<<"auth_response">>, <<"ok">>}),
|
||||
logger:debug("[ws_channel] auth uuid: ~p", [UUID]),
|
||||
{noreply, State#state{uuid = UUID, is_authed = true, host_pid = HostPid}}
|
||||
else
|
||||
{error, Reason} ->
|
||||
logger:warning("[ws_channel] uuid: ~p, auth failed with reason: ~p", [UUID, Reason]),
|
||||
case send_reply_frame(Transport, Socket, Ref, {<<"auth_response">>, {<<"error">>, {<<"failed">>, Reason}}}) of
|
||||
ok ->
|
||||
{stop, Reason, State};
|
||||
{error, SendReason} ->
|
||||
logger:warning("[ws_channel] uuid: ~p, send auth failed response failed: ~p", [UUID, SendReason]),
|
||||
{stop, {send_failed, SendReason}, State}
|
||||
end
|
||||
end;
|
||||
handle_request_frame(Ref, {<<"container">>, ContainerCommand}, State) ->
|
||||
logger:warning("[ws_channel] unsupported request message type: container, ref: ~p, command: ~p", [Ref, ContainerCommand]),
|
||||
{stop, normal, State};
|
||||
handle_request_frame(Ref, Body, State) ->
|
||||
logger:warning("[ws_channel] unsupported request body, ref: ~p, body: ~p", [Ref, Body]),
|
||||
{stop, normal, State}.
|
||||
|
||||
-spec handle_message_frame(term(), #state{}) ->
|
||||
{noreply, #state{}} | {stop, term(), #state{}}.
|
||||
handle_message_frame(<<"ping">>, State = #state{transport = Transport, socket = Socket}) ->
|
||||
Packet = term_to_binary({<<"message">>, <<"pong">>}),
|
||||
case Transport:send(Socket, Packet) of
|
||||
ok ->
|
||||
{noreply, State};
|
||||
{error, Reason} ->
|
||||
logger:warning("[ssl_channel] send pong failed, reason: ~p", [Reason]),
|
||||
{stop, {send_failed, Reason}, State}
|
||||
end;
|
||||
handle_message_frame({<<"data">>, #{<<"route_key">> := RouteKey, <<"metric">> := Metric}}, State = #state{host_pid = HostPid}) when is_pid(HostPid) ->
|
||||
iot_host:handle(HostPid, {data, RouteKey, Metric}),
|
||||
{noreply, State};
|
||||
handle_message_frame({<<"task_event">>, Event}, State = #state{uuid = UUID, host_pid = HostPid}) when is_binary(UUID), is_pid(HostPid) ->
|
||||
handle_event_stream_frame(UUID, Event),
|
||||
{noreply, State};
|
||||
handle_message_frame(Body, State) ->
|
||||
logger:warning("[ssl_channel] unsupported message body: ~p", [Body]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_event_stream_frame(binary(), map()) -> ok.
|
||||
handle_event_stream_frame(UUID, #{<<"task_id">> := TaskId, <<"type">> := <<"close">>, <<"stream">> := Reason}) ->
|
||||
logger:debug("[ssl_channel] get uuid: ~p, task_id: ~p, close with reason: ~p", [UUID, TaskId, Reason]),
|
||||
iot_container_task_sup:close(UUID, TaskId, Reason);
|
||||
handle_event_stream_frame(UUID, #{<<"task_id">> := TaskId, <<"type">> := Type, <<"stream">> := Stream}) ->
|
||||
logger:debug("[ssl_channel] get uuid: ~p, task_id: ~p, type: ~ts, stream: ~ts", [UUID, TaskId, Type, Stream]),
|
||||
iot_container_task_sup:stream(UUID, TaskId, Type, Stream);
|
||||
handle_event_stream_frame(UUID, Event) ->
|
||||
logger:warning("[ssl_channel] invalid task_event, uuid: ~p, event: ~p", [UUID, Event]),
|
||||
ok.
|
||||
|
||||
-spec handle_command_response_frame(request_ref(), tuple(), #state{}) ->
|
||||
{noreply, #state{}}.
|
||||
handle_command_response_frame(Ref, Reply, State = #state{inflight = Inflight}) when is_binary(Ref) ->
|
||||
case maps:take(Ref, Inflight) of
|
||||
error ->
|
||||
{noreply, State};
|
||||
{#inflight_command{receiver_pid = ReceiverPid, timer_ref = TimerRef}, NInflight} ->
|
||||
erlang:cancel_timer(TimerRef),
|
||||
deliver_command_response(ReceiverPid, Ref, Reply),
|
||||
{noreply, State#state{inflight = NInflight}}
|
||||
end;
|
||||
handle_command_response_frame(Ref, Reply, State) ->
|
||||
logger:warning("[ws_channel] unexpected command_response frame, ref: ~p, reply: ~p", [Ref, Reply]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec send_reply_frame(module(), any(), request_ref(), tuple()) -> ok | {error, term()}.
|
||||
send_reply_frame(Transport, Socket, Ref, Reply) ->
|
||||
Packet = term_to_binary({<<"response">>, Ref, Reply}),
|
||||
Transport:send(Socket, Packet).
|
||||
|
||||
-spec decode_command_response({binary(), term()} | tuple()) ->
|
||||
ok | {ok, term()} | {error, term()}.
|
||||
decode_command_response({<<"container">>, <<"ok">>}) ->
|
||||
ok;
|
||||
decode_command_response({<<"container">>, {<<"ok">>, Result}}) ->
|
||||
{ok, Result};
|
||||
decode_command_response({<<"container">>, {<<"error">>, Reason}}) ->
|
||||
{error, Reason};
|
||||
decode_command_response(_Reply) ->
|
||||
{error, invalid_response}.
|
||||
|
||||
-spec deliver_command_response(undefined | pid(), request_ref(), tuple()) -> ok.
|
||||
deliver_command_response(undefined, _Ref, _Reply) ->
|
||||
ok;
|
||||
deliver_command_response(ReceiverPid, Ref, Reply) when is_pid(ReceiverPid) ->
|
||||
case is_process_alive(ReceiverPid) of
|
||||
true ->
|
||||
ReceiverPid ! {command_reply, Ref, decode_command_response(Reply)};
|
||||
false ->
|
||||
logger:warning("[ws_channel] get command_response: ~p, ref: ~p, but receiver_pid is deaded", [Reply, Ref])
|
||||
end.
|
||||
|
||||
-spec deliver_command_error(undefined | pid(), request_ref(), term()) -> ok.
|
||||
deliver_command_error(ReceiverPid, Ref, Reason) when is_pid(ReceiverPid) ->
|
||||
ReceiverPid ! {command_reply, Ref, {error, Reason}},
|
||||
ok;
|
||||
deliver_command_error(_ReceiverPid, _Ref, _Reason) ->
|
||||
ok.
|
||||
|
||||
-spec reply_command_timeout(undefined | pid(), request_ref()) -> ok.
|
||||
reply_command_timeout(ReceiverPid, Ref) when is_pid(ReceiverPid) ->
|
||||
ReceiverPid ! {command_reply, Ref, {error, timeout}},
|
||||
ok;
|
||||
reply_command_timeout(_ReceiverPid, _Ref) ->
|
||||
ok.
|
||||
|
||||
-spec reply_command_closed(request_ref(), #inflight_command{}, term()) -> ok.
|
||||
reply_command_closed(Ref, #inflight_command{receiver_pid = ReceiverPid}, Reason) when is_pid(ReceiverPid) ->
|
||||
ReceiverPid ! {command_reply, Ref, {error, {channel_closed, Reason}}},
|
||||
ok;
|
||||
reply_command_closed(_Ref, _CommandInfo, _Reason) ->
|
||||
ok.
|
||||
|
||||
%% 检测token是否是合法值
|
||||
-spec auth(Token :: binary(), UUID :: binary(), Timestamp :: integer()) ->
|
||||
ok | {error, Reason :: binary()}.
|
||||
auth(Token, UUID, Timestamp) when is_binary(Token), is_binary(UUID), is_integer(Timestamp) ->
|
||||
%% 1分钟内有效
|
||||
Now = iot_util:current_time(),
|
||||
case Timestamp =< Now andalso Now - Timestamp =< 60 of
|
||||
true ->
|
||||
case efka_client_store:auth(UUID, Token) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
{error, <<"invalid token">>}
|
||||
end;
|
||||
false ->
|
||||
{error, <<"invalid timestamp">>}
|
||||
end.
|
||||
|
||||
-spec encode_command_body({container, map()}) -> {binary(), map()}.
|
||||
encode_command_body({container, Request}) ->
|
||||
{<<"container">>, safe_term(Request)}.
|
||||
|
||||
-spec safe_term(term()) -> term().
|
||||
safe_term(true) ->
|
||||
true;
|
||||
safe_term(false) ->
|
||||
false;
|
||||
safe_term(undefined) ->
|
||||
undefined;
|
||||
safe_term(Value) when is_atom(Value) ->
|
||||
atom_to_binary(Value, utf8);
|
||||
safe_term(Value) when is_map(Value) ->
|
||||
maps:from_list([{safe_term(K), safe_term(V)} || {K, V} <- maps:to_list(Value)]);
|
||||
safe_term(Value) when is_list(Value) ->
|
||||
[safe_term(Item) || Item <- Value];
|
||||
safe_term(Value) when is_tuple(Value) ->
|
||||
list_to_tuple([safe_term(Item) || Item <- tuple_to_list(Value)]);
|
||||
safe_term(Value) ->
|
||||
Value.
|
||||
|
||||
-spec start_idle_timer() -> reference().
|
||||
start_idle_timer() ->
|
||||
erlang:start_timer(?SSL_IDLE_TIMEOUT, self(), ssl_idle_timeout).
|
||||
|
||||
-spec reset_idle_timer(#state{}) -> #state{}.
|
||||
reset_idle_timer(State = #state{idle_timer_ref = TimerRef}) ->
|
||||
cancel_timer(TimerRef),
|
||||
State#state{idle_timer_ref = start_idle_timer()}.
|
||||
|
||||
-spec cancel_timer(undefined | reference()) -> ok.
|
||||
cancel_timer(undefined) ->
|
||||
ok;
|
||||
cancel_timer(TimerRef) ->
|
||||
_ = erlang:cancel_timer(TimerRef),
|
||||
ok.
|
||||
@ -1,37 +1,36 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @copyright (C) 2026, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 24. 12月 2023 15:42
|
||||
%%% Created : 09. 5月 2026 18:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(iot_api).
|
||||
-module(udp_server).
|
||||
-author("anlicheng").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-define(HEARTBEAT_VERSION, 1).
|
||||
-define(HEARTBEAT_NONCE_BYTES, 16).
|
||||
-define(HEARTBEAT_MAC_BYTES, 32).
|
||||
|
||||
%% API
|
||||
-export([start_link/0]).
|
||||
-export([ai_event/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-define(SERVER, ?MODULE).
|
||||
-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).
|
||||
|
||||
-record(state, {
|
||||
|
||||
socket :: gen_udp:socket()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
ai_event(Id) when is_integer(Id) ->
|
||||
gen_server:cast(?MODULE, {ai_event, Id}).
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link() ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
@ -48,7 +47,12 @@ start_link() ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([]) ->
|
||||
{ok, #state{}}.
|
||||
ok = iot_log:set_metadata(),
|
||||
{ok, UdpServerProps} = application:get_env(iot, udp_server),
|
||||
Port = proplists:get_value(port, UdpServerProps),
|
||||
logger:debug("[udp_server] start at port: ~p", [Port]),
|
||||
{ok, Socket} = gen_udp:open(Port, [binary, {active, true}]),
|
||||
{ok, #state{socket = Socket}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
@ -69,33 +73,7 @@ handle_call(_Request, _From, State = #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({ai_event, Id}, State = #state{}) ->
|
||||
spawn_monitor(fun() ->
|
||||
Token = iot_util:md5(<<?API_TOKEN/binary, (integer_to_binary(Id))/binary, ?API_TOKEN/binary>>),
|
||||
{ok, Url} = application:get_env(iot, api_url),
|
||||
|
||||
Headers = [
|
||||
{<<"content-type">>, <<"application/json">>}
|
||||
],
|
||||
ReqData = #{
|
||||
<<"token">> => Token,
|
||||
<<"id">> => Id
|
||||
},
|
||||
Body = iolist_to_binary(jiffy:encode(ReqData, [force_utf8])),
|
||||
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
lager:debug("[iot_api] send body: ~p, get error is: ~p", [Body, RespBody]),
|
||||
hackney:close(ClientRef);
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]);
|
||||
{error, Reason} ->
|
||||
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, Reason])
|
||||
end
|
||||
end),
|
||||
|
||||
handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
@ -104,15 +82,11 @@ handle_cast({ai_event, Id}, State = #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
%% Task进程挂掉
|
||||
handle_info({'DOWN', _MRef, process, _Pid, normal}, State) ->
|
||||
handle_info({udp, Socket, Ip, Port, Packet}, State = #state{socket = Socket}) ->
|
||||
handle_heartbeat_packet(Packet, {Ip, Port}),
|
||||
{noreply, State};
|
||||
|
||||
handle_info({'DOWN', _MRef, process, _Pid, Reason}, State) ->
|
||||
lager:notice("[iot_api] task process down with reason: ~p", [Reason]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info(_Info, State = #state{}) ->
|
||||
handle_info(Info, State = #state{}) ->
|
||||
logger:warning("[udp_server] ignore unknown info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
@ -122,7 +96,8 @@ handle_info(_Info, State = #state{}) ->
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(_Reason, _State = #state{}) ->
|
||||
terminate(_Reason, _State = #state{socket = Socket}) ->
|
||||
gen_udp:close(Socket),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
@ -136,3 +111,31 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
-spec handle_heartbeat_packet(binary(), term()) -> ok.
|
||||
handle_heartbeat_packet(Packet, Peer) when is_binary(Packet) ->
|
||||
case decode_heartbeat_packet(Packet) of
|
||||
{ok, UUID, Timestamp, Payload, Mac} ->
|
||||
case efka_client_store:verify_heartbeat(UUID, Timestamp, Payload, Mac) of
|
||||
true ->
|
||||
Pid = iot_host:get_pid(UUID),
|
||||
iot_host:heartbeat(Pid);
|
||||
false ->
|
||||
logger:warning("[udp_server] ignore invalid heartbeat auth from peer: ~p, uuid: ~p", [Peer, UUID]),
|
||||
ok
|
||||
end;
|
||||
error ->
|
||||
logger:warning("[udp_server] ignore invalid heartbeat packet from peer: ~p, packet: ~p", [Peer, Packet]),
|
||||
ok
|
||||
end.
|
||||
|
||||
-spec decode_heartbeat_packet(binary()) ->
|
||||
{ok, UUID :: binary(), Timestamp :: integer(), Payload :: binary(), Mac :: binary()} | error.
|
||||
decode_heartbeat_packet(Packet = <<?HEARTBEAT_VERSION:8, UUIDLen:16, UUID:UUIDLen/binary,
|
||||
Timestamp:64/unsigned-big, _Nonce:?HEARTBEAT_NONCE_BYTES/binary, _Mac:?HEARTBEAT_MAC_BYTES/binary>>)
|
||||
when UUIDLen > 0 ->
|
||||
PayloadSize = byte_size(Packet) - ?HEARTBEAT_MAC_BYTES,
|
||||
<<Payload:PayloadSize/binary, Mac:?HEARTBEAT_MAC_BYTES/binary>> = Packet,
|
||||
{ok, UUID, Timestamp, Payload, Mac};
|
||||
decode_heartbeat_packet(_Packet) ->
|
||||
error.
|
||||
@ -1,34 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 18. 8月 2025 18:40
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(http_client).
|
||||
-author("anlicheng").
|
||||
|
||||
%% API
|
||||
-export([post/3]).
|
||||
|
||||
%% Headers = [
|
||||
%% {<<"content-type">>, <<"application/json">>}
|
||||
%% ]
|
||||
-spec post(Url :: string(), Headers :: list(), Body :: binary()) -> {ok, RespBody :: binary()} | {error, Reason :: any()}.
|
||||
post(Url, Headers, Body) when is_list(Url), is_list(Headers), is_binary(Body) ->
|
||||
case hackney:request(post, Url, Headers, Body, [{pool, false}]) of
|
||||
{ok, 200, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
lager:debug("[iot_api] send body: ~p, get error is: ~p", [Body, RespBody]),
|
||||
hackney:close(ClientRef),
|
||||
{ok, RespBody};
|
||||
{ok, HttpCode, _, ClientRef} ->
|
||||
{ok, RespBody} = hackney:body(ClientRef),
|
||||
hackney:close(ClientRef),
|
||||
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, {HttpCode, RespBody}]),
|
||||
{error, {HttpCode, RespBody}};
|
||||
{error, Reason} ->
|
||||
lager:warning("[iot_api] send body: ~p, get error is: ~p", [Body, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
@ -1,97 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 12. 8月 2025 15:12
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_mnesia).
|
||||
-author("aresei").
|
||||
-include("endpoint.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
-define(TAB, endpoint).
|
||||
|
||||
%% API
|
||||
-export([create_table/0]).
|
||||
-export([insert/1, delete/1, check_name/1]).
|
||||
-export([get_endpoint/1]).
|
||||
-export([as_map/1]).
|
||||
|
||||
create_table() ->
|
||||
%% id生成器
|
||||
mnesia:create_table(endpoint, [
|
||||
{attributes, record_info(fields, endpoint)},
|
||||
{record_name, endpoint},
|
||||
{disc_copies, [node()]},
|
||||
{type, ordered_set}
|
||||
]).
|
||||
|
||||
-spec check_name(Name :: binary()) -> boolean() | {error, Reason :: any()}.
|
||||
check_name(Name) when is_binary(Name) ->
|
||||
Fun = fun() ->
|
||||
Q = qlc:q([E || E <- mnesia:table(?TAB), E#endpoint.name =:= Name]),
|
||||
case qlc:e(Q) of
|
||||
[] ->
|
||||
false;
|
||||
[_|_] ->
|
||||
true
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(Fun) of
|
||||
{'atomic', Res} ->
|
||||
Res;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec get_endpoint(Id :: integer()) -> error | {ok, Endpoint :: #endpoint{}}.
|
||||
get_endpoint(Id) when is_integer(Id) ->
|
||||
case mnesia:dirty_read(?TAB, Id) of
|
||||
[] ->
|
||||
error;
|
||||
[Endpoint | _] ->
|
||||
{ok, Endpoint}
|
||||
end.
|
||||
|
||||
-spec insert(Endpoint :: #endpoint{}) -> ok | {error, Reason :: term()}.
|
||||
insert(Endpoint = #endpoint{}) ->
|
||||
case mnesia:transaction(fun() -> mnesia:write(?TAB, Endpoint, write) end) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec delete(Id :: integer()) -> ok | {error, Reason :: any()}.
|
||||
delete(Id) when is_integer(Id) ->
|
||||
case mnesia:transaction(fun() -> mnesia:delete(?TAB, Id, write) end) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec as_map(Endpoint :: #endpoint{}) -> map().
|
||||
as_map(#endpoint{id = Id, name = Name, title = Title, config = Config, updated_at = UpdateTs, created_at = CreateTs}) ->
|
||||
{ConfigKey, ConfigMap} =
|
||||
case Config of
|
||||
#http_endpoint{url = Url, pool_size = PoolSize} ->
|
||||
{<<"http">>, #{<<"url">> => Url, <<"pool_size">> => PoolSize}};
|
||||
#mqtt_endpoint{host = Host, port = Port, client_id = ClientId, username = Username, password = Password, topic = Topic, qos = Qos} ->
|
||||
{<<"mqtt">>, #{<<"host">> => Host, <<"port">> => Port, <<"client_id">> => ClientId, <<"username">> => Username, <<"password">> => Password, <<"topic">> => Topic, <<"qos">> => Qos}};
|
||||
#kafka_endpoint{username = Username, password = Password, bootstrap_servers = BootstrapServers, topic = Topic} ->
|
||||
{<<"kafka">>, #{<<"username">> => Username, <<"password">> => Password, <<"bootstrap_servers">> => BootstrapServers, <<"topic">> => Topic}};
|
||||
#mysql_endpoint{host = Host, port = Port, username = Username, password = Password, database = Database, table_name = TableName} ->
|
||||
{<<"mysql">>, #{<<"host">> => Host, <<"port">> => Port, <<"username">> => Username, <<"password">> => Password, <<"database">> => Database, <<"table_name">> => TableName}}
|
||||
end,
|
||||
|
||||
Map = #{
|
||||
<<"id">> => Id,
|
||||
<<"name">> => Name,
|
||||
<<"title">> => Title,
|
||||
<<"update_ts">> => UpdateTs,
|
||||
<<"create_ts">> => CreateTs
|
||||
},
|
||||
Map#{ConfigKey => ConfigMap}.
|
||||
@ -1,214 +0,0 @@
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 06. 7月 2023 12:02
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(endpoint_mysql).
|
||||
|
||||
-include("endpoint.hrl").
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/3]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
%% 消息重发间隔
|
||||
-define(RETRY_INTERVAL, 5000).
|
||||
|
||||
-define(DISCONNECTED, disconnected).
|
||||
-define(CONNECTED, connected).
|
||||
|
||||
-record(state, {
|
||||
endpoint :: #endpoint{},
|
||||
buffer :: endpoint_buffer:buffer(),
|
||||
pool_pid :: undefined | pid(),
|
||||
|
||||
status = ?DISCONNECTED
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||
%% initialize. To ensure a synchronized start-up procedure, this
|
||||
%% function does not return until Module:init/1 has returned.
|
||||
start_link(LocalName, AliasName, Endpoint = #endpoint{}) when is_atom(LocalName), is_atom(AliasName) ->
|
||||
gen_statem:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_statem callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||
%% process to initialize.
|
||||
init([AliasName, Endpoint]) ->
|
||||
iot_name_server:register(AliasName, self()),
|
||||
erlang:process_flag(trap_exit, true),
|
||||
%% 创建转发器, 避免阻塞当前进程的创建,因此采用了延时初始化的机制
|
||||
erlang:start_timer(0, self(), create_postman),
|
||||
%% 初始化存储
|
||||
Buffer = endpoint_buffer:new(Endpoint, 10),
|
||||
|
||||
{ok, #state{endpoint = Endpoint, buffer = Buffer, status = ?DISCONNECTED}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||
State :: #state{}) ->
|
||||
{reply, Reply :: term(), NewState :: #state{}} |
|
||||
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_call(get_stat, _From, State = #state{buffer = Buffer}) ->
|
||||
Stat = endpoint_buffer:stat(Buffer),
|
||||
{reply, {ok, Stat}, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling cast messages
|
||||
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_cast({forward, ServiceId, Format, Metric}, State = #state{buffer = Buffer}) ->
|
||||
NBuffer = endpoint_buffer:append({ServiceId, Format, Metric}, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}};
|
||||
|
||||
handle_cast(cleanup, State = #state{buffer = Buffer}) ->
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling all non call/cast messages
|
||||
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({timeout, _, create_postman}, State = #state{status = ?DISCONNECTED, buffer = Buffer,
|
||||
endpoint = #endpoint{title = Title, config = #mysql_endpoint{host = Host, port = Port, username = Username, password = Password, database = Database}}}) ->
|
||||
|
||||
lager:debug("[iot_endpoint] endpoint: ~p, create postman", [Title]),
|
||||
WorkerArgs = [
|
||||
{host, binary_to_list(Host)},
|
||||
{port, Port},
|
||||
{user, binary_to_list(Username)},
|
||||
{password, binary_to_list(Password)},
|
||||
{keep_alive, true},
|
||||
{database, binary_to_list(Database)},
|
||||
{queries, [<<"set names utf8">>]}
|
||||
],
|
||||
|
||||
%% 启动工作的线程池
|
||||
PoolSize = 5,
|
||||
case poolboy:start_link([{size, PoolSize}, {max_overflow, PoolSize}, {worker_module, mysql}], WorkerArgs) of
|
||||
{ok, PoolPid} ->
|
||||
NBuffer = endpoint_buffer:trigger_n(Buffer),
|
||||
{noreply, State#state{pool_pid = PoolPid, buffer = NBuffer, status = ?CONNECTED}};
|
||||
ignore ->
|
||||
retry_connect(),
|
||||
{noreply, State};
|
||||
{error, Reason} ->
|
||||
lager:warning("[mqtt_postman] start connect pool, get error: ~p", [Reason]),
|
||||
retry_connect(),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% 离线时,忽略数据发送逻辑
|
||||
handle_info({next_data, _Id, _Tuple}, State = #state{status = ?DISCONNECTED}) ->
|
||||
{noreply, State};
|
||||
%% 发送数据到mqtt服务器
|
||||
handle_info({next_data, Id, {ServiceId, Metric}}, State = #state{status = ?CONNECTED, pool_pid = PoolPid, buffer = Buffer,
|
||||
endpoint = #endpoint{title = Title, config = #mysql_endpoint{table_name = Table, fields_map = FieldsMap}}}) ->
|
||||
|
||||
case insert_sql(Table, ServiceId, FieldsMap, Metric) of
|
||||
{ok, InsertSql, Values} ->
|
||||
case poolboy:transaction(PoolPid, fun(ConnPid) -> mysql:query(ConnPid, InsertSql, Values) end) of
|
||||
ok ->
|
||||
NBuffer = endpoint_buffer:ack(Id, Buffer),
|
||||
{noreply, State#state{buffer = NBuffer}};
|
||||
Error ->
|
||||
lager:warning("[endpoint_mysql] endpoint: ~p, insert mysql get error: ~p", [Title, Error]),
|
||||
{noreply, State}
|
||||
end;
|
||||
error ->
|
||||
lager:debug("[endpoint_mysql] endpoint: ~p, make sql error", [Title]),
|
||||
{noreply, State}
|
||||
end;
|
||||
|
||||
%% postman进程挂掉时,重新建立新的
|
||||
handle_info({'EXIT', PoolPid, Reason}, State = #state{endpoint = #endpoint{title = Title}, pool_pid = PoolPid}) ->
|
||||
lager:warning("[enpoint_mqtt] endpoint: ~p, conn pid exit with reason: ~p", [Title, Reason]),
|
||||
retry_connect(),
|
||||
{noreply, disconnected, State#state{pool_pid = undefined, status = ?DISCONNECTED}};
|
||||
|
||||
handle_info(Info, State = #state{status = Status}) ->
|
||||
lager:warning("[iot_endpoint] unknown message: ~p, status: ~p", [Info, Status]),
|
||||
{noreply, State}.
|
||||
|
||||
%% @private
|
||||
%% @doc This function is called by a gen_server when it is about to
|
||||
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||
%% with Reason. The return value is ignored.
|
||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||
State :: #state{}) -> term()).
|
||||
terminate(Reason, #state{endpoint = #endpoint{title = Title}, buffer = Buffer}) ->
|
||||
lager:debug("[iot_endpoint] endpoint: ~p, terminate with reason: ~p", [Title, Reason]),
|
||||
endpoint_buffer:cleanup(Buffer),
|
||||
ok.
|
||||
|
||||
%% @private
|
||||
%% @doc Convert process state when code is changed
|
||||
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||
Extra :: term()) ->
|
||||
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
retry_connect() ->
|
||||
erlang:start_timer(?RETRY_INTERVAL, self(), create_postman).
|
||||
|
||||
-spec insert_sql(Table :: binary(), ServiceId :: binary(), FieldsMap :: map(), Metric :: binary()) ->
|
||||
error | {ok, Sql :: binary(), Values :: list()}.
|
||||
insert_sql(Table, ServiceId, FieldsMap, Metric) when is_binary(Table), is_binary(ServiceId), is_binary(Metric) ->
|
||||
case line_format:parse(Metric) of
|
||||
error ->
|
||||
error;
|
||||
{ok, #{<<"measurement">> := Measurement, <<"tags">> := Tags, <<"fields">> := Fields, <<"timestamp">> := Timestamp}} ->
|
||||
Map = maps:merge(Tags, Fields),
|
||||
NMap = Map#{<<"measurement">> => Measurement, <<"timestamp">> => Timestamp},
|
||||
TableFields = lists:flatmap(fun({TableField, F}) ->
|
||||
case maps:find(F, NMap) of
|
||||
error ->
|
||||
[];
|
||||
{ok, Val} ->
|
||||
[{TableField, Val}]
|
||||
end
|
||||
end, maps:to_list(FieldsMap)),
|
||||
|
||||
{Keys, Values} = kvs(TableFields),
|
||||
FieldSql = iolist_to_binary(lists:join(<<", ">>, Keys)),
|
||||
Placeholders = lists:duplicate(length(Keys), <<"?">>),
|
||||
ValuesPlaceholder = iolist_to_binary(lists:join(<<", ">>, Placeholders)),
|
||||
|
||||
{ok, <<"INSERT INTO ", Table/binary, "(", FieldSql/binary, ") VALUES(", ValuesPlaceholder/binary, ")">>, Values}
|
||||
end.
|
||||
|
||||
-spec kvs(Fields :: list()) -> {Keys :: list(), Values :: list()}.
|
||||
kvs(Fields) when is_list(Fields) ->
|
||||
{Keys0, Values0} = lists:foldl(fun({K, V}, {Acc0, Acc1}) -> {[K|Acc0], [V|Acc1]} end, {[], []}, Fields),
|
||||
{lists:reverse(Keys0), lists:reverse(Values0)}.
|
||||
@ -1,121 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 04. 7月 2023 12:31
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(service_config_model).
|
||||
-author("aresei").
|
||||
-include("iot_tables.hrl").
|
||||
-include_lib("stdlib/include/qlc.hrl").
|
||||
|
||||
-define(TAB, service_config).
|
||||
|
||||
%% API
|
||||
-export([create_table/0]).
|
||||
-export([insert/4, update/4, get_config/1, delete/1]).
|
||||
-export([as_map/1]).
|
||||
|
||||
create_table() ->
|
||||
%% id生成器
|
||||
mnesia:create_table(service_config, [
|
||||
{attributes, record_info(fields, service_config)},
|
||||
{record_name, service_config},
|
||||
{disc_copies, [node()]},
|
||||
{type, ordered_set}
|
||||
]).
|
||||
|
||||
-spec insert(ServiceId :: binary(), HostUUID :: binary(), ConfigJson :: binary(), LastEditUser :: integer()) -> ok | {error, Reason :: term()}.
|
||||
insert(ServiceId, HostUUID, ConfigJson, LastEditUser) when is_binary(ServiceId), is_binary(HostUUID), is_binary(ConfigJson), is_integer(LastEditUser) ->
|
||||
ServiceConfig = #service_config{
|
||||
service_id = ServiceId,
|
||||
host_uuid = HostUUID,
|
||||
config_json = ConfigJson,
|
||||
last_config_json = <<>>,
|
||||
last_edit_user = LastEditUser,
|
||||
create_ts = iot_util:current_time(),
|
||||
update_ts = iot_util:current_time()
|
||||
},
|
||||
case mnesia:transaction(fun() -> mnesia:write(?TAB, ServiceConfig, write) end) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec update(ServiceId :: binary(), HostUUID :: binary(), ConfigJson :: binary(), LastEditUser :: integer()) -> ok | {error, Reason :: term()}.
|
||||
update(ServiceId, HostUUID, ConfigJson, LastEditUser) when is_binary(ServiceId), is_binary(HostUUID), is_binary(ConfigJson), is_integer(LastEditUser) ->
|
||||
Fun = fun() ->
|
||||
case mnesia:read(?TAB, ServiceId, write) of
|
||||
[] ->
|
||||
ServiceConfig = #service_config{
|
||||
service_id = ServiceId,
|
||||
host_uuid = HostUUID,
|
||||
config_json = ConfigJson,
|
||||
last_config_json = <<>>,
|
||||
last_edit_user = LastEditUser,
|
||||
create_ts = iot_util:current_time(),
|
||||
update_ts = iot_util:current_time()
|
||||
},
|
||||
mnesia:write(?TAB, ServiceConfig, write);
|
||||
[ServiceConfig0 = #service_config{config_json = OldConfigJson}] ->
|
||||
NServiceConfig = ServiceConfig0#service_config{
|
||||
config_json = ConfigJson,
|
||||
last_config_json = OldConfigJson,
|
||||
last_edit_user = LastEditUser,
|
||||
update_ts = iot_util:current_time()
|
||||
},
|
||||
mnesia:write(?TAB, NServiceConfig, write)
|
||||
end
|
||||
end,
|
||||
|
||||
case mnesia:transaction(Fun) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec get_config(ServiceId :: any()) -> error | {ok, Config :: #service_config{}}.
|
||||
get_config(ServiceId) when is_binary(ServiceId) ->
|
||||
case mnesia:dirty_read(?TAB, ServiceId) of
|
||||
[] ->
|
||||
error;
|
||||
[Config] ->
|
||||
{ok, Config}
|
||||
end.
|
||||
|
||||
-spec delete(ServiceId :: binary()) -> ok | {error, Reason :: any()}.
|
||||
delete(ServiceId) when is_binary(ServiceId) ->
|
||||
Fun = fun() ->
|
||||
case mnesia:read(?TAB, ServiceId, write) of
|
||||
[] ->
|
||||
ok;
|
||||
[ServiceConfig0 = #service_config{config_json = OldConfigJson}] ->
|
||||
NServiceConfig = ServiceConfig0#service_config{
|
||||
config_json = <<"">>,
|
||||
last_config_json = OldConfigJson,
|
||||
update_ts = iot_util:current_time()
|
||||
},
|
||||
mnesia:write(?TAB, NServiceConfig, write)
|
||||
end
|
||||
end,
|
||||
case mnesia:transaction(Fun) of
|
||||
{'atomic', ok} ->
|
||||
ok;
|
||||
{'aborted', Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
-spec as_map(ServiceConfig :: #service_config{}) -> map().
|
||||
as_map(#service_config{service_id = ServiceId, config_json = ConfigJson, last_config_json = LastConfigJson, last_edit_user = LastEditUser, update_ts = UpdateTs, create_ts = CreateTs}) ->
|
||||
#{
|
||||
<<"service_id">> => ServiceId,
|
||||
<<"config_json">> => ConfigJson,
|
||||
<<"last_config_json">> => LastConfigJson,
|
||||
<<"last_edit_user">> => LastEditUser,
|
||||
<<"update_ts">> => UpdateTs,
|
||||
<<"create_ts">> => CreateTs
|
||||
}.
|
||||
@ -1,176 +0,0 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author licheng5
|
||||
%%% @copyright (C) 2020, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 26. 4月 2020 3:36 下午
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(service_handler).
|
||||
-author("licheng5").
|
||||
-include("iot.hrl").
|
||||
|
||||
%% API
|
||||
-export([handle_request/4]).
|
||||
|
||||
%% 下发config.json, 微服务接受后,保存服务配置
|
||||
handle_request("POST", "/service/push_config", _,
|
||||
#{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"last_edit_user">> := LastEditUser, <<"config_json">> := ConfigJson, <<"timeout">> := Timeout0})
|
||||
when is_binary(UUID), is_binary(ServiceId), is_binary(ConfigJson), is_integer(Timeout0) ->
|
||||
|
||||
%% 检查ConfigJson是否是合法的json字符串
|
||||
case iot_util:is_json(ConfigJson) of
|
||||
true ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
Timeout = Timeout0 * 1000,
|
||||
case iot_host:async_service_config(Pid, ServiceId, ConfigJson, Timeout) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, Timeout) of
|
||||
{ok, Result} ->
|
||||
%% 更新配置信息到数据库
|
||||
case service_config_model:update(ServiceId, UUID, ConfigJson, LastEditUser) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[service_handler] set_config service_id: ~p, get error: ~p", [ServiceId, Reason]),
|
||||
{ok, 200, iot_util:json_error(-1, <<"set service config failed">>)}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(-1, Reason)}
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"config is invalid json">>)}
|
||||
end;
|
||||
|
||||
%% 获取服务配置信息
|
||||
handle_request("GET", "/service/get_config", #{<<"service_id">> := ServiceId}, _) when is_binary(ServiceId) ->
|
||||
case service_config_model:get_config(ServiceId) of
|
||||
error ->
|
||||
{ok, 200, iot_util:json_error(-1, <<"service config not found">>)};
|
||||
{ok, Config} ->
|
||||
{ok, 200, iot_util:json_data(service_config_model:as_map(Config))}
|
||||
end;
|
||||
|
||||
%% 删除对应的主机信息
|
||||
handle_request("POST", "/service/delete_config", _, #{<<"service_id">> := ServiceId}) when is_binary(ServiceId) ->
|
||||
case service_config_model:delete(ServiceId) of
|
||||
ok ->
|
||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||
{error, Reason} ->
|
||||
lager:debug("[service_handler] delete config of service_id: ~p, error: ~p", [ServiceId, Reason]),
|
||||
{ok, 200, iot_util:json_error(-1, <<"delete service config errror">>)}
|
||||
end;
|
||||
|
||||
%% 部署微服务
|
||||
handle_request("POST", "/service/deploy", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId, <<"service_id">> := ServiceId, <<"tar_url">> := TarUrl})
|
||||
when is_binary(UUID), is_integer(TaskId), is_binary(ServiceId), is_binary(TarUrl) ->
|
||||
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:deploy_service(Pid, TaskId, ServiceId, TarUrl) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 启动服务
|
||||
handle_request("POST", "/service/start", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:start_service(Pid, ServiceId) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 停止服务
|
||||
handle_request("POST", "/service/stop", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId}) when is_binary(UUID), is_binary(ServiceId) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:stop_service(Pid, ServiceId) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
%% 远程调用微服务, 返回值的格式为json
|
||||
handle_request("POST", "/service/invoke", _, #{<<"uuid">> := UUID, <<"service_id">> := ServiceId, <<"payload">> := Payload, <<"timeout">> := Timeout0})
|
||||
when is_binary(UUID), is_binary(ServiceId), is_binary(Payload), is_integer(Timeout0) ->
|
||||
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
Timeout = Timeout0 * 1000,
|
||||
case iot_host:invoke_service(Pid, ServiceId, Payload, Timeout) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, Timeout) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request("POST", "/service/task_log", _, #{<<"uuid">> := UUID, <<"task_id">> := TaskId}) when is_binary(UUID), is_integer(TaskId) ->
|
||||
case iot_host:get_pid(UUID) of
|
||||
undefined ->
|
||||
{ok, 200, iot_util:json_error(404, <<"host not found">>)};
|
||||
Pid when is_pid(Pid) ->
|
||||
case iot_host:task_log(Pid, TaskId) of
|
||||
{ok, Ref} ->
|
||||
case iot_host:await_reply(Ref, 5000) of
|
||||
{ok, Result} ->
|
||||
{ok, 200, iot_util:json_data(Result)};
|
||||
{error, Reason} ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end;
|
||||
{error, Reason} when is_binary(Reason) ->
|
||||
{ok, 200, iot_util:json_error(400, Reason)}
|
||||
end
|
||||
end;
|
||||
|
||||
handle_request(_, Path, _, _) ->
|
||||
Path1 = list_to_binary(Path),
|
||||
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% helper methods
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
@ -1,110 +0,0 @@
|
||||
[
|
||||
{iot, [
|
||||
{http_server, [
|
||||
{port, 18090},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{tcp_server, [
|
||||
{port, 18092},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{redis_server, [
|
||||
{port, 16379},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{udp_server, [
|
||||
{port, 18080}
|
||||
]},
|
||||
|
||||
{api_url, "http://39.98.184.67:8800/api/v1/taskLog"},
|
||||
|
||||
%% 目标服务器地址
|
||||
{emqx_server, [
|
||||
{host, {39, 98, 184, 67}},
|
||||
{port, 1883},
|
||||
{tcp_opts, []},
|
||||
{username, "test"},
|
||||
{password, "test1234"},
|
||||
{keepalive, 86400},
|
||||
{retry_interval, 5}
|
||||
]},
|
||||
|
||||
%% 权限检验时的预埋token
|
||||
{pre_tokens, [
|
||||
{<<"test">>, <<"iot2023">>}
|
||||
]},
|
||||
|
||||
{pools, [
|
||||
%% mysql连接池配置
|
||||
{mysql_iot,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
|
||||
[
|
||||
{host, "47.111.101.3"},
|
||||
{port, 3306},
|
||||
{user, "root"},
|
||||
{connect_mode, synchronous},
|
||||
{keep_alive, true},
|
||||
{password, "r3a-7Qrh#3Q"},
|
||||
{database, "nannong_demo"}
|
||||
]
|
||||
},
|
||||
|
||||
%% redis连接池
|
||||
{redis_pool,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, eredis}],
|
||||
[
|
||||
{host, "127.0.0.1"},
|
||||
{port, 6379},
|
||||
{database, 1}
|
||||
]
|
||||
}
|
||||
]}
|
||||
|
||||
]},
|
||||
|
||||
|
||||
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||
{lager, [
|
||||
{colored, true},
|
||||
%% Whether to write a crash log, and where. Undefined means no crash logger.
|
||||
{crash_log, "trade_hub.crash.log"},
|
||||
%% Maximum size in bytes of events in the crash log - defaults to 65536
|
||||
{crash_log_msg_size, 65536},
|
||||
%% Maximum size of the crash log in bytes, before its rotated, set
|
||||
%% to 0 to disable rotation - default is 0
|
||||
{crash_log_size, 10485760},
|
||||
%% What time to rotate the crash log - default is no time
|
||||
%% rotation. See the README for a description of this format.
|
||||
{crash_log_date, "$D0"},
|
||||
%% Number of rotated crash logs to keep, 0 means keep only the
|
||||
%% current one - default is 0
|
||||
{crash_log_count, 5},
|
||||
%% Whether to redirect error_logger messages into lager - defaults to true
|
||||
{error_logger_redirect, true},
|
||||
|
||||
%% How big the gen_event mailbox can get before it is switched into sync mode
|
||||
{async_threshold, 20},
|
||||
%% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold'
|
||||
%% to async_threshold - async_threshold_window
|
||||
{async_threshold_window, 5},
|
||||
|
||||
{handlers, [
|
||||
%% debug | info | warning | error, 日志级别
|
||||
{lager_console_backend, debug},
|
||||
{lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]}
|
||||
]}
|
||||
]}
|
||||
|
||||
].
|
||||
@ -1,85 +0,0 @@
|
||||
[
|
||||
{iot, [
|
||||
{http_server, [
|
||||
{port, 18080},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{redis_server, [
|
||||
{port, 16379},
|
||||
{acceptors, 500},
|
||||
{max_connections, 10240},
|
||||
{backlog, 10240}
|
||||
]},
|
||||
|
||||
{udp_server, [
|
||||
{port, 18080}
|
||||
]},
|
||||
|
||||
%% 权限检验时的预埋token
|
||||
{pre_tokens, [
|
||||
{<<"test">>, <<"iot2023">>}
|
||||
]},
|
||||
|
||||
{api_url, "https://lgsiot.njau.edu.cn/api/v1/taskLog"},
|
||||
|
||||
{influxdb, [
|
||||
{host, "172.19.0.4"},
|
||||
{port, 8086},
|
||||
{token, <<"A-ZRjqMK_7NR45lXXEiR7AEtYCd1ETzq9Z61FTMQLb5O4-1hSf8sCrjdPB84e__xsrItKHL3qjJALgbYN-H_VQ==">>}
|
||||
]},
|
||||
|
||||
{pools, [
|
||||
%% redis连接池
|
||||
{redis_pool,
|
||||
[{size, 10}, {max_overflow, 20}, {worker_module, eredis}],
|
||||
[
|
||||
{host, "172.30.6.175"},
|
||||
{port, 26379},
|
||||
{database, 1}
|
||||
]
|
||||
}
|
||||
|
||||
]}
|
||||
|
||||
]},
|
||||
|
||||
|
||||
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||
{lager, [
|
||||
{colored, true},
|
||||
%% Whether to write a crash log, and where. Undefined means no crash logger.
|
||||
{crash_log, "trade_hub.crash.log"},
|
||||
%% Maximum size in bytes of events in the crash log - defaults to 65536
|
||||
{crash_log_msg_size, 65536},
|
||||
%% Maximum size of the crash log in bytes, before its rotated, set
|
||||
%% to 0 to disable rotation - default is 0
|
||||
{crash_log_size, 10485760},
|
||||
%% What time to rotate the crash log - default is no time
|
||||
%% rotation. See the README for a description of this format.
|
||||
{crash_log_date, "$D0"},
|
||||
%% Number of rotated crash logs to keep, 0 means keep only the
|
||||
%% current one - default is 0
|
||||
{crash_log_count, 5},
|
||||
%% Whether to redirect error_logger messages into lager - defaults to true
|
||||
{error_logger_redirect, true},
|
||||
|
||||
%% How big the gen_event mailbox can get before it is switched into sync mode
|
||||
{async_threshold, 20},
|
||||
%% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold'
|
||||
%% to async_threshold - async_threshold_window
|
||||
{async_threshold_window, 5},
|
||||
|
||||
{handlers, [
|
||||
%% debug | info | warning | error, 日志级别
|
||||
{lager_console_backend, debug},
|
||||
{lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]},
|
||||
{lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]}
|
||||
]}
|
||||
]}
|
||||
|
||||
].
|
||||
82
config/sys.config.src
Normal file
82
config/sys.config.src
Normal file
@ -0,0 +1,82 @@
|
||||
[
|
||||
{iot, [
|
||||
{http_server, [
|
||||
{port, 18090},
|
||||
{acceptors, 5},
|
||||
{max_connections, 1024},
|
||||
{backlog, 1024}
|
||||
]},
|
||||
|
||||
{ssl_server, [
|
||||
{port, 1443},
|
||||
{acceptors, 5},
|
||||
{max_connections, 1024},
|
||||
{backlog, 1024}
|
||||
]},
|
||||
|
||||
{udp_server, [
|
||||
{port, 24000}
|
||||
]},
|
||||
|
||||
{ctrl_server, [
|
||||
{socket_path, "${IOT_CTRL_SOCKET_PATH:-/var/lib/iot/ctl.sock}"},
|
||||
{backlog, ${IOT_CTRL_SOCKET_BACKLOG:-128}}
|
||||
]},
|
||||
|
||||
{api_url, "${IOT_API_URL}"}
|
||||
]},
|
||||
|
||||
{endpoint, [
|
||||
%% 支持的协议
|
||||
{endpoints, [
|
||||
{root_dir, "${ENDPOINT_ROOT_DIR}"},
|
||||
{support_protocols, [
|
||||
http
|
||||
]}
|
||||
]},
|
||||
|
||||
{endpoint_log, [
|
||||
{path, "${ENDPOINT_LOG_PATH}"},
|
||||
{max_bytes, ${ENDPOINT_LOG_MAX_BYTES:-10485760}},
|
||||
{max_files, ${ENDPOINT_LOG_MAX_FILES:-10}}
|
||||
]}
|
||||
]},
|
||||
|
||||
%% 系统日志配置,使用 OTP logger
|
||||
{kernel, [
|
||||
%% 设置 Logger 的 primary log level
|
||||
{logger_level, ${IOT_LOG_LEVEL:-debug}},
|
||||
{logger, [
|
||||
{handler, default, logger_std_h,
|
||||
#{
|
||||
level => ${IOT_CONSOLE_LOG_LEVEL:-debug},
|
||||
filter_default => stop,
|
||||
filters => [
|
||||
{iot_domain, {fun logger_filters:domain/2, {log, sub, [iot]}}},
|
||||
{endpoint_domain, {fun logger_filters:domain/2, {log, sub, [endpoint]}}}
|
||||
],
|
||||
formatter => {logger_formatter, #{template => [time, " [", level, "] ", msg, "\n"]}}
|
||||
}
|
||||
},
|
||||
|
||||
{handler, disk, logger_disk_log_h,
|
||||
#{
|
||||
level => ${IOT_DISK_LOG_LEVEL:-debug},
|
||||
filter_default => stop,
|
||||
filters => [
|
||||
{iot_domain, {fun logger_filters:domain/2, {log, sub, [iot]}}},
|
||||
{endpoint_domain, {fun logger_filters:domain/2, {log, sub, [endpoint]}}}
|
||||
],
|
||||
config => #{
|
||||
file => "${IOT_DISK_LOG_FILE:-log/debug.log}",
|
||||
max_no_files => ${IOT_DISK_LOG_MAX_FILES:-10},
|
||||
max_no_bytes => ${IOT_DISK_LOG_MAX_BYTES:-524288000}
|
||||
},
|
||||
formatter => {logger_formatter, #{template => [time, " [", level, "] ", msg, "\n"]}}
|
||||
}
|
||||
}
|
||||
|
||||
]}
|
||||
]}
|
||||
|
||||
].
|
||||
@ -3,15 +3,13 @@
|
||||
-setcookie iot_cookie
|
||||
|
||||
+K true
|
||||
+A30
|
||||
+A 128
|
||||
|
||||
-mnesia dir '"/usr/local/var/mnesia/iot"'
|
||||
-mnesia dir '"${IOT_MNESIA_DIR}"'
|
||||
-mnesia dump_log_write_threshold 50000
|
||||
-mnesia dc_dump_limit 40
|
||||
|
||||
-sbt db
|
||||
|
||||
+K true
|
||||
+A 128
|
||||
+P 1048576
|
||||
+t 10485760
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user