Compare commits

...

14 Commits

Author SHA1 Message Date
537dd71e75 fix ctrl shell 2026-06-10 16:03:15 +08:00
d2b30cc995 fix ctrl shell 2026-06-10 16:01:02 +08:00
d3e89eceea fix rebar config 2026-06-10 15:56:51 +08:00
33952d176f fix env 2026-06-01 15:02:27 +08:00
27dff14451 add go command support 2026-06-01 14:31:15 +08:00
4ed3a67f29 处理配置文件 2026-05-31 16:01:32 +08:00
41dcccb7d7 切换端口 2026-05-30 22:14:22 +08:00
bbe35e3b87 fix config 2026-05-30 16:43:55 +08:00
29f5bfb17c fix endpoint_log 2026-05-30 16:31:47 +08:00
037eb775b4 处理配置文件 2026-05-30 16:16:35 +08:00
d65755b890 处理配置文件 2026-05-30 16:12:08 +08:00
3defc05815 拆分独立的enpoint 2026-05-30 15:25:40 +08:00
f96dd3fea7 fix codec 2026-01-13 11:56:02 +08:00
25bb8b0514 change to docker 2025-11-12 15:49:03 +08:00
114 changed files with 9888 additions and 7263 deletions

1167
HTTP_API_README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +0,0 @@
iot
=====
An OTP application
## erlang client sdk
https://github.com/emqx/emqtt
Build
-----
$ rebar3 compile

View File

@ -10,6 +10,7 @@
-record(http_endpoint, { -record(http_endpoint, {
url = <<>> :: binary(), url = <<>> :: binary(),
token = <<>> :: binary(),
pool_size = 10 :: integer() pool_size = 10 :: integer()
}). }).
@ -33,7 +34,7 @@
-record(endpoint, { -record(endpoint, {
id :: integer(), id :: integer(),
%% %%
name :: binary(), matcher :: binary(),
%% %%
title = <<>> :: binary(), title = <<>> :: binary(),
%% , : #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}} %% , : #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}

View 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, []}
]}.

View 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.

View 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']}.

View 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.

View 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).

View File

@ -13,7 +13,7 @@
-behaviour(gen_server). -behaviour(gen_server).
%% API %% API
-export([start_link/3]). -export([start_link/2]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -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) %% @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()}). {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link(LocalName, AliasName, Endpoint = #endpoint{config = #http_endpoint{}}) when is_atom(LocalName), is_atom(AliasName) -> start_link(LocalName, Endpoint = #endpoint{config = #http_endpoint{}}) ->
gen_server:start_link({local, LocalName}, ?MODULE, [AliasName, Endpoint], []). gen_server:start_link({via, gproc, {n, l, LocalName}}, ?MODULE, [Endpoint], []).
%%%=================================================================== %%%===================================================================
%%% gen_server callbacks %%% gen_server callbacks
@ -44,9 +44,10 @@ start_link(LocalName, AliasName, Endpoint = #endpoint{config = #http_endpoint{}}
-spec(init(Args :: term()) -> -spec(init(Args :: term()) ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore). {stop, Reason :: term()} | ignore).
init([AliasName, Endpoint]) -> init([Endpoint = #endpoint{matcher = Matcher, config = #http_endpoint{pool_size = PoolSize}}]) ->
Buffer = endpoint_buffer:new(Endpoint, 10), ok = endpoint_util:set_metadata(),
true = gproc:reg({n, l, AliasName}), endpoint_subscription:subscribe(Matcher, self()),
Buffer = endpoint_buffer:new(Endpoint, PoolSize),
{ok, #state{endpoint = Endpoint, buffer = Buffer}}. {ok, #state{endpoint = Endpoint, buffer = Buffer}}.
%% @private %% @private
@ -69,13 +70,17 @@ handle_call(get_stat, _From, State = #state{buffer = Buffer}) ->
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_cast({forward, ServiceId, Metric}, State = #state{buffer = Buffer}) -> handle_cast({forward, Metric}, State = #state{buffer = Buffer}) ->
NBuffer = endpoint_buffer:append({ServiceId, Metric}, Buffer), NBuffer = endpoint_buffer:append(Metric, Buffer),
{noreply, State#state{buffer = NBuffer}}; {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}) -> handle_cast(cleanup, State = #state{buffer = Buffer}) ->
endpoint_buffer:cleanup(Buffer), NBuffer = endpoint_buffer:cleanup(Buffer),
{noreply, State}. {noreply, State#state{buffer = NBuffer}}.
%% @private %% @private
%% @doc Handling all non call/cast messages %% @doc Handling all non call/cast messages
@ -83,28 +88,25 @@ handle_cast(cleanup, State = #state{buffer = Buffer}) ->
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_info({next_data, Id, {ServiceId, Metric}}, State = #state{buffer = Buffer, endpoint = #endpoint{config = #http_endpoint{url = Url}}}) -> handle_info({next_data, Id, Metric}, State = #state{buffer = Buffer, endpoint = #endpoint{config = #http_endpoint{url = Url, token = Token}}}) ->
Headers = [ Headers = [{<<"Content-Type">>, <<"application/json">>}] ++ patch_headers(Metric, Token),
{<<"Content-Type">>, <<"application/octet-stream">>},
{<<"Service-Id">>, ServiceId}
],
case hackney:request(post, Url, Headers, Metric) of case hackney:request(post, Url, Headers, Metric) of
{ok, 200, _, ClientRef} -> {ok, HttpCode, _, ClientRef} when HttpCode >= 200, HttpCode < 300 ->
{ok, RespBody} = hackney:body(ClientRef), RespBody = read_response_body(ClientRef),
hackney:close(ClientRef), logger:debug("[endpoint_http] url: ~p, response is: ~p", [Url, RespBody]),
lager:debug("[endpoint_http] url: ~p, response is: ~p", [Url, RespBody]),
NBuffer = endpoint_buffer:ack(Id, Buffer), NBuffer = endpoint_buffer:ack(Id, Buffer),
{noreply, State#state{buffer = NBuffer}}; {noreply, State#state{buffer = NBuffer}};
{ok, HttpCode, _, ClientRef} -> {ok, HttpCode, _, ClientRef} ->
{ok, RespBody} = hackney:body(ClientRef), RespBody = read_response_body(ClientRef),
hackney:close(ClientRef), logger:warning("[endpoint_http] url: ~p, http_code: ~p, response is: ~p", [Url, HttpCode, RespBody]),
lager:debug("[endpoint_http] url: ~p, http_code: ~p, response is: ~p", [Url, HttpCode, RespBody]),
{noreply, State}; {noreply, State};
{error, Reason} -> {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} {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 %% @private
%% @doc This function is called by a gen_server when it is about to %% @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. %% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()). State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) -> terminate(_Reason, #state{buffer = Buffer}) ->
_ = endpoint_buffer:cleanup(Buffer),
ok. ok.
%% @private %% @private
@ -127,3 +130,28 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%%=================================================================== %%%===================================================================
%%% Internal functions %%% 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(_, _) ->
[].

View 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.

View 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}.

View 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.

View 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.

View File

@ -0,0 +1,457 @@
%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY>
%%% @doc
%%% Endpoint
%%%
%%% publish
%%% exact wildcard
%%%
%%% <ul>
%%% <li> endpoint_subscription_exactpublish RouteKey
%%% ets:lookup/2</li>
%%% <li> ETS trietrie
%%% 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 noderoot node id 0 trie 使
%%% integer node idsubscribe/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)].

View 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}}.

View 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.

View 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)).

View 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()
}).

View File

@ -13,6 +13,10 @@
-define(HOST_ONLINE, 1). -define(HOST_ONLINE, 1).
-define(HOST_NOT_JOINED, -1). -define(HOST_NOT_JOINED, -1).
%%
-define(HOST_DENIED, 0).
-define(HOST_AUTHORIZED, 1).
%% 线 %% 线
-define(DEVICE_OFFLINE, 0). -define(DEVICE_OFFLINE, 0).
-define(DEVICE_ONLINE, 1). -define(DEVICE_ONLINE, 1).
@ -22,93 +26,3 @@
-define(TASK_STATUS_INIT, -1). %% -define(TASK_STATUS_INIT, -1). %%
-define(TASK_STATUS_FAILED, 0). %% 线 -define(TASK_STATUS_FAILED, 0). %% 线
-define(TASK_STATUS_OK, 1). %% 线 -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()
}).

View File

@ -1,23 +1,17 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% @author anlicheng %%% @author anlicheng
%%% @copyright (C) 2025, <COMPANY> %%% @copyright (C) 2026, <COMPANY>
%%% @doc %%% @doc
%%% %%%
%%% @end %%% @end
%%% Created : 08. 5 2025 12:08 %%% Created : 24. 4 2026 16:54
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-author("anlicheng"). -author("anlicheng").
%% -record(efka_client, {
-record(service_config, { uuid :: binary(),
service_id :: binary(), token_hash :: binary(),
host_uuid :: binary(), salt :: binary(),
config_json = <<>> :: binary(), heartbeat_secret :: binary(),
%% timestamp = 0 :: integer()
last_config_json = <<>> :: binary(), }).
%% id
last_edit_user :: integer(),
%% 0: , 1:
update_ts = 0,
create_ts = 0
}).

View File

@ -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.

View 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).

View File

@ -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}

View 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-----

View 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-----

View 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-----

View 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).

View File

@ -57,7 +57,7 @@ get_precision(Timestamp) when is_integer(Timestamp) ->
<<"ms">> <<"ms">>
end. 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) -> write_data(Measurement, Tags, FieldsList, Timestamp) when is_binary(Measurement), is_map(Tags), is_list(FieldsList), is_integer(Timestamp) ->
%% key的选项 %% key的选项
NFieldsList = lists:filter(fun data_filter/1, FieldsList), NFieldsList = lists:filter(fun data_filter/1, FieldsList),
@ -76,12 +76,12 @@ write_data(Measurement, Tags, FieldsList, Timestamp) when is_binary(Measurement)
ok ok
end. 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, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_list(Points) ->
write(Pid, Bucket, Org, <<"ms">>, Points). write(Pid, Bucket, Org, <<"ms">>, Points).
%% Precision的值为: ms|ns|s; (ms) %% 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) -> 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}). 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} | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore). {stop, Reason :: term()} | ignore).
init([InfluxProps]) -> init([InfluxProps]) ->
ok = iot_log:set_metadata(),
Token = proplists:get_value(token, InfluxProps), Token = proplists:get_value(token, InfluxProps),
Host = proplists:get_value(host, InfluxProps), Host = proplists:get_value(host, InfluxProps),
Port = proplists:get_value(port, InfluxProps), Port = proplists:get_value(port, InfluxProps),
@ -151,18 +152,18 @@ handle_cast({write, Bucket, Org, Precision, Points}, State = #state{host = Host,
query => Query 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 case hackney:request(post, Url, Headers, GZipBody, [{pool, false}]) of
{ok, StatusCode, _RespHeaders, ClientRef} -> {ok, StatusCode, _RespHeaders, ClientRef} ->
case hackney:body(ClientRef) of case hackney:body(ClientRef) of
{ok, RespBody} -> {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} -> {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, end,
hackney:close(ClientRef); hackney:close(ClientRef);
{error, Reason} -> {error, Reason} ->
lager:warning("[influx_client] request result is: ~p", [Reason]) logger:warning("[influx_client] request result is: ~p", [Reason])
end, end,
{noreply, State}. {noreply, State}.

View File

@ -45,7 +45,7 @@ field_val(V) when is_float(V) ->
field_val(V) when is_binary(V) -> field_val(V) when is_binary(V) ->
<<$", V/binary, $">>; <<$", V/binary, $">>;
field_val(V) when is_list(V); is_map(V) -> 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, $">>; <<$", "base64:", S/binary, $">>;
field_val(true) -> field_val(true) ->
<<"true">>; <<"true">>;
@ -56,4 +56,3 @@ as_list(L) when is_list(L) ->
L; L;
as_list(L) when is_map(L) -> as_list(L) when is_map(L) ->
maps:to_list(L). maps:to_list(L).

View 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.

View 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])).

View 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.

View File

@ -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).

View File

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

View File

@ -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).

View File

@ -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).

View File

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

View File

@ -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).

View File

@ -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.

View File

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

View File

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

View 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]).

View File

@ -4,56 +4,42 @@
%%% @doc %%% @doc
%%% %%%
%%% @end %%% @end
%%% Created : 17. 8 2025 00:26 %%% Created : 17. 11 2025 16:48
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(iot_name_server). -module(endpoint_mqtt_subscriber).
-author("anlicheng"). -author("anlicheng").
-author("aresei").
-include("iot.hrl").
-behaviour(gen_server). -behaviour(gen_server).
%% API %% API
-export([start_link/0]). -export([start_link/0]).
-export([whereis_alias/1, register/2]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE). -define(SERVER, ?MODULE).
-define(TAB, iot_name_server).
%%
-define(Topics,[
{<<"/dhlr/data">>, 0}
]).
-record(state, { -record(state, {
%% #{Pid => Name} conn_pid :: pid()
pid_names = #{},
refs = []
}). }).
%%%=================================================================== %%%===================================================================
%%% API %%% 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) %% @doc Spawns the server and registers the local name (unique)
-spec(start_link() -> -spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}). {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() -> start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%%%=================================================================== %%%===================================================================
%%% gen_server callbacks %%% gen_server callbacks
@ -65,9 +51,41 @@ start_link() ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore). {stop, Reason :: term()} | ignore).
init([]) -> init([]) ->
%% %% emqx服务器的连接
ets:new(?TAB, [named_table, ordered_set, public, {keypos, 1}]), ClientId = <<"mqtt-client-test-host-subscriber">>,
{ok, #state{}}. 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 %% @private
%% @doc Handling call messages %% @doc Handling call messages
@ -79,10 +97,8 @@ init([]) ->
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_call({register, Name, Pid}, _From, State = #state{refs = Refs, pid_names = PidNames}) -> handle_call(_Info, _From, State = #state{conn_pid = _ConnPid}) ->
true = ets:insert(?TAB, {Name, Pid}), {reply, ok, State}.
MRef = erlang:monitor(process, Pid),
{reply, ok, State#state{refs = [MRef|Refs], pid_names = maps:put(Pid, Name, PidNames)}}.
%% @private %% @private
%% @doc Handling cast messages %% @doc Handling cast messages
@ -99,20 +115,21 @@ handle_cast(_Request, State = #state{}) ->
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_info({'DOWN', MRef, process, Pid, Reason}, State = #state{refs = Refs, pid_names = PidNames}) -> handle_info({disconnect, ReasonCode, Properties}, State = #state{}) ->
% lager:debug("[iot_name_server] pid: ~p, down with reason: ~p", [Reason]), logger:debug("[iot_mqtt_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
case lists:member(MRef, Refs) of {stop, disconnected, State};
true -> %% json反序列需要在host进程进行
case maps:take(Pid, PidNames) of handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) ->
error -> logger:debug("[iot_mqtt_subscriber] Recv a topic: ~p, publish packet: ~p, qos: ~p", [Topic, Payload, Qos]),
{noreply, State#state{refs = lists:delete(MRef, Refs)}}; %% host进程去处理
{Name, NPidNames} -> {noreply, State};
true = ets:delete(?TAB, Name), handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
{noreply, State#state{pid_names = NPidNames, refs = lists:delete(MRef, Refs)}} logger:debug("[iot_mqtt_subscriber] receive puback packet: ~p", [Packet]),
end; {noreply, State};
false ->
{noreply, State} handle_info(Info, State = #state{}) ->
end. logger:debug("[iot_mqtt_subscriber] get info: ~p", [Info]),
{noreply, State}.
%% @private %% @private
%% @doc This function is called by a gen_server when it is about to %% @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. %% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> 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. ok.
%% @private %% @private
@ -134,4 +160,4 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%%=================================================================== %%%===================================================================
%%% Internal functions %%% Internal functions
%%%=================================================================== %%%===================================================================

View File

@ -13,13 +13,5 @@
-export([test/0]). -export([test/0]).
test() -> test() ->
{error, disabled}.
{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">>}).

View File

@ -12,8 +12,7 @@
%% API %% API
-export([rsa_encode/1]). -export([rsa_encode/1]).
-export([insert_services/1]). -export([test_influxdb/0]).
-export([test_mqtt/0, test_influxdb/0]).
test_influxdb() -> test_influxdb() ->
UUID = <<"device123123">>, UUID = <<"device123123">>,
@ -29,41 +28,20 @@ test_influxdb() ->
end) end)
end, lists:seq(1, 100)). 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) -> rsa_encode(Data) when is_binary(Data) ->
%% %%
PublicPemFile = "/tmp/keys/public.pem", PublicPemFile = "/tmp/keys/public.pem",
%% %%
{ok, PubBin} = file:read_file(PublicPemFile), {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), [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), 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), EncData = public_key:encrypt_public(Data, PubKey),
lager:debug("enc data is: ~p", [EncData]), logger:debug("enc data is: ~p", [EncData]),
rsa_decode(EncData), rsa_decode(EncData),
@ -75,13 +53,13 @@ rsa_decode(EncData) when is_binary(EncData) ->
%% %%
{ok, PubBin} = file:read_file(PublicPemFile), {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), [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), 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), PlainData = public_key:decrypt_private(EncData, PubKey),
lager:debug("plain data is: ~p", [PlainData]), logger:debug("plain data is: ~p", [PlainData]),
ok. ok.

View 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))).

View File

@ -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).

View File

@ -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
%%%===================================================================

View File

@ -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.

View File

@ -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
%%%===================================================================

View File

@ -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']}.

View File

@ -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
%%%===================================================================

View 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.

View 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])).

View 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.

View 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']}.

View 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.

View 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">>)}.

View 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">>}.

View File

@ -18,7 +18,7 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
handle_request("GET", "/host/metric", #{<<"uuid">> := UUID}, _) -> 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 case iot_host:get_pid(UUID) of
undefined -> undefined ->
{ok, 200, iot_util:json_error(404, <<"host not found">>)}; {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)} {ok, 200, iot_util:json_data(StatusInfo)}
end; 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) -> handle_request("POST", "/host/delete", _, #{<<"uuid">> := UUID}) when is_binary(UUID) ->
case iot_host_sup:delete_host(UUID) of case iot_host:get_pid(UUID) of
ok -> Pid when is_pid(Pid) ->
lager:debug("[host_handler] will delete host uuid: ~p", [UUID]), ok = iot_host_sup:delete_host(UUID),
{ok, 200, iot_util:json_data(<<"success">>)}; {ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} -> undefined ->
lager:debug("[host_handler] delete host uuid: ~p, get error is: ~p", [UUID, Reason]),
{ok, 200, iot_util:json_error(404, <<"error">>)} {ok, 200, iot_util:json_error(404, <<"error">>)}
end; 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) -> handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> := true}) when is_binary(UUID) ->
case iot_host_sup:ensured_host_started(UUID) of case iot_host_sup:ensured_host_started(UUID) of
{error, Reason} -> {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, 200, iot_util:json_error(400, <<"host not found">>)};
{ok, Pid} when is_pid(Pid) -> {ok, Pid} when is_pid(Pid) ->
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]), logger:debug("[host_handler] activate host_id: ~p, start", [UUID]),
ok = iot_host:activate(Pid, true), auth_response(iot_host:activate(Pid, true))
{ok, 200, iot_util:json_data(<<"success">>)}
end; end;
%% %%
handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> := false}) when is_binary(UUID) -> handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> := false}) when is_binary(UUID) ->
case iot_host_sup:ensured_host_started(UUID) of case iot_host_sup:ensured_host_started(UUID) of
{error, Reason} -> {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, 200, iot_util:json_error(400, <<"host not found">>)};
{ok, Pid} when is_pid(Pid) -> {ok, Pid} when is_pid(Pid) ->
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]), logger:debug("[host_handler] activate host_id: ~p, start", [UUID]),
ok = iot_host:activate(Pid, false), auth_response(iot_host:activate(Pid, false))
{ok, 200, iot_util:json_data(<<"success">>)}
end; end;
%% %%
handle_request("POST", "/host/pub", _, #{<<"uuid">> := UUID, <<"topic">> := Topic, <<"content">> := Content}) handle_request("POST", "/host/pub", _, #{<<"uuid">> := UUID, <<"topic">> := Topic, <<"qos">> := Qos0, <<"content">> := Content})
when is_binary(UUID), is_binary(Topic), is_binary(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 case iot_host_sup:ensured_host_started(UUID) of
{error, Reason} -> {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, 200, iot_util:json_error(400, <<"host not found">>)};
{ok, Pid} when is_pid(Pid) -> {ok, Pid} when is_pid(Pid) ->
ok = iot_host:pub(Pid, Topic, Content), case iot_host:pub(Pid, Topic, Qos, Content) of
{ok, 200, iot_util:json_data(<<"success">>)} ok ->
{ok, 200, iot_util:json_data(<<"success">>)};
{error, Reason} ->
{ok, 200, iot_util:json_error(400, Reason)}
end
end; end;
handle_request(_, Path, _, _) -> handle_request(_, Path, _, _) ->
@ -112,4 +98,29 @@ handle_request(_, Path, _, _) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% helper methods %% 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])).

View File

@ -6,49 +6,34 @@
%%% @end %%% @end
%%% Created : 08. 5 2025 13:00 %%% Created : 08. 5 2025 13:00
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(http_server). -module(http_protocol).
-author("anlicheng"). -author("anlicheng").
%% API %% API
-export([start/0]).
-export([init/2]). -export([init/2]).
%% http服务 -define(MAX_BODY_BYTES, 10 * 1024 * 1024).
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]).
init(Req0, Opts = [Mod|_]) -> init(Req0, Opts = [Mod|_]) ->
ok = iot_log:set_metadata(),
Method = binary_to_list(cowboy_req:method(Req0)), Method = binary_to_list(cowboy_req:method(Req0)),
Path = binary_to_list(cowboy_req:path(Req0)), Path = binary_to_list(cowboy_req:path(Req0)),
GetParams0 = cowboy_req:parse_qs(Req0), GetParams0 = cowboy_req:parse_qs(Req0),
GetParams = maps:from_list(GetParams0), 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 try Mod:handle_request(Method, Path, GetParams, PostParams) of
{ok, StatusCode, Resp} -> {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]), [Path, GetParams, PostParams, Resp]),
AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>), AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>),
Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of
@ -72,7 +57,7 @@ init(Req0, Opts = [Mod|_]) ->
}, ErrResp, Req1), }, ErrResp, Req1),
{ok, Req2, Opts}; {ok, Req2, Opts};
_:Error:Stack -> _: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, #{ Req2 = cowboy_req:reply(500, #{
<<"Content-Type">> => <<"text/html;charset=utf-8">> <<"Content-Type">> => <<"text/html;charset=utf-8">>
}, <<"Internal Server Error">>, Req1), }, <<"Internal Server Error">>, Req1),
@ -87,17 +72,18 @@ parse_body(Req0) ->
ContentType = cowboy_req:header(<<"content-type">>, Req0), ContentType = cowboy_req:header(<<"content-type">>, Req0),
case ContentType of case ContentType of
<<"application/json", _/binary>> -> <<"application/json", _/binary>> ->
{ok, Body, Req1} = read_body(Req0), case read_body(Req0) of
case Body =/= <<"">> of {ok, Body, Req1} ->
true -> decode_json_body(Body, Req1);
{ok, catch jiffy:decode(Body, [return_maps]), Req1}; {error, payload_too_large, Req1} ->
false -> {error, 413, iot_util:json_error(413, <<"payload too large">>), Req1}
{ok, #{}, Req1}
end; end;
<<"application/x-www-form-urlencoded">> -> <<"application/x-www-form-urlencoded">> ->
{ok, PostParams0, Req1} = cowboy_req:read_urlencoded_body(Req0), case cowboy_req:read_urlencoded_body(Req0) of
PostParams = maps:from_list(PostParams0), {ok, PostParams0, Req1} ->
{ok, PostParams, Req1}; PostParams = maps:from_list(PostParams0),
{ok, PostParams, Req1}
end;
_ -> _ ->
{ok, #{}, Req0} {ok, #{}, Req0}
end. end.
@ -108,7 +94,33 @@ read_body(Req) ->
read_body(Req, AccData) -> read_body(Req, AccData) ->
case cowboy_req:read_body(Req) of case cowboy_req:read_body(Req) of
{ok, Data, Req1} -> {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} -> {more, Data, Req1} ->
read_body(Req1, <<AccData/binary, Data/binary>>) NAccData = <<AccData/binary, Data/binary>>,
end. 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.

View File

@ -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">>)}.

View File

@ -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">>)}.

View File

@ -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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View File

@ -6,20 +6,16 @@
{applications, {applications,
[ [
sync, sync,
endpoint,
emqtt, emqtt,
eredis, eredis,
ranch, ranch,
cowboy, cowboy,
lager,
jiffy,
brod, brod,
parse_trans,
hackney, hackney,
poolboy, poolboy,
mysql,
gproc, gproc,
% gpb, % gpb,
esockd,
mnesia, mnesia,
crypto, crypto,
public_key, public_key,
@ -27,6 +23,7 @@
erts, erts,
runtime_tools, runtime_tools,
observer, observer,
inets,
kernel, kernel,
stdlib stdlib
]}, ]},

View File

@ -9,47 +9,100 @@
-export([start/2, stop/1]). -export([start/2, stop/1]).
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
ok = iot_log:set_metadata(),
io:setopts([{encoding, unicode}]), io:setopts([{encoding, unicode}]),
%% %%
erlang:system_flag(fullsweep_after, 16), erlang:system_flag(fullsweep_after, 16),
%% mnesia数据库
start_mnesia(),
%% http服务 iot_mnesia:safe_start(),
http_server:start(),
%% tcp服务 %% http服务 supervisor simulator API
tcp_server:start(), start_http_server(),
%% endpoints的相关依赖
start_endpoints(),
%% ssl服务
start_ssl_server(),
iot_sup:start_link(). iot_sup:start_link().
stop(_State) -> stop(_State) ->
stop_started_services(),
ok. ok.
%% internal functions %% internal functions
%% start_endpoints() ->
start_mnesia() -> EndpointInfos = iot_api_client:get_all_endpoints(),
ok = ensure_mnesia_schema(), Endpoints = lists:filtermap(fun(EndpointInfo) ->
%% case endpoint:endpoint_record(EndpointInfo) of
ok = mnesia:start(), error ->
Tables = mnesia:system_info(tables), false;
lager:debug("[iot_app] tables: ~p", [Tables]), {ok, Endpoint} ->
%% {true, Endpoint}
ok. end
end, EndpointInfos),
ok = endpoint_adapter_sup:start_endpoints(Endpoints).
-spec ensure_mnesia_schema() -> any(). start_http_server() ->
ensure_mnesia_schema() -> {ok, Props} = application:get_env(iot, http_server),
case mnesia:system_info(use_dir) of Acceptors = proplists:get_value(acceptors, Props, 50),
true -> MaxConnections = proplists:get_value(max_connections, Props, 10240),
ok; Backlog = proplists:get_value(backlog, Props, 1024),
false -> Port = proplists:get_value(port, Props),
mnesia:stop(),
case mnesia:create_schema([node()]) of Dispatcher = cowboy_router:compile([
ok -> ok; {'_', [
{error, {_, {already_exists, _}}} -> ok; {"/host/[...]", http_protocol, [host_handler]},
Error -> {"/container/[...]", http_protocol, [container_handler]},
lager:debug("[iot_app] create mnesia schema failed with error: ~p", [Error]), {"/endpoint/[...]", http_protocol, [endpoint_handler]},
throw({init_schema, Error}) {"/simulator/[...]", http_protocol, [simulator_api_handler]},
end {"/event_stream", event_stream_handler, []}
end. ]}
]),
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.

View File

@ -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.

View File

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

View File

@ -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
}.

View File

@ -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']}.

View File

@ -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
View 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]}).

View File

@ -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.

View File

@ -29,21 +29,30 @@ init([]) ->
Specs = [ Specs = [
#{ #{
id => 'iot_name_server', id => 'iot_container_task_sup',
start => {'iot_name_server', start_link, []}, start => {'iot_container_task_sup', start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['iot_name_server']
},
#{
id => 'endpoint_sup',
start => {'endpoint_sup', start_link, []},
restart => permanent, restart => permanent,
shutdown => 2000, shutdown => 2000,
type => supervisor, 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 %% internal functions
pools() -> pools() ->
{ok, Pools} = application:get_env(iot, pools), case application:get_env(iot, pools) of
lists:map(fun({Name, PoolArgs, WorkerArgs}) -> undefined ->
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs) [];
end, Pools). {ok, Pools} ->
lists:map(fun({Name, PoolArgs, WorkerArgs}) ->
poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs)
end, Pools)
end.

View File

@ -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.

View File

@ -10,14 +10,15 @@
-author("licheng5"). -author("licheng5").
%% API %% 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([timestamp/0, number_format/2, current_time/0, timestamp_of_seconds/0, float_to_binary/2, int_format/2]).
-export([step/3, chunks/2, rand_bytes/1, uuid/0, md5/1, parse_mapper/1]). -export([step/3, rand_bytes/1, uuid/0, md5/1, parse_mapper/1]).
-export([json_data/1, json_error/2, is_json/1]). -export([json_data/1, json_error/2, is_json/1]).
-export([queue_limited_in/3, assert_call/2, assert/2]). -export([queue_limited_in/3, assert_call/2, assert/2]).
-export([sha256/1]).
-spec is_json(Json :: term()) -> boolean(). -spec is_json(Json :: term()) -> boolean().
is_json(Json) when is_binary(Json) -> 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) -> Result when is_list(Result) orelse is_map(Result) ->
true; true;
_ -> _ ->
@ -60,33 +61,18 @@ step(Start, End, Step, Acc) when Start < End ->
step(_, _, _, Acc) -> step(_, _, _, Acc) ->
lists:reverse(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) -> json_data(Data) ->
jiffy:encode(#{ iolist_to_binary(json:encode(#{
<<"result">> => Data <<"result">> => Data
}, [force_utf8]). })).
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) -> json_error(ErrCode, ErrMessage) when is_integer(ErrCode) ->
jiffy:encode(#{ iolist_to_binary(json:encode(#{
<<"error">> => #{ <<"error">> => #{
<<"code">> => ErrCode, <<"code">> => ErrCode,
<<"message">> => ErrMessage <<"message">> => ErrMessage
} }
}, [force_utf8]). })).
uuid() -> uuid() ->
rand_bytes(16). rand_bytes(16).
@ -112,6 +98,14 @@ assert_call(true, Fun) ->
assert_call(false, _) -> assert_call(false, _) ->
ok. 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(). -spec md5(Str :: binary()) -> binary().
md5(Str) when is_binary(Str) -> md5(Str) when is_binary(Str) ->
list_to_binary(lists:flatten([hex(X) || <<X:4>> <= erlang:md5(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())}. -spec parse_mapper(Mapper :: binary() | string()) -> error | {ok, F :: fun((binary(), any()) -> any())}.
parse_mapper(Mapper) when is_binary(Mapper) -> parse_mapper(_Mapper) ->
parse_mapper(binary_to_list(Mapper)); %% Dynamic Erlang eval is intentionally disabled in runtime code.
parse_mapper(Mapper) when is_list(Mapper) -> error.
{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.
-spec float_to_binary(Num :: number(), integer()) -> binary(). -spec float_to_binary(Num :: number(), integer()) -> binary().
float_to_binary(V, _) when is_integer(V) -> float_to_binary(V, _) when is_integer(V) ->
@ -149,13 +134,3 @@ assert(false, F) when is_function(F) ->
F(); F();
assert(false, Msg) -> assert(false, Msg) ->
throw(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.

View 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.

View 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.

View File

@ -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.

View File

@ -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).

View File

@ -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

View 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.

View File

@ -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}.

View File

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

View 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.

View File

@ -1,37 +1,36 @@
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
%%% @author anlicheng %%% @author anlicheng
%%% @copyright (C) 2023, <COMPANY> %%% @copyright (C) 2026, <COMPANY>
%%% @doc %%% @doc
%%% %%%
%%% @end %%% @end
%%% Created : 24. 12 2023 15:42 %%% Created : 09. 5 2026 18:02
%%%------------------------------------------------------------------- %%%-------------------------------------------------------------------
-module(iot_api). -module(udp_server).
-author("anlicheng"). -author("anlicheng").
-behaviour(gen_server). -behaviour(gen_server).
-define(HEARTBEAT_VERSION, 1).
-define(HEARTBEAT_NONCE_BYTES, 16).
-define(HEARTBEAT_MAC_BYTES, 32).
%% API %% API
-export([start_link/0]). -export([start_link/0]).
-export([ai_event/1]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE). -define(SERVER, ?MODULE).
-define(API_TOKEN, <<"wv6fGyBhl*7@AsD9">>).
-record(state, { -record(state, {
socket :: gen_udp:socket()
}). }).
%%%=================================================================== %%%===================================================================
%%% API %%% 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) %% @doc Spawns the server and registers the local name (unique)
-spec(start_link() -> -spec(start_link() ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}). {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
@ -48,7 +47,12 @@ start_link() ->
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
{stop, Reason :: term()} | ignore). {stop, Reason :: term()} | ignore).
init([]) -> 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 %% @private
%% @doc Handling call messages %% @doc Handling call messages
@ -69,33 +73,7 @@ handle_call(_Request, _From, State = #state{}) ->
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
handle_cast({ai_event, Id}, State = #state{}) -> handle_cast(_Request, 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),
{noreply, State}. {noreply, State}.
%% @private %% @private
@ -104,15 +82,11 @@ handle_cast({ai_event, Id}, State = #state{}) ->
{noreply, NewState :: #state{}} | {noreply, NewState :: #state{}} |
{noreply, NewState :: #state{}, timeout() | hibernate} | {noreply, NewState :: #state{}, timeout() | hibernate} |
{stop, Reason :: term(), NewState :: #state{}}). {stop, Reason :: term(), NewState :: #state{}}).
%% Task进程挂掉 handle_info({udp, Socket, Ip, Port, Packet}, State = #state{socket = Socket}) ->
handle_info({'DOWN', _MRef, process, _Pid, normal}, State) -> handle_heartbeat_packet(Packet, {Ip, Port}),
{noreply, State}; {noreply, State};
handle_info(Info, State = #state{}) ->
handle_info({'DOWN', _MRef, process, _Pid, Reason}, State) -> logger:warning("[udp_server] ignore unknown info: ~p", [Info]),
lager:notice("[iot_api] task process down with reason: ~p", [Reason]),
{noreply, State};
handle_info(_Info, State = #state{}) ->
{noreply, State}. {noreply, State}.
%% @private %% @private
@ -122,7 +96,8 @@ handle_info(_Info, State = #state{}) ->
%% with Reason. The return value is ignored. %% with Reason. The return value is ignored.
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
State :: #state{}) -> term()). State :: #state{}) -> term()).
terminate(_Reason, _State = #state{}) -> terminate(_Reason, _State = #state{socket = Socket}) ->
gen_udp:close(Socket),
ok. ok.
%% @private %% @private
@ -136,3 +111,31 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
%%%=================================================================== %%%===================================================================
%%% Internal functions %%% 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.

View File

@ -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.

View File

@ -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}.

View File

@ -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)}.

View File

@ -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
}.

View File

@ -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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View File

@ -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}]}
]}
]}
].

View File

@ -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
View 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"]}}
}
}
]}
]}
].

View File

@ -3,15 +3,13 @@
-setcookie iot_cookie -setcookie iot_cookie
+K true +K true
+A30 +A 128
-mnesia dir '"/usr/local/var/mnesia/iot"' -mnesia dir '"${IOT_MNESIA_DIR}"'
-mnesia dump_log_write_threshold 50000 -mnesia dump_log_write_threshold 50000
-mnesia dc_dump_limit 40 -mnesia dc_dump_limit 40
-sbt db -sbt db
+K true
+A 128
+P 1048576 +P 1048576
+t 10485760 +t 10485760

Some files were not shown because too many files have changed in this diff Show More