Merge remote-tracking branch 'origin/develop'

This commit is contained in:
zhanghongtong 2019-09-06 19:18:11 +08:00
commit fcf861703f
31 changed files with 1040 additions and 1078 deletions

View File

@ -65,7 +65,6 @@ cd _rel/emqx && ./bin/emqx console
你可通过以下途径与 EMQ 社区及开发者联系: 你可通过以下途径与 EMQ 社区及开发者联系:
- [EMQX Slack](http://emqx.slack.com) - [EMQX Slack](http://emqx.slack.com)
- [Mailing Lists](<emqtt@googlegroups.com>)
- [Twitter](https://twitter.com/emqtt) - [Twitter](https://twitter.com/emqtt)
- [Forum](https://groups.google.com/d/forum/emqtt) - [Forum](https://groups.google.com/d/forum/emqtt)
- [Blog](https://medium.com/@emqtt) - [Blog](https://medium.com/@emqtt)

View File

@ -66,7 +66,6 @@ The [EMQ X Roadmap uses Github milestones](https://github.com/emqx/emqx/mileston
You can reach the EMQ community and developers via the following channels: You can reach the EMQ community and developers via the following channels:
- [EMQX Slack](http://emqx.slack.com) - [EMQX Slack](http://emqx.slack.com)
- [Mailing Lists](<emqtt@googlegroups.com>)
- [Twitter](https://twitter.com/emqtt) - [Twitter](https://twitter.com/emqtt)
- [Forum](https://groups.google.com/d/forum/emqtt) - [Forum](https://groups.google.com/d/forum/emqtt)
- [Blog](https://medium.com/@emqtt) - [Blog](https://medium.com/@emqtt)

View File

@ -1,21 +1,19 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% [ACL](https://docs.emqx.io/broker/v3/en/config.html)
%% %%
%% [ACL](http://emqtt.io/docs/v2/config.html#allow-anonymous-and-acl-file) %% -type(who() :: all | binary() |
%%
%% -type who() :: all | binary() |
%% {ipaddr, esockd_access:cidr()} | %% {ipaddr, esockd_access:cidr()} |
%% {client, binary()} | %% {client, binary()} |
%% {user, binary()}. %% {user, binary()}).
%% %%
%% -type access() :: subscribe | publish | pubsub. %% -type(access() :: subscribe | publish | pubsub).
%% %%
%% -type topic() :: binary(). %% -type(topic() :: binary()).
%% %%
%% -type rule() :: {allow, all} | %% -type(rule() :: {allow, all} |
%% {allow, who(), access(), list(topic())} | %% {allow, who(), access(), list(topic())} |
%% {deny, all} | %% {deny, all} |
%% {deny, who(), access(), list(topic())}. %% {deny, who(), access(), list(topic())}).
%%
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}. {allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.
@ -25,3 +23,4 @@
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
{allow, all}. {allow, all}.

View File

@ -204,7 +204,7 @@
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling -define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling
rap => 0, %% Retain as Publish rap => 1, %% Retain as Publish
nl => 0, %% No Local nl => 0, %% No Local
qos => 0 %% QoS qos => 0 %% QoS
}). }).

View File

@ -43,6 +43,6 @@
-define(LOG(Level, Format, Args), -define(LOG(Level, Format, Args),
begin begin
(logger:log(Level,#{},#{report_cb => fun(_) -> {'$logger_header'()++(Format), (Args)} end})) (logger:log(Level,#{},#{report_cb => fun(_) -> {'$logger_header'()++(Format), (Args)} end, mfa => {?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY}}))
end). end).

View File

@ -1,14 +1,16 @@
{application, emqx, [ {application, emqx,
[{description, "EMQ X Broker"},
{id, "emqx"}, {id, "emqx"},
{vsn, "git"}, {vsn, "git"},
{description, "EMQ X Broker"},
{modules, []}, {modules, []},
{registered, []}, {registered, []},
{applications, [kernel,stdlib,jsx,gproc,gen_rpc,esockd,cowboy, {applications, [kernel,stdlib,jsx,gproc,gen_rpc,esockd,cowboy,
sasl,os_mon]}, sasl,os_mon]},
{env, []},
{mod, {emqx_app,[]}}, {mod, {emqx_app,[]}},
{maintainers, ["Feng Lee <feng@emqx.io>"]}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},
{links, [{"Github", "https://github.com/emqx/emqx"}]} {maintainers, ["EMQ X Team <contact@emqx.io>"]},
{links, [{"Homepage", "https://emqx.io/"},
{"Github", "https://github.com/emqx/emqx"}
]}
]}. ]}.

View File

@ -33,8 +33,8 @@
, caps/1 , caps/1
]). ]).
%% for tests %% Exports for unit tests:(
-export([set/3]). -export([set_field/3]).
-export([ handle_in/2 -export([ handle_in/2
, handle_out/2 , handle_out/2
@ -45,14 +45,16 @@
, terminate/2 , terminate/2
]). ]).
%% Ensure timer -export([ received/2
-export([ensure_timer/2]). , sent/2
]).
-export([gc/3]). -import(emqx_misc,
[ run_fold/2
-import(emqx_misc, [maybe_apply/2]). , run_fold/3
, pipeline/3
-import(emqx_access_control, [check_acl/3]). , maybe_apply/2
]).
-export_type([channel/0]). -export_type([channel/0]).
@ -68,15 +70,14 @@
%% Timers %% Timers
timers :: #{atom() => disabled | maybe(reference())}, timers :: #{atom() => disabled | maybe(reference())},
%% GC State %% GC State
gc_state :: emqx_gc:gc_state(), gc_state :: maybe(emqx_gc:gc_state()),
%% OOM Policy %% OOM Policy
oom_policy :: emqx_oom:oom_policy(), oom_policy :: maybe(emqx_oom:oom_policy()),
%% Connected %% Connected
connected :: boolean(), connected :: undefined | boolean(),
%% Disonnected
disconnected :: boolean(),
%% Connected at %% Connected at
connected_at :: erlang:timestamp(), connected_at :: erlang:timestamp(),
%% Disconnected at
disconnected_at :: erlang:timestamp(), disconnected_at :: erlang:timestamp(),
%% Takeover %% Takeover
takeover :: boolean(), takeover :: boolean(),
@ -97,6 +98,10 @@
will_timer => will_message will_timer => will_message
}). }).
-define(ATTR_KEYS, [client, session, protocol, connected, connected_at, disconnected_at]).
-define(INFO_KEYS, ?ATTR_KEYS ++ [keepalive, gc_state, disconnected_at]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Init the channel %% Init the channel
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -117,22 +122,22 @@ init(ConnInfo, Options) ->
client_id => <<>>, client_id => <<>>,
mountpoint => MountPoint, mountpoint => MountPoint,
is_bridge => false, is_bridge => false,
is_superuser => false}, ConnInfo), is_superuser => false
}, ConnInfo),
EnableStats = emqx_zone:get_env(Zone, enable_stats, true), EnableStats = emqx_zone:get_env(Zone, enable_stats, true),
StatsTimer = if StatsTimer = if
EnableStats -> undefined; EnableStats -> undefined;
?Otherwise -> disabled ?Otherwise -> disabled
end, end,
GcState = emqx_gc:init(emqx_zone:get_env(Zone, force_gc_policy, false)), GcState = maybe_apply(fun emqx_gc:init/1,
OomPolicy = emqx_oom:init(emqx_zone:get_env(Zone, force_shutdown_policy)), emqx_zone:get_env(Zone, force_gc_policy)),
OomPolicy = maybe_apply(fun emqx_oom:init/1,
emqx_zone:get_env(Zone, force_shutdown_policy)),
#channel{client = Client, #channel{client = Client,
session = undefined,
protocol = undefined,
gc_state = GcState, gc_state = GcState,
oom_policy = OomPolicy, oom_policy = OomPolicy,
timers = #{stats_timer => StatsTimer}, timers = #{stats_timer => StatsTimer},
connected = false, connected = undefined,
disconnected = false,
takeover = false, takeover = false,
resuming = false, resuming = false,
pendings = [] pendings = []
@ -145,27 +150,14 @@ peer_cert_as_username(Options) ->
%% Info, Attrs and Caps %% Info, Attrs and Caps
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Get infos of the channel.
-spec(info(channel()) -> emqx_types:infos()). -spec(info(channel()) -> emqx_types:infos()).
info(#channel{client = Client, info(Channel) ->
session = Session, maps:from_list(info(?INFO_KEYS, Channel)).
protocol = Protocol,
keepalive = Keepalive,
gc_state = GCState,
oom_policy = OomPolicy,
connected = Connected,
connected_at = ConnectedAt
}) ->
#{client => Client,
session => maybe_apply(fun emqx_session:info/1, Session),
protocol => maybe_apply(fun emqx_protocol:info/1, Protocol),
keepalive => maybe_apply(fun emqx_keepalive:info/1, Keepalive),
gc_state => emqx_gc:info(GCState),
oom_policy => emqx_oom:info(OomPolicy),
connected => Connected,
connected_at => ConnectedAt
}.
-spec(info(atom(), channel()) -> term()). -spec(info(list(atom())|atom(), channel()) -> term()).
info(Keys, Channel) when is_list(Keys) ->
[{Key, info(Key, Channel)} || Key <- Keys];
info(client, #channel{client = Client}) -> info(client, #channel{client = Client}) ->
Client; Client;
info(session, #channel{session = Session}) -> info(session, #channel{session = Session}) ->
@ -174,10 +166,10 @@ info(protocol, #channel{protocol = Protocol}) ->
maybe_apply(fun emqx_protocol:info/1, Protocol); maybe_apply(fun emqx_protocol:info/1, Protocol);
info(keepalive, #channel{keepalive = Keepalive}) -> info(keepalive, #channel{keepalive = Keepalive}) ->
maybe_apply(fun emqx_keepalive:info/1, Keepalive); maybe_apply(fun emqx_keepalive:info/1, Keepalive);
info(gc_state, #channel{gc_state = GCState}) -> info(gc_state, #channel{gc_state = GcState}) ->
emqx_gc:info(GCState); maybe_apply(fun emqx_gc:info/1, GcState);
info(oom_policy, #channel{oom_policy = Policy}) -> info(oom_policy, #channel{oom_policy = OomPolicy}) ->
emqx_oom:info(Policy); maybe_apply(fun emqx_oom:info/1, OomPolicy);
info(connected, #channel{connected = Connected}) -> info(connected, #channel{connected = Connected}) ->
Connected; Connected;
info(connected_at, #channel{connected_at = ConnectedAt}) -> info(connected_at, #channel{connected_at = ConnectedAt}) ->
@ -185,20 +177,17 @@ info(connected_at, #channel{connected_at = ConnectedAt}) ->
info(disconnected_at, #channel{disconnected_at = DisconnectedAt}) -> info(disconnected_at, #channel{disconnected_at = DisconnectedAt}) ->
DisconnectedAt. DisconnectedAt.
%% @doc Get attrs of the channel.
-spec(attrs(channel()) -> emqx_types:attrs()). -spec(attrs(channel()) -> emqx_types:attrs()).
attrs(#channel{client = Client, attrs(Channel) ->
session = Session, maps:from_list([{Key, attr(Key, Channel)} || Key <- ?ATTR_KEYS]).
protocol = Protocol,
connected = Connected, attr(protocol, #channel{protocol = Proto}) ->
connected_at = ConnectedAt}) -> maybe_apply(fun emqx_protocol:attrs/1, Proto);
#{client => Client, attr(session, #channel{session = Session}) ->
session => maybe_apply(fun emqx_session:attrs/1, Session), maybe_apply(fun emqx_session:attrs/1, Session);
protocol => maybe_apply(fun emqx_protocol:attrs/1, Protocol), attr(Key, Channel) -> info(Key, Channel).
connected => Connected,
connected_at => ConnectedAt
}.
%%TODO: ChanStats?
-spec(stats(channel()) -> emqx_types:stats()). -spec(stats(channel()) -> emqx_types:stats()).
stats(#channel{session = Session}) -> stats(#channel{session = Session}) ->
emqx_session:stats(Session). emqx_session:stats(Session).
@ -207,16 +196,11 @@ stats(#channel{session = Session}) ->
caps(#channel{client = #{zone := Zone}}) -> caps(#channel{client = #{zone := Zone}}) ->
emqx_mqtt_caps:get_caps(Zone). emqx_mqtt_caps:get_caps(Zone).
%%-------------------------------------------------------------------- %% For tests
%% For unit tests set_field(Name, Val, Channel) ->
%%-------------------------------------------------------------------- Fields = record_info(fields, channel),
Pos = emqx_misc:index_of(Name, Fields),
set(client, Client, Channel) -> setelement(Pos+1, Channel, Val).
Channel#channel{client = Client};
set(session, Session, Channel) ->
Channel#channel{session = Session};
set(protocol, Protocol, Channel) ->
Channel#channel{protocol = Protocol}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle incoming packet %% Handle incoming packet
@ -244,7 +228,8 @@ handle_in(?CONNECT_PACKET(ConnPkt), Channel) ->
handle_out({connack, ReasonCode}, NChannel) handle_out({connack, ReasonCode}, NChannel)
end; end;
handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), Channel = #channel{protocol = Protocol}) -> handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId),
Channel = #channel{protocol = Protocol}) ->
case pipeline([fun validate_packet/2, case pipeline([fun validate_packet/2,
fun process_alias/2, fun process_alias/2,
fun check_publish/2], Packet, Channel) of fun check_publish/2], Packet, Channel) of
@ -255,41 +240,52 @@ handle_in(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId), Channel = #channel{p
?LOG(warning, "Cannot publish message to ~s due to ~s", ?LOG(warning, "Cannot publish message to ~s due to ~s",
[Topic, emqx_reason_codes:text(ReasonCode, ProtoVer)]), [Topic, emqx_reason_codes:text(ReasonCode, ProtoVer)]),
handle_out({disconnect, ReasonCode}, NChannel) handle_out({disconnect, ReasonCode}, NChannel)
% case QoS of
% ?QOS_0 -> handle_out({puberr, ReasonCode}, NChannel);
% ?QOS_1 -> handle_out({puback, PacketId, ReasonCode}, NChannel);
% ?QOS_2 -> handle_out({pubrec, PacketId, ReasonCode}, NChannel)
% end
end; end;
%%TODO: How to handle the ReasonCode? handle_in(?PUBACK_PACKET(PacketId, _ReasonCode),
handle_in(?PUBACK_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> Channel = #channel{client = Client, session = Session}) ->
case emqx_session:puback(PacketId, Session) of case emqx_session:puback(PacketId, Session) of
{ok, Publishes, NSession} -> {ok, Msg, Publishes, NSession} ->
ok = emqx_hooks:run('message.acked', [Client, Msg]),
handle_out({publish, Publishes}, Channel#channel{session = NSession}); handle_out({publish, Publishes}, Channel#channel{session = NSession});
{ok, NSession} -> {ok, Msg, NSession} ->
ok = emqx_hooks:run('message.acked', [Client, Msg]),
{ok, Channel#channel{session = NSession}}; {ok, Channel#channel{session = NSession}};
{error, _NotFound} -> {error, ?RC_PACKET_IDENTIFIER_IN_USE} ->
%%TODO: How to handle NotFound, inc metrics? ?LOG(warning, "The PUBACK PacketId ~w is inuse.", [PacketId]),
ok = emqx_metrics:inc('packets.puback.inuse'),
{ok, Channel};
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId]),
ok = emqx_metrics:inc('packets.puback.missed'),
{ok, Channel} {ok, Channel}
end; end;
%%TODO: How to handle the ReasonCode? handle_in(?PUBREC_PACKET(PacketId, _ReasonCode),
handle_in(?PUBREC_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> Channel = #channel{client = Client, session = Session}) ->
case emqx_session:pubrec(PacketId, Session) of case emqx_session:pubrec(PacketId, Session) of
{ok, NSession} -> {ok, Msg, NSession} ->
handle_out({pubrel, PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); ok = emqx_hooks:run('message.acked', [Client, Msg]),
{error, ReasonCode} -> NChannel = Channel#channel{session = NSession},
handle_out({pubrel, PacketId, ReasonCode}, Channel) handle_out({pubrel, PacketId, ?RC_SUCCESS}, NChannel);
{error, RC = ?RC_PACKET_IDENTIFIER_IN_USE} ->
?LOG(warning, "The PUBREC PacketId ~w is inuse.", [PacketId]),
ok = emqx_metrics:inc('packets.pubrec.inuse'),
handle_out({pubrel, PacketId, RC}, Channel);
{error, RC = ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
?LOG(warning, "The PUBREC ~w is not found.", [PacketId]),
ok = emqx_metrics:inc('packets.pubrec.missed'),
handle_out({pubrel, PacketId, RC}, Channel)
end; end;
%%TODO: How to handle the ReasonCode?
handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> handle_in(?PUBREL_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) ->
case emqx_session:pubrel(PacketId, Session) of case emqx_session:pubrel(PacketId, Session) of
{ok, NSession} -> {ok, NSession} ->
handle_out({pubcomp, PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession}); handle_out({pubcomp, PacketId, ?RC_SUCCESS}, Channel#channel{session = NSession});
{error, ReasonCode} -> {error, NotFound} ->
handle_out({pubcomp, PacketId, ReasonCode}, Channel) ?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId]),
ok = emqx_metrics:inc('packets.pubrel.missed'),
handle_out({pubcomp, PacketId, NotFound}, Channel)
end; end;
handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) -> handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = Session}) ->
@ -298,36 +294,27 @@ handle_in(?PUBCOMP_PACKET(PacketId, _ReasonCode), Channel = #channel{session = S
handle_out({publish, Publishes}, Channel#channel{session = NSession}); handle_out({publish, Publishes}, Channel#channel{session = NSession});
{ok, NSession} -> {ok, NSession} ->
{ok, Channel#channel{session = NSession}}; {ok, Channel#channel{session = NSession}};
{error, _NotFound} -> {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} ->
%% TODO: how to handle NotFound? ?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]),
ok = emqx_metrics:inc('packets.pubcomp.missed'),
{ok, Channel} {ok, Channel}
end; end;
handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), handle_in(Packet = ?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), Channel) ->
Channel = #channel{client = Client}) ->
case validate_packet(Packet, Channel) of case validate_packet(Packet, Channel) of
ok -> ok ->
TopicFilters1 = [emqx_topic:parse(TopicFilter, SubOpts) TopicFilters = preprocess_subscribe(Properties, RawTopicFilters, Channel),
|| {TopicFilter, SubOpts} <- TopicFilters], {ReasonCodes, NChannel} = process_subscribe(TopicFilters, Channel),
TopicFilters2 = emqx_hooks:run_fold('client.subscribe',
[Client, Properties],
TopicFilters1),
TopicFilters3 = enrich_subid(Properties, TopicFilters2),
{ReasonCodes, NChannel} = process_subscribe(TopicFilters3, Channel),
handle_out({suback, PacketId, ReasonCodes}, NChannel); handle_out({suback, PacketId, ReasonCodes}, NChannel);
{error, ReasonCode} -> {error, ReasonCode} ->
handle_out({disconnect, ReasonCode}, Channel) handle_out({disconnect, ReasonCode}, Channel)
end; end;
handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), handle_in(Packet = ?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), Channel) ->
Channel = #channel{client = Client}) ->
case validate_packet(Packet, Channel) of case validate_packet(Packet, Channel) of
ok -> ok ->
TopicFilters1 = lists:map(fun emqx_topic:parse/1, TopicFilters), TopicFilters = preprocess_unsubscribe(Properties, RawTopicFilters, Channel),
TopicFilters2 = emqx_hooks:run_fold('client.unsubscribe', {ReasonCodes, NChannel} = process_unsubscribe(TopicFilters, Channel),
[Client, Properties],
TopicFilters1),
{ReasonCodes, NChannel} = process_unsubscribe(TopicFilters2, Channel),
handle_out({unsuback, PacketId, ReasonCodes}, NChannel); handle_out({unsuback, PacketId, ReasonCodes}, NChannel);
{error, ReasonCode} -> {error, ReasonCode} ->
handle_out({disconnect, ReasonCode}, Channel) handle_out({disconnect, ReasonCode}, Channel)
@ -347,7 +334,7 @@ handle_in(?DISCONNECT_PACKET(RC, Properties), Channel = #channel{session = Sessi
?RC_SUCCESS -> Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)}; ?RC_SUCCESS -> Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)};
_ -> Channel _ -> Channel
end, end,
Channel2 = ensure_disconnected(Channel1#channel{session = emqx_session:update_expiry_interval(Interval, Session)}), Channel2 = Channel1#channel{session = emqx_session:update_expiry_interval(Interval, Session)},
case Interval of case Interval of
?UINT_MAX -> ?UINT_MAX ->
{ok, ensure_timer(will_timer, Channel2)}; {ok, ensure_timer(will_timer, Channel2)};
@ -382,6 +369,7 @@ process_connect(ConnPkt, Channel) ->
NChannel = Channel#channel{session = Session}, NChannel = Channel#channel{session = Session},
handle_out({connack, ?RC_SUCCESS, sp(false)}, NChannel); handle_out({connack, ?RC_SUCCESS, sp(false)}, NChannel);
{ok, #{session := Session, present := true, pendings := Pendings}} -> {ok, #{session := Session, present := true, pendings := Pendings}} ->
%%TODO: improve later.
NPendings = lists:usort(lists:append(Pendings, emqx_misc:drain_deliver())), NPendings = lists:usort(lists:append(Pendings, emqx_misc:drain_deliver())),
NChannel = Channel#channel{session = Session, NChannel = Channel#channel{session = Session,
resuming = true, resuming = true,
@ -397,39 +385,58 @@ process_connect(ConnPkt, Channel) ->
%% Process Publish %% Process Publish
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Process Publish process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), Channel) ->
process_publish(Packet = ?PUBLISH_PACKET(_QoS, _Topic, PacketId), Msg = publish_to_msg(Packet, Channel),
Channel = #channel{client = Client, protocol = Protocol}) -> process_publish(PacketId, Msg, Channel).
Msg = emqx_packet:to_message(Client, Packet),
%%TODO: Improve later.
Msg1 = emqx_message:set_flag(dup, false, emqx_message:set_header(proto_ver, emqx_protocol:info(proto_ver, Protocol), Msg)),
process_publish(PacketId, mount(Client, Msg1), Channel).
process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) -> process_publish(_PacketId, Msg = #message{qos = ?QOS_0}, Channel) ->
_ = emqx_broker:publish(Msg), _ = emqx_broker:publish(Msg),
{ok, Channel}; {ok, Channel};
process_publish(PacketId, Msg = #message{qos = ?QOS_1}, Channel) -> process_publish(PacketId, Msg = #message{qos = ?QOS_1}, Channel) ->
Deliveries = emqx_broker:publish(Msg), ReasonCode = case emqx_broker:publish(Msg) of
ReasonCode = emqx_reason_codes:puback(Deliveries), [] -> ?RC_NO_MATCHING_SUBSCRIBERS;
_ -> ?RC_SUCCESS
end,
handle_out({puback, PacketId, ReasonCode}, Channel); handle_out({puback, PacketId, ReasonCode}, Channel);
process_publish(PacketId, Msg = #message{qos = ?QOS_2}, process_publish(PacketId, Msg = #message{qos = ?QOS_2},
Channel = #channel{session = Session}) -> Channel = #channel{session = Session}) ->
case emqx_session:publish(PacketId, Msg, Session) of case emqx_session:publish(PacketId, Msg, Session) of
{ok, Deliveries, NSession} -> {ok, Results, NSession} ->
ReasonCode = emqx_reason_codes:puback(Deliveries), RC = case Results of
[] -> ?RC_NO_MATCHING_SUBSCRIBERS;
_ -> ?RC_SUCCESS
end,
NChannel = Channel#channel{session = NSession}, NChannel = Channel#channel{session = NSession},
handle_out({pubrec, PacketId, ReasonCode}, handle_out({pubrec, PacketId, RC}, ensure_timer(await_timer, NChannel));
ensure_timer(await_timer, NChannel)); {error, RC = ?RC_PACKET_IDENTIFIER_IN_USE} ->
{error, ReasonCode} -> ok = emqx_metrics:inc('packets.publish.inuse'),
handle_out({pubrec, PacketId, ReasonCode}, Channel) handle_out({pubrec, PacketId, RC}, Channel);
{error, RC = ?RC_RECEIVE_MAXIMUM_EXCEEDED} ->
?LOG(warning, "Dropped qos2 packet ~w due to awaiting_rel is full", [PacketId]),
ok = emqx_metrics:inc('messages.qos2.dropped'),
handle_out({pubrec, PacketId, RC}, Channel)
end. end.
publish_to_msg(Packet, #channel{client = Client = #{mountpoint := MountPoint}}) ->
Msg = emqx_packet:to_message(Client, Packet),
Msg1 = emqx_message:set_flag(dup, false, Msg),
emqx_mountpoint:mount(MountPoint, Msg1).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Process Subscribe %% Process Subscribe
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-compile({inline, [preprocess_subscribe/3]}).
preprocess_subscribe(Properties, RawTopicFilters, #channel{client = Client}) ->
RunHook = fun(TopicFilters) ->
emqx_hooks:run_fold('client.subscribe',
[Client, Properties], TopicFilters)
end,
Enrich = fun(TopicFilters) -> enrich_subid(Properties, TopicFilters) end,
run_fold([fun parse_topic_filters/1, RunHook, Enrich], RawTopicFilters).
process_subscribe(TopicFilters, Channel) -> process_subscribe(TopicFilters, Channel) ->
process_subscribe(TopicFilters, [], Channel). process_subscribe(TopicFilters, [], Channel).
@ -440,16 +447,18 @@ process_subscribe([{TopicFilter, SubOpts}|More], Acc, Channel) ->
{RC, NChannel} = do_subscribe(TopicFilter, SubOpts, Channel), {RC, NChannel} = do_subscribe(TopicFilter, SubOpts, Channel),
process_subscribe(More, [RC|Acc], NChannel). process_subscribe(More, [RC|Acc], NChannel).
do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, do_subscribe(TopicFilter, SubOpts = #{qos := QoS}, Channel =
Channel = #channel{client = Client, session = Session}) -> #channel{client = Client = #{mountpoint := MountPoint},
session = Session}) ->
case check_subscribe(TopicFilter, SubOpts, Channel) of case check_subscribe(TopicFilter, SubOpts, Channel) of
ok -> TopicFilter1 = mount(Client, TopicFilter), ok ->
SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), Channel), TopicFilter1 = emqx_mountpoint:mount(MountPoint, TopicFilter),
case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, Session) of SubOpts1 = enrich_subopts(maps:merge(?DEFAULT_SUBOPTS, SubOpts), Channel),
{ok, NSession} -> case emqx_session:subscribe(Client, TopicFilter1, SubOpts1, Session) of
{QoS, Channel#channel{session = NSession}}; {ok, NSession} ->
{error, RC} -> {RC, Channel} {QoS, Channel#channel{session = NSession}};
end; {error, RC} -> {RC, Channel}
end;
{error, RC} -> {RC, Channel} {error, RC} -> {RC, Channel}
end. end.
@ -457,6 +466,15 @@ do_subscribe(TopicFilter, SubOpts = #{qos := QoS},
%% Process Unsubscribe %% Process Unsubscribe
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-compile({inline, [preprocess_unsubscribe/3]}).
preprocess_unsubscribe(Properties, RawTopicFilter, #channel{client = Client}) ->
RunHook = fun(TopicFilters) ->
emqx_hooks:run_fold('client.unsubscribe',
[Client, Properties], TopicFilters)
end,
run_fold([fun parse_topic_filters/1, RunHook], RawTopicFilter).
-compile({inline, [process_unsubscribe/2]}).
process_unsubscribe(TopicFilters, Channel) -> process_unsubscribe(TopicFilters, Channel) ->
process_unsubscribe(TopicFilters, [], Channel). process_unsubscribe(TopicFilters, [], Channel).
@ -464,12 +482,14 @@ process_unsubscribe([], Acc, Channel) ->
{lists:reverse(Acc), Channel}; {lists:reverse(Acc), Channel};
process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, Channel) -> process_unsubscribe([{TopicFilter, SubOpts}|More], Acc, Channel) ->
{RC, Channel1} = do_unsubscribe(TopicFilter, SubOpts, Channel), {RC, NChannel} = do_unsubscribe(TopicFilter, SubOpts, Channel),
process_unsubscribe(More, [RC|Acc], Channel1). process_unsubscribe(More, [RC|Acc], NChannel).
do_unsubscribe(TopicFilter, _SubOpts, do_unsubscribe(TopicFilter, _SubOpts, Channel =
Channel = #channel{client = Client, session = Session}) -> #channel{client = Client = #{mountpoint := MountPoint},
case emqx_session:unsubscribe(Client, mount(Client, TopicFilter), Session) of session = Session}) ->
TopicFilter1 = emqx_mountpoint:mount(MountPoint, TopicFilter),
case emqx_session:unsubscribe(Client, TopicFilter1, Session) of
{ok, NSession} -> {ok, NSession} ->
{?RC_SUCCESS, Channel#channel{session = NSession}}; {?RC_SUCCESS, Channel#channel{session = NSession}};
{error, RC} -> {RC, Channel} {error, RC} -> {RC, Channel}
@ -479,15 +499,15 @@ do_unsubscribe(TopicFilter, _SubOpts,
%% Handle outgoing packet %% Handle outgoing packet
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%%TODO: RunFold or Pipeline
handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{client = Client}) -> handle_out({connack, ?RC_SUCCESS, SP}, Channel = #channel{client = Client}) ->
ok = emqx_hooks:run('client.connected', AckProps = run_fold([fun enrich_caps/2,
[Client, ?RC_SUCCESS, attrs(Channel)]), fun enrich_server_keepalive/2,
AckProps = emqx_misc:run_fold([fun enrich_caps/2, fun enrich_assigned_clientid/2
fun enrich_server_keepalive/2, ], #{}, Channel),
fun enrich_assigned_clientid/2
], #{}, Channel),
AckPacket = ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps),
Channel1 = ensure_keepalive(AckProps, ensure_connected(Channel)), Channel1 = ensure_keepalive(AckProps, ensure_connected(Channel)),
ok = emqx_hooks:run('client.connected', [Client, ?RC_SUCCESS, attrs(Channel1)]),
AckPacket = ?CONNACK_PACKET(?RC_SUCCESS, SP, AckProps),
case maybe_resume_session(Channel1) of case maybe_resume_session(Channel1) of
ignore -> {ok, AckPacket, Channel1}; ignore -> {ok, AckPacket, Channel1};
{ok, Publishes, NSession} -> {ok, Publishes, NSession} ->
@ -502,7 +522,10 @@ handle_out({connack, ReasonCode}, Channel = #channel{client = Client,
protocol = Protocol protocol = Protocol
}) -> }) ->
ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(Channel)]), ok = emqx_hooks:run('client.connected', [Client, ReasonCode, attrs(Channel)]),
ProtoVer = emqx_protocol:info(proto_ver, Protocol), ProtoVer = case Protocol of
undefined -> undefined;
_ -> emqx_protocol:info(proto_ver, Protocol)
end,
ReasonCode1 = if ReasonCode1 = if
ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode; ProtoVer == ?MQTT_PROTO_V5 -> ReasonCode;
true -> emqx_reason_codes:compat(connack, ReasonCode) true -> emqx_reason_codes:compat(connack, ReasonCode)
@ -510,9 +533,10 @@ handle_out({connack, ReasonCode}, Channel = #channel{client = Client,
Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer), Reason = emqx_reason_codes:name(ReasonCode1, ProtoVer),
{stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel}; {stop, {shutdown, Reason}, ?CONNACK_PACKET(ReasonCode1), Channel};
handle_out({deliver, Delivers}, Channel = #channel{session = Session, handle_out({deliver, Delivers}, Channel = #channel{session = Session,
connected = false}) -> connected = false}) ->
{ok, Channel#channel{session = emqx_session:enqueue(Delivers, Session)}}; NSession = emqx_session:enqueue(Delivers, Session),
{ok, Channel#channel{session = NSession}};
handle_out({deliver, Delivers}, Channel = #channel{takeover = true, handle_out({deliver, Delivers}, Channel = #channel{takeover = true,
pendings = Pendings}) -> pendings = Pendings}) ->
@ -527,23 +551,33 @@ handle_out({deliver, Delivers}, Channel = #channel{session = Session}) ->
{ok, Channel#channel{session = NSession}} {ok, Channel#channel{session = NSession}}
end; end;
handle_out({publish, Publishes}, Channel) -> handle_out({publish, [Publish]}, Channel) ->
Packets = lists:map( handle_out(Publish, Channel);
fun(Publish) ->
element(2, handle_out(Publish, Channel))
end, Publishes),
{ok, Packets, Channel};
handle_out({publish, PacketId, Msg}, Channel = #channel{client = Client}) -> handle_out({publish, Publishes}, Channel) when is_list(Publishes) ->
Msg1 = emqx_hooks:run_fold('message.deliver', [Client], Packets = lists:foldl(
emqx_message:update_expiry(Msg)), fun(Publish, Acc) ->
Packet = emqx_packet:from_message(PacketId, unmount(Client, Msg1)), case handle_out(Publish, Channel) of
{ok, Packet, Channel}; {ok, Packet, _Ch} ->
[Packet|Acc];
{ok, _Ch} -> Acc
end
end, [], Publishes),
{ok, lists:reverse(Packets), Channel};
%% TODO: How to handle the puberr? %% Ignore loop deliver
handle_out({puberr, _ReasonCode}, Channel) -> handle_out({publish, _PacketId, #message{from = ClientId,
flags = #{nl := true}}},
Channel = #channel{client = #{client_id := ClientId}}) ->
{ok, Channel}; {ok, Channel};
handle_out({publish, PacketId, Msg}, Channel =
#channel{client = Client = #{mountpoint := MountPoint}}) ->
Msg1 = emqx_message:update_expiry(Msg),
Msg2 = emqx_hooks:run_fold('message.delivered', [Client], Msg1),
Msg3 = emqx_mountpoint:unmount(MountPoint, Msg2),
{ok, emqx_packet:from_message(PacketId, Msg3), Channel};
handle_out({puback, PacketId, ReasonCode}, Channel) -> handle_out({puback, PacketId, ReasonCode}, Channel) ->
{ok, ?PUBACK_PACKET(PacketId, ReasonCode), Channel}; {ok, ?PUBACK_PACKET(PacketId, ReasonCode), Channel};
@ -556,25 +590,21 @@ handle_out({pubrec, PacketId, ReasonCode}, Channel) ->
handle_out({pubcomp, PacketId, ReasonCode}, Channel) -> handle_out({pubcomp, PacketId, ReasonCode}, Channel) ->
{ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), Channel}; {ok, ?PUBCOMP_PACKET(PacketId, ReasonCode), Channel};
handle_out({suback, PacketId, ReasonCodes}, handle_out({suback, PacketId, ReasonCodes}, Channel = #channel{protocol = Protocol}) ->
Channel = #channel{protocol = Protocol}) -> ReasonCodes1 = case emqx_protocol:info(proto_ver, Protocol) of
ReasonCodes1 = ?MQTT_PROTO_V5 -> ReasonCodes;
case emqx_protocol:info(proto_ver, Protocol) of _Ver ->
?MQTT_PROTO_V5 -> ReasonCodes; [emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes]
_Ver -> end,
[emqx_reason_codes:compat(suback, RC) || RC <- ReasonCodes]
end,
{ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel}; {ok, ?SUBACK_PACKET(PacketId, ReasonCodes1), Channel};
handle_out({unsuback, PacketId, ReasonCodes}, handle_out({unsuback, PacketId, ReasonCodes}, Channel = #channel{protocol = Protocol}) ->
Channel = #channel{protocol = Protocol}) -> Unsuback = case emqx_protocol:info(proto_ver, Protocol) of
Packet = case emqx_protocol:info(proto_ver, Protocol) of ?MQTT_PROTO_V5 ->
?MQTT_PROTO_V5 -> ?UNSUBACK_PACKET(PacketId, ReasonCodes);
?UNSUBACK_PACKET(PacketId, ReasonCodes); _Ver -> ?UNSUBACK_PACKET(PacketId)
%% Ignore reason codes if not MQTT5 end,
_Ver -> ?UNSUBACK_PACKET(PacketId) {ok, Unsuback, Channel};
end,
{ok, Packet, Channel};
handle_out({disconnect, ReasonCode}, Channel = #channel{protocol = Protocol}) -> handle_out({disconnect, ReasonCode}, Channel = #channel{protocol = Protocol}) ->
case emqx_protocol:info(proto_ver, Protocol) of case emqx_protocol:info(proto_ver, Protocol) of
@ -595,6 +625,12 @@ handle_out({Type, Data}, Channel) ->
%% Handle call %% Handle call
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
handle_call(kick, Channel) ->
{stop, {shutdown, kicked}, ok, Channel};
handle_call(discard, Channel) ->
{stop, {shutdown, discarded}, ok, Channel};
%% Session Takeover %% Session Takeover
handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) -> handle_call({takeover, 'begin'}, Channel = #channel{session = Session}) ->
{ok, Session, Channel#channel{takeover = true}}; {ok, Session, Channel#channel{takeover = true}};
@ -613,6 +649,13 @@ handle_call(Req, Channel) ->
%% Handle cast %% Handle cast
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(handle_cast(Msg :: term(), channel())
-> ok | {ok, channel()} | {stop, Reason :: term(), channel()}).
handle_cast({register, Attrs, Stats}, #channel{client = #{client_id := ClientId}}) ->
ok = emqx_cm:register_channel(ClientId),
emqx_cm:set_chan_attrs(ClientId, Attrs),
emqx_cm:set_chan_stats(ClientId, Stats);
handle_cast(Msg, Channel) -> handle_cast(Msg, Channel) ->
?LOG(error, "Unexpected cast: ~p", [Msg]), ?LOG(error, "Unexpected cast: ~p", [Msg]),
{ok, Channel}. {ok, Channel}.
@ -623,26 +666,24 @@ handle_cast(Msg, Channel) ->
-spec(handle_info(Info :: term(), channel()) -spec(handle_info(Info :: term(), channel())
-> {ok, channel()} | {stop, Reason :: term(), channel()}). -> {ok, channel()} | {stop, Reason :: term(), channel()}).
handle_info({subscribe, TopicFilters}, Channel = #channel{client = Client}) -> handle_info({subscribe, RawTopicFilters}, Channel) ->
TopicFilters1 = emqx_hooks:run_fold('client.subscribe', TopicFilters = preprocess_subscribe(#{'Internal' => true},
[Client, #{'Internal' => true}], RawTopicFilters, Channel),
parse(subscribe, TopicFilters)), {_ReasonCodes, NChannel} = process_subscribe(TopicFilters, Channel),
{_ReasonCodes, NChannel} = process_subscribe(TopicFilters1, Channel),
{ok, NChannel}; {ok, NChannel};
handle_info({unsubscribe, TopicFilters}, Channel = #channel{client = Client}) -> handle_info({unsubscribe, RawTopicFilters}, Channel) ->
TopicFilters1 = emqx_hooks:run_fold('client.unsubscribe', TopicFilters = preprocess_unsubscribe(#{'Internal' => true},
[Client, #{'Internal' => true}], RawTopicFilters, Channel),
parse(unsubscribe, TopicFilters)), {_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters, Channel),
{_ReasonCodes, NChannel} = process_unsubscribe(TopicFilters1, Channel),
{ok, NChannel}; {ok, NChannel};
handle_info(sock_closed, Channel = #channel{disconnected = true}) -> handle_info(disconnected, Channel = #channel{connected = undefined}) ->
{ok, Channel};
handle_info(sock_closed, Channel = #channel{connected = false}) ->
shutdown(closed, Channel); shutdown(closed, Channel);
handle_info(sock_closed, Channel = #channel{protocol = Protocol,
session = Session}) -> handle_info(disconnected, Channel = #channel{protocol = Protocol,
session = Session}) ->
%% TODO: Why handle will_msg here?
publish_will_msg(emqx_protocol:info(will_msg, Protocol)), publish_will_msg(emqx_protocol:info(will_msg, Protocol)),
NChannel = Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)}, NChannel = Channel#channel{protocol = emqx_protocol:clear_will_msg(Protocol)},
Interval = emqx_session:info(expiry_interval, Session), Interval = emqx_session:info(expiry_interval, Session),
@ -779,23 +820,20 @@ terminate(Reason, #channel{client = Client,
true -> publish_will_msg(emqx_protocol:info(will_msg, Protocol)) true -> publish_will_msg(emqx_protocol:info(will_msg, Protocol))
end. end.
-spec(received(pos_integer(), channel()) -> channel()).
received(Oct, Channel) ->
ensure_timer(stats_timer, maybe_gc_and_check_oom(Oct, Channel)).
-spec(sent(pos_integer(), channel()) -> channel()).
sent(Oct, Channel) ->
ensure_timer(stats_timer, maybe_gc_and_check_oom(Oct, Channel)).
%%TODO: Improve will msg:) %%TODO: Improve will msg:)
publish_will_msg(undefined) -> publish_will_msg(undefined) ->
ok; ok;
publish_will_msg(Msg) -> publish_will_msg(Msg) ->
emqx_broker:publish(Msg). emqx_broker:publish(Msg).
%%--------------------------------------------------------------------
%% GC the channel.
%%--------------------------------------------------------------------
gc(_Cnt, _Oct, Channel = #channel{gc_state = undefined}) ->
Channel;
gc(Cnt, Oct, Channel = #channel{gc_state = GCSt}) ->
{Ok, GCSt1} = emqx_gc:run(Cnt, Oct, GCSt),
Ok andalso emqx_metrics:inc('channel.gc.cnt'),
Channel#channel{gc_state = GCSt1}.
%% @doc Validate incoming packet. %% @doc Validate incoming packet.
-spec(validate_packet(emqx_types:packet(), channel()) -spec(validate_packet(emqx_types:packet(), channel())
-> ok | {error, emqx_types:reason_code()}). -> ok | {error, emqx_types:reason_code()}).
@ -899,42 +937,38 @@ init_protocol(ConnPkt, Channel) ->
%% Enrich client %% Enrich client
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
enrich_client(ConnPkt, Channel) -> enrich_client(ConnPkt = #mqtt_packet_connect{is_bridge = IsBridge},
pipeline([fun set_username/2, Channel = #channel{client = Client}) ->
fun maybe_use_username_as_clientid/2, {ok, NConnPkt, NClient} = pipeline([fun set_username/2,
fun maybe_assign_clientid/2, fun maybe_username_as_clientid/2,
fun set_rest_client_fields/2], ConnPkt, Channel). fun maybe_assign_clientid/2,
fun fix_mountpoint/2
], ConnPkt, Client),
{ok, NConnPkt, Channel#channel{client = NClient#{is_bridge => IsBridge}}}.
maybe_use_username_as_clientid(_ConnPkt, Channel = #channel{client = #{username := undefined}}) -> %% Username may be not undefined if peer_cert_as_username
{ok, Channel}; set_username(#mqtt_packet_connect{username = Username}, Client = #{username := undefined}) ->
maybe_use_username_as_clientid(_ConnPkt, Channel = #channel{client = Client = #{zone := Zone, {ok, Client#{username => Username}};
username := Username}}) -> set_username(_ConnPkt, Client) ->
NClient = {ok, Client}.
case emqx_zone:get_env(Zone, use_username_as_clientid, false) of
true -> Client#{client_id => Username};
false -> Client
end,
{ok, Channel#channel{client = NClient}}.
maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, maybe_username_as_clientid(_ConnPkt, Client = #{username := undefined}) ->
Channel = #channel{client = Client}) -> {ok, Client};
maybe_username_as_clientid(_ConnPkt, Client = #{zone := Zone, username := Username}) ->
case emqx_zone:get_env(Zone, use_username_as_clientid, false) of
true -> {ok, Client#{client_id => Username}};
false -> ok
end.
maybe_assign_clientid(#mqtt_packet_connect{client_id = <<>>}, Client) ->
RandClientId = emqx_guid:to_base62(emqx_guid:gen()), RandClientId = emqx_guid:to_base62(emqx_guid:gen()),
{ok, Channel#channel{client = Client#{client_id => RandClientId}}}; {ok, Client#{client_id => RandClientId}};
maybe_assign_clientid(#mqtt_packet_connect{client_id = ClientId}, Client) ->
{ok, Client#{client_id => ClientId}}.
maybe_assign_clientid(#mqtt_packet_connect{client_id = ClientId}, fix_mountpoint(_ConnPkt, #{mountpoint := undefined}) -> ok;
Channel = #channel{client = Client}) -> fix_mountpoint(_ConnPkt, Client = #{mountpoint := Mountpoint}) ->
{ok, Channel#channel{client = Client#{client_id => ClientId}}}. {ok, Client#{mountpoint := emqx_mountpoint:replvar(Mountpoint, Client)}}.
%% Username maybe not undefined if peer_cert_as_username
set_username(#mqtt_packet_connect{username = Username},
Channel = #channel{client = Client = #{username := undefined}}) ->
{ok, Channel#channel{client = Client#{username => Username}}};
set_username(_ConnPkt, Channel) ->
{ok, Channel}.
set_rest_client_fields(#mqtt_packet_connect{is_bridge = IsBridge},
Channel = #channel{client = Client}) ->
{ok, Channel#channel{client = Client#{is_bridge => IsBridge}}}.
%% @doc Set logger metadata. %% @doc Set logger metadata.
set_logger_meta(_ConnPkt, #channel{client = #{client_id := ClientId}}) -> set_logger_meta(_ConnPkt, #channel{client = #{client_id := ClientId}}) ->
@ -1016,7 +1050,8 @@ check_publish(Packet, Channel) ->
%% Check Pub ACL %% Check Pub ACL
check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}}, check_pub_acl(#mqtt_packet{variable = #mqtt_packet_publish{topic_name = Topic}},
#channel{client = Client}) -> #channel{client = Client}) ->
case is_acl_enabled(Client) andalso check_acl(Client, publish, Topic) of case is_acl_enabled(Client) andalso
emqx_access_control:check_acl(Client, publish, Topic) of
false -> ok; false -> ok;
allow -> ok; allow -> ok;
deny -> {error, ?RC_NOT_AUTHORIZED} deny -> {error, ?RC_NOT_AUTHORIZED}
@ -1057,7 +1092,7 @@ check_subscribe(TopicFilter, SubOpts, Channel) ->
%% Check Sub ACL %% Check Sub ACL
check_sub_acl(TopicFilter, #channel{client = Client}) -> check_sub_acl(TopicFilter, #channel{client = Client}) ->
case is_acl_enabled(Client) andalso case is_acl_enabled(Client) andalso
check_acl(Client, subscribe, TopicFilter) of emqx_access_control:check_acl(Client, subscribe, TopicFilter) of
false -> allow; false -> allow;
Result -> Result Result -> Result
end. end.
@ -1116,10 +1151,10 @@ enrich_assigned_clientid(AckProps, #channel{client = #{client_id := ClientId},
end. end.
ensure_connected(Channel) -> ensure_connected(Channel) ->
Channel#channel{connected = true, connected_at = os:timestamp(), disconnected = false}. Channel#channel{connected = true, connected_at = os:timestamp(), disconnected_at = undefined}.
ensure_disconnected(Channel) -> ensure_disconnected(Channel) ->
Channel#channel{connected = false, disconnected_at = os:timestamp(), disconnected = true}. Channel#channel{connected = false, disconnected_at = os:timestamp()}.
ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) -> ensure_keepalive(#{'Server-Keep-Alive' := Interval}, Channel) ->
ensure_keepalive_timer(Interval, Channel); ensure_keepalive_timer(Interval, Channel);
@ -1147,53 +1182,32 @@ maybe_resume_session(#channel{session = Session,
{ok, lists:append(Publishes, More), Session2} {ok, lists:append(Publishes, More), Session2}
end. end.
%%-------------------------------------------------------------------- %% @doc Is ACL enabled?
%% Is ACL enabled?
%%--------------------------------------------------------------------
is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) -> is_acl_enabled(#{zone := Zone, is_superuser := IsSuperuser}) ->
(not IsSuperuser) andalso emqx_zone:get_env(Zone, enable_acl, true). (not IsSuperuser) andalso emqx_zone:get_env(Zone, enable_acl, true).
%%-------------------------------------------------------------------- %% @doc Parse Topic Filters
%% Parse Topic Filters -compile({inline, [parse_topic_filters/1]}).
%%-------------------------------------------------------------------- parse_topic_filters(TopicFilters) ->
parse(subscribe, TopicFilters) ->
[emqx_topic:parse(TopicFilter, SubOpts) || {TopicFilter, SubOpts} <- TopicFilters];
parse(unsubscribe, TopicFilters) ->
lists:map(fun emqx_topic:parse/1, TopicFilters). lists:map(fun emqx_topic:parse/1, TopicFilters).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Mount/Unmount %% Maybe GC and Check OOM
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
mount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> maybe_gc_and_check_oom(_Oct, Channel = #channel{gc_state = undefined}) ->
emqx_mountpoint:mount( Channel;
emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). maybe_gc_and_check_oom(Oct, Channel = #channel{gc_state = GCSt,
oom_policy = OomPolicy}) ->
{IsGC, GCSt1} = emqx_gc:run(1, Oct, GCSt),
IsGC andalso emqx_metrics:inc('channel.gc.cnt'),
IsGC andalso maybe_apply(fun check_oom/1, OomPolicy),
Channel#channel{gc_state = GCSt1}.
unmount(Client = #{mountpoint := MountPoint}, TopicOrMsg) -> check_oom(OomPolicy) ->
emqx_mountpoint:unmount( case emqx_oom:check(OomPolicy) of
emqx_mountpoint:replvar(MountPoint, Client), TopicOrMsg). ok -> ok;
Shutdown -> self() ! Shutdown
%%--------------------------------------------------------------------
%% Pipeline
%%--------------------------------------------------------------------
pipeline([], Packet, Channel) ->
{ok, Packet, Channel};
pipeline([Fun|More], Packet, Channel) ->
case Fun(Packet, Channel) of
ok -> pipeline(More, Packet, Channel);
{ok, NChannel} ->
pipeline(More, Packet, NChannel);
{ok, NPacket, NChannel} ->
pipeline(More, NPacket, NChannel);
{error, ReasonCode} ->
{error, ReasonCode, Channel};
{error, ReasonCode, NChannel} ->
{error, ReasonCode, NChannel}
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -1,40 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2019 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_cli).
-export([ print/1
, print/2
, usage/1
, usage/2
]).
print(Msg) ->
io:format(Msg), lists:flatten(io_lib:format("~p", [Msg])).
print(Format, Args) ->
io:format(Format, Args), lists:flatten(io_lib:format(Format, Args)).
usage(CmdList) ->
lists:map(
fun({Cmd, Descr}) ->
io:format("~-48s# ~s~n", [Cmd, Descr]),
lists:flatten(io_lib:format("~-48s# ~s~n", [Cmd, Descr]))
end, CmdList).
usage(Format, Args) ->
usage([{Format, Args}]).

View File

@ -174,7 +174,7 @@ open_session(false, Client = #{client_id := ClientId}, Options) ->
case takeover_session(ClientId) of case takeover_session(ClientId) of
{ok, ConnMod, ChanPid, Session} -> {ok, ConnMod, ChanPid, Session} ->
ok = emqx_session:resume(ClientId, Session), ok = emqx_session:resume(ClientId, Session),
Pendings = ConnMod:takeover(ChanPid, 'end'), Pendings = ConnMod:call(ChanPid, {takeover, 'end'}),
{ok, #{session => Session, {ok, #{session => Session,
present => true, present => true,
pendings => Pendings}}; pendings => Pendings}};
@ -205,7 +205,7 @@ takeover_session(ClientId) ->
takeover_session(ClientId, ChanPid) when node(ChanPid) == node() -> takeover_session(ClientId, ChanPid) when node(ChanPid) == node() ->
case get_chan_attrs(ClientId, ChanPid) of case get_chan_attrs(ClientId, ChanPid) of
#{client := #{conn_mod := ConnMod}} -> #{client := #{conn_mod := ConnMod}} ->
Session = ConnMod:takeover(ChanPid, 'begin'), Session = ConnMod:call(ChanPid, {takeover, 'begin'}),
{ok, ConnMod, ChanPid, Session}; {ok, ConnMod, ChanPid, Session};
undefined -> undefined ->
{error, not_found} {error, not_found}
@ -234,7 +234,7 @@ discard_session(ClientId) when is_binary(ClientId) ->
discard_session(ClientId, ChanPid) when node(ChanPid) == node() -> discard_session(ClientId, ChanPid) when node(ChanPid) == node() ->
case get_chan_attrs(ClientId, ChanPid) of case get_chan_attrs(ClientId, ChanPid) of
#{client := #{conn_mod := ConnMod}} -> #{client := #{conn_mod := ConnMod}} ->
ConnMod:discard(ChanPid); ConnMod:call(ChanPid, discard);
undefined -> ok undefined -> ok
end; end;

View File

@ -32,21 +32,15 @@
-export([ info/1 -export([ info/1
, attrs/1 , attrs/1
, stats/1 , stats/1
, state/1
]). ]).
%% For Debug -export([call/2]).
-export([get_state/1]).
-export([ kick/1
, discard/1
, takeover/2
]).
%% state callbacks %% state callbacks
-export([ idle/3 -export([ idle/3
, connected/3 , connected/3
, disconnected/3 , disconnected/3
, takeovering/3
]). ]).
%% gen_statem callbacks %% gen_statem callbacks
@ -56,28 +50,44 @@
, terminate/3 , terminate/3
]). ]).
-record(state, { -record(connection, {
transport :: esockd:transport(), %% TCP/TLS Transport
socket :: esockd:socket(), transport :: esockd:transport(),
peername :: emqx_types:peername(), %% TCP/TLS Socket
sockname :: emqx_types:peername(), socket :: esockd:socket(),
conn_state :: running | blocked, %% Peername of the connection
active_n :: pos_integer(), peername :: emqx_types:peername(),
rate_limit :: maybe(esockd_rate_limit:bucket()), %% Sockname of the connection
pub_limit :: maybe(esockd_rate_limit:bucket()), sockname :: emqx_types:peername(),
%% The {active, N} option
active_n :: pos_integer(),
%% The active state
active_state :: running | blocked,
%% Rate Limit
rate_limit :: maybe(esockd_rate_limit:bucket()),
%% Publish Limit
pub_limit :: maybe(esockd_rate_limit:bucket()),
%% Limit Timer
limit_timer :: maybe(reference()), limit_timer :: maybe(reference()),
%% Parser State
parse_state :: emqx_frame:parse_state(), parse_state :: emqx_frame:parse_state(),
serialize :: fun((emqx_types:packet()) -> iodata()), %% Serialize function
chan_state :: emqx_channel:channel() serialize :: fun((emqx_types:packet()) -> iodata()),
%% Channel State
chan_state :: emqx_channel:channel()
}). }).
-type(state() :: #state{}). -type(connection() :: #connection{}).
-define(ACTIVE_N, 100). -define(ACTIVE_N, 100).
-define(HANDLE(T, C, D), handle((T), (C), (D))). -define(HANDLE(T, C, D), handle((T), (C), (D))).
-define(ATTR_KEYS, [socktype, peername, sockname]).
-define(INFO_KEYS, [socktype, peername, sockname, active_n, active_state,
rate_limit, pub_limit]).
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]).
%% @doc Start the connection.
-spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist()) -spec(start_link(esockd:transport(), esockd:socket(), proplists:proplist())
-> {ok, pid()}). -> {ok, pid()}).
start_link(Transport, Socket, Options) -> start_link(Transport, Socket, Options) ->
@ -87,56 +97,53 @@ start_link(Transport, Socket, Options) ->
%% API %% API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% @doc Get infos of the channel. %% @doc Get infos of the connection.
-spec(info(pid() | state()) -> emqx_types:infos()). -spec(info(pid()|connection()) -> emqx_types:infos()).
info(CPid) when is_pid(CPid) -> info(CPid) when is_pid(CPid) ->
call(CPid, info); call(CPid, info);
info(#state{transport = Transport, info(Conn = #connection{chan_state = ChanState}) ->
socket = Socket, ConnInfo = info(?INFO_KEYS, Conn),
peername = Peername,
sockname = Sockname,
conn_state = ConnState,
active_n = ActiveN,
rate_limit = RateLimit,
pub_limit = PubLimit,
chan_state = ChanState}) ->
ConnInfo = #{socktype => Transport:type(Socket),
peername => Peername,
sockname => Sockname,
conn_state => ConnState,
active_n => ActiveN,
rate_limit => limit_info(RateLimit),
pub_limit => limit_info(PubLimit)
},
ChanInfo = emqx_channel:info(ChanState), ChanInfo = emqx_channel:info(ChanState),
maps:merge(ConnInfo, ChanInfo). maps:merge(ChanInfo, #{connection => maps:from_list(ConnInfo)}).
info(Keys, Conn) when is_list(Keys) ->
[{Key, info(Key, Conn)} || Key <- Keys];
info(socktype, #connection{transport = Transport, socket = Socket}) ->
Transport:type(Socket);
info(peername, #connection{peername = Peername}) ->
Peername;
info(sockname, #connection{sockname = Sockname}) ->
Sockname;
info(active_n, #connection{active_n = ActiveN}) ->
ActiveN;
info(active_state, #connection{active_state = ActiveSt}) ->
ActiveSt;
info(rate_limit, #connection{rate_limit = RateLimit}) ->
limit_info(RateLimit);
info(pub_limit, #connection{pub_limit = PubLimit}) ->
limit_info(PubLimit);
info(chan_state, #connection{chan_state = ChanState}) ->
emqx_channel:info(ChanState).
limit_info(Limit) -> limit_info(Limit) ->
emqx_misc:maybe_apply(fun esockd_rate_limit:info/1, Limit). emqx_misc:maybe_apply(fun esockd_rate_limit:info/1, Limit).
%% @doc Get attrs of the channel. %% @doc Get attrs of the connection.
-spec(attrs(pid() | state()) -> emqx_types:attrs()). -spec(attrs(pid()|connection()) -> emqx_types:attrs()).
attrs(CPid) when is_pid(CPid) -> attrs(CPid) when is_pid(CPid) ->
call(CPid, attrs); call(CPid, attrs);
attrs(#state{transport = Transport, attrs(Conn = #connection{chan_state = ChanState}) ->
socket = Socket, ConnAttrs = info(?ATTR_KEYS, Conn),
peername = Peername,
sockname = Sockname,
chan_state = ChanState}) ->
ConnAttrs = #{socktype => Transport:type(Socket),
peername => Peername,
sockname => Sockname
},
ChanAttrs = emqx_channel:attrs(ChanState), ChanAttrs = emqx_channel:attrs(ChanState),
maps:merge(ConnAttrs, ChanAttrs). maps:merge(ChanAttrs, #{connection => maps:from_list(ConnAttrs)}).
%% @doc Get stats of the channel. %% @doc Get stats of the channel.
-spec(stats(pid() | state()) -> emqx_types:stats()). -spec(stats(pid()|connection()) -> emqx_types:stats()).
stats(CPid) when is_pid(CPid) -> stats(CPid) when is_pid(CPid) ->
call(CPid, stats); call(CPid, stats);
stats(#state{transport = Transport, stats(#connection{transport = Transport,
socket = Socket, socket = Socket,
chan_state = ChanState}) -> chan_state = ChanState}) ->
ProcStats = emqx_misc:proc_stats(), ProcStats = emqx_misc:proc_stats(),
SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of SockStats = case Transport:getstat(Socket, ?SOCK_STATS) of
{ok, Ss} -> Ss; {ok, Ss} -> Ss;
@ -146,25 +153,13 @@ stats(#state{transport = Transport,
ChanStats = emqx_channel:stats(ChanState), ChanStats = emqx_channel:stats(ChanState),
lists:append([ProcStats, SockStats, ConnStats, ChanStats]). lists:append([ProcStats, SockStats, ConnStats, ChanStats]).
-spec(get_state(pid()) -> state()). %% For debug
get_state(CPid) -> -spec(state(pid()) -> connection()).
call(CPid, get_state). state(CPid) -> call(CPid, state).
-spec(kick(pid()) -> ok). %% kick|discard|takeover
kick(CPid) -> -spec(call(pid(), Req :: term()) -> Reply :: term()).
call(CPid, kick). call(CPid, Req) -> gen_statem:call(CPid, Req).
-spec(discard(pid()) -> ok).
discard(CPid) ->
gen_statem:cast(CPid, discard).
-spec(takeover(pid(), 'begin'|'end') -> Result :: term()).
takeover(CPid, Phase) ->
gen_statem:call(CPid, {takeover, Phase}).
%% @private
call(CPid, Req) ->
gen_statem:call(CPid, Req, infinity).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% gen_statem callbacks %% gen_statem callbacks
@ -187,22 +182,21 @@ init({Transport, RawSocket, Options}) ->
peercert => Peercert, peercert => Peercert,
conn_mod => ?MODULE}, Options), conn_mod => ?MODULE}, Options),
IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000),
State = #state{transport = Transport, State = #connection{transport = Transport,
socket = Socket, socket = Socket,
peername = Peername, peername = Peername,
sockname = Sockname, sockname = Sockname,
conn_state = running, active_n = ActiveN,
active_n = ActiveN, active_state = running,
rate_limit = RateLimit, rate_limit = RateLimit,
pub_limit = PubLimit, pub_limit = PubLimit,
parse_state = ParseState, parse_state = ParseState,
chan_state = ChanState chan_state = ChanState
}, },
gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}], gen_statem:enter_loop(?MODULE, [{hibernate_after, 2 * IdleTimout}],
idle, State, self(), [IdleTimout]). idle, State, self(), [IdleTimout]).
init_limiter(undefined) -> init_limiter(undefined) -> undefined;
undefined;
init_limiter({Rate, Burst}) -> init_limiter({Rate, Burst}) ->
esockd_rate_limit:new(Rate, Burst). esockd_rate_limit:new(Rate, Burst).
@ -220,16 +214,13 @@ idle(enter, _, State) ->
end; end;
idle(timeout, _Timeout, State) -> idle(timeout, _Timeout, State) ->
stop(idle_timeout, State); shutdown(idle_timeout, State);
idle(cast, {incoming, Packet = ?CONNECT_PACKET( idle(cast, {incoming, Packet = ?CONNECT_PACKET(ConnPkt)}, State) ->
#mqtt_packet_connect{ #mqtt_packet_connect{proto_ver = ProtoVer} = ConnPkt,
proto_ver = ProtoVer} NState = State#connection{serialize = serialize_fun(ProtoVer)},
)}, State) -> SuccFun = fun(NewSt) -> {next_state, connected, NewSt} end,
State1 = State#state{serialize = serialize_fun(ProtoVer)}, handle_incoming(Packet, SuccFun, NState);
handle_incoming(Packet, fun(NewSt) ->
{next_state, connected, NewSt}
end, State1);
idle(cast, {incoming, Packet}, State) -> idle(cast, {incoming, Packet}, State) ->
?LOG(warning, "Unexpected incoming: ~p", [Packet]), ?LOG(warning, "Unexpected incoming: ~p", [Packet]),
@ -241,17 +232,10 @@ idle(EventType, Content, State) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Connected State %% Connected State
connected(enter, _PrevSt, State = #state{chan_state = ChanState}) -> connected(enter, _PrevSt, State) ->
#{client_id := ClientId} = emqx_channel:info(client, ChanState), ok = register_self(State),
ok = emqx_cm:register_channel(ClientId),
ok = emqx_cm:set_chan_attrs(ClientId, attrs(State)),
keep_state_and_data; keep_state_and_data;
connected(cast, {incoming, Packet = ?PACKET(?CONNECT)}, State) ->
?LOG(warning, "Unexpected connect: ~p", [Packet]),
Shutdown = fun(NewSt) -> shutdown(?RC_PROTOCOL_ERROR, NewSt) end,
handle_outgoing(?DISCONNECT_PACKET(?RC_PROTOCOL_ERROR), Shutdown, State);
connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) -> connected(cast, {incoming, Packet}, State) when is_record(Packet, mqtt_packet) ->
handle_incoming(Packet, fun keep_state/1, State); handle_incoming(Packet, fun keep_state/1, State);
@ -264,10 +248,14 @@ connected(EventType, Content, State) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Disconnected State %% Disconnected State
disconnected(enter, _, _State) -> disconnected(enter, _, State = #connection{chan_state = ChanState}) ->
%% TODO: What to do? case emqx_channel:handle_info(disconnected, ChanState) of
%% CleanStart is true {ok, NChanState} ->
keep_state_and_data; ok = register_self(State#connection{chan_state = NChanState}),
keep_state(State#connection{chan_state = NChanState});
{stop, Reason, NChanState} ->
stop(Reason, State#connection{chan_state = NChanState})
end;
disconnected(info, Deliver = {deliver, _Topic, _Msg}, State) -> disconnected(info, Deliver = {deliver, _Topic, _Msg}, State) ->
handle_deliver([Deliver], State); handle_deliver([Deliver], State);
@ -276,15 +264,8 @@ disconnected(EventType, Content, State) ->
?HANDLE(EventType, Content, State). ?HANDLE(EventType, Content, State).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Takeovering State
takeovering(enter, _PreState, State) ->
{keep_state, State};
takeovering(EventType, Content, State) ->
?HANDLE(EventType, Content, State).
%% Handle call %% Handle call
handle({call, From}, info, State) -> handle({call, From}, info, State) ->
reply(From, info(State), State); reply(From, info(State), State);
@ -294,60 +275,53 @@ handle({call, From}, attrs, State) ->
handle({call, From}, stats, State) -> handle({call, From}, stats, State) ->
reply(From, stats(State), State); reply(From, stats(State), State);
handle({call, From}, get_state, State) -> handle({call, From}, state, State) ->
reply(From, State, State); reply(From, State, State);
handle({call, From}, kick, State) -> handle({call, From}, Req, State = #connection{chan_state = ChanState}) ->
ok = gen_statem:reply(From, ok),
shutdown(kicked, State);
handle({call, From}, Req, State = #state{chan_state = ChanState}) ->
case emqx_channel:handle_call(Req, ChanState) of case emqx_channel:handle_call(Req, ChanState) of
{ok, Reply, NChanState} -> {ok, Reply, NChanState} ->
reply(From, Reply, State#state{chan_state = NChanState}); reply(From, Reply, State#connection{chan_state = NChanState});
{stop, Reason, Reply, NChanState} -> {stop, Reason, Reply, NChanState} ->
ok = gen_statem:reply(From, Reply), ok = gen_statem:reply(From, Reply),
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#connection{chan_state = NChanState})
end; end;
handle(cast, discard, State) -> %%--------------------------------------------------------------------
shutdown(discarded, State);
%% Handle cast %% Handle cast
handle(cast, Msg, State = #state{chan_state = ChanState}) ->
handle(cast, Msg, State = #connection{chan_state = ChanState}) ->
case emqx_channel:handle_cast(Msg, ChanState) of case emqx_channel:handle_cast(Msg, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
keep_state(State#state{chan_state = NChanState}); keep_state(State#connection{chan_state = NChanState});
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#connection{chan_state = NChanState})
end; end;
%%--------------------------------------------------------------------
%% Handle info
%% Handle incoming data %% Handle incoming data
handle(info, {Inet, _Sock, Data}, State = #state{chan_state = ChanState}) handle(info, {Inet, _Sock, Data}, State = #connection{chan_state = ChanState})
when Inet == tcp; Inet == ssl -> when Inet == tcp; Inet == ssl ->
Oct = iolist_size(Data),
?LOG(debug, "RECV ~p", [Data]), ?LOG(debug, "RECV ~p", [Data]),
Oct = iolist_size(Data),
emqx_pd:update_counter(incoming_bytes, Oct), emqx_pd:update_counter(incoming_bytes, Oct),
ok = emqx_metrics:inc('bytes.received', Oct), ok = emqx_metrics:inc('bytes.received', Oct),
NChanState = emqx_channel:ensure_timer( NChanState = emqx_channel:received(Oct, ChanState),
stats_timer, emqx_channel:gc(1, Oct, ChanState)), NState = State#connection{chan_state = NChanState},
process_incoming(Data, State#state{chan_state = NChanState}); process_incoming(Data, NState);
handle(info, {Error, _Sock, Reason}, State) handle(info, {Error, _Sock, Reason}, State)
when Error == tcp_error; Error == ssl_error -> when Error == tcp_error; Error == ssl_error ->
shutdown(Reason, State); shutdown(Reason, State);
handle(info, {Closed, _Sock}, State = #state{chan_state = ChanState}) handle(info, {Closed, _Sock}, State)
when Closed == tcp_closed; Closed == ssl_closed -> when Closed == tcp_closed; Closed == ssl_closed ->
case emqx_channel:handle_info(sock_closed, ChanState) of {next_state, disconnected, State};
{ok, NChanState} ->
{next_state, disconnected, State#state{chan_state = NChanState}};
{stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState})
end;
handle(info, {Passive, _Sock}, State) when Passive == tcp_passive; handle(info, {Passive, _Sock}, State)
Passive == ssl_passive -> when Passive == tcp_passive; Passive == ssl_passive ->
%% Rate limit here:) %% Rate limit here:)
NState = ensure_rate_limit(State), NState = ensure_rate_limit(State),
case activate_socket(NState) of case activate_socket(NState) of
@ -358,25 +332,24 @@ handle(info, {Passive, _Sock}, State) when Passive == tcp_passive;
handle(info, activate_socket, State) -> handle(info, activate_socket, State) ->
%% Rate limit timer expired. %% Rate limit timer expired.
NState = State#state{conn_state = running}, NState = State#connection{active_state = running,
limit_timer = undefined
},
case activate_socket(NState) of case activate_socket(NState) of
ok -> ok -> keep_state(NState);
keep_state(NState#state{limit_timer = undefined});
{error, Reason} -> {error, Reason} ->
shutdown(Reason, NState) shutdown(Reason, NState)
end; end;
handle(info, {inet_reply, _Sock, ok}, State = #state{chan_state = ChanState}) -> handle(info, {inet_reply, _Sock, ok}, _State) ->
%% something sent %% something sent
NChanState = emqx_channel:ensure_timer(stats_timer, ChanState), keep_state_and_data;
keep_state(State#state{chan_state = NChanState});
handle(info, {inet_reply, _Sock, {error, Reason}}, State) -> handle(info, {inet_reply, _Sock, {error, Reason}}, State) ->
shutdown(Reason, State); shutdown(Reason, State);
handle(info, {timeout, TRef, keepalive}, handle(info, {timeout, TRef, keepalive},
State = #state{transport = Transport, socket = Socket}) State = #connection{transport = Transport, socket = Socket}) ->
when is_reference(TRef) ->
case Transport:getstat(Socket, [recv_oct]) of case Transport:getstat(Socket, [recv_oct]) of
{ok, [{recv_oct, RecvOct}]} -> {ok, [{recv_oct, RecvOct}]} ->
handle_timeout(TRef, {keepalive, RecvOct}, State); handle_timeout(TRef, {keepalive, RecvOct}, State);
@ -384,37 +357,41 @@ handle(info, {timeout, TRef, keepalive},
shutdown(Reason, State) shutdown(Reason, State)
end; end;
handle(info, {timeout, TRef, emit_stats}, State) when is_reference(TRef) -> handle(info, {timeout, TRef, emit_stats}, State) ->
handle_timeout(TRef, {emit_stats, stats(State)}, State); handle_timeout(TRef, {emit_stats, stats(State)}, State);
handle(info, {timeout, TRef, Msg}, State) when is_reference(TRef) -> handle(info, {timeout, TRef, Msg}, State) ->
handle_timeout(TRef, Msg, State); handle_timeout(TRef, Msg, State);
handle(info, {shutdown, conflict, {ClientId, NewPid}}, State) ->
?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]),
shutdown(conflict, State);
handle(info, {shutdown, Reason}, State) -> handle(info, {shutdown, Reason}, State) ->
shutdown(Reason, State); shutdown(Reason, State);
handle(info, Info, State = #state{chan_state = ChanState}) -> handle(info, Info, State = #connection{chan_state = ChanState}) ->
case emqx_channel:handle_info(Info, ChanState) of case emqx_channel:handle_info(Info, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
keep_state(State#state{chan_state = NChanState}); keep_state(State#connection{chan_state = NChanState});
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#connection{chan_state = NChanState})
end. end.
code_change(_Vsn, State, Data, _Extra) -> code_change(_Vsn, State, Data, _Extra) ->
{ok, State, Data}. {ok, State, Data}.
terminate(Reason, _StateName, #state{transport = Transport, terminate(Reason, _StateName, State) ->
socket = Socket, #connection{transport = Transport,
chan_state = ChanState}) -> socket = Socket,
chan_state = ChanState} = State,
?LOG(debug, "Terminated for ~p", [Reason]), ?LOG(debug, "Terminated for ~p", [Reason]),
ok = Transport:fast_close(Socket), ok = Transport:fast_close(Socket),
emqx_channel:terminate(Reason, ChanState). emqx_channel:terminate(Reason, ChanState).
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
register_self(State = #connection{chan_state = ChanState}) ->
emqx_channel:handle_cast({register, attrs(State), stats(State)}, ChanState).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Process incoming data %% Process incoming data
@ -425,13 +402,13 @@ process_incoming(Data, State) ->
process_incoming(<<>>, Packets, State) -> process_incoming(<<>>, Packets, State) ->
{keep_state, State, next_incoming_events(Packets)}; {keep_state, State, next_incoming_events(Packets)};
process_incoming(Data, Packets, State = #state{parse_state = ParseState}) -> process_incoming(Data, Packets, State = #connection{parse_state = ParseState, chan_state = ChanState}) ->
try emqx_frame:parse(Data, ParseState) of try emqx_frame:parse(Data, ParseState) of
{ok, NParseState} -> {ok, NParseState} ->
NState = State#state{parse_state = NParseState}, NState = State#connection{parse_state = NParseState},
{keep_state, NState, next_incoming_events(Packets)}; {keep_state, NState, next_incoming_events(Packets)};
{ok, Packet, Rest, NParseState} -> {ok, Packet, Rest, NParseState} ->
NState = State#state{parse_state = NParseState}, NState = State#connection{parse_state = NParseState},
process_incoming(Rest, [Packet|Packets], NState); process_incoming(Rest, [Packet|Packets], NState);
{error, Reason} -> {error, Reason} ->
shutdown(Reason, State) shutdown(Reason, State)
@ -439,54 +416,64 @@ process_incoming(Data, Packets, State = #state{parse_state = ParseState}) ->
error:Reason:Stk -> error:Reason:Stk ->
?LOG(error, "Parse failed for ~p~n\ ?LOG(error, "Parse failed for ~p~n\
Stacktrace:~p~nError data:~p", [Reason, Stk, Data]), Stacktrace:~p~nError data:~p", [Reason, Stk, Data]),
shutdown(parse_error, State) case emqx_channel:handle_out({disconnect, emqx_reason_codes:mqtt_frame_error(Reason)}, ChanState) of
{stop, Reason0, OutPackets, NChanState} ->
Shutdown = fun(NewSt) -> stop(Reason0, NewSt) end,
NState = State#connection{chan_state = NChanState},
handle_outgoing(OutPackets, Shutdown, NState);
{stop, Reason0, NChanState} ->
stop(Reason0, State#connection{chan_state = NChanState})
end
end. end.
next_incoming_events(Packets) when is_list(Packets) -> -compile({inline, [next_incoming_events/1]}).
next_incoming_events(Packets) ->
[next_event(cast, {incoming, Packet}) || Packet <- Packets]. [next_event(cast, {incoming, Packet}) || Packet <- Packets].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle incoming packet %% Handle incoming packet
handle_incoming(Packet = ?PACKET(Type), SuccFun, handle_incoming(Packet = ?PACKET(Type), SuccFun,
State = #state{chan_state = ChanState}) -> State = #connection{chan_state = ChanState}) ->
_ = inc_incoming_stats(Type), _ = inc_incoming_stats(Type),
ok = emqx_metrics:inc_recv(Packet), ok = emqx_metrics:inc_recv(Packet),
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
case emqx_channel:handle_in(Packet, ChanState) of case emqx_channel:handle_in(Packet, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
SuccFun(State#state{chan_state= NChanState}); SuccFun(State#connection{chan_state= NChanState});
{ok, OutPackets, NChanState} -> {ok, OutPackets, NChanState} ->
handle_outgoing(OutPackets, SuccFun, State#state{chan_state = NChanState}); handle_outgoing(OutPackets, SuccFun,
State#connection{chan_state = NChanState});
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}); stop(Reason, State#connection{chan_state = NChanState});
{stop, Reason, OutPacket, NChanState} -> {stop, Reason, OutPackets, NChanState} ->
Shutdown = fun(NewSt) -> shutdown(Reason, NewSt) end, Shutdown = fun(NewSt) -> stop(Reason, NewSt) end,
handle_outgoing(OutPacket, Shutdown, State#state{chan_state = NChanState}) NState = State#connection{chan_state = NChanState},
handle_outgoing(OutPackets, Shutdown, NState)
end. end.
%%------------------------------------------------------------------- %%-------------------------------------------------------------------
%% Handle deliver %% Handle deliver
handle_deliver(Delivers, State = #state{chan_state = ChanState}) -> handle_deliver(Delivers, State = #connection{chan_state = ChanState}) ->
case emqx_channel:handle_out({deliver, Delivers}, ChanState) of case emqx_channel:handle_out({deliver, Delivers}, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
keep_state(State#state{chan_state = NChanState}); keep_state(State#connection{chan_state = NChanState});
{ok, Packets, NChanState} -> {ok, Packets, NChanState} ->
NState = State#state{chan_state = NChanState}, NState = State#connection{chan_state = NChanState},
handle_outgoing(Packets, fun keep_state/1, NState); handle_outgoing(Packets, fun keep_state/1, NState);
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#connection{chan_state = NChanState})
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle outgoing packets %% Handle outgoing packets
handle_outgoing(Packets, SuccFun, State = #state{serialize = Serialize}) handle_outgoing(Packets, SuccFun, State = #connection{serialize = Serialize})
when is_list(Packets) -> when is_list(Packets) ->
send(lists:map(Serialize, Packets), SuccFun, State); send(lists:map(Serialize, Packets), SuccFun, State);
handle_outgoing(Packet, SuccFun, State = #state{serialize = Serialize}) -> handle_outgoing(Packet, SuccFun, State = #connection{serialize = Serialize}) ->
send(Serialize(Packet), SuccFun, State). send(Serialize(Packet), SuccFun, State).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -496,18 +483,21 @@ serialize_fun(ProtoVer) ->
fun(Packet = ?PACKET(Type)) -> fun(Packet = ?PACKET(Type)) ->
?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]),
_ = inc_outgoing_stats(Type), _ = inc_outgoing_stats(Type),
_ = emqx_metrics:inc_sent(Packet),
emqx_frame:serialize(Packet, ProtoVer) emqx_frame:serialize(Packet, ProtoVer)
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Send data %% Send data
send(IoData, SuccFun, State = #state{transport = Transport, send(IoData, SuccFun, State = #connection{transport = Transport,
socket = Socket}) -> socket = Socket,
chan_state = ChanState}) ->
Oct = iolist_size(IoData), Oct = iolist_size(IoData),
ok = emqx_metrics:inc('bytes.sent', Oct), ok = emqx_metrics:inc('bytes.sent', Oct),
case Transport:async_send(Socket, IoData) of case Transport:async_send(Socket, IoData) of
ok -> SuccFun(State); ok -> NChanState = emqx_channel:sent(Oct, ChanState),
SuccFun(State#connection{chan_state = NChanState});
{error, Reason} -> {error, Reason} ->
shutdown(Reason, State) shutdown(Reason, State)
end. end.
@ -515,49 +505,51 @@ send(IoData, SuccFun, State = #state{transport = Transport,
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle timeout %% Handle timeout
handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) -> handle_timeout(TRef, Msg, State = #connection{chan_state = ChanState}) ->
case emqx_channel:timeout(TRef, Msg, ChanState) of case emqx_channel:timeout(TRef, Msg, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
keep_state(State#state{chan_state = NChanState}); keep_state(State#connection{chan_state = NChanState});
{ok, Packets, NChanState} -> {ok, Packets, NChanState} ->
handle_outgoing(Packets, fun keep_state/1, handle_outgoing(Packets, fun keep_state/1,
State#state{chan_state = NChanState}); State#connection{chan_state = NChanState});
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#connection{chan_state = NChanState})
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Ensure rate limit %% Ensure rate limit
ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl}) -> -define(ENABLED(Rl), (Rl =/= undefined)).
Limiters = [{Pl, #state.pub_limit, emqx_pd:reset_counter(incoming_pubs)},
{Rl, #state.rate_limit, emqx_pd:reset_counter(incoming_bytes)}], ensure_rate_limit(State = #connection{rate_limit = Rl, pub_limit = Pl}) ->
Pubs = emqx_pd:reset_counter(incoming_pubs),
Bytes = emqx_pd:reset_counter(incoming_bytes),
Limiters = [{Pl, #connection.pub_limit, Pubs} || ?ENABLED(Pl)] ++
[{Rl, #connection.rate_limit, Bytes} || ?ENABLED(Rl)],
ensure_rate_limit(Limiters, State). ensure_rate_limit(Limiters, State).
ensure_rate_limit([], State) -> ensure_rate_limit([], State) ->
State; State;
ensure_rate_limit([{undefined, _Pos, _Cnt}|Limiters], State) ->
ensure_rate_limit(Limiters, State);
ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) -> ensure_rate_limit([{Rl, Pos, Cnt}|Limiters], State) ->
case esockd_rate_limit:check(Cnt, Rl) of case esockd_rate_limit:check(Cnt, Rl) of
{0, Rl1} -> {0, Rl1} ->
ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); ensure_rate_limit(Limiters, setelement(Pos, State, Rl1));
{Pause, Rl1} -> {Pause, Rl1} ->
?LOG(debug, "Rate limit pause connection ~pms", [Pause]), ?LOG(debug, "Pause ~pms due to rate limit", [Pause]),
TRef = erlang:send_after(Pause, self(), activate_socket), TRef = erlang:send_after(Pause, self(), activate_socket),
setelement(Pos, State#state{conn_state = blocked, NState = State#connection{active_state = blocked,
limit_timer = TRef}, Rl1) limit_timer = TRef},
setelement(Pos, NState, Rl1)
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Activate Socket %% Activate Socket
activate_socket(#state{conn_state = blocked}) -> activate_socket(#connection{active_state = blocked}) ->
ok; ok;
activate_socket(#state{transport = Transport, activate_socket(#connection{transport = Transport,
socket = Socket, socket = Socket,
active_n = N}) -> active_n = N}) ->
Transport:setopts(Socket, [{active, N}]). Transport:setopts(Socket, [{active, N}]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -570,11 +562,11 @@ activate_socket(#state{transport = Transport,
inc_incoming_stats(Type) -> inc_incoming_stats(Type) ->
emqx_pd:update_counter(recv_pkt, 1), emqx_pd:update_counter(recv_pkt, 1),
case Type == ?PUBLISH of if
true -> Type == ?PUBLISH ->
emqx_pd:update_counter(recv_msg, 1), emqx_pd:update_counter(recv_msg, 1),
emqx_pd:update_counter(incoming_pubs, 1); emqx_pd:update_counter(incoming_pubs, 1);
false -> ok true -> ok
end. end.
inc_outgoing_stats(Type) -> inc_outgoing_stats(Type) ->

View File

@ -50,45 +50,38 @@
-define(ENABLED(X), (is_integer(X) andalso X > 0)). -define(ENABLED(X), (is_integer(X) andalso X > 0)).
%% @doc Initialize force GC state. %% @doc Initialize force GC state.
-spec(init(opts() | false) -> maybe(gc_state())). -spec(init(opts()) -> gc_state()).
init(#{count := Count, bytes := Bytes}) -> init(#{count := Count, bytes := Bytes}) ->
Cnt = [{cnt, {Count, Count}} || ?ENABLED(Count)], Cnt = [{cnt, {Count, Count}} || ?ENABLED(Count)],
Oct = [{oct, {Bytes, Bytes}} || ?ENABLED(Bytes)], Oct = [{oct, {Bytes, Bytes}} || ?ENABLED(Bytes)],
?GCS(maps:from_list(Cnt ++ Oct)); ?GCS(maps:from_list(Cnt ++ Oct)).
init(false) -> undefined.
%% @doc Try to run GC based on reduntions of count or bytes. %% @doc Try to run GC based on reduntions of count or bytes.
-spec(run(pos_integer(), pos_integer(), gc_state()) -spec(run(pos_integer(), pos_integer(), gc_state())
-> {boolean(), gc_state()}). -> {boolean(), gc_state()}).
run(Cnt, Oct, ?GCS(St)) -> run(Cnt, Oct, ?GCS(St)) ->
{Res, St1} = run([{cnt, Cnt}, {oct, Oct}], St), {Res, St1} = run([{cnt, Cnt}, {oct, Oct}], St),
{Res, ?GCS(St1)}; {Res, ?GCS(St1)}.
run(_Cnt, _Oct, undefined) ->
{false, undefined}.
run([], St) -> run([], St) ->
{false, St}; {false, St};
run([{K, N}|T], St) -> run([{K, N}|T], St) ->
case dec(K, N, St) of case dec(K, N, St) of
{true, St1} -> {true, St1} ->
{true, do_gc(St1)}; true = erlang:garbage_collect(),
{true, do_reset(St1)};
{false, St1} -> {false, St1} ->
run(T, St1) run(T, St1)
end. end.
%% @doc Info of GC state. %% @doc Info of GC state.
-spec(info(gc_state()) -> maybe(map())). -spec(info(maybe(gc_state())) -> maybe(map())).
info(?GCS(St)) -> info(?GCS(St)) -> St.
St;
info(undefined) ->
undefined.
%% @doc Reset counters to zero. %% @doc Reset counters to zero.
-spec(reset(gc_state()) -> gc_state()). -spec(reset(maybe(gc_state())) -> gc_state()).
reset(?GCS(St)) -> reset(?GCS(St)) ->
?GCS(do_reset(St)); ?GCS(do_reset(St)).
reset(undefined) ->
undefined.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
@ -105,10 +98,6 @@ dec(Key, Num, St) ->
{true, St} {true, St}
end. end.
do_gc(St) ->
true = erlang:garbage_collect(),
do_reset(St).
do_reset(St) -> do_reset(St) ->
do_reset(cnt, do_reset(oct, St)). do_reset(cnt, do_reset(oct, St)).

View File

@ -20,13 +20,16 @@
-export([ merge_opts/2 -export([ merge_opts/2
, maybe_apply/2 , maybe_apply/2
, run_fold/2
, run_fold/3 , run_fold/3
, pipeline/3
, start_timer/2 , start_timer/2
, start_timer/3 , start_timer/3
, cancel_timer/1 , cancel_timer/1
, proc_name/2 , proc_name/2
, proc_stats/0 , proc_stats/0
, proc_stats/1 , proc_stats/1
, index_of/2
]). ]).
-export([ drain_deliver/0 -export([ drain_deliver/0
@ -40,7 +43,7 @@
]}). ]}).
%% @doc Merge options %% @doc Merge options
-spec(merge_opts(list(), list()) -> list()). -spec(merge_opts(Opts, Opts) -> Opts when Opts :: proplists:proplist()).
merge_opts(Defaults, Options) -> merge_opts(Defaults, Options) ->
lists:foldl( lists:foldl(
fun({Opt, Val}, Acc) -> fun({Opt, Val}, Acc) ->
@ -57,11 +60,34 @@ maybe_apply(_Fun, undefined) ->
maybe_apply(Fun, Arg) when is_function(Fun) -> maybe_apply(Fun, Arg) when is_function(Fun) ->
erlang:apply(Fun, [Arg]). erlang:apply(Fun, [Arg]).
run_fold([], Acc) ->
Acc;
run_fold([Fun|More], Acc) ->
run_fold(More, Fun(Acc)).
%% @doc RunFold
run_fold([], Acc, _State) -> run_fold([], Acc, _State) ->
Acc; Acc;
run_fold([Fun|More], Acc, State) -> run_fold([Fun|More], Acc, State) ->
run_fold(More, Fun(Acc, State), State). run_fold(More, Fun(Acc, State), State).
%% @doc Pipeline
pipeline([], Input, State) ->
{ok, Input, State};
pipeline([Fun|More], Input, State) ->
case Fun(Input, State) of
ok -> pipeline(More, Input, State);
{ok, NState} ->
pipeline(More, Input, NState);
{ok, NInput, NState} ->
pipeline(More, NInput, NState);
{error, Reason} ->
{error, Reason, State};
{error, Reason, NState} ->
{error, Reason, NState}
end.
-spec(start_timer(integer(), term()) -> reference()). -spec(start_timer(integer(), term()) -> reference()).
start_timer(Interval, Msg) -> start_timer(Interval, Msg) ->
start_timer(Interval, self(), Msg). start_timer(Interval, self(), Msg).
@ -123,3 +149,14 @@ drain_down(Cnt, Acc) ->
drain_down(0, Acc) drain_down(0, Acc)
end. end.
%% lists:index_of/2
index_of(E, L) ->
index_of(E, 1, L).
index_of(_E, _I, []) ->
error(badarg);
index_of(E, I, [E|_]) ->
I;
index_of(E, I, [_|L]) ->
index_of(E, I+1, L).

View File

@ -51,7 +51,6 @@ on_client_connected(#{client_id := ClientId,
proto_ver := ProtoVer, proto_ver := ProtoVer,
keepalive := Keepalive keepalive := Keepalive
}, Env) -> }, Env) ->
case emqx_json:safe_encode(maps:merge(#{clientid => ClientId, case emqx_json:safe_encode(maps:merge(#{clientid => ClientId,
username => Username, username => Username,
ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)), ipaddress => iolist_to_binary(esockd_net:ntoa(IpAddr)),
@ -67,6 +66,9 @@ on_client_connected(#{client_id := ClientId,
?LOG(error, "Encoding connected event error: ~p", [Reason]) ?LOG(error, "Encoding connected event error: ~p", [Reason])
end. end.
on_client_disconnected(#{client_id := ClientId, on_client_disconnected(#{client_id := ClientId,
username := Username}, Reason, Env) -> username := Username}, Reason, Env) ->
case emqx_json:safe_encode(#{clientid => ClientId, case emqx_json:safe_encode(#{clientid => ClientId,

View File

@ -21,6 +21,11 @@
-include_lib("emqx.hrl"). -include_lib("emqx.hrl").
-include_lib("emqx_mqtt.hrl"). -include_lib("emqx_mqtt.hrl").
-ifdef(TEST).
-compile(export_all).
-compile(nowarn_export_all).
-endif.
%% APIs %% APIs
-export([ rewrite_subscribe/4 -export([ rewrite_subscribe/4
, rewrite_unsubscribe/4 , rewrite_unsubscribe/4
@ -86,4 +91,3 @@ compile(Rules) ->
{ok, MP} = re:compile(Re), {ok, MP} = re:compile(Re),
{rewrite, Topic, MP, Dest} {rewrite, Topic, MP, Dest}
end, Rules). end, Rules).

View File

@ -41,8 +41,7 @@
-define(DISABLED, 0). -define(DISABLED, 0).
%% @doc Init the OOM policy. %% @doc Init the OOM policy.
-spec(init(maybe(opts())) -> oom_policy()). -spec(init(opts()) -> oom_policy()).
init(undefined) -> undefined;
init(#{message_queue_len := MaxQLen, init(#{message_queue_len := MaxQLen,
max_heap_size := MaxHeapSizeInBytes}) -> max_heap_size := MaxHeapSizeInBytes}) ->
MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize), MaxHeapSize = MaxHeapSizeInBytes div erlang:system_info(wordsize),
@ -60,8 +59,7 @@ init(#{message_queue_len := MaxQLen,
%% `ok': There is nothing out of the ordinary. %% `ok': There is nothing out of the ordinary.
%% `shutdown': Some numbers (message queue length hit the limit), %% `shutdown': Some numbers (message queue length hit the limit),
%% hence shutdown for greater good (system stability). %% hence shutdown for greater good (system stability).
-spec(check(maybe(oom_policy())) -> ok | {shutdown, reason()}). -spec(check(oom_policy()) -> ok | {shutdown, reason()}).
check(undefined) -> ok;
check({oom_policy, #{message_queue_len := MaxQLen, check({oom_policy, #{message_queue_len := MaxQLen,
max_heap_size := MaxHeapSize}}) -> max_heap_size := MaxHeapSize}}) ->
Qlength = proc_info(message_queue_len), Qlength = proc_info(message_queue_len),
@ -71,18 +69,15 @@ check({oom_policy, #{message_queue_len := MaxQLen,
{fun() -> is_exceeded(HeapSize, MaxHeapSize) end, {fun() -> is_exceeded(HeapSize, MaxHeapSize) end,
{shutdown, proc_heap_too_large}}]). {shutdown, proc_heap_too_large}}]).
do_check([]) -> do_check([]) -> ok;
ok;
do_check([{Pred, Result} | Rest]) -> do_check([{Pred, Result} | Rest]) ->
case Pred() of case Pred() of
true -> Result; true -> Result;
false -> do_check(Rest) false -> do_check(Rest)
end. end.
-spec(info(maybe(oom_policy())) -> maybe(opts())). -spec(info(oom_policy()) -> opts()).
info(undefined) -> undefined; info({oom_policy, Opts}) -> Opts.
info({oom_policy, Opts}) ->
Opts.
-compile({inline, -compile({inline,
[ is_exceeded/2 [ is_exceeded/2

View File

@ -19,6 +19,10 @@
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-export([ type/1
, qos/1
]).
-export([ proto_name/1 -export([ proto_name/1
, type_name/1 , type_name/1
, validate/1 , validate/1
@ -30,6 +34,12 @@
-compile(inline). -compile(inline).
type(#mqtt_packet{header = #mqtt_packet_header{type = Type}}) ->
Type.
qos(#mqtt_packet{header = #mqtt_packet_header{qos = QoS}}) ->
QoS.
%% @doc Protocol name of the version. %% @doc Protocol name of the version.
-spec(proto_name(emqx_types:version()) -> binary()). -spec(proto_name(emqx_types:version()) -> binary()).
proto_name(?MQTT_PROTO_V3) -> proto_name(?MQTT_PROTO_V3) ->
@ -167,11 +177,10 @@ will_msg(#mqtt_packet_connect{client_id = ClientId,
will_qos = QoS, will_qos = QoS,
will_topic = Topic, will_topic = Topic,
will_props = Properties, will_props = Properties,
will_payload = Payload, will_payload = Payload}) ->
proto_ver = ProtoVer}) ->
Msg = emqx_message:make(ClientId, QoS, Topic, Payload), Msg = emqx_message:make(ClientId, QoS, Topic, Payload),
Msg#message{flags = #{dup => false, retain => Retain}, Msg#message{flags = #{dup => false, retain => Retain},
headers = merge_props(#{username => Username, proto_ver => ProtoVer}, Properties)}. headers = merge_props(#{username => Username}, Properties)}.
merge_props(Headers, undefined) -> merge_props(Headers, undefined) ->
Headers; Headers;

View File

@ -56,25 +56,19 @@
-opaque(protocol() :: #protocol{}). -opaque(protocol() :: #protocol{}).
-define(INFO_KEYS, record_info(fields, protocol)).
-define(ATTR_KEYS, [proto_name, proto_ver, clean_start, keepalive]).
-spec(init(#mqtt_packet_connect{}) -> protocol()). -spec(init(#mqtt_packet_connect{}) -> protocol()).
init(#mqtt_packet_connect{proto_name = ProtoName, init(#mqtt_packet_connect{proto_name = ProtoName,
proto_ver = ProtoVer, proto_ver = ProtoVer,
will_props = WillProps,
clean_start = CleanStart, clean_start = CleanStart,
keepalive = Keepalive, keepalive = Keepalive,
properties = Properties, properties = Properties,
client_id = ClientId, client_id = ClientId,
username = Username username = Username} = ConnPkt) ->
} = ConnPkt) -> WillMsg = emqx_packet:will_msg(ConnPkt),
WillMsg = emqx_packet:will_msg(
case ProtoVer of
?MQTT_PROTO_V5 ->
WillDelayInterval = get_property('Will-Delay-Interval', WillProps, 0),
ConnPkt#mqtt_packet_connect{
will_props = set_property('Will-Delay-Interval', WillDelayInterval, WillProps)};
_ ->
ConnPkt
end),
#protocol{proto_name = ProtoName, #protocol{proto_name = ProtoName,
proto_ver = ProtoVer, proto_ver = ProtoVer,
clean_start = CleanStart, clean_start = CleanStart,
@ -85,30 +79,15 @@ init(#mqtt_packet_connect{proto_name = ProtoName,
conn_props = Properties conn_props = Properties
}. }.
info(#protocol{proto_name = ProtoName, -spec(info(protocol()) -> emqx_types:infos()).
proto_ver = ProtoVer, info(Proto) ->
clean_start = CleanStart, maps:from_list(info(?INFO_KEYS, Proto)).
keepalive = Keepalive,
client_id = ClientId,
username = Username,
will_msg = WillMsg,
conn_props = ConnProps,
topic_aliases = Aliases }) ->
#{proto_name => ProtoName,
proto_ver => ProtoVer,
clean_start => CleanStart,
keepalive => Keepalive,
client_id => ClientId,
username => Username,
will_msg => WillMsg,
conn_props => ConnProps,
topic_aliases => Aliases
}.
-spec(info(atom()|list(atom()), protocol()) -> term()).
info(Keys, Proto) when is_list(Keys) ->
[{Key, info(Key, Proto)} || Key <- Keys];
info(proto_name, #protocol{proto_name = ProtoName}) -> info(proto_name, #protocol{proto_name = ProtoName}) ->
ProtoName; ProtoName;
info(proto_ver, undefined) ->
?MQTT_PROTO_V4;
info(proto_ver, #protocol{proto_ver = ProtoVer}) -> info(proto_ver, #protocol{proto_ver = ProtoVer}) ->
ProtoVer; ProtoVer;
info(clean_start, #protocol{clean_start = CleanStart}) -> info(clean_start, #protocol{clean_start = CleanStart}) ->
@ -130,35 +109,23 @@ info(conn_props, #protocol{conn_props = ConnProps}) ->
info(topic_aliases, #protocol{topic_aliases = Aliases}) -> info(topic_aliases, #protocol{topic_aliases = Aliases}) ->
Aliases. Aliases.
attrs(#protocol{proto_name = ProtoName, -spec(attrs(protocol()) -> emqx_types:attrs()).
proto_ver = ProtoVer, attrs(Proto) ->
clean_start = CleanStart, maps:from_list(info(?ATTR_KEYS, Proto)).
keepalive = Keepalive}) ->
#{proto_name => ProtoName,
proto_ver => ProtoVer,
clean_start => CleanStart,
keepalive => Keepalive
}.
-spec(find_alias(emqx_types:alias_id(), protocol())
-> {ok, emqx_types:topic()} | false).
find_alias(_AliasId, #protocol{topic_aliases = undefined}) -> find_alias(_AliasId, #protocol{topic_aliases = undefined}) ->
false; false;
find_alias(AliasId, #protocol{topic_aliases = Aliases}) -> find_alias(AliasId, #protocol{topic_aliases = Aliases}) ->
maps:find(AliasId, Aliases). maps:find(AliasId, Aliases).
save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = undefined}) -> -spec(save_alias(emqx_types:alias_id(), emqx_types:topic(), protocol())
Protocol#protocol{topic_aliases = #{AliasId => Topic}}; -> protocol()).
save_alias(AliasId, Topic, Protocol = #protocol{topic_aliases = Aliases}) -> save_alias(AliasId, Topic, Proto = #protocol{topic_aliases = undefined}) ->
Protocol#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}. Proto#protocol{topic_aliases = #{AliasId => Topic}};
save_alias(AliasId, Topic, Proto = #protocol{topic_aliases = Aliases}) ->
Proto#protocol{topic_aliases = maps:put(AliasId, Topic, Aliases)}.
clear_will_msg(Protocol) -> clear_will_msg(Protocol) ->
Protocol#protocol{will_msg = undefined}. Protocol#protocol{will_msg = undefined}.
set_property(Name, Value, undefined) ->
#{Name => Value};
set_property(Name, Value, Props) ->
Props#{Name => Value}.
get_property(_Name, undefined, Default) ->
Default;
get_property(Name, Props, Default) ->
maps:get(Name, Props, Default).

View File

@ -24,7 +24,7 @@
, text/1 , text/1
, text/2 , text/2
, connack_error/1 , connack_error/1
, puback/1 , mqtt_frame_error/1
]). ]).
-export([compat/2]). -export([compat/2]).
@ -176,6 +176,5 @@ connack_error(banned) -> ?RC_BANNED;
connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD; connack_error(bad_authentication_method) -> ?RC_BAD_AUTHENTICATION_METHOD;
connack_error(_) -> ?RC_NOT_AUTHORIZED. connack_error(_) -> ?RC_NOT_AUTHORIZED.
%%TODO: This function should be removed. mqtt_frame_error(mqtt_frame_too_large) -> ?RC_PACKET_TOO_LARGE;
puback([]) -> ?RC_NO_MATCHING_SUBSCRIBERS; mqtt_frame_error(_) -> ?RC_MALFORMED_PACKET.
puback(L) when is_list(L) -> ?RC_SUCCESS.

View File

@ -58,6 +58,9 @@
, stats/1 , stats/1
]). ]).
%% Exports for unit tests
-export([set_field/3]).
-export([update_expiry_interval/2]). -export([update_expiry_interval/2]).
-export([ subscribe/4 -export([ subscribe/4
@ -85,9 +88,6 @@
-export_type([session/0]). -export_type([session/0]).
%% For test case
-export([set_pkt_id/2]).
-import(emqx_zone, [get_env/3]). -import(emqx_zone, [get_env/3]).
-record(session, { -record(session, {
@ -118,8 +118,11 @@
await_rel_timeout :: timeout(), await_rel_timeout :: timeout(),
%% Session Expiry Interval %% Session Expiry Interval
expiry_interval :: timeout(), expiry_interval :: timeout(),
%% Enqueue Count
enqueue_cnt :: non_neg_integer(),
%% Created at %% Created at
created_at :: erlang:timestamp() created_at :: erlang:timestamp()
}). }).
-opaque(session() :: #session{}). -opaque(session() :: #session{}).
@ -127,6 +130,14 @@
-type(publish() :: {publish, emqx_types:packet_id(), emqx_types:message()}). -type(publish() :: {publish, emqx_types:packet_id(), emqx_types:message()}).
-define(DEFAULT_BATCH_N, 1000). -define(DEFAULT_BATCH_N, 1000).
-define(ATTR_KEYS, [expiry_interval, created_at]).
-define(INFO_KEYS, [subscriptions, max_subscriptions, upgrade_qos, inflight,
max_inflight, retry_interval, mqueue_len, max_mqueue,
mqueue_dropped, next_pkt_id, awaiting_rel, max_awaiting_rel,
await_rel_timeout, expiry_interval, created_at]).
-define(STATS_KEYS, [subscriptions_cnt, max_subscriptions, inflight, max_inflight,
mqueue_len, max_mqueue, mqueue_dropped, awaiting_rel,
max_awaiting_rel, enqueue_cnt]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Init a session %% Init a session
@ -147,6 +158,7 @@ init(#{zone := Zone}, #{max_inflight := MaxInflight,
max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100), max_awaiting_rel = get_env(Zone, max_awaiting_rel, 100),
await_rel_timeout = get_env(Zone, await_rel_timeout, 3600*1000), await_rel_timeout = get_env(Zone, await_rel_timeout, 3600*1000),
expiry_interval = ExpiryInterval, expiry_interval = ExpiryInterval,
enqueue_cnt = 0,
created_at = os:timestamp() created_at = os:timestamp()
}. }.
@ -157,42 +169,26 @@ init_mqueue(Zone) ->
default_priority => get_env(Zone, mqueue_default_priority, lowest) default_priority => get_env(Zone, mqueue_default_priority, lowest)
}). }).
%%-------------------------------------------------------------------- %% @doc Get infos of the session.
%% Infos of the session
%%--------------------------------------------------------------------
-spec(info(session()) -> emqx_types:infos()). -spec(info(session()) -> emqx_types:infos()).
info(#session{max_subscriptions = MaxSubscriptions, info(Session) ->
subscriptions = Subscriptions, maps:from_list(info(?INFO_KEYS, Session)).
upgrade_qos = UpgradeQoS,
inflight = Inflight,
retry_interval = RetryInterval,
mqueue = MQueue,
next_pkt_id = PacketId,
max_awaiting_rel = MaxAwaitingRel,
awaiting_rel = AwaitingRel,
await_rel_timeout = AwaitRelTimeout,
expiry_interval = ExpiryInterval,
created_at = CreatedAt}) ->
#{subscriptions => Subscriptions,
max_subscriptions => MaxSubscriptions,
upgrade_qos => UpgradeQoS,
inflight => emqx_inflight:size(Inflight),
max_inflight => emqx_inflight:max_size(Inflight),
retry_interval => RetryInterval,
mqueue_len => emqx_mqueue:len(MQueue),
max_mqueue => emqx_mqueue:max_len(MQueue),
mqueue_dropped => emqx_mqueue:dropped(MQueue),
next_pkt_id => PacketId,
awaiting_rel => maps:size(AwaitingRel),
max_awaiting_rel => MaxAwaitingRel,
await_rel_timeout => AwaitRelTimeout,
expiry_interval => ExpiryInterval,
created_at => CreatedAt
}.
%% Get attrs of the session.
-spec(attrs(session()) -> emqx_types:attrs()).
attrs(Session) ->
maps:from_list(info(?ATTR_KEYS, Session)).
%% @doc Get stats of the session.
-spec(stats(session()) -> emqx_types:stats()).
stats(Session) -> info(?STATS_KEYS, Session).
info(Keys, Session) when is_list(Keys) ->
[{Key, info(Key, Session)} || Key <- Keys];
info(subscriptions, #session{subscriptions = Subs}) -> info(subscriptions, #session{subscriptions = Subs}) ->
Subs; Subs;
info(subscriptions_cnt, #session{subscriptions = Subs}) ->
maps:size(Subs);
info(max_subscriptions, #session{max_subscriptions = MaxSubs}) -> info(max_subscriptions, #session{max_subscriptions = MaxSubs}) ->
MaxSubs; MaxSubs;
info(upgrade_qos, #session{upgrade_qos = UpgradeQoS}) -> info(upgrade_qos, #session{upgrade_qos = UpgradeQoS}) ->
@ -219,47 +215,20 @@ info(await_rel_timeout, #session{await_rel_timeout = Timeout}) ->
Timeout; Timeout;
info(expiry_interval, #session{expiry_interval = Interval}) -> info(expiry_interval, #session{expiry_interval = Interval}) ->
Interval; Interval;
info(enqueue_cnt, #session{enqueue_cnt = Cnt}) ->
Cnt;
info(created_at, #session{created_at = CreatedAt}) -> info(created_at, #session{created_at = CreatedAt}) ->
CreatedAt. CreatedAt.
%% For tests
set_field(Name, Val, Channel) ->
Fields = record_info(fields, session),
Pos = emqx_misc:index_of(Name, Fields),
setelement(Pos+1, Channel, Val).
update_expiry_interval(ExpiryInterval, Session) -> update_expiry_interval(ExpiryInterval, Session) ->
Session#session{expiry_interval = ExpiryInterval}. Session#session{expiry_interval = ExpiryInterval}.
%%--------------------------------------------------------------------
%% Attrs of the session
%%--------------------------------------------------------------------
-spec(attrs(session()) -> emqx_types:attrs()).
attrs(undefined) ->
#{};
attrs(#session{expiry_interval = ExpiryInterval,
created_at = CreatedAt}) ->
#{expiry_interval => ExpiryInterval,
created_at => CreatedAt
}.
%%--------------------------------------------------------------------
%% Stats of the session
%%--------------------------------------------------------------------
%% @doc Get stats of the session.
-spec(stats(session()) -> emqx_types:stats()).
stats(#session{subscriptions = Subscriptions,
max_subscriptions = MaxSubscriptions,
inflight = Inflight,
mqueue = MQueue,
awaiting_rel = AwaitingRel,
max_awaiting_rel = MaxAwaitingRel}) ->
[{subscriptions, maps:size(Subscriptions)},
{max_subscriptions, MaxSubscriptions},
{inflight, emqx_inflight:size(Inflight)},
{max_inflight, emqx_inflight:max_size(Inflight)},
{mqueue_len, emqx_mqueue:len(MQueue)},
{max_mqueue, emqx_mqueue:max_len(MQueue)},
{mqueue_dropped, emqx_mqueue:dropped(MQueue)},
{awaiting_rel, maps:size(AwaitingRel)},
{max_awaiting_rel, MaxAwaitingRel}].
-spec(takeover(session()) -> ok). -spec(takeover(session()) -> ok).
takeover(#session{subscriptions = Subs}) -> takeover(#session{subscriptions = Subs}) ->
lists:foreach(fun({TopicFilter, _SubOpts}) -> lists:foreach(fun({TopicFilter, _SubOpts}) ->
@ -268,7 +237,6 @@ takeover(#session{subscriptions = Subs}) ->
-spec(resume(emqx_types:client_id(), session()) -> ok). -spec(resume(emqx_types:client_id(), session()) -> ok).
resume(ClientId, #session{subscriptions = Subs}) -> resume(ClientId, #session{subscriptions = Subs}) ->
?LOG(info, "Session is resumed."),
%% 1. Subscribe again. %% 1. Subscribe again.
lists:foreach(fun({TopicFilter, SubOpts}) -> lists:foreach(fun({TopicFilter, SubOpts}) ->
ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts) ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts)
@ -295,8 +263,8 @@ redeliver(Session = #session{inflight = Inflight}) ->
%% Client -> Broker: SUBSCRIBE %% Client -> Broker: SUBSCRIBE
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(subscribe(emqx_types:client(), emqx_types:topic(), emqx_types:subopts(), -spec(subscribe(emqx_types:client(), emqx_types:topic(), emqx_types:subopts(), session())
session()) -> {ok, session()} | {error, emqx_types:reason_code()}). -> {ok, session()} | {error, emqx_types:reason_code()}).
subscribe(Client, TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) -> subscribe(Client, TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) ->
case is_subscriptions_full(Session) case is_subscriptions_full(Session)
andalso (not maps:is_key(TopicFilter, Subs)) of andalso (not maps:is_key(TopicFilter, Subs)) of
@ -311,8 +279,9 @@ is_subscriptions_full(#session{max_subscriptions = MaxLimit,
subscriptions = Subs}) -> subscriptions = Subs}) ->
maps:size(Subs) >= MaxLimit. maps:size(Subs) >= MaxLimit.
do_subscribe(Client = #{client_id := ClientId}, -compile({inline, [do_subscribe/4]}).
TopicFilter, SubOpts, Session = #session{subscriptions = Subs}) -> do_subscribe(Client = #{client_id := ClientId}, TopicFilter, SubOpts,
Session = #session{subscriptions = Subs}) ->
case IsNew = (not maps:is_key(TopicFilter, Subs)) of case IsNew = (not maps:is_key(TopicFilter, Subs)) of
true -> true ->
ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts); ok = emqx_broker:subscribe(TopicFilter, ClientId, SubOpts);
@ -320,9 +289,8 @@ do_subscribe(Client = #{client_id := ClientId},
_ = emqx_broker:set_subopts(TopicFilter, SubOpts) _ = emqx_broker:set_subopts(TopicFilter, SubOpts)
end, end,
ok = emqx_hooks:run('session.subscribed', ok = emqx_hooks:run('session.subscribed',
[Client, TopicFilter, SubOpts#{new => IsNew}]), [Client, TopicFilter, SubOpts#{is_new => IsNew}]),
Subs1 = maps:put(TopicFilter, SubOpts, Subs), {ok, Session#session{subscriptions = maps:put(TopicFilter, SubOpts, Subs)}}.
{ok, Session#session{subscriptions = Subs1}}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Client -> Broker: UNSUBSCRIBE %% Client -> Broker: UNSUBSCRIBE
@ -353,8 +321,6 @@ publish(PacketId, Msg = #message{qos = ?QOS_2}, Session) ->
false -> false ->
do_publish(PacketId, Msg, Session); do_publish(PacketId, Msg, Session);
true -> true ->
?LOG(warning, "Dropped qos2 packet ~w for too many awaiting_rel", [PacketId]),
ok = emqx_metrics:inc('messages.qos2.dropped'),
{error, ?RC_RECEIVE_MAXIMUM_EXCEEDED} {error, ?RC_RECEIVE_MAXIMUM_EXCEEDED}
end; end;
@ -386,42 +352,39 @@ do_publish(PacketId, Msg = #message{timestamp = Ts},
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(puback(emqx_types:packet_id(), session()) -spec(puback(emqx_types:packet_id(), session())
-> {ok, session()} | {ok, list(publish()), session()} | -> {ok, emqx_types:message(), session()}
{error, emqx_types:reason_code()}). | {ok, emqx_types:message(), list(publish()), session()}
| {error, emqx_types:reason_code()}).
puback(PacketId, Session = #session{inflight = Inflight}) -> puback(PacketId, Session = #session{inflight = Inflight}) ->
case emqx_inflight:lookup(PacketId, Inflight) of case emqx_inflight:lookup(PacketId, Inflight) of
{value, {Msg, _Ts}} when is_record(Msg, message) -> {value, {Msg, _Ts}} when is_record(Msg, message) ->
ok = emqx_hooks:run('message.acked', [Msg]),
Inflight1 = emqx_inflight:delete(PacketId, Inflight), Inflight1 = emqx_inflight:delete(PacketId, Inflight),
dequeue(Session#session{inflight = Inflight1}); return_with(Msg, dequeue(Session#session{inflight = Inflight1}));
{value, {_OtherPub, _Ts}} -> {value, {_Pubrel, _Ts}} ->
?LOG(warning, "The PacketId has been used, PacketId: ~p", [PacketId]),
{error, ?RC_PACKET_IDENTIFIER_IN_USE}; {error, ?RC_PACKET_IDENTIFIER_IN_USE};
none -> none ->
?LOG(warning, "The PUBACK PacketId ~w is not found", [PacketId]),
ok = emqx_metrics:inc('packets.puback.missed'),
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}
end. end.
return_with(Msg, {ok, Session}) ->
{ok, Msg, Session};
return_with(Msg, {ok, Publishes, Session}) ->
{ok, Msg, Publishes, Session}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Client -> Broker: PUBREC %% Client -> Broker: PUBREC
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(pubrec(emqx_types:packet_id(), session()) -spec(pubrec(emqx_types:packet_id(), session())
-> {ok, session()} | {error, emqx_types:reason_code()}). -> {ok, emqx_types:message(), session()} | {error, emqx_types:reason_code()}).
pubrec(PacketId, Session = #session{inflight = Inflight}) -> pubrec(PacketId, Session = #session{inflight = Inflight}) ->
case emqx_inflight:lookup(PacketId, Inflight) of case emqx_inflight:lookup(PacketId, Inflight) of
{value, {Msg, _Ts}} when is_record(Msg, message) -> {value, {Msg, _Ts}} when is_record(Msg, message) ->
ok = emqx_hooks:run('message.acked', [Msg]),
Inflight1 = emqx_inflight:update(PacketId, {pubrel, os:timestamp()}, Inflight), Inflight1 = emqx_inflight:update(PacketId, {pubrel, os:timestamp()}, Inflight),
{ok, Session#session{inflight = Inflight1}}; {ok, Msg, Session#session{inflight = Inflight1}};
{value, {pubrel, _Ts}} -> {value, {pubrel, _Ts}} ->
?LOG(warning, "The PUBREC ~w is duplicated", [PacketId]),
ok = emqx_metrics:inc('packets.pubrec.inuse'),
{error, ?RC_PACKET_IDENTIFIER_IN_USE}; {error, ?RC_PACKET_IDENTIFIER_IN_USE};
none -> none ->
?LOG(warning, "The PUBREC ~w is not found.", [PacketId]),
ok = emqx_metrics:inc('packets.pubrec.missed'),
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}
end. end.
@ -436,8 +399,6 @@ pubrel(PacketId, Session = #session{awaiting_rel = AwaitingRel}) ->
{_Ts, AwaitingRel1} -> {_Ts, AwaitingRel1} ->
{ok, Session#session{awaiting_rel = AwaitingRel1}}; {ok, Session#session{awaiting_rel = AwaitingRel1}};
error -> error ->
?LOG(warning, "The PUBREL PacketId ~w is not found", [PacketId]),
ok = emqx_metrics:inc('packets.pubrel.missed'),
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}
end. end.
@ -446,16 +407,14 @@ pubrel(PacketId, Session = #session{awaiting_rel = AwaitingRel}) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(pubcomp(emqx_types:packet_id(), session()) -spec(pubcomp(emqx_types:packet_id(), session())
-> {ok, session()} | {ok, list(publish()), session()} | -> {ok, session()} | {ok, list(publish()), session()}
{error, emqx_types:reason_code()}). | {error, emqx_types:reason_code()}).
pubcomp(PacketId, Session = #session{inflight = Inflight}) -> pubcomp(PacketId, Session = #session{inflight = Inflight}) ->
case emqx_inflight:contain(PacketId, Inflight) of case emqx_inflight:contain(PacketId, Inflight) of
true -> true ->
Inflight1 = emqx_inflight:delete(PacketId, Inflight), Inflight1 = emqx_inflight:delete(PacketId, Inflight),
dequeue(Session#session{inflight = Inflight1}); dequeue(Session#session{inflight = Inflight1});
false -> false ->
?LOG(warning, "The PUBCOMP PacketId ~w is not found", [PacketId]),
ok = emqx_metrics:inc('packets.pubcomp.missed'),
{error, ?RC_PACKET_IDENTIFIER_NOT_FOUND} {error, ?RC_PACKET_IDENTIFIER_NOT_FOUND}
end. end.
@ -493,7 +452,7 @@ batch_n(Inflight) ->
deliver(Delivers, Session = #session{subscriptions = Subs}) deliver(Delivers, Session = #session{subscriptions = Subs})
when is_list(Delivers) -> when is_list(Delivers) ->
Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session) Msgs = [enrich_subopt(get_subopts(Topic, Subs), Msg, Session)
|| {deliver, Topic, Msg} <- Delivers], || {deliver, Topic, Msg} <- Delivers],
deliver(Msgs, [], Session). deliver(Msgs, [], Session).
@ -515,23 +474,20 @@ deliver([Msg = #message{qos = QoS}|More], Acc,
deliver(More, [Publish|Acc], next_pkt_id(Session1)) deliver(More, [Publish|Acc], next_pkt_id(Session1))
end. end.
enqueue(Delivers, Session = #session{subscriptions = Subs}) enqueue(Delivers, Session = #session{subscriptions = Subs}) when is_list(Delivers) ->
when is_list(Delivers) -> Msgs = [enrich_subopt(get_subopts(Topic, Subs), Msg, Session)
Msgs = [enrich(get_subopts(Topic, Subs), Msg, Session)
|| {deliver, Topic, Msg} <- Delivers], || {deliver, Topic, Msg} <- Delivers],
lists:foldl(fun enqueue/2, Session, Msgs); lists:foldl(fun enqueue/2, Session, Msgs);
enqueue(Msg, Session = #session{mqueue = Q}) when is_record(Msg, message) -> enqueue(Msg, Session = #session{mqueue = Q, enqueue_cnt = Cnt})
emqx_pd:update_counter(enqueue_stats, 1), when is_record(Msg, message) ->
{Dropped, NewQ} = emqx_mqueue:in(Msg, Q), {Dropped, NewQ} = emqx_mqueue:in(Msg, Q),
if if is_record(Dropped, message) ->
Dropped =/= undefined -> ?LOG(warning, "Dropped msg due to mqueue is full: ~s",
%% TODO:... [emqx_message:format(Dropped)]);
%% SessProps = #{client_id => ClientId, username => Username}, true -> ok
ok; %% = emqx_hooks:run('message.dropped', [SessProps, Dropped]);
true -> ok
end, end,
Session#session{mqueue = NewQ}. Session#session{mqueue = NewQ, enqueue_cnt = Cnt+1}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Awaiting ACK for QoS1/QoS2 Messages %% Awaiting ACK for QoS1/QoS2 Messages
@ -550,26 +506,26 @@ get_subopts(Topic, SubMap) ->
error -> [] error -> []
end. end.
enrich([], Msg, _Session) -> enrich_subopt([], Msg, _Session) -> Msg;
Msg; enrich_subopt([{nl, 1}|Opts], Msg, Session) ->
%%enrich([{nl, 1}|_Opts], #message{from = ClientId}, #session{client_id = ClientId}) -> enrich_subopt(Opts, emqx_message:set_flag(nl, Msg), Session);
%% ignore; enrich_subopt([{nl, 0}|Opts], Msg, Session) ->
enrich([{nl, _}|Opts], Msg, Session) -> enrich_subopt(Opts, Msg, Session);
enrich(Opts, Msg, Session); enrich_subopt([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS},
enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= true}) -> Session = #session{upgrade_qos= true}) ->
enrich(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session); enrich_subopt(Opts, Msg#message{qos = max(SubQoS, PubQoS)}, Session);
enrich([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS}, Session = #session{upgrade_qos= false}) -> enrich_subopt([{qos, SubQoS}|Opts], Msg = #message{qos = PubQoS},
enrich(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session); Session = #session{upgrade_qos= false}) ->
enrich([{rap, 0}|Opts], Msg = #message{flags = Flags, headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) -> enrich_subopt(Opts, Msg#message{qos = min(SubQoS, PubQoS)}, Session);
enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); enrich_subopt([{rap, 1}|Opts], Msg, Session) ->
enrich([{rap, _}|Opts], Msg = #message{headers = #{proto_ver := ?MQTT_PROTO_V5}}, Session) -> enrich_subopt(Opts, Msg, Session);
enrich(Opts, Msg, Session); enrich_subopt([{rap, 0}|Opts], Msg = #message{headers = #{retained := true}}, Session) ->
enrich([{rap, _}|Opts], Msg = #message{headers = #{retained := true}}, Session = #session{}) -> enrich_subopt(Opts, Msg, Session);
enrich(Opts, Msg, Session); enrich_subopt([{rap, 0}|Opts], Msg, Session) ->
enrich([{rap, _}|Opts], Msg = #message{flags = Flags}, Session) -> enrich_subopt(Opts, emqx_message:set_flag(retain, false, Msg), Session);
enrich(Opts, Msg#message{flags = maps:put(retain, false, Flags)}, Session); enrich_subopt([{subid, SubId}|Opts], Msg, Session) ->
enrich([{subid, SubId}|Opts], Msg, Session) -> Msg1 = emqx_message:set_header('Subscription-Identifier', SubId, Msg),
enrich(Opts, emqx_message:set_header('Subscription-Identifier', SubId, Msg), Session). enrich_subopt(Opts, Msg1, Session).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Retry Delivery %% Retry Delivery
@ -608,8 +564,9 @@ retry_delivery(PacketId, Msg, Now, Acc, Inflight) when is_record(Msg, message) -
ok = emqx_metrics:inc('messages.expired'), ok = emqx_metrics:inc('messages.expired'),
{Acc, emqx_inflight:delete(PacketId, Inflight)}; {Acc, emqx_inflight:delete(PacketId, Inflight)};
false -> false ->
{[{publish, PacketId, Msg}|Acc], Msg1 = emqx_message:set_flag(dup, true, Msg),
emqx_inflight:update(PacketId, {Msg, Now}, Inflight)} {[{publish, PacketId, Msg1}|Acc],
emqx_inflight:update(PacketId, {Msg1, Now}, Inflight)}
end; end;
retry_delivery(PacketId, pubrel, Now, Acc, Inflight) -> retry_delivery(PacketId, pubrel, Now, Acc, Inflight) ->
@ -654,10 +611,3 @@ next_pkt_id(Session = #session{next_pkt_id = 16#FFFF}) ->
next_pkt_id(Session = #session{next_pkt_id = Id}) -> next_pkt_id(Session = #session{next_pkt_id = Id}) ->
Session#session{next_pkt_id = Id + 1}. Session#session{next_pkt_id = Id + 1}.
%%---------------------------------------------------------------------
%% For Test case
%%---------------------------------------------------------------------
set_pkt_id(Session, PktId) ->
Session#session{next_pkt_id = PktId}.

View File

@ -167,6 +167,7 @@ bin(L) when is_list(L) -> list_to_binary(L).
levels(Topic) when is_binary(Topic) -> levels(Topic) when is_binary(Topic) ->
length(tokens(Topic)). length(tokens(Topic)).
-compile({inline, [tokens/1]}).
%% @doc Split topic to tokens. %% @doc Split topic to tokens.
-spec(tokens(topic()) -> list(binary())). -spec(tokens(topic()) -> list(binary())).
tokens(Topic) -> tokens(Topic) ->
@ -204,12 +205,12 @@ join([]) ->
join([W]) -> join([W]) ->
bin(W); bin(W);
join(Words) -> join(Words) ->
{_, Bin} = {_, Bin} = lists:foldr(
lists:foldr(fun(W, {true, Tail}) -> fun(W, {true, Tail}) ->
{false, <<W/binary, Tail/binary>>}; {false, <<W/binary, Tail/binary>>};
(W, {false, Tail}) -> (W, {false, Tail}) ->
{false, <<W/binary, "/", Tail/binary>>} {false, <<W/binary, "/", Tail/binary>>}
end, {true, <<>>}, [bin(W) || W <- Words]), end, {true, <<>>}, [bin(W) || W <- Words]),
Bin. Bin.
-spec(parse(topic() | {topic(), map()}) -> {topic(), #{share => binary()}}). -spec(parse(topic() | {topic(), map()}) -> {topic(), #{share => binary()}}).

View File

@ -65,7 +65,7 @@ trace(publish, #message{topic = <<"$SYS/", _/binary>>}) ->
ignore; ignore;
trace(publish, #message{from = From, topic = Topic, payload = Payload}) trace(publish, #message{from = From, topic = Topic, payload = Payload})
when is_binary(From); is_atom(From) -> when is_binary(From); is_atom(From) ->
emqx_logger:info(#{topic => Topic}, "PUBLISH to ~s: ~p", [Topic, Payload]). emqx_logger:info(#{topic => Topic, mfa => {?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY} }, "PUBLISH to ~s: ~p", [Topic, Payload]).
%% @doc Start to trace client_id or topic. %% @doc Start to trace client_id or topic.
-spec(start_trace(trace_who(), logger:level(), string()) -> ok | {error, term()}). -spec(start_trace(trace_who(), logger:level(), string()) -> ok | {error, term()}).

View File

@ -31,7 +31,7 @@
, subid/0 , subid/0
]). ]).
-export_type([ conn/0 -export_type([ conninfo/0
, client/0 , client/0
, client_id/0 , client_id/0
, username/0 , username/0
@ -43,6 +43,7 @@
-export_type([ connack/0 -export_type([ connack/0
, subopts/0 , subopts/0
, reason_code/0 , reason_code/0
, alias_id/0
, properties/0 , properties/0
]). ]).
@ -94,12 +95,12 @@
-type(topic() :: emqx_topic:topic()). -type(topic() :: emqx_topic:topic()).
-type(subid() :: binary() | atom()). -type(subid() :: binary() | atom()).
-type(conn() :: #{peername := peername(), -type(conninfo() :: #{peername := peername(),
sockname := peername(), sockname := peername(),
peercert := esockd_peercert:peercert(), peercert := esockd_peercert:peercert(),
conn_mod := module(), conn_mod := module(),
atom() => term() atom() => term()
}). }).
-type(client() :: #{zone := zone(), -type(client() :: #{zone := zone(),
conn_mod := maybe(module()), conn_mod := maybe(module()),
peername := peername(), peername := peername(),
@ -142,6 +143,7 @@
}). }).
-type(reason_code() :: 0..16#FF). -type(reason_code() :: 0..16#FF).
-type(packet_id() :: 1..16#FFFF). -type(packet_id() :: 1..16#FFFF).
-type(alias_id() :: 0..16#FFFF).
-type(properties() :: #{atom() => term()}). -type(properties() :: #{atom() => term()}).
-type(topic_filters() :: list({topic(), subopts()})). -type(topic_filters() :: list({topic(), subopts()})).
-type(packet() :: #mqtt_packet{}). -type(packet() :: #mqtt_packet{}).

View File

@ -27,12 +27,10 @@
-export([ info/1 -export([ info/1
, attrs/1 , attrs/1
, stats/1 , stats/1
, state/1
]). ]).
-export([ kick/1 -export([call/2]).
, discard/1
, takeover/2
]).
%% WebSocket callbacks %% WebSocket callbacks
-export([ init/2 -export([ init/2
@ -42,19 +40,29 @@
, terminate/3 , terminate/3
]). ]).
-record(state, { -record(ws_connection, {
peername :: emqx_types:peername(), %% Peername of the ws connection.
sockname :: emqx_types:peername(), peername :: emqx_types:peername(),
fsm_state :: idle | connected | disconnected, %% Sockname of the ws connection
serialize :: fun((emqx_types:packet()) -> iodata()), sockname :: emqx_types:peername(),
%% FSM state
fsm_state :: idle | connected | disconnected,
%% Parser State
parse_state :: emqx_frame:parse_state(), parse_state :: emqx_frame:parse_state(),
chan_state :: emqx_channel:channel(), %% Serialize function
pendings :: list(), serialize :: fun((emqx_types:packet()) -> iodata()),
%% Channel State
chan_state :: emqx_channel:channel(),
%% Out Pending Packets
pendings :: list(emqx_types:packet()),
%% The stop reason
stop_reason :: term() stop_reason :: term()
}). }).
-type(state() :: #state{}). -type(ws_connection() :: #ws_connection{}).
-define(INFO_KEYS, [socktype, peername, sockname, active_state]).
-define(ATTR_KEYS, [socktype, peername, sockname]).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]).
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
@ -62,40 +70,44 @@
%% API %% API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(info(pid() | state()) -> emqx_types:infos()). -spec(info(pid()|ws_connection()) -> emqx_types:infos()).
info(WSPid) when is_pid(WSPid) -> info(WsPid) when is_pid(WsPid) ->
call(WSPid, info); call(WsPid, info);
info(#state{peername = Peername, info(WsConn = #ws_connection{chan_state = ChanState}) ->
sockname = Sockname, ConnInfo = info(?INFO_KEYS, WsConn),
chan_state = ChanState}) ->
ConnInfo = #{socktype => websocket,
peername => Peername,
sockname => Sockname,
conn_state => running
},
ChanInfo = emqx_channel:info(ChanState), ChanInfo = emqx_channel:info(ChanState),
maps:merge(ConnInfo, ChanInfo). maps:merge(ChanInfo, #{connection => maps:from_list(ConnInfo)}).
-spec(attrs(pid() | state()) -> emqx_types:attrs()). info(Keys, WsConn) when is_list(Keys) ->
attrs(WSPid) when is_pid(WSPid) -> [{Key, info(Key, WsConn)} || Key <- Keys];
call(WSPid, attrs); info(socktype, #ws_connection{}) ->
attrs(#state{peername = Peername, websocket;
sockname = Sockname, info(peername, #ws_connection{peername = Peername}) ->
chan_state = ChanState}) -> Peername;
ConnAttrs = #{socktype => websocket, info(sockname, #ws_connection{sockname = Sockname}) ->
peername => Peername, Sockname;
sockname => Sockname info(active_state, #ws_connection{}) ->
}, running;
info(chan_state, #ws_connection{chan_state = ChanState}) ->
emqx_channel:info(ChanState).
-spec(attrs(pid()|ws_connection()) -> emqx_types:attrs()).
attrs(WsPid) when is_pid(WsPid) ->
call(WsPid, attrs);
attrs(WsConn = #ws_connection{chan_state = ChanState}) ->
ConnAttrs = info(?ATTR_KEYS, WsConn),
ChanAttrs = emqx_channel:attrs(ChanState), ChanAttrs = emqx_channel:attrs(ChanState),
maps:merge(ConnAttrs, ChanAttrs). maps:merge(ChanAttrs, #{connection => maps:from_list(ConnAttrs)}).
-spec(stats(pid() | state()) -> emqx_types:stats()). -spec(stats(pid()|ws_connection()) -> emqx_types:stats()).
stats(WSPid) when is_pid(WSPid) -> stats(WsPid) when is_pid(WsPid) ->
call(WSPid, stats); call(WsPid, stats);
stats(#state{chan_state = ChanState}) -> stats(#ws_connection{chan_state = ChanState}) ->
ProcStats = emqx_misc:proc_stats(), ProcStats = emqx_misc:proc_stats(),
SockStats = wsock_stats(),
ConnStats = conn_stats(),
ChanStats = emqx_channel:stats(ChanState), ChanStats = emqx_channel:stats(ChanState),
lists:append([ProcStats, wsock_stats(), conn_stats(), ChanStats]). lists:append([ProcStats, SockStats, ConnStats, ChanStats]).
wsock_stats() -> wsock_stats() ->
[{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS]. [{Key, emqx_pd:get_counter(Key)} || Key <- ?SOCK_STATS].
@ -103,22 +115,14 @@ wsock_stats() ->
conn_stats() -> conn_stats() ->
[{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS]. [{Name, emqx_pd:get_counter(Name)} || Name <- ?CONN_STATS].
-spec(kick(pid()) -> ok). -spec(state(pid()) -> ws_connection()).
kick(CPid) -> state(WsPid) -> call(WsPid, state).
call(CPid, kick).
-spec(discard(pid()) -> ok). %% kick|discard|takeover
discard(WSPid) -> -spec(call(pid(), Req :: term()) -> Reply :: term()).
WSPid ! {cast, discard}, ok. call(WsPid, Req) when is_pid(WsPid) ->
Mref = erlang:monitor(process, WsPid),
-spec(takeover(pid(), 'begin'|'end') -> Result :: term()). WsPid ! {call, {self(), Mref}, Req},
takeover(CPid, Phase) ->
call(CPid, {takeover, Phase}).
%% @private
call(WSPid, Req) when is_pid(WSPid) ->
Mref = erlang:monitor(process, WSPid),
WSPid ! {call, {self(), Mref}, Req},
receive receive
{Mref, Reply} -> {Mref, Reply} ->
erlang:demonitor(Mref, [flush]), erlang:demonitor(Mref, [flush]),
@ -142,10 +146,10 @@ init(Req, Opts) ->
I -> I I -> I
end, end,
Compress = proplists:get_value(compress, Opts, false), Compress = proplists:get_value(compress, Opts, false),
WsOpts = #{compress => Compress, WsOpts = #{compress => Compress,
deflate_opts => DeflateOptions, deflate_opts => DeflateOptions,
max_frame_size => MaxFrameSize, max_frame_size => MaxFrameSize,
idle_timeout => IdleTimeout idle_timeout => IdleTimeout
}, },
case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of
undefined -> undefined ->
@ -156,7 +160,7 @@ init(Req, Opts) ->
<<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), <<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req),
{cowboy_websocket, Resp, [Req, Opts], WsOpts}; {cowboy_websocket, Resp, [Req, Opts], WsOpts};
_ -> _ ->
{ok, cowboy_req:reply(400, Req), #state{}} {ok, cowboy_req:reply(400, Req), WsOpts}
end. end.
websocket_init([Req, Opts]) -> websocket_init([Req, Opts]) ->
@ -169,54 +173,51 @@ websocket_init([Req, Opts]) ->
?LOG(error, "Illegal cookie"), ?LOG(error, "Illegal cookie"),
undefined; undefined;
Error:Reason -> Error:Reason ->
?LOG(error, "Cookie is parsed failed, Error: ~p, Reason ~p", ?LOG(error, "Failed to parse cookie, Error: ~p, Reason ~p",
[Error, Reason]), [Error, Reason]),
undefined undefined
end, end,
ChanState = emqx_channel:init(#{peername => Peername, ChanState = emqx_channel:init(#{peername => Peername,
sockname => Sockname, sockname => Sockname,
peercert => Peercert, peercert => Peercert,
ws_cookie => WsCookie, ws_cookie => WsCookie,
conn_mod => ?MODULE conn_mod => ?MODULE
}, Opts), }, Opts),
Zone = proplists:get_value(zone, Opts), Zone = proplists:get_value(zone, Opts),
MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE), MaxSize = emqx_zone:get_env(Zone, max_packet_size, ?MAX_PACKET_SIZE),
ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}), ParseState = emqx_frame:initial_parse_state(#{max_size => MaxSize}),
emqx_logger:set_metadata_peername(esockd_net:format(Peername)), emqx_logger:set_metadata_peername(esockd_net:format(Peername)),
{ok, #state{peername = Peername, {ok, #ws_connection{peername = Peername,
sockname = Sockname, sockname = Sockname,
fsm_state = idle, fsm_state = idle,
parse_state = ParseState, parse_state = ParseState,
chan_state = ChanState, chan_state = ChanState,
pendings = [] pendings = []}}.
}}.
websocket_handle({binary, Data}, State) when is_list(Data) -> websocket_handle({binary, Data}, State) when is_list(Data) ->
websocket_handle({binary, iolist_to_binary(Data)}, State); websocket_handle({binary, iolist_to_binary(Data)}, State);
websocket_handle({binary, Data}, State = #state{chan_state = ChanState}) websocket_handle({binary, Data}, State = #ws_connection{chan_state = ChanState}) ->
when is_binary(Data) ->
?LOG(debug, "RECV ~p", [Data]), ?LOG(debug, "RECV ~p", [Data]),
Oct = iolist_size(Data), Oct = iolist_size(Data),
emqx_pd:update_counter(recv_cnt, 1), ok = inc_recv_stats(1, Oct),
emqx_pd:update_counter(recv_oct, Oct), NChanState = emqx_channel:received(Oct, ChanState),
ok = emqx_metrics:inc('bytes.received', Oct), NState = State#ws_connection{chan_state = NChanState},
NChanState = emqx_channel:ensure_timer( process_incoming(Data, NState);
stats_timer, emqx_channel:gc(1, Oct, ChanState)),
process_incoming(Data, State#state{chan_state = NChanState});
%% Pings should be replied with pongs, cowboy does it automatically %% Pings should be replied with pongs, cowboy does it automatically
%% Pongs can be safely ignored. Clause here simply prevents crash. %% Pongs can be safely ignored. Clause here simply prevents crash.
websocket_handle(Frame, State) websocket_handle(Frame, State)
when Frame =:= ping; Frame =:= pong -> when Frame =:= ping; Frame =:= pong ->
{ok, State}; {ok, State};
websocket_handle({FrameType, _}, State) websocket_handle({FrameType, _}, State)
when FrameType =:= ping; FrameType =:= pong -> when FrameType =:= ping; FrameType =:= pong ->
{ok, State}; {ok, State};
%% According to mqtt spec[https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901285]
websocket_handle({FrameType, _}, State) -> websocket_handle({FrameType, _}, State) ->
?LOG(error, "Frame error: unexpected frame - ~p", [FrameType]), ?LOG(error, "Unexpected frame - ~p", [FrameType]),
stop(unexpected_ws_frame, State). stop({shutdown, unexpected_ws_frame}, State).
websocket_info({call, From, info}, State) -> websocket_info({call, From, info}, State) ->
gen_server:reply(From, info(State)), gen_server:reply(From, info(State)),
@ -230,62 +231,52 @@ websocket_info({call, From, stats}, State) ->
gen_server:reply(From, stats(State)), gen_server:reply(From, stats(State)),
{ok, State}; {ok, State};
websocket_info({call, From, kick}, State) -> websocket_info({call, From, state}, State) ->
gen_server:reply(From, ok), gen_server:reply(From, State),
stop(kicked, State); {ok, State};
websocket_info({call, From, Req}, State = #state{chan_state = ChanState}) -> websocket_info({call, From, Req}, State = #ws_connection{chan_state = ChanState}) ->
case emqx_channel:handle_call(Req, ChanState) of case emqx_channel:handle_call(Req, ChanState) of
{ok, Reply, NChanState} -> {ok, Reply, NChanState} ->
_ = gen_server:reply(From, Reply), _ = gen_server:reply(From, Reply),
{ok, State#state{chan_state = NChanState}}; {ok, State#ws_connection{chan_state = NChanState}};
{stop, Reason, Reply, NChanState} -> {stop, Reason, Reply, NChanState} ->
_ = gen_server:reply(From, Reply), _ = gen_server:reply(From, Reply),
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#ws_connection{chan_state = NChanState})
end; end;
websocket_info({cast, discard}, State) -> websocket_info({cast, Msg}, State = #ws_connection{chan_state = ChanState}) ->
stop(discarded, State);
websocket_info({cast, Msg}, State = #state{chan_state = ChanState}) ->
case emqx_channel:handle_cast(Msg, ChanState) of case emqx_channel:handle_cast(Msg, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
{ok, State#state{chan_state = NChanState}}; {ok, State#ws_connection{chan_state = NChanState}};
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#ws_connection{chan_state = NChanState})
end; end;
websocket_info({incoming, Packet = ?CONNECT_PACKET( websocket_info({incoming, Packet = ?CONNECT_PACKET(ConnPkt)},
#mqtt_packet_connect{ State = #ws_connection{fsm_state = idle}) ->
proto_ver = ProtoVer} #mqtt_packet_connect{proto_ver = ProtoVer} = ConnPkt,
)}, NState = State#ws_connection{serialize = serialize_fun(ProtoVer)},
State = #state{fsm_state = idle}) -> handle_incoming(Packet, fun connected/1, NState);
handle_incoming(Packet, fun connected/1,
State#state{serialize = serialize_fun(ProtoVer)});
websocket_info({incoming, Packet}, State = #state{fsm_state = idle}) -> websocket_info({incoming, Packet}, State = #ws_connection{fsm_state = idle}) ->
?LOG(warning, "Unexpected incoming: ~p", [Packet]), ?LOG(warning, "Unexpected incoming: ~p", [Packet]),
stop(unexpected_incoming_packet, State); stop(unexpected_incoming_packet, State);
websocket_info({incoming, Packet = ?PACKET(?CONNECT)}, websocket_info({incoming, Packet}, State = #ws_connection{fsm_state = connected})
State = #state{fsm_state = connected}) ->
?LOG(warning, "Unexpected connect: ~p", [Packet]),
stop(unexpected_incoming_connect, State);
websocket_info({incoming, Packet}, State = #state{fsm_state = connected})
when is_record(Packet, mqtt_packet) -> when is_record(Packet, mqtt_packet) ->
handle_incoming(Packet, fun reply/1, State); handle_incoming(Packet, fun reply/1, State);
websocket_info(Deliver = {deliver, _Topic, _Msg}, websocket_info(Deliver = {deliver, _Topic, _Msg},
State = #state{chan_state = ChanState}) -> State = #ws_connection{chan_state = ChanState}) ->
Delivers = emqx_misc:drain_deliver([Deliver]), Delivers = emqx_misc:drain_deliver([Deliver]),
case emqx_channel:handle_out({deliver, Delivers}, ChanState) of case emqx_channel:handle_out({deliver, Delivers}, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
reply(State#state{chan_state = NChanState}); reply(State#ws_connection{chan_state = NChanState});
{ok, Packets, NChanState} -> {ok, Packets, NChanState} ->
reply(enqueue(Packets, State#state{chan_state = NChanState})); reply(enqueue(Packets, State#ws_connection{chan_state = NChanState}));
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#ws_connection{chan_state = NChanState})
end; end;
websocket_info({timeout, TRef, keepalive}, State) when is_reference(TRef) -> websocket_info({timeout, TRef, keepalive}, State) when is_reference(TRef) ->
@ -298,26 +289,22 @@ websocket_info({timeout, TRef, emit_stats}, State) when is_reference(TRef) ->
websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) -> websocket_info({timeout, TRef, Msg}, State) when is_reference(TRef) ->
handle_timeout(TRef, Msg, State); handle_timeout(TRef, Msg, State);
websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
?LOG(warning, "Clientid '~s' conflict with ~p", [ClientId, NewPid]),
stop(conflict, State);
websocket_info({shutdown, Reason}, State) -> websocket_info({shutdown, Reason}, State) ->
stop(Reason, State); stop({shutdown, Reason}, State);
websocket_info({stop, Reason}, State) -> websocket_info({stop, Reason}, State) ->
stop(Reason, State); stop(Reason, State);
websocket_info(Info, State = #state{chan_state = ChanState}) -> websocket_info(Info, State = #ws_connection{chan_state = ChanState}) ->
case emqx_channel:handle_info(Info, ChanState) of case emqx_channel:handle_info(Info, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
{ok, State#state{chan_state = NChanState}}; {ok, State#ws_connection{chan_state = NChanState}};
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#ws_connection{chan_state = NChanState})
end. end.
terminate(SockError, _Req, #state{chan_state = ChanState, terminate(SockError, _Req, #ws_connection{chan_state = ChanState,
stop_reason = Reason}) -> stop_reason = Reason}) ->
?LOG(debug, "Terminated for ~p, sockerror: ~p", ?LOG(debug, "Terminated for ~p, sockerror: ~p",
[Reason, SockError]), [Reason, SockError]),
emqx_channel:terminate(Reason, ChanState). emqx_channel:terminate(Reason, ChanState).
@ -325,24 +312,22 @@ terminate(SockError, _Req, #state{chan_state = ChanState,
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Connected callback %% Connected callback
connected(State = #state{chan_state = ChanState}) -> connected(State = #ws_connection{chan_state = ChanState}) ->
NState = State#state{fsm_state = connected}, ok = emqx_channel:handle_cast({register, attrs(State), stats(State)}, ChanState),
#{client_id := ClientId} = emqx_channel:info(client, ChanState), reply(State#ws_connection{fsm_state = connected}).
ok = emqx_cm:register_channel(ClientId),
ok = emqx_cm:set_chan_attrs(ClientId, attrs(NState)),
reply(NState).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle timeout %% Handle timeout
handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) -> handle_timeout(TRef, Msg, State = #ws_connection{chan_state = ChanState}) ->
case emqx_channel:timeout(TRef, Msg, ChanState) of case emqx_channel:timeout(TRef, Msg, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
{ok, State#state{chan_state = NChanState}}; {ok, State#ws_connection{chan_state = NChanState}};
{ok, Packets, NChanState} -> {ok, Packets, NChanState} ->
reply(enqueue(Packets, State#state{chan_state = NChanState})); NState = State#ws_connection{chan_state = NChanState},
reply(enqueue(Packets, NState));
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state = NChanState}) stop(Reason, State#ws_connection{chan_state = NChanState})
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -351,13 +336,13 @@ handle_timeout(TRef, Msg, State = #state{chan_state = ChanState}) ->
process_incoming(<<>>, State) -> process_incoming(<<>>, State) ->
{ok, State}; {ok, State};
process_incoming(Data, State = #state{parse_state = ParseState}) -> process_incoming(Data, State = #ws_connection{parse_state = ParseState, chan_state = ChanState}) ->
try emqx_frame:parse(Data, ParseState) of try emqx_frame:parse(Data, ParseState) of
{ok, NParseState} -> {ok, NParseState} ->
{ok, State#state{parse_state = NParseState}}; {ok, State#ws_connection{parse_state = NParseState}};
{ok, Packet, Rest, NParseState} -> {ok, Packet, Rest, NParseState} ->
self() ! {incoming, Packet}, self() ! {incoming, Packet},
process_incoming(Rest, State#state{parse_state = NParseState}); process_incoming(Rest, State#ws_connection{parse_state = NParseState});
{error, Reason} -> {error, Reason} ->
?LOG(error, "Frame error: ~p", [Reason]), ?LOG(error, "Frame error: ~p", [Reason]),
stop(Reason, State) stop(Reason, State)
@ -365,34 +350,46 @@ process_incoming(Data, State = #state{parse_state = ParseState}) ->
error:Reason:Stk -> error:Reason:Stk ->
?LOG(error, "Parse failed for ~p~n\ ?LOG(error, "Parse failed for ~p~n\
Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]), Stacktrace:~p~nFrame data: ~p", [Reason, Stk, Data]),
stop(parse_error, State) case emqx_channel:handle_out({disconnect, emqx_reason_codes:mqtt_frame_error(Reason)}, ChanState) of
{stop, Reason0, OutPackets, NChanState} ->
NState = State#ws_connection{chan_state = NChanState},
stop(Reason0, enqueue(OutPackets, NState));
{stop, Reason0, NChanState} ->
stop(Reason0, State#ws_connection{chan_state = NChanState})
end
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle incoming packets %% Handle incoming packets
handle_incoming(Packet = ?PACKET(Type), SuccFun, State = #state{chan_state = ChanState}) -> handle_incoming(Packet = ?PACKET(Type), SuccFun,
State = #ws_connection{chan_state = ChanState}) ->
_ = inc_incoming_stats(Type), _ = inc_incoming_stats(Type),
ok = emqx_metrics:inc_recv(Packet), ok = emqx_metrics:inc_recv(Packet),
?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]), ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)]),
case emqx_channel:handle_in(Packet, ChanState) of case emqx_channel:handle_in(Packet, ChanState) of
{ok, NChanState} -> {ok, NChanState} ->
SuccFun(State#state{chan_state= NChanState}); SuccFun(State#ws_connection{chan_state= NChanState});
{ok, OutPackets, NChanState} -> {ok, OutPackets, NChanState} ->
SuccFun(enqueue(OutPackets, State#state{chan_state= NChanState})); NState = State#ws_connection{chan_state= NChanState},
SuccFun(enqueue(OutPackets, NState));
{stop, Reason, NChanState} -> {stop, Reason, NChanState} ->
stop(Reason, State#state{chan_state= NChanState}); stop(Reason, State#ws_connection{chan_state= NChanState});
{stop, Reason, OutPacket, NChanState} -> {stop, Reason, OutPacket, NChanState} ->
stop(Reason, enqueue(OutPacket, State#state{chan_state= NChanState})) NState = State#ws_connection{chan_state= NChanState},
stop(Reason, enqueue(OutPacket, NState))
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Handle outgoing packets %% Handle outgoing packets
handle_outgoing(Packets, #state{serialize = Serialize}) -> handle_outgoing(Packets, State = #ws_connection{serialize = Serialize,
chan_state = ChanState}) ->
Data = lists:map(Serialize, Packets), Data = lists:map(Serialize, Packets),
emqx_pd:update_counter(send_oct, iolist_size(Data)), Oct = iolist_size(Data),
{binary, Data}. ok = inc_sent_stats(length(Packets), Oct),
NChanState = emqx_channel:sent(Oct, ChanState),
{{binary, Data}, State#ws_connection{chan_state = NChanState}}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Serialize fun %% Serialize fun
@ -401,42 +398,60 @@ serialize_fun(ProtoVer) ->
fun(Packet = ?PACKET(Type)) -> fun(Packet = ?PACKET(Type)) ->
?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]), ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)]),
_ = inc_outgoing_stats(Type), _ = inc_outgoing_stats(Type),
_ = emqx_metrics:inc_sent(Packet),
emqx_frame:serialize(Packet, ProtoVer) emqx_frame:serialize(Packet, ProtoVer)
end. end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Inc incoming/outgoing stats %% Inc incoming/outgoing stats
-compile({inline,
[ inc_recv_stats/2
, inc_incoming_stats/1
, inc_outgoing_stats/1
, inc_sent_stats/2
]}).
inc_recv_stats(Cnt, Oct) ->
emqx_pd:update_counter(recv_cnt, Cnt),
emqx_pd:update_counter(recv_oct, Oct),
emqx_metrics:inc('bytes.received', Oct).
inc_incoming_stats(Type) -> inc_incoming_stats(Type) ->
emqx_pd:update_counter(recv_pkt, 1), emqx_pd:update_counter(recv_pkt, 1),
(Type == ?PUBLISH) (Type == ?PUBLISH)
andalso emqx_pd:update_counter(recv_msg, 1). andalso emqx_pd:update_counter(recv_msg, 1).
inc_outgoing_stats(Type) -> inc_outgoing_stats(Type) ->
emqx_pd:update_counter(send_cnt, 1),
emqx_pd:update_counter(send_pkt, 1), emqx_pd:update_counter(send_pkt, 1),
(Type == ?PUBLISH) (Type == ?PUBLISH)
andalso emqx_pd:update_counter(send_msg, 1). andalso emqx_pd:update_counter(send_msg, 1).
inc_sent_stats(Cnt, Oct) ->
emqx_pd:update_counter(send_cnt, Cnt),
emqx_pd:update_counter(send_oct, Oct),
emqx_metrics:inc('bytes.sent', Oct).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Reply or Stop %% Reply or Stop
reply(State = #state{pendings = []}) -> -compile({inline, [reply/1]}).
{ok, State};
reply(State = #state{chan_state = ChanState, pendings = Pendings}) ->
Reply = handle_outgoing(Pendings, State),
NChanState = emqx_channel:ensure_timer(stats_timer, ChanState),
{reply, Reply, State#state{chan_state = NChanState, pendings = []}}.
stop(Reason, State = #state{pendings = []}) -> reply(State = #ws_connection{pendings = []}) ->
{stop, State#state{stop_reason = Reason}}; {ok, State};
stop(Reason, State = #state{pendings = Pendings}) -> reply(State = #ws_connection{pendings = Pendings}) ->
Reply = handle_outgoing(Pendings, State), {Reply, NState} = handle_outgoing(Pendings, State),
{reply, [Reply, close], {reply, Reply, NState#ws_connection{pendings = []}}.
State#state{pendings = [], stop_reason = Reason}}.
stop(Reason, State = #ws_connection{pendings = []}) ->
{stop, State#ws_connection{stop_reason = Reason}};
stop(Reason, State = #ws_connection{pendings = Pendings}) ->
{Reply, State1} = handle_outgoing(Pendings, State),
State2 = State1#ws_connection{pendings = [], stop_reason = Reason},
{reply, [Reply, close], State2}.
enqueue(Packet, State) when is_record(Packet, mqtt_packet) -> enqueue(Packet, State) when is_record(Packet, mqtt_packet) ->
enqueue([Packet], State); enqueue([Packet], State);
enqueue(Packets, State = #state{pendings = Pendings}) -> enqueue(Packets, State = #ws_connection{pendings = Pendings}) ->
State#state{pendings = lists:append(Pendings, Packets)}. State#ws_connection{pendings = lists:append(Pendings, Packets)}.

View File

@ -166,7 +166,8 @@ t_handle_deliver(_) ->
Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>), Msg0 = emqx_message:make(<<"clientx">>, ?QOS_0, <<"t0">>, <<"qos0">>),
Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>), Msg1 = emqx_message:make(<<"clientx">>, ?QOS_1, <<"t1">>, <<"qos1">>),
Delivers = [{deliver, <<"+">>, Msg0}, {deliver, <<"+">>, Msg1}], Delivers = [{deliver, <<"+">>, Msg0}, {deliver, <<"+">>, Msg1}],
{ok, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1) {ok, Packets, _Ch} = emqx_channel:handle_out({deliver, Delivers}, Channel1),
?assertEqual([?QOS_0, ?QOS_1], [emqx_packet:qos(Pkt)|| Pkt <- Packets])
end). end).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -178,7 +179,8 @@ t_handle_connack(_) ->
fun(Channel) -> fun(Channel) ->
{ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _} {ok, ?CONNACK_PACKET(?RC_SUCCESS, SP, _), _}
= handle_out({connack, ?RC_SUCCESS, 0}, Channel), = handle_out({connack, ?RC_SUCCESS, 0}, Channel),
{stop, {shutdown, unauthorized_client}, ?CONNACK_PACKET(5), _} {stop, {shutdown, not_authorized},
?CONNACK_PACKET(?RC_NOT_AUTHORIZED), _}
= handle_out({connack, ?RC_NOT_AUTHORIZED}, Channel) = handle_out({connack, ?RC_NOT_AUTHORIZED}, Channel)
end). end).
@ -278,18 +280,20 @@ with_channel(Fun) ->
Channel = emqx_channel:init(ConnInfo, Options), Channel = emqx_channel:init(ConnInfo, Options),
ConnPkt = #mqtt_packet_connect{ ConnPkt = #mqtt_packet_connect{
proto_name = <<"MQTT">>, proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4, proto_ver = ?MQTT_PROTO_V5,
clean_start = true, clean_start = true,
keepalive = 30, keepalive = 30,
properties = #{}, properties = #{},
client_id = <<"clientid">>, client_id = <<"clientid">>,
username = <<"username">> username = <<"username">>,
password = <<"passwd">>
}, },
Protocol = emqx_protocol:init(ConnPkt), Protocol = emqx_protocol:init(ConnPkt),
Session = emqx_session:init(#{zone => testing}, Session = emqx_session:init(#{zone => testing},
#{max_inflight => 100, #{max_inflight => 100,
expiry_interval => 0 expiry_interval => 0
}), }),
Fun(emqx_channel:set(protocol, Protocol, Fun(emqx_channel:set_field(protocol, Protocol,
emqx_channel:set(session, Session, Channel))). emqx_channel:set_field(
session, Session, Channel))).

View File

@ -100,7 +100,7 @@ t_cm(_) ->
emqtt:subscribe(C, <<"mytopic">>, 0), emqtt:subscribe(C, <<"mytopic">>, 0),
ct:sleep(1200), ct:sleep(1200),
Stats = emqx_cm:get_chan_stats(ClientId), Stats = emqx_cm:get_chan_stats(ClientId),
?assertEqual(1, proplists:get_value(subscriptions, Stats)), ?assertEqual(1, proplists:get_value(subscriptions_cnt, Stats)),
emqx_zone:set_env(external, idle_timeout, IdleTimeout). emqx_zone:set_env(external, idle_timeout, IdleTimeout).
t_cm_registry(_) -> t_cm_registry(_) ->

View File

@ -24,7 +24,6 @@
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_init(_) -> t_init(_) ->
?assertEqual(undefined, emqx_gc:init(false)),
GC1 = emqx_gc:init(#{count => 10, bytes => 0}), GC1 = emqx_gc:init(#{count => 10, bytes => 0}),
?assertEqual(#{cnt => {10, 10}}, emqx_gc:info(GC1)), ?assertEqual(#{cnt => {10, 10}}, emqx_gc:info(GC1)),
GC2 = emqx_gc:init(#{count => 0, bytes => 10}), GC2 = emqx_gc:init(#{count => 0, bytes => 10}),
@ -33,9 +32,6 @@ t_init(_) ->
?assertEqual(#{cnt => {10, 10}, oct => {10, 10}}, emqx_gc:info(GC3)). ?assertEqual(#{cnt => {10, 10}, oct => {10, 10}}, emqx_gc:info(GC3)).
t_run(_) -> t_run(_) ->
Undefined = emqx_gc:init(false),
?assertEqual(undefined, Undefined),
?assertEqual({false, undefined}, emqx_gc:run(1, 1, Undefined)),
GC = emqx_gc:init(#{count => 10, bytes => 10}), GC = emqx_gc:init(#{count => 10, bytes => 10}),
?assertEqual({true, GC}, emqx_gc:run(1, 1000, GC)), ?assertEqual({true, GC}, emqx_gc:run(1, 1000, GC)),
?assertEqual({true, GC}, emqx_gc:run(1000, 1, GC)), ?assertEqual({true, GC}, emqx_gc:run(1000, 1, GC)),
@ -51,12 +47,10 @@ t_run(_) ->
?assertEqual({false, DisabledGC}, emqx_gc:run(1, 1, DisabledGC)). ?assertEqual({false, DisabledGC}, emqx_gc:run(1, 1, DisabledGC)).
t_info(_) -> t_info(_) ->
?assertEqual(undefined, emqx_gc:info(undefined)),
GC = emqx_gc:init(#{count => 10, bytes => 0}), GC = emqx_gc:init(#{count => 10, bytes => 0}),
?assertEqual(#{cnt => {10, 10}}, emqx_gc:info(GC)). ?assertEqual(#{cnt => {10, 10}}, emqx_gc:info(GC)).
t_reset(_) -> t_reset(_) ->
?assertEqual(undefined, emqx_gc:reset(undefined)),
GC = emqx_gc:init(#{count => 10, bytes => 10}), GC = emqx_gc:init(#{count => 10, bytes => 10}),
{false, GC1} = emqx_gc:run(5, 5, GC), {false, GC1} = emqx_gc:run(5, 5, GC),
?assertEqual(#{cnt => {10, 5}, oct => {10, 5}}, emqx_gc:info(GC1)), ?assertEqual(#{cnt => {10, 5}, oct => {10, 5}}, emqx_gc:info(GC1)),

View File

@ -22,60 +22,92 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(SOCKOPTS, [binary, -define(SOCKOPTS, [binary,
{packet, raw}, {packet, raw},
{reuseaddr, true}, {reuseaddr, true},
{backlog, 512}, {backlog, 512},
{nodelay, true}]). {nodelay, true}
]).
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_merge_opts() -> t_merge_opts(_) ->
Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw, Opts = emqx_misc:merge_opts(?SOCKOPTS, [raw,
binary, binary,
{backlog, 1024}, {backlog, 1024},
{nodelay, false}, {nodelay, false},
{max_clients, 1024}, {max_clients, 1024},
{acceptors, 16}]), {acceptors, 16}
]),
?assertEqual(1024, proplists:get_value(backlog, Opts)), ?assertEqual(1024, proplists:get_value(backlog, Opts)),
?assertEqual(1024, proplists:get_value(max_clients, Opts)), ?assertEqual(1024, proplists:get_value(max_clients, Opts)),
[binary, raw, ?assertEqual([binary, raw,
{acceptors, 16}, {acceptors, 16},
{backlog, 1024}, {backlog, 1024},
{max_clients, 1024}, {max_clients, 1024},
{nodelay, false}, {nodelay, false},
{packet, raw}, {packet, raw},
{reuseaddr, true}] = lists:sort(Opts). {reuseaddr, true}], lists:sort(Opts)).
t_timer_cancel_flush() -> t_maybe_apply(_) ->
?assertEqual(undefined, emqx_misc:maybe_apply(fun(A) -> A end, undefined)),
?assertEqual(a, emqx_misc:maybe_apply(fun(A) -> A end, a)).
t_run_fold(_) ->
?assertEqual(1, emqx_misc:run_fold([], 1, state)),
Add = fun(I, St) -> I+St end,
Mul = fun(I, St) -> I*St end,
?assertEqual(6, emqx_misc:run_fold([Add, Mul], 1, 2)).
t_pipeline(_) ->
?assertEqual({ok, input, state}, emqx_misc:pipeline([], input, state)),
Funs = [fun(_I, _St) -> ok end,
fun(_I, St) -> {ok, St+1} end,
fun(I, St) -> {ok, I+1, St+1} end,
fun(I, St) -> {ok, I*2, St*2} end],
?assertEqual({ok, 4, 6}, emqx_misc:pipeline(Funs, 1, 1)).
t_start_timer(_) ->
TRef = emqx_misc:start_timer(1, tmsg),
timer:sleep(2),
?assertEqual([{timeout, TRef, tmsg}], drain()),
ok = emqx_misc:cancel_timer(TRef).
t_cancel_timer(_) ->
Timer = emqx_misc:start_timer(0, foo), Timer = emqx_misc:start_timer(0, foo),
ok = emqx_misc:cancel_timer(Timer), ok = emqx_misc:cancel_timer(Timer),
receive ?assertEqual([], drain()).
{timeout, Timer, foo} ->
error(unexpected)
after 0 -> ok
end.
t_proc_name(_) -> t_proc_name(_) ->
'TODO'. ?assertEqual(emqx_pool_1, emqx_misc:proc_name(emqx_pool, 1)).
t_proc_stats(_) -> t_proc_stats(_) ->
'TODO'. Pid1 = spawn(fun() -> exit(normal) end),
timer:sleep(10),
?assertEqual([], emqx_misc:proc_stats(Pid1)),
Pid2 = spawn(fun() -> timer:sleep(100) end),
Pid2 ! msg,
timer:sleep(10),
?assertMatch([{mailbox_len, 1}|_], emqx_misc:proc_stats(Pid2)).
t_drain_deliver(_) -> t_drain_deliver(_) ->
'TODO'. self() ! {deliver, t1, m1},
self() ! {deliver, t2, m2},
?assertEqual([{deliver, t1, m1},
{deliver, t2, m2}
], emqx_misc:drain_deliver()).
t_drain_down(_) -> t_drain_down(_) ->
'TODO'. {Pid1, _Ref1} = erlang:spawn_monitor(fun() -> ok end),
{Pid2, _Ref2} = erlang:spawn_monitor(fun() -> ok end),
timer:sleep(100),
?assertEqual([Pid1, Pid2], emqx_misc:drain_down(2)).
%% drain self() msg queue for deterministic test behavior
drain() -> drain() ->
_ = drain([]), % maybe log drain([]).
ok.
drain(Acc) -> drain(Acc) ->
receive receive
Msg -> Msg -> drain([Msg|Acc])
drain([Msg | Acc])
after after
0 -> 0 ->
lists:reverse(Acc) lists:reverse(Acc)

View File

@ -19,63 +19,32 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-import(emqx_mod_rewrite, -include("emqx_mqtt.hrl").
[ rewrite_subscribe/4
, rewrite_unsubscribe/4
, rewrite_publish/2
]).
-include_lib("emqx.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(TEST_RULES, [<<"x/# ^x/y/(.+)$ z/y/$1">>, -define(rules, [{rewrite,<<"x/#">>,<<"^x/y/(.+)$">>,<<"z/y/$1">>},
<<"y/+/z/# ^y/(.+)/z/(.+)$ y/z/$2">> {rewrite,<<"y/+/z/#">>,<<"^y/(.+)/z/(.+)$">>,<<"y/z/$2">>}]).
]).
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
%%-------------------------------------------------------------------- t_rewrite_rule(_Config) ->
%% Test cases {ok, _} = emqx_hooks:start_link(),
%%-------------------------------------------------------------------- ok = emqx_mod_rewrite:load(?rules),
RawTopicFilters = [{<<"x/y/2">>, opts},
t_rewrite_subscribe(_) -> {<<"x/1/2">>, opts},
?assertEqual({ok, [{<<"test">>, #{}}]}, {<<"y/a/z/b">>, opts},
rewrite(subscribe, [{<<"test">>, #{}}])), {<<"y/def">>, opts}],
?assertEqual({ok, [{<<"z/y/test">>, #{}}]}, SubTopicFilters = emqx_hooks:run_fold('client.subscribe', [client, properties], RawTopicFilters),
rewrite(subscribe, [{<<"x/y/test">>, #{}}])), UnSubTopicFilters = emqx_hooks:run_fold('client.unsubscribe', [client, properties], RawTopicFilters),
?assertEqual({ok, [{<<"y/z/test_topic">>, #{}}]}, Messages = [emqx_hooks:run_fold('message.publish', [], emqx_message:make(Topic, <<"payload">>))
rewrite(subscribe, [{<<"y/test/z/test_topic">>, #{}}])). || {Topic, _Opts} <- RawTopicFilters],
ExpectedTopicFilters = [{<<"z/y/2">>, opts},
t_rewrite_unsubscribe(_) -> {<<"x/1/2">>, opts},
?assertEqual({ok, [{<<"test">>, #{}}]}, {<<"y/z/b">>, opts},
rewrite(unsubscribe, [{<<"test">>, #{}}])), {<<"y/def">>, opts}],
?assertEqual({ok, [{<<"z/y/test">>, #{}}]}, ?assertEqual(ExpectedTopicFilters, SubTopicFilters),
rewrite(unsubscribe, [{<<"x/y/test">>, #{}}])), ?assertEqual(ExpectedTopicFilters, UnSubTopicFilters),
?assertEqual({ok, [{<<"y/z/test_topic">>, #{}}]}, [?assertEqual(ExpectedTopic, emqx_message:topic(Message))
rewrite(unsubscribe, [{<<"y/test/z/test_topic">>, #{}}])). || {{ExpectedTopic, _opts}, Message} <- lists:zip(ExpectedTopicFilters, Messages)],
ok = emqx_mod_rewrite:unload(?rules),
t_rewrite_publish(_) -> ok = emqx_hooks:stop().
?assertMatch({ok, #message{topic = <<"test">>}},
rewrite(publish, #message{topic = <<"test">>})),
?assertMatch({ok, #message{topic = <<"z/y/test">>}},
rewrite(publish, #message{topic = <<"x/y/test">>})),
?assertMatch({ok, #message{topic = <<"y/z/test_topic">>}},
rewrite(publish, #message{topic = <<"y/test/z/test_topic">>})).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
rewrite(subscribe, TopicFilters) ->
rewrite_subscribe(#{}, #{}, TopicFilters, rules());
rewrite(unsubscribe, TopicFilters) ->
rewrite_unsubscribe(#{}, #{}, TopicFilters, rules());
rewrite(publish, Msg) -> rewrite_publish(Msg, rules()).
rules() ->
[begin
[Topic, Re, Dest] = string:split(Rule, " ", all),
{ok, MP} = re:compile(Re),
{rewrite, Topic, MP, Dest}
end || Rule <- ?TEST_RULES].

View File

@ -24,7 +24,6 @@
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_init(_) -> t_init(_) ->
?assertEqual(undefined, emqx_oom:init(undefined)),
Opts = #{message_queue_len => 10, Opts = #{message_queue_len => 10,
max_heap_size => 1024*1024*8 max_heap_size => 1024*1024*8
}, },
@ -34,7 +33,6 @@ t_init(_) ->
}, emqx_oom:info(Oom)). }, emqx_oom:info(Oom)).
t_check(_) -> t_check(_) ->
?assertEqual(ok, emqx_oom:check(undefined)),
Opts = #{message_queue_len => 10, Opts = #{message_queue_len => 10,
max_heap_size => 1024*1024*8 max_heap_size => 1024*1024*8
}, },

View File

@ -24,26 +24,58 @@
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
t_init_and_info(_) -> init_per_suite(Config) ->
ConnPkt = #mqtt_packet_connect{ [{proto, init_protocol()}|Config].
proto_name = <<"MQTT">>,
proto_ver = ?MQTT_PROTO_V4, init_protocol() ->
is_bridge = false, emqx_protocol:init(#mqtt_packet_connect{
clean_start = true, proto_name = <<"MQTT">>,
keepalive = 30, proto_ver = ?MQTT_PROTO_V5,
properties = #{}, is_bridge = false,
client_id = <<"clientid">>, clean_start = true,
username = <<"username">>, keepalive = 30,
password = <<"passwd">> properties = #{},
}, client_id = <<"clientid">>,
Proto = emqx_protocol:init(ConnPkt), username = <<"username">>,
password = <<"passwd">>
}).
end_per_suite(_Config) -> ok.
t_init_info_1(Config) ->
Proto = proplists:get_value(proto, Config),
?assertEqual(#{proto_name => <<"MQTT">>,
proto_ver => ?MQTT_PROTO_V5,
clean_start => true,
keepalive => 30,
conn_props => #{},
will_msg => undefined,
client_id => <<"clientid">>,
username => <<"username">>,
topic_aliases => undefined
}, emqx_protocol:info(Proto)).
t_init_info_2(Config) ->
Proto = proplists:get_value(proto, Config),
?assertEqual(<<"MQTT">>, emqx_protocol:info(proto_name, Proto)), ?assertEqual(<<"MQTT">>, emqx_protocol:info(proto_name, Proto)),
?assertEqual(?MQTT_PROTO_V4, emqx_protocol:info(proto_ver, Proto)), ?assertEqual(?MQTT_PROTO_V5, emqx_protocol:info(proto_ver, Proto)),
?assertEqual(true, emqx_protocol:info(clean_start, Proto)), ?assertEqual(true, emqx_protocol:info(clean_start, Proto)),
?assertEqual(30, emqx_protocol:info(keepalive, Proto)),
?assertEqual(<<"clientid">>, emqx_protocol:info(client_id, Proto)), ?assertEqual(<<"clientid">>, emqx_protocol:info(client_id, Proto)),
?assertEqual(<<"username">>, emqx_protocol:info(username, Proto)), ?assertEqual(<<"username">>, emqx_protocol:info(username, Proto)),
?assertEqual(undefined, emqx_protocol:info(will_msg, Proto)), ?assertEqual(undefined, emqx_protocol:info(will_msg, Proto)),
?assertEqual(#{}, emqx_protocol:info(conn_props, Proto)). ?assertEqual(0, emqx_protocol:info(will_delay_interval, Proto)),
?assertEqual(#{}, emqx_protocol:info(conn_props, Proto)),
?assertEqual(undefined, emqx_protocol:info(topic_aliases, Proto)).
t_find_save_alias(Config) ->
Proto = proplists:get_value(proto, Config),
?assertEqual(undefined, emqx_protocol:info(topic_aliases, Proto)),
?assertEqual(false, emqx_protocol:find_alias(1, Proto)),
Proto1 = emqx_protocol:save_alias(1, <<"t1">>, Proto),
Proto2 = emqx_protocol:save_alias(2, <<"t2">>, Proto1),
?assertEqual(#{1 => <<"t1">>, 2 => <<"t2">>},
emqx_protocol:info(topic_aliases, Proto2)),
?assertEqual({ok, <<"t1">>}, emqx_protocol:find_alias(1, Proto2)),
?assertEqual({ok, <<"t2">>}, emqx_protocol:find_alias(2, Proto2)).

View File

@ -30,7 +30,6 @@
, emqx_message , emqx_message
, emqx_hooks , emqx_hooks
, emqx_zone , emqx_zone
, emqx_pd
]). ]).
all() -> emqx_ct:all(?MODULE). all() -> emqx_ct:all(?MODULE).
@ -83,7 +82,7 @@ apply_op(Session, attrs) ->
apply_op(Session, stats) -> apply_op(Session, stats) ->
Stats = emqx_session:stats(Session), Stats = emqx_session:stats(Session),
?assert(is_list(Stats)), ?assert(is_list(Stats)),
?assertEqual(9, length(Stats)), ?assertEqual(10, length(Stats)),
Session; Session;
apply_op(Session, {info, InfoArg}) -> apply_op(Session, {info, InfoArg}) ->
_Ret = emqx_session:info(InfoArg, Session), _Ret = emqx_session:info(InfoArg, Session),
@ -113,16 +112,16 @@ apply_op(Session, {publish, {PacketId, Msg}}) ->
end; end;
apply_op(Session, {puback, PacketId}) -> apply_op(Session, {puback, PacketId}) ->
case emqx_session:puback(PacketId, Session) of case emqx_session:puback(PacketId, Session) of
{ok, _Msg} -> {ok, _Msg, NSession} ->
Session; NSession;
{ok, _Deliver, NSession} -> {ok, _Msg, _Publishes, NSession} ->
NSession; NSession;
{error, _ErrorCode} -> {error, _ErrorCode} ->
Session Session
end; end;
apply_op(Session, {pubrec, PacketId}) -> apply_op(Session, {pubrec, PacketId}) ->
case emqx_session:pubrec(PacketId, Session) of case emqx_session:pubrec(PacketId, Session) of
{ok, NSession} -> {ok, _Msg, NSession} ->
NSession; NSession;
{error, _ErrorCode} -> {error, _ErrorCode} ->
Session Session
@ -283,7 +282,7 @@ session() ->
{zone(), option()}, {zone(), option()},
begin begin
Session = emqx_session:init(#{zone => Zone}, Options), Session = emqx_session:init(#{zone => Zone}, Options),
emqx_session:set_pkt_id(Session, 16#ffff) emqx_session:set_field(next_pkt_id, 16#ffff, Session)
end). end).
%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%
@ -327,6 +326,5 @@ do_mock(emqx_message) ->
do_mock(emqx_hooks) -> do_mock(emqx_hooks) ->
meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end); meck:expect(emqx_hooks, run, fun(_Hook, _Args) -> ok end);
do_mock(emqx_zone) -> do_mock(emqx_zone) ->
meck:expect(emqx_zone, get_env, fun(Env, Key, Default) -> maps:get(Key, Env, Default) end); meck:expect(emqx_zone, get_env, fun(Env, Key, Default) -> maps:get(Key, Env, Default) end).
do_mock(emqx_pd) ->
meck:expect(emqx_pd, update_counter, fun(_stats, _num) -> ok end).