Merge pull request #1738 from emqtt/emqx30-dev

Merge emqx30-dev to emqx30
This commit is contained in:
Feng Lee 2018-08-23 17:53:41 +08:00 committed by GitHub
commit cae09fb815
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 3376 additions and 2950 deletions

View File

@ -1,8 +1,8 @@
language: erlang language: erlang
otp_release: otp_release:
- 20.0 - 21.0
- 20.1 - 21.0.4
script: script:
- make - make

View File

@ -4,19 +4,15 @@ PROJECT = emqx
PROJECT_DESCRIPTION = EMQ X Broker PROJECT_DESCRIPTION = EMQ X Broker
PROJECT_VERSION = 3.0 PROJECT_VERSION = 3.0
DEPS = goldrush gproc lager esockd ekka mochiweb pbkdf2 lager_syslog bcrypt clique jsx DEPS = jsx gproc gen_rpc lager ekka esockd cowboy clique
dep_goldrush = git https://github.com/basho/goldrush 0.1.9
dep_gproc = git https://github.com/uwiger/gproc 0.7.0
dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0 dep_jsx = git https://github.com/talentdeficit/jsx 2.9.0
dep_getopt = git https://github.com/jcomellas/getopt v0.8.2 dep_gproc = git https://github.com/uwiger/gproc 0.8.0
dep_lager = git https://github.com/basho/lager master dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1
dep_lager_syslog = git https://github.com/basho/lager_syslog dep_lager = git https://github.com/erlang-lager/lager 3.6.4
dep_esockd = git https://github.com/emqtt/esockd emqx30 dep_esockd = git https://github.com/emqx/esockd emqx30
dep_ekka = git https://github.com/emqtt/ekka develop dep_ekka = git https://github.com/emqx/ekka emqx30
dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30 dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0
dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1
dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master
dep_clique = git https://github.com/emqx/clique dep_clique = git https://github.com/emqx/clique
NO_AUTOPATCH = gen_rpc cuttlefish NO_AUTOPATCH = gen_rpc cuttlefish
@ -25,7 +21,7 @@ ERLC_OPTS += +debug_info
ERLC_OPTS += +'{parse_transform, lager_transform}' ERLC_OPTS += +'{parse_transform, lager_transform}'
BUILD_DEPS = cuttlefish BUILD_DEPS = cuttlefish
dep_cuttlefish = git https://github.com/emqtt/cuttlefish dep_cuttlefish = git https://github.com/emqx/cuttlefish emqx30
TEST_DEPS = emqx_ct_helplers TEST_DEPS = emqx_ct_helplers
dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers dep_emqx_ct_helplers = git git@github.com:emqx/emqx_ct_helpers
@ -47,8 +43,7 @@ COVER = true
PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl lager compiler mnesia PLT_APPS = sasl asn1 ssl syntax_tools runtime_tools crypto xmerl os_mon inets public_key ssl lager compiler mnesia
DIALYZER_DIRS := ebin/ DIALYZER_DIRS := ebin/
DIALYZER_OPTS := --verbose --statistics -Werror_handling \ DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns
-Wrace_conditions #-Wunmatched_returns
include erlang.mk include erlang.mk

4
TODO
View File

@ -1,5 +1,7 @@
1. Update the README.md 1. Update the README.md
2. Update the Documentation 2. Update the Documentation
3. Shared subscription strategy and dispatch strategy 3. Shared subscription and dispatch strategy
4. Remove lager syslog:
dep_lager_syslog = git https://github.com/basho/lager_syslog

Binary file not shown.

View File

@ -24,3 +24,4 @@
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}. {deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
{allow, all}.

File diff suppressed because it is too large Load Diff

View File

@ -24,42 +24,51 @@
-define(ERTS_MINIMUM_REQUIRED, "10.0"). -define(ERTS_MINIMUM_REQUIRED, "10.0").
%%--------------------------------------------------------------------
%% PubSub
%%--------------------------------------------------------------------
-type(pubsub() :: publish | subscribe).
-define(PS(I), (I =:= publish orelse I =:= subscribe)).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Topics' prefix: $SYS | $queue | $share %% Topics' prefix: $SYS | $queue | $share
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% System Topic %% System topic
-define(SYSTOP, <<"$SYS/">>). -define(SYSTOP, <<"$SYS/">>).
%% Queue Topic %% Queue topic
-define(QUEUE, <<"$queue/">>). -define(QUEUE, <<"$queue/">>).
%% Shared Topic %% Shared topic
-define(SHARE, <<"$share/">>). -define(SHARE, <<"$share/">>).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Topic, subscription and subscriber %% Topic, subscription and subscriber
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-type(qos() :: integer()).
-type(topic() :: binary()). -type(topic() :: binary()).
-type(suboption() :: {qos, qos()} -type(subid() :: binary() | atom()).
| {share, '$queue'}
| {share, binary()}
| {atom(), term()}).
-record(subscription, {subid :: binary() | atom(), -type(subopts() :: #{qos => integer(), share => '$queue' | binary(), atom() => term()}).
-record(subscription, {
topic :: topic(), topic :: topic(),
subopts :: list(suboption())}). subid :: subid(),
subopts :: subopts()
}).
-type(subscription() :: #subscription{}). -type(subscription() :: #subscription{}).
-type(subscriber() :: binary() | pid() | {binary(), pid()}). -type(subscriber() :: {pid(), subid()}).
-type(topic_table() :: [{topic(), subopts()}]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Client and session %% Client and Session
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()). -type(protocol() :: mqtt | 'mqtt-sn' | coap | stomp | none | atom()).
@ -70,18 +79,19 @@
-type(username() :: binary() | atom()). -type(username() :: binary() | atom()).
-type(mountpoint() :: binary()). -type(zone() :: atom()).
-type(zone() :: undefined | atom()). -record(client, {
id :: client_id(),
-record(client, {id :: client_id(),
pid :: pid(), pid :: pid(),
zone :: zone(), zone :: zone(),
peername :: peername(),
username :: username(),
protocol :: protocol(), protocol :: protocol(),
attributes :: #{atom() => term()}, peername :: peername(),
connected_at :: erlang:timestamp()}). peercert :: nossl | binary(),
username :: username(),
clean_start :: boolean(),
attributes :: map()
}).
-type(client() :: #client{}). -type(client() :: #client{}).
@ -90,61 +100,51 @@
-type(session() :: #session{}). -type(session() :: #session{}).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Message and delivery %% Payload, Message and Delivery
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-type(message_id() :: binary() | undefined). -type(qos() :: integer()).
-type(message_flag() :: sys | qos | dup | retain | atom()). -type(payload() :: binary() | iodata()).
-type(message_flags() :: #{message_flag() => boolean() | integer()}). -type(message_flag() :: dup | sys | retain | atom()).
-type(message_headers() :: #{protocol => protocol(),
packet_id => pos_integer(),
priority => non_neg_integer(),
ttl => pos_integer(),
atom() => term()}).
-type(payload() :: binary()).
%% See 'Application Message' in MQTT Version 5.0 %% See 'Application Message' in MQTT Version 5.0
-record(message, -record(message, {
{ id :: message_id(), %% Message guid %% Global unique message ID
qos :: qos(), %% Message qos id :: binary() | pos_integer(),
from :: atom() | client(), %% Message from %% Message QoS
sender :: pid(), %% The pid of the sender/publisher qos = 0 :: qos(),
flags :: message_flags(), %% Message flags %% Message from
headers :: message_headers(), %% Message headers from :: atom() | client_id(),
topic :: topic(), %% Message topic %% Message flags
properties :: map(), %% Message user properties flags :: #{message_flag() => boolean()},
payload :: payload(), %% Message payload %% Message headers, or MQTT 5.0 Properties
timestamp :: erlang:timestamp() %% Timestamp headers = #{} :: map(),
%% Topic that the message is published to
topic :: topic(),
%% Message Payload
payload :: binary(),
%% Timestamp
timestamp :: erlang:timestamp()
}). }).
-type(message() :: #message{}). -type(message() :: #message{}).
-record(delivery, -record(delivery, {
{ node :: node(), %% The node that created the delivery sender :: pid(), %% Sender of the delivery
message :: message(), %% The message delivered message :: message(), %% The message delivered
flows :: list() %% The message flow path flows :: list() %% The dispatch path of message
}). }).
-type(delivery() :: #delivery{}). -type(delivery() :: #delivery{}).
%%--------------------------------------------------------------------
%% PubSub
%%--------------------------------------------------------------------
-type(pubsub() :: publish | subscribe).
-define(PS(I), (I =:= publish orelse I =:= subscribe)).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Route %% Route
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(route, -record(route, {
{ topic :: topic(), topic :: topic(),
dest :: node() | {binary(), node()} dest :: node() | {binary(), node()}
}). }).
@ -156,20 +156,20 @@
-type(trie_node_id() :: binary() | atom()). -type(trie_node_id() :: binary() | atom()).
-record(trie_node, -record(trie_node, {
{ node_id :: trie_node_id(), node_id :: trie_node_id(),
edge_count = 0 :: non_neg_integer(), edge_count = 0 :: non_neg_integer(),
topic :: topic() | undefined, topic :: topic() | undefined,
flags :: list(atom()) flags :: list(atom())
}). }).
-record(trie_edge, -record(trie_edge, {
{ node_id :: trie_node_id(), node_id :: trie_node_id(),
word :: binary() | atom() word :: binary() | atom()
}). }).
-record(trie, -record(trie, {
{ edge :: #trie_edge{}, edge :: #trie_edge{},
node_id :: trie_node_id() node_id :: trie_node_id()
}). }).
@ -177,11 +177,11 @@
%% Alarm %% Alarm
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(alarm, -record(alarm, {
{ id :: binary(), id :: binary(),
severity :: notice | warning | error | critical, severity :: notice | warning | error | critical,
title :: iolist() | binary(), title :: iolist(),
summary :: iolist() | binary(), summary :: iolist(),
timestamp :: erlang:timestamp() timestamp :: erlang:timestamp()
}). }).
@ -191,13 +191,13 @@
%% Plugin %% Plugin
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(plugin, -record(plugin, {
{ name :: atom(), name :: atom(),
version :: string(), version :: string(),
dir :: string(), dir :: string(),
descr :: string(), descr :: string(),
vendor :: string(), vendor :: string(),
active :: boolean(), active = false :: boolean(),
info :: map() info :: map()
}). }).
@ -207,8 +207,8 @@
%% Command %% Command
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(command, -record(command, {
{ name :: atom(), name :: atom(),
action :: atom(), action :: atom(),
args = [] :: list(), args = [] :: list(),
opts = [] :: list(), opts = [] :: list(),

View File

@ -78,18 +78,17 @@
%% Maximum ClientId Length. %% Maximum ClientId Length.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-define(MAX_CLIENTID_LEN, 1024). -define(MAX_CLIENTID_LEN, 65535).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% MQTT Client %% MQTT Client
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(mqtt_client, {
-record(mqtt_client, client_id :: binary() | undefined,
{ client_id :: binary() | undefined,
client_pid :: pid(), client_pid :: pid(),
username :: binary() | undefined, username :: binary() | undefined,
peername :: {inet:ip_address(), inet:port_number()}, peername :: {inet:ip_address(), inet:port_number()},
clean_sess :: boolean(), clean_start :: boolean(),
proto_ver :: mqtt_version(), proto_ver :: mqtt_version(),
keepalive = 0 :: non_neg_integer(), keepalive = 0 :: non_neg_integer(),
will_topic :: undefined | binary(), will_topic :: undefined | binary(),
@ -207,8 +206,8 @@
%% MQTT Packet Fixed Header %% MQTT Packet Fixed Header
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(mqtt_packet_header, -record(mqtt_packet_header, {
{ type = ?RESERVED :: mqtt_packet_type(), type = ?RESERVED :: mqtt_packet_type(),
dup = false :: boolean(), dup = false :: boolean(),
qos = ?QOS_0 :: mqtt_qos(), qos = ?QOS_0 :: mqtt_qos(),
retain = false :: boolean() retain = false :: boolean()
@ -235,8 +234,8 @@
-type(mqtt_subopts() :: #mqtt_subopts{}). -type(mqtt_subopts() :: #mqtt_subopts{}).
-record(mqtt_packet_connect, -record(mqtt_packet_connect, {
{ proto_name = <<"MQTT">> :: binary(), proto_name = <<"MQTT">> :: binary(),
proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(), proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(),
is_bridge = false :: boolean(), is_bridge = false :: boolean(),
clean_start = true :: boolean(), clean_start = true :: boolean(),
@ -253,55 +252,55 @@
password = undefined :: undefined | binary() password = undefined :: undefined | binary()
}). }).
-record(mqtt_packet_connack, -record(mqtt_packet_connack, {
{ ack_flags :: 0 | 1, ack_flags :: 0 | 1,
reason_code :: mqtt_reason_code(), reason_code :: mqtt_reason_code(),
properties :: mqtt_properties() properties :: mqtt_properties()
}). }).
-record(mqtt_packet_publish, -record(mqtt_packet_publish, {
{ topic_name :: mqtt_topic(), topic_name :: mqtt_topic(),
packet_id :: mqtt_packet_id(), packet_id :: mqtt_packet_id(),
properties :: mqtt_properties() properties :: mqtt_properties()
}). }).
-record(mqtt_packet_puback, -record(mqtt_packet_puback, {
{ packet_id :: mqtt_packet_id(), packet_id :: mqtt_packet_id(),
reason_code :: mqtt_reason_code(), reason_code :: mqtt_reason_code(),
properties :: mqtt_properties() properties :: mqtt_properties()
}). }).
-record(mqtt_packet_subscribe, -record(mqtt_packet_subscribe, {
{ packet_id :: mqtt_packet_id(), packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(), properties :: mqtt_properties(),
topic_filters :: [{mqtt_topic(), mqtt_subopts()}] topic_filters :: [{mqtt_topic(), mqtt_subopts()}]
}). }).
-record(mqtt_packet_suback, -record(mqtt_packet_suback, {
{ packet_id :: mqtt_packet_id(), packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(), properties :: mqtt_properties(),
reason_codes :: list(mqtt_reason_code()) reason_codes :: list(mqtt_reason_code())
}). }).
-record(mqtt_packet_unsubscribe, -record(mqtt_packet_unsubscribe, {
{ packet_id :: mqtt_packet_id(), packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(), properties :: mqtt_properties(),
topic_filters :: [mqtt_topic()] topic_filters :: [mqtt_topic()]
}). }).
-record(mqtt_packet_unsuback, -record(mqtt_packet_unsuback, {
{ packet_id :: mqtt_packet_id(), packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(), properties :: mqtt_properties(),
reason_codes :: list(mqtt_reason_code()) reason_codes :: list(mqtt_reason_code())
}). }).
-record(mqtt_packet_disconnect, -record(mqtt_packet_disconnect, {
{ reason_code :: mqtt_reason_code(), reason_code :: mqtt_reason_code(),
properties :: mqtt_properties() properties :: mqtt_properties()
}). }).
-record(mqtt_packet_auth, -record(mqtt_packet_auth, {
{ reason_code :: mqtt_reason_code(), reason_code :: mqtt_reason_code(),
properties :: mqtt_properties() properties :: mqtt_properties()
}). }).
@ -309,8 +308,8 @@
%% MQTT Control Packet %% MQTT Control Packet
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-record(mqtt_packet, -record(mqtt_packet, {
{ header :: #mqtt_packet_header{}, header :: #mqtt_packet_header{},
variable :: #mqtt_packet_connect{} variable :: #mqtt_packet_connect{}
| #mqtt_packet_connack{} | #mqtt_packet_connack{}
| #mqtt_packet_publish{} | #mqtt_packet_publish{}
@ -364,9 +363,12 @@
variable = #mqtt_packet_auth{reason_code = ReasonCode, variable = #mqtt_packet_auth{reason_code = ReasonCode,
properties = Properties}}). properties = Properties}}).
-define(PUBLISH_PACKET(Qos, PacketId), -define(PUBLISH_PACKET(QoS),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}).
-define(PUBLISH_PACKET(QoS, PacketId),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = Qos}, qos = QoS},
variable = #mqtt_packet_publish{packet_id = PacketId}}). variable = #mqtt_packet_publish{packet_id = PacketId}}).
-define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), -define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload),
@ -464,6 +466,11 @@
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
variable = #mqtt_packet_unsuback{packet_id = PacketId}}). variable = #mqtt_packet_unsuback{packet_id = PacketId}}).
-define(UNSUBACK_PACKET(PacketId, ReasonCodes),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
variable = #mqtt_packet_unsuback{packet_id = PacketId,
reason_codes = ReasonCodes}}).
-define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), -define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
variable = #mqtt_packet_unsuback{packet_id = PacketId, variable = #mqtt_packet_unsuback{packet_id = PacketId,
@ -486,43 +493,3 @@
-define(PACKET(Type), -define(PACKET(Type),
#mqtt_packet{header = #mqtt_packet_header{type = Type}}). #mqtt_packet{header = #mqtt_packet_header{type = Type}}).
%%--------------------------------------------------------------------
%% MQTT Message
%%--------------------------------------------------------------------
-type(mqtt_msg_id() :: binary() | undefined).
-type(mqtt_msg_from() :: atom() | {binary(), undefined | binary()}).
-record(mqtt_message,
{ %% Global unique message ID
id :: mqtt_msg_id(),
%% PacketId
packet_id :: mqtt_packet_id(),
%% ClientId and Username
from :: mqtt_msg_from(),
%% Topic that the message is published to
topic :: binary(),
%% Message QoS
qos = ?QOS0 :: mqtt_qos(),
%% Message Flags
flags = [] :: [retain | dup | sys],
%% Retain flag
retain = false :: boolean(),
%% Dup flag
dup = false :: boolean(),
%% $SYS flag
sys = false :: boolean(),
%% Properties
properties = [] :: list(),
%% Payload
payload :: binary(),
%% Timestamp
timestamp :: erlang:timestamp()
}).
-type(mqtt_message() :: #mqtt_message{}).
-define(WILL_MSG(Qos, Retain, Topic, Props, Payload),
#mqtt_message{qos = Qos, retain = Retain, topic = Topic, properties = Props, payload = Payload}).

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
{vsn,"3.0"}, {vsn,"3.0"},
{modules,[]}, {modules,[]},
{registered,[emqx_sup]}, {registered,[emqx_sup]},
{applications,[kernel,stdlib,gproc,lager,esockd,mochiweb,lager_syslog,pbkdf2,bcrypt,clique,jsx]}, {applications,[kernel,stdlib,jsx,gproc,gen_rpc,lager,esockd,minirest]},
{env,[]}, {env,[]},
{mod,{emqx_app,[]}}, {mod,{emqx_app,[]}},
{maintainers,["Feng Lee <feng@emqx.io>"]}, {maintainers,["Feng Lee <feng@emqx.io>"]},

View File

@ -74,7 +74,7 @@ subscribe(Topic) ->
subscribe(Topic, Subscriber) -> subscribe(Topic, Subscriber) ->
emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)). emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber)).
-spec(subscribe(topic() | string(), subscriber() | string(), [suboption()]) -> ok | {error, term()}). -spec(subscribe(topic() | string(), subscriber() | string(), subopts()) -> ok | {error, term()}).
subscribe(Topic, Subscriber, Options) -> subscribe(Topic, Subscriber, Options) ->
emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options).
@ -95,11 +95,11 @@ unsubscribe(Topic, Subscriber) ->
%% PubSub management API %% PubSub management API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-spec(get_subopts(topic() | string(), subscriber()) -> [suboption()]). -spec(get_subopts(topic() | string(), subscriber()) -> subopts()).
get_subopts(Topic, Subscriber) -> get_subopts(Topic, Subscriber) ->
emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)). emqx_broker:get_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber)).
-spec(set_subopts(topic() | string(), subscriber(), [suboption()]) -> ok). -spec(set_subopts(topic() | string(), subscriber(), subopts()) -> ok).
set_subopts(Topic, Subscriber, Options) when is_list(Options) -> set_subopts(Topic, Subscriber, Options) when is_list(Options) ->
emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options). emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options).
@ -110,7 +110,7 @@ topics() -> emqx_router:topics().
subscribers(Topic) -> subscribers(Topic) ->
emqx_broker:subscribers(iolist_to_binary(Topic)). emqx_broker:subscribers(iolist_to_binary(Topic)).
-spec(subscriptions(subscriber() | string()) -> [{topic(), list(suboption())}]). -spec(subscriptions(subscriber() | string()) -> [{topic(), subopts()}]).
subscriptions(Subscriber) -> subscriptions(Subscriber) ->
emqx_broker:subscriptions(list_to_subid(Subscriber)). emqx_broker:subscriptions(list_to_subid(Subscriber)).
@ -166,8 +166,8 @@ shutdown() ->
shutdown(Reason) -> shutdown(Reason) ->
emqx_logger:error("emqx shutdown for ~s", [Reason]), emqx_logger:error("emqx shutdown for ~s", [Reason]),
emqx_plugins:unload(), emqx_plugins:unload(),
lists:foreach(fun application:stop/1, [emqx, ekka, mochiweb, esockd, gproc]). lists:foreach(fun application:stop/1, [emqx, ekka, cowboy, esockd, gproc]).
reboot() -> reboot() ->
lists:foreach(fun application:start/1, [gproc, esockd, mochiweb, ekka, emqx]). lists:foreach(fun application:start/1, [gproc, esockd, cowboy, ekka, emqx]).

View File

@ -22,6 +22,8 @@
-export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1, -export([start_link/0, auth/2, check_acl/3, reload_acl/0, lookup_mods/1,
register_mod/3, register_mod/4, unregister_mod/2, stop/0]). register_mod/3, register_mod/4, unregister_mod/2, stop/0]).
-export([clean_acl_cache/1, clean_acl_cache/2]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]). terminate/2, code_change/3]).
@ -50,9 +52,9 @@ start_link() ->
register_default_mod() -> register_default_mod() ->
case emqx_config:get_env(acl_file) of case emqx_config:get_env(acl_file) of
{ok, File} -> undefined -> ok;
emqx_access_control:register_mod(acl, emqx_acl_internal, [File]); File ->
undefined -> ok emqx_access_control:register_mod(acl, emqx_acl_internal, [File])
end. end.
%% @doc Authenticate Client. %% @doc Authenticate Client.
@ -127,6 +129,12 @@ tab_key(acl) -> acl_modules.
stop() -> stop() ->
gen_server:stop(?MODULE, normal, infinity). gen_server:stop(?MODULE, normal, infinity).
%%TODO: Support ACL cache...
clean_acl_cache(_ClientId) ->
ok.
clean_acl_cache(_ClientId, _Topic) ->
ok.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -81,7 +81,7 @@ handle_event({set_alarm, Alarm = #alarm{timestamp = undefined}}, State)->
handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) -> handle_event({set_alarm, Alarm = #alarm{id = AlarmId}}, State = #state{alarms = Alarms}) ->
case encode_alarm(Alarm) of case encode_alarm(Alarm) of
{ok, Json} -> {ok, Json} ->
emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json)); ok = emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json));
{error, Reason} -> {error, Reason} ->
emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason]) emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason])
end, end,
@ -131,7 +131,9 @@ encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title,
{ts, emqx_time:now_secs(Ts)}]). {ts, emqx_time:now_secs(Ts)}]).
alarm_msg(Type, AlarmId, Json) -> alarm_msg(Type, AlarmId, Json) ->
emqx_message:make(?ALARM_MGR, #{sys => true, qos => 0}, topic(Type, AlarmId), Json). Msg = emqx_message:make(?ALARM_MGR, topic(Type, AlarmId), Json),
emqx_message:set_headers(#{'Content-Type' => <<"application/json">>},
emqx_message:set_flags(#{sys => true}, Msg)).
topic(alert, AlarmId) -> topic(alert, AlarmId) ->
emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>);

View File

@ -31,7 +31,7 @@ start(_Type, _Args) ->
emqx_modules:load(), emqx_modules:load(),
emqx_plugins:init(), emqx_plugins:init(),
emqx_plugins:load(), emqx_plugins:load(),
emqx_listeners:start_all(), emqx_listeners:start(),
start_autocluster(), start_autocluster(),
register(emqx, self()), register(emqx, self()),
print_vsn(), print_vsn(),

View File

@ -16,10 +16,6 @@
-include("emqx.hrl"). -include("emqx.hrl").
-export([passwd_hash/2]).
-type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Authentication behavihour %% Authentication behavihour
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -46,33 +42,3 @@ behaviour_info(_Other) ->
-endif. -endif.
%% @doc Password Hash
-spec(passwd_hash(hash_type(), binary() | tuple()) -> binary()).
passwd_hash(plain, Password) ->
Password;
passwd_hash(md5, Password) ->
hexstring(crypto:hash(md5, Password));
passwd_hash(sha, Password) ->
hexstring(crypto:hash(sha, Password));
passwd_hash(sha256, Password) ->
hexstring(crypto:hash(sha256, Password));
passwd_hash(pbkdf2, {Salt, Password, Macfun, Iterations, Dklen}) ->
case pbkdf2:pbkdf2(Macfun, Password, Salt, Iterations, Dklen) of
{ok, Hexstring} -> pbkdf2:to_hex(Hexstring);
{error, Error} ->
emqx_logger:error("[AuthMod] PasswdHash with pbkdf2 error:~p", [Error]), <<>>
end;
passwd_hash(bcrypt, {Salt, Password}) ->
case bcrypt:hashpw(Password, Salt) of
{ok, HashPassword} -> list_to_binary(HashPassword);
{error, Error}->
emqx_logger:error("[AuthMod] PasswdHash with bcrypt error:~p", [Error]), <<>>
end.
hexstring(<<X:128/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~32.16.0b", [X]));
hexstring(<<X:160/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~40.16.0b", [X]));
hexstring(<<X:256/big-unsigned-integer>>) ->
iolist_to_binary(io_lib:format("~64.16.0b", [X])).

View File

@ -14,43 +14,100 @@
-module(emqx_base62). -module(emqx_base62).
-export([encode/1, decode/1]). -export([encode/1,
encode/2,
decode/1,
decode/2]).
%% @doc Encode an integer to base62 string %% @doc Encode any data to base62 binary
-spec(encode(non_neg_integer()) -> binary()). -spec encode(string()
encode(I) when is_integer(I) andalso I > 0 -> | integer()
list_to_binary(encode(I, [])). | binary()) -> float().
encode(I) when is_integer(I) ->
encode(integer_to_binary(I));
encode(S) when is_list(S)->
encode(list_to_binary(S));
encode(B) when is_binary(B) ->
encode(B, <<>>).
encode(I, Acc) when I < 62 -> %% encode(D, string) ->
[char(I) | Acc]; %% binary_to_list(encode(D)).
encode(I, Acc) ->
encode(I div 62, [char(I rem 62) | Acc]).
char(I) when I < 10 -> %% @doc Decode base62 binary to origin data binary
$0 + I; decode(L) when is_list(L) ->
decode(list_to_binary(L));
char(I) when I < 36 ->
$A + I - 10;
char(I) when I < 62 ->
$a + I - 36.
%% @doc Decode base62 string to an integer
-spec(decode(string() | binary()) -> integer()).
decode(B) when is_binary(B) -> decode(B) when is_binary(B) ->
decode(binary_to_list(B)); decode(B, <<>>).
decode(S) when is_list(S) ->
decode(S, 0).
decode([], I) ->
I;
decode([C|S], I) ->
decode(S, I * 62 + byte(C)).
byte(C) when $0 =< C andalso C =< $9 ->
C - $0; %%====================================================================
byte(C) when $A =< C andalso C =< $Z -> %% Internal functions
C - $A + 10; %%====================================================================
byte(C) when $a =< C andalso C =< $z ->
C - $a + 36. encode(D, string) ->
binary_to_list(encode(D));
encode(<<Index1:6, Index2:6, Index3:6, Index4:6, Rest/binary>>, Acc) ->
CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3), encode_char(Index4)],
NewAcc = <<Acc/binary,(iolist_to_binary(CharList))/binary>>,
encode(Rest, NewAcc);
encode(<<Index1:6, Index2:6, Index3:4>>, Acc) ->
CharList = [encode_char(Index1), encode_char(Index2), encode_char(Index3)],
NewAcc = <<Acc/binary,(iolist_to_binary(CharList))/binary>>,
encode(<<>>, NewAcc);
encode(<<Index1:6, Index2:2>>, Acc) ->
CharList = [encode_char(Index1), encode_char(Index2)],
NewAcc = <<Acc/binary,(iolist_to_binary(CharList))/binary>>,
encode(<<>>, NewAcc);
encode(<<>>, Acc) ->
Acc.
decode(D, integer) ->
binary_to_integer(decode(D));
decode(D, string) ->
binary_to_list(decode(D));
decode(<<Head:8, Rest/binary>>, Acc)
when bit_size(Rest) >= 8->
case Head == $9 of
true ->
<<Head1:8, Rest1/binary>> = Rest,
DecodeChar = decode_char(9, Head1),
<<_:2, RestBit:6>> = <<DecodeChar>>,
NewAcc = <<Acc/bitstring, RestBit:6>>,
decode(Rest1, NewAcc);
false ->
DecodeChar = decode_char(Head),
<<_:2, RestBit:6>> = <<DecodeChar>>,
NewAcc = <<Acc/bitstring, RestBit:6>>,
decode(Rest, NewAcc)
end;
decode(<<Head:8, Rest/binary>>, Acc) ->
DecodeChar = decode_char(Head),
LeftBitSize = bit_size(Acc) rem 8,
RightBitSize = 8 - LeftBitSize,
<<_:LeftBitSize, RestBit:RightBitSize>> = <<DecodeChar>>,
NewAcc = <<Acc/bitstring, RestBit:RightBitSize>>,
decode(Rest, NewAcc);
decode(<<>>, Acc) ->
Acc.
encode_char(I) when I < 26 ->
$A + I;
encode_char(I) when I < 52 ->
$a + I - 26;
encode_char(I) when I < 61 ->
$0 + I - 52;
encode_char(I) ->
[$9, $A + I - 61].
decode_char(I) when I >= $a andalso I =< $z ->
I + 26 - $a;
decode_char(I) when I >= $0 andalso I =< $8->
I + 52 - $0;
decode_char(I) when I >= $A andalso I =< $Z->
I - $A.
decode_char(9, I) ->
I + 61 - $A.

View File

@ -64,9 +64,7 @@ init([Pool, Id, Node, Topic, Options]) ->
emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]), emqx_broker:subscribe(Topic, self(), [{share, Share}, {qos, ?QOS_0}]),
State = parse_opts(Options, #state{node = Node, subtopic = Topic}), State = parse_opts(Options, #state{node = Node, subtopic = Topic}),
%%TODO: queue.... %%TODO: queue....
MQueue = emqx_mqueue:new(qname(Node, Topic), MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]),
[{max_len, State#state.max_queue_len}],
emqx_alarm:alarm_fun()),
{ok, State#state{pool = Pool, id = Id, mqueue = MQueue}}; {ok, State#state{pool = Pool, id = Id, mqueue = MQueue}};
false -> false ->
{stop, {cannot_connect_node, Node}} {stop, {cannot_connect_node, Node}}
@ -74,8 +72,8 @@ init([Pool, Id, Node, Topic, Options]) ->
parse_opts([], State) -> parse_opts([], State) ->
State; State;
parse_opts([{qos, Qos} | Opts], State) -> parse_opts([{qos, QoS} | Opts], State) ->
parse_opts(Opts, State#state{qos = Qos}); parse_opts(Opts, State#state{qos = QoS});
parse_opts([{topic_suffix, Suffix} | Opts], State) -> parse_opts([{topic_suffix, Suffix} | Opts], State) ->
parse_opts(Opts, State#state{topic_suffix= Suffix}); parse_opts(Opts, State#state{topic_suffix= Suffix});
parse_opts([{topic_prefix, Prefix} | Opts], State) -> parse_opts([{topic_prefix, Prefix} | Opts], State) ->

254
src/emqx_bridge1.erl Normal file
View File

@ -0,0 +1,254 @@
%% Copyright (c) 2018 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_bridge1).
-behaviour(gen_server).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-import(proplists, [get_value/2, get_value/3]).
-export([start_link/2, start_bridge/1, stop_bridge/1, status/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-record(state, {client_pid, options, reconnect_time, reconnect_count,
def_reconnect_count, type, mountpoint, queue, store_type,
max_pending_messages}).
-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false,
packet_id, topic, props, payload}).
start_link(Name, Options) ->
gen_server:start_link({local, name(Name)}, ?MODULE, [Options], []).
start_bridge(Name) ->
gen_server:call(name(Name), start_bridge).
stop_bridge(Name) ->
gen_server:call(name(Name), stop_bridge).
status(Pid) ->
gen_server:call(Pid, status).
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([Options]) ->
process_flag(trap_exit, true),
case get_value(start_type, Options, manual) of
manual -> ok;
auto -> erlang:send_after(1000, self(), start)
end,
ReconnectCount = get_value(reconnect_count, Options, 10),
ReconnectTime = get_value(reconnect_time, Options, 30000),
MaxPendingMsg = get_value(max_pending_messages, Options, 10000),
Mountpoint = format_mountpoint(get_value(mountpoint, Options)),
StoreType = get_value(store_type, Options, memory),
Type = get_value(type, Options, in),
Queue = [],
{ok, #state{type = Type,
mountpoint = Mountpoint,
queue = Queue,
store_type = StoreType,
options = Options,
reconnect_count = ReconnectCount,
reconnect_time = ReconnectTime,
def_reconnect_count = ReconnectCount,
max_pending_messages = MaxPendingMsg}}.
handle_call(start_bridge, _From, State = #state{client_pid = undefined}) ->
{noreply, NewState} = handle_info(start, State),
{reply, <<"start bridge successfully">>, NewState};
handle_call(start_bridge, _From, State) ->
{reply, <<"bridge already started">>, State};
handle_call(stop_bridge, _From, State = #state{client_pid = undefined}) ->
{reply, <<"bridge not started">>, State};
handle_call(stop_bridge, _From, State = #state{client_pid = Pid}) ->
emqx_client:disconnect(Pid),
{reply, <<"stop bridge successfully">>, State};
handle_call(status, _From, State = #state{client_pid = undefined}) ->
{reply, <<"Stopped">>, State};
handle_call(status, _From, State = #state{client_pid = _Pid})->
{reply, <<"Running">>, State};
handle_call(Req, _From, State) ->
emqx_logger:error("[Bridge] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Msg, State) ->
emqx_logger:error("[Bridge] unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(start, State = #state{reconnect_count = 0}) ->
{noreply, State};
%%----------------------------------------------------------------
%% start in message bridge
%%----------------------------------------------------------------
handle_info(start, State = #state{options = Options,
client_pid = undefined,
reconnect_time = ReconnectTime,
reconnect_count = ReconnectCount,
type = in}) ->
case emqx_client:start_link([{owner, self()}|options(Options)]) of
{ok, ClientPid, _} ->
Subs = get_value(subscriptions, Options, []),
[emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs],
{noreply, State#state{client_pid = ClientPid}};
{error,_} ->
erlang:send_after(ReconnectTime, self(), start),
{noreply, State = #state{reconnect_count = ReconnectCount-1}}
end;
%%----------------------------------------------------------------
%% start out message bridge
%%----------------------------------------------------------------
handle_info(start, State = #state{options = Options,
client_pid = undefined,
reconnect_time = ReconnectTime,
reconnect_count = ReconnectCount,
type = out}) ->
case emqx_client:start_link([{owner, self()}|options(Options)]) of
{ok, ClientPid, _} ->
Subs = get_value(subscriptions, Options, []),
[emqx_client:subscribe(ClientPid, {i2b(Topic), Qos}) || {Topic, Qos} <- Subs],
ForwardRules = string:tokens(get_value(forward_rule, Options, ""), ","),
[emqx_broker:subscribe(i2b(Topic)) || Topic <- ForwardRules, emqx_topic:validate({filter, i2b(Topic)})],
{noreply, State#state{client_pid = ClientPid}};
{error,_} ->
erlang:send_after(ReconnectTime, self(), start),
{noreply, State = #state{reconnect_count = ReconnectCount-1}}
end;
%%----------------------------------------------------------------
%% received local node message
%%----------------------------------------------------------------
handle_info({dispatch, _, #message{topic = Topic, payload = Payload, flags = #{retain := Retain}}},
State = #state{client_pid = Pid, mountpoint = Mountpoint, queue = Queue,
store_type = StoreType, max_pending_messages = MaxPendingMsg}) ->
Msg = #mqtt_msg{qos = 1,
retain = Retain,
topic = mountpoint(Mountpoint, Topic),
payload = Payload},
case emqx_client:publish(Pid, Msg) of
{ok, PkgId} ->
{noreply, State#state{queue = store(StoreType, {PkgId, Msg}, Queue, MaxPendingMsg)}};
{error, Reason} ->
emqx_logger:error("Publish fail:~p", [Reason]),
{noreply, State}
end;
%%----------------------------------------------------------------
%% received remote node message
%%----------------------------------------------------------------
handle_info({publish, #{qos := QoS, dup := Dup, retain := Retain, topic := Topic,
properties := Props, payload := Payload}}, State) ->
NewMsg0 = emqx_message:make(bridge, QoS, Topic, Payload),
NewMsg1 = emqx_message:set_headers(Props, emqx_message:set_flags(#{dup => Dup, retain=> Retain}, NewMsg0)),
emqx_broker:publish(NewMsg1),
{noreply, State};
%%----------------------------------------------------------------
%% received remote puback message
%%----------------------------------------------------------------
handle_info({puback, #{packet_id := PkgId}}, State = #state{queue = Queue, store_type = StoreType}) ->
% lists:keydelete(PkgId, 1, Queue)
{noreply, State#state{queue = delete(StoreType, PkgId, Queue)}};
handle_info({'EXIT', Pid, normal}, State = #state{client_pid = Pid}) ->
{noreply, State#state{client_pid = undefined}};
handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = Pid,
reconnect_time = ReconnectTime,
def_reconnect_count = DefReconnectCount}) ->
lager:warning("emqx bridge stop reason:~p", [Reason]),
erlang:send_after(ReconnectTime, self(), start),
{noreply, State#state{client_pid = undefined, reconnect_count = DefReconnectCount}};
handle_info(Info, State) ->
emqx_logger:error("[Bridge] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, #state{}) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
proto_ver(mqtt3) -> v3;
proto_ver(mqtt4) -> v4;
proto_ver(mqtt5) -> v5.
address(Address) ->
case string:tokens(Address, ":") of
[Host] -> {Host, 1883};
[Host, Port] -> {Host, list_to_integer(Port)}
end.
options(Options) ->
options(Options, []).
options([], Acc) ->
Acc;
options([{username, Username}| Options], Acc) ->
options(Options, [{username, Username}|Acc]);
options([{proto_ver, ProtoVer}| Options], Acc) ->
options(Options, [{proto_ver, proto_ver(ProtoVer)}|Acc]);
options([{password, Password}| Options], Acc) ->
options(Options, [{password, Password}|Acc]);
options([{keepalive, Keepalive}| Options], Acc) ->
options(Options, [{keepalive, Keepalive}|Acc]);
options([{client_id, ClientId}| Options], Acc) ->
options(Options, [{client_id, ClientId}|Acc]);
options([{clean_start, CleanStart}| Options], Acc) ->
options(Options, [{clean_start, CleanStart}|Acc]);
options([{address, Address}| Options], Acc) ->
{Host, Port} = address(Address),
options(Options, [{host, Host}, {port, Port}|Acc]);
options([_Option | Options], Acc) ->
options(Options, Acc).
name(Id) ->
list_to_atom(lists:concat([?MODULE, "_", Id])).
i2b(L) -> iolist_to_binary(L).
mountpoint(undefined, Topic) ->
Topic;
mountpoint(Prefix, Topic) ->
<<Prefix/binary, Topic/binary>>.
format_mountpoint(undefined) ->
undefined;
format_mountpoint(Prefix) ->
binary:replace(i2b(Prefix), <<"${node}">>, atom_to_binary(node(), utf8)).
store(memory, Data, Queue, MaxPendingMsg) when length(Queue) =< MaxPendingMsg ->
[Data | Queue];
store(memory, _Data, Queue, _MaxPendingMsg) ->
lager:error("Beyond max pending messages"),
Queue;
store(disk, Data, Queue, _MaxPendingMsg)->
[Data | Queue].
delete(memory, PkgId, Queue) ->
lists:keydelete(PkgId, 1, Queue);
delete(disk, PkgId, Queue) ->
lists:keydelete(PkgId, 1, Queue).

45
src/emqx_bridge1_sup.erl Normal file
View File

@ -0,0 +1,45 @@
%% Copyright (c) 2018 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_bridge1_sup).
-behavior(supervisor).
-include("emqx.hrl").
-export([start_link/0, bridges/0]).
%% Supervisor callbacks
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% @doc List all bridges
-spec(bridges() -> [{node(), topic(), pid()}]).
bridges() ->
[{Name, emqx_bridge1:status(Pid)} || {Name, Pid, _, _} <- supervisor:which_children(?MODULE)].
init([]) ->
BridgesOpts = emqx_config:get_env(bridges, []),
Bridges = [spec(Opts)|| Opts <- BridgesOpts],
{ok, {{one_for_one, 10, 100}, Bridges}}.
spec({Id, Options})->
#{id => Id,
start => {emqx_bridge1, start_link, [Id, Options]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [emqx_bridge1]}.

View File

@ -20,8 +20,10 @@
-export([start_link/2]). -export([start_link/2]).
-export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]). -export([subscribe/1, subscribe/2, subscribe/3, subscribe/4]).
-export([publish/1, publish/2, safe_publish/1]). -export([multi_subscribe/1, multi_subscribe/2, multi_subscribe/3]).
-export([unsubscribe/1, unsubscribe/2]). -export([publish/1, safe_publish/1]).
-export([unsubscribe/1, unsubscribe/2, unsubscribe/3]).
-export([multi_unsubscribe/1, multi_unsubscribe/2, multi_unsubscribe/3]).
-export([dispatch/2, dispatch/3]). -export([dispatch/2, dispatch/3]).
-export([subscriptions/1, subscribers/1, subscribed/2]). -export([subscriptions/1, subscribers/1, subscribed/2]).
-export([get_subopts/2, set_subopts/3]). -export([get_subopts/2, set_subopts/3]).
@ -31,102 +33,141 @@
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]). code_change/3]).
-record(state, {pool, id, submon}). -record(state, {pool, id, submap, submon}).
-record(subscribe, {topic, subpid, subid, subopts = #{}}).
-record(unsubscribe, {topic, subpid, subid}).
%% The default request timeout
-define(TIMEOUT, 60000).
-define(BROKER, ?MODULE). -define(BROKER, ?MODULE).
-define(TIMEOUT, 120000).
%% ETS tables %% ETS tables
-define(SUBOPTION, emqx_suboption). -define(SUBOPTION, emqx_suboption).
-define(SUBSCRIBER, emqx_subscriber). -define(SUBSCRIBER, emqx_subscriber).
-define(SUBSCRIPTION, emqx_subscription). -define(SUBSCRIPTION, emqx_subscription).
-define(is_subid(Id), (is_binary(Id) orelse is_atom(Id))).
-spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}). -spec(start_link(atom(), pos_integer()) -> {ok, pid()} | ignore | {error, term()}).
start_link(Pool, Id) -> start_link(Pool, Id) ->
gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE,
?MODULE, [Pool, Id], [{hibernate_after, 2000}]). [Pool, Id], [{hibernate_after, 2000}]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Subscribe/Unsubscribe %% Subscribe
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
-spec(subscribe(topic()) -> ok | {error, term()}). -spec(subscribe(topic()) -> ok).
subscribe(Topic) when is_binary(Topic) -> subscribe(Topic) when is_binary(Topic) ->
subscribe(Topic, self()). subscribe(Topic, self()).
-spec(subscribe(topic(), subscriber()) -> ok | {error, term()}). -spec(subscribe(topic(), pid() | subid()) -> ok).
subscribe(Topic, Subscriber) when is_binary(Topic) -> subscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) ->
subscribe(Topic, Subscriber, []). subscribe(Topic, SubPid, undefined);
subscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) ->
subscribe(Topic, self(), SubId).
-spec(subscribe(topic(), subscriber(), [suboption()]) -> ok | {error, term()}). -spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok).
subscribe(Topic, Subscriber, Options) when is_binary(Topic) -> subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
subscribe(Topic, Subscriber, Options, ?TIMEOUT). subscribe(Topic, SubPid, SubId, #{});
subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
subscribe(Topic, SubPid, SubId, #{});
subscribe(Topic, SubPid, SubOpts) when is_binary(Topic), is_pid(SubPid), is_map(SubOpts) ->
subscribe(Topic, SubPid, undefined, SubOpts);
subscribe(Topic, SubId, SubOpts) when is_binary(Topic), ?is_subid(SubId), is_map(SubOpts) ->
subscribe(Topic, self(), SubId, SubOpts).
-spec(subscribe(topic(), subscriber(), [suboption()], timeout()) -spec(subscribe(topic(), pid(), subid(), subopts()) -> ok).
-> ok | {error, term()}). subscribe(Topic, SubPid, SubId, SubOpts) when is_binary(Topic), is_pid(SubPid),
subscribe(Topic, Subscriber, Options, Timeout) -> ?is_subid(SubId), is_map(SubOpts) ->
{Topic1, Options1} = emqx_topic:parse(Topic, Options), Broker = pick(SubPid),
SubReq = {subscribe, Topic1, with_subpid(Subscriber), Options1}, SubReq = #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts},
async_call(pick(Subscriber), SubReq, Timeout). wait_for_reply(async_call(Broker, SubReq), ?TIMEOUT).
-spec(unsubscribe(topic()) -> ok | {error, term()}). -spec(multi_subscribe(topic_table()) -> ok).
multi_subscribe(TopicTable) when is_list(TopicTable) ->
multi_subscribe(TopicTable, self()).
-spec(multi_subscribe(topic_table(), pid() | subid()) -> ok).
multi_subscribe(TopicTable, SubPid) when is_pid(SubPid) ->
multi_subscribe(TopicTable, SubPid, undefined);
multi_subscribe(TopicTable, SubId) when ?is_subid(SubId) ->
multi_subscribe(TopicTable, self(), SubId).
-spec(multi_subscribe(topic_table(), pid(), subid()) -> ok).
multi_subscribe(TopicTable, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) ->
Broker = pick(SubPid),
SubReq = fun(Topic, SubOpts) ->
#subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}
end,
wait_for_replies([async_call(Broker, SubReq(Topic, SubOpts))
|| {Topic, SubOpts} <- TopicTable], ?TIMEOUT).
%%------------------------------------------------------------------------------
%% Unsubscribe
%%------------------------------------------------------------------------------
-spec(unsubscribe(topic()) -> ok).
unsubscribe(Topic) when is_binary(Topic) -> unsubscribe(Topic) when is_binary(Topic) ->
unsubscribe(Topic, self()). unsubscribe(Topic, self()).
-spec(unsubscribe(topic(), subscriber()) -> ok | {error, term()}). -spec(unsubscribe(topic(), pid() | subid()) -> ok).
unsubscribe(Topic, Subscriber) when is_binary(Topic) -> unsubscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) ->
unsubscribe(Topic, Subscriber, ?TIMEOUT). unsubscribe(Topic, SubPid, undefined);
unsubscribe(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) ->
unsubscribe(Topic, self(), SubId).
-spec(unsubscribe(topic(), subscriber(), timeout()) -> ok | {error, term()}). -spec(unsubscribe(topic(), pid(), subid()) -> ok).
unsubscribe(Topic, Subscriber, Timeout) -> unsubscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
{Topic1, _} = emqx_topic:parse(Topic), Broker = pick(SubPid),
UnsubReq = {unsubscribe, Topic1, with_subpid(Subscriber)}, UnsubReq = #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId},
async_call(pick(Subscriber), UnsubReq, Timeout). wait_for_reply(async_call(Broker, UnsubReq), ?TIMEOUT).
-spec(multi_unsubscribe([topic()]) -> ok).
multi_unsubscribe(Topics) ->
multi_unsubscribe(Topics, self()).
-spec(multi_unsubscribe([topic()], pid() | subid()) -> ok).
multi_unsubscribe(Topics, SubPid) when is_pid(SubPid) ->
multi_unsubscribe(Topics, SubPid, undefined);
multi_unsubscribe(Topics, SubId) when ?is_subid(SubId) ->
multi_unsubscribe(Topics, self(), SubId).
-spec(multi_unsubscribe([topic()], pid(), subid()) -> ok).
multi_unsubscribe(Topics, SubPid, SubId) when is_pid(SubPid), ?is_subid(SubId) ->
Broker = pick(SubPid),
UnsubReq = fun(Topic) ->
#unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}
end,
wait_for_replies([async_call(Broker, UnsubReq(Topic)) || Topic <- Topics], ?TIMEOUT).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Publish %% Publish
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
-spec(publish(topic(), payload()) -> delivery() | stopped). -spec(publish(message()) -> delivery()).
publish(Topic, Payload) when is_binary(Topic), is_binary(Payload) -> publish(Msg) when is_record(Msg, message) ->
publish(emqx_message:make(Topic, Payload)). _ = emqx_tracer:trace(publish, Msg),
-spec(publish(message()) -> {ok, delivery()} | {error, stopped}).
publish(Msg = #message{from = From}) ->
%% Hook to trace?
_ = trace(publish, From, Msg),
case emqx_hooks:run('message.publish', [], Msg) of case emqx_hooks:run('message.publish', [], Msg) of
{ok, Msg1 = #message{topic = Topic}} -> {ok, Msg1 = #message{topic = Topic}} ->
{ok, route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1))}; route(aggre(emqx_router:match_routes(Topic)), delivery(Msg1));
{stop, Msg1} -> {stop, Msg1} ->
emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]), emqx_logger:warning("Stop publishing: ~p", [Msg]), delivery(Msg1)
{error, stopped}
end. end.
%% called internally %% Called internally
safe_publish(Msg) -> safe_publish(Msg) when is_record(Msg, message) ->
try try
publish(Msg) publish(Msg)
catch catch
_:Error:Stacktrace -> _:Error:Stacktrace ->
emqx_logger:error("[Broker] publish error: ~p~n~p~n~p", [Error, Msg, Stacktrace]) emqx_logger:error("[Broker] publish error: ~p~n~p~n~p", [Error, Msg, Stacktrace])
after
ok
end. end.
%%------------------------------------------------------------------------------ delivery(Msg) ->
%% Trace #delivery{sender = self(), message = Msg, flows = []}.
%%------------------------------------------------------------------------------
trace(publish, From, _Msg) when is_atom(From) ->
%% Dont' trace '$SYS' publish
ignore;
trace(public, #client{id = ClientId, username = Username},
#message{topic = Topic, payload = Payload}) ->
emqx_logger:info([{client, ClientId}, {topic, Topic}],
"~s/~s PUBLISH to ~s: ~p", [Username, ClientId, Topic, Payload]);
trace(public, From, #message{topic = Topic, payload = Payload})
when is_binary(From); is_list(From) ->
emqx_logger:info([{client, From}, {topic, Topic}],
"~s PUBLISH to ~s: ~p", [From, Topic, Payload]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Route %% Route
@ -186,12 +227,8 @@ dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) ->
Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]} Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]}
end. end.
dispatch(SubPid, Topic, Msg) when is_pid(SubPid) -> dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) ->
SubPid ! {dispatch, Topic, Msg}; SubPid ! {dispatch, Topic, Msg};
dispatch({SubId, SubPid}, Topic, Msg) when is_binary(SubId), is_pid(SubPid) ->
SubPid ! {dispatch, Topic, Msg};
dispatch(SubId, Topic, Msg) when is_binary(SubId) ->
emqx_sm:dispatch(SubId, Topic, Msg);
dispatch({share, _Group, _Sub}, _Topic, _Msg) -> dispatch({share, _Group, _Sub}, _Topic, _Msg) ->
ignored. ignored.
@ -200,12 +237,11 @@ dropped(<<"$SYS/", _/binary>>) ->
dropped(_Topic) -> dropped(_Topic) ->
emqx_metrics:inc('messages/dropped'). emqx_metrics:inc('messages/dropped').
delivery(Msg) -> -spec(subscribers(topic()) -> [subscriber()]).
#delivery{node = node(), message = Msg, flows = []}.
subscribers(Topic) -> subscribers(Topic) ->
try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end. try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end.
-spec(subscriptions(subscriber()) -> [{topic(), subopts()}]).
subscriptions(Subscriber) -> subscriptions(Subscriber) ->
lists:map(fun({_, {share, _Group, Topic}}) -> lists:map(fun({_, {share, _Group, Topic}}) ->
subscription(Topic, Subscriber); subscription(Topic, Subscriber);
@ -216,51 +252,49 @@ subscriptions(Subscriber) ->
subscription(Topic, Subscriber) -> subscription(Topic, Subscriber) ->
{Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}. {Topic, ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)}.
-spec(subscribed(topic(), subscriber()) -> boolean()). -spec(subscribed(topic(), pid() | subid() | subscriber()) -> boolean()).
subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) -> subscribed(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) ->
ets:member(?SUBOPTION, {Topic, SubPid}); length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) == 1;
subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) -> subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) ->
length(ets:match_object(?SUBOPTION, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1; length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) == 1;
subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) -> subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}). ets:member(?SUBOPTION, {Topic, {SubPid, SubId}}).
-spec(get_subopts(topic(), subscriber()) -> [suboption()]). -spec(get_subopts(topic(), subscriber()) -> subopts()).
get_subopts(Topic, Subscriber) when is_binary(Topic) -> get_subopts(Topic, Subscriber) when is_binary(Topic) ->
try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2) try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)
catch error:badarg -> [] catch error:badarg -> []
end. end.
-spec(set_subopts(topic(), subscriber(), [suboption()]) -> boolean()). -spec(set_subopts(topic(), subscriber(), subopts()) -> boolean()).
set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) -> set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_map(Opts) ->
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of
[{_, OldOpts}] -> [{_, OldOpts}] ->
Opts1 = lists:usort(lists:umerge(Opts, OldOpts)), ets:insert(?SUBOPTION, {{Topic, Subscriber}, maps:merge(OldOpts, Opts)});
ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1});
[] -> false [] -> false
end. end.
with_subpid(SubPid) when is_pid(SubPid) -> async_call(Broker, Req) ->
SubPid;
with_subpid(SubId) when is_binary(SubId) ->
{SubId, self()};
with_subpid({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) ->
{SubId, SubPid}.
async_call(Broker, Msg, Timeout) ->
From = {self(), Tag = make_ref()}, From = {self(), Tag = make_ref()},
ok = gen_server:cast(Broker, {From, Msg}), ok = gen_server:cast(Broker, {From, Req}),
Tag.
wait_for_replies(Tags, Timeout) ->
lists:foreach(
fun(Tag) ->
wait_for_reply(Tag, Timeout)
end, Tags).
wait_for_reply(Tag, Timeout) ->
receive receive
{Tag, Reply} -> Reply {Tag, Reply} -> Reply
after Timeout -> after Timeout ->
{error, timeout} exit(timeout)
end. end.
%% Pick a broker
pick(SubPid) when is_pid(SubPid) -> pick(SubPid) when is_pid(SubPid) ->
gproc_pool:pick_worker(broker, SubPid); gproc_pool:pick_worker(broker, SubPid).
pick(SubId) when is_binary(SubId) ->
gproc_pool:pick_worker(broker, SubId);
pick({SubId, SubPid}) when is_binary(SubId), is_pid(SubPid) ->
pick(SubPid).
-spec(topics() -> [topic()]). -spec(topics() -> [topic()]).
topics() -> emqx_router:topics(). topics() -> emqx_router:topics().
@ -271,33 +305,35 @@ topics() -> emqx_router:topics().
init([Pool, Id]) -> init([Pool, Id]) ->
true = gproc_pool:connect_worker(Pool, {Pool, Id}), true = gproc_pool:connect_worker(Pool, {Pool, Id}),
{ok, #state{pool = Pool, id = Id, submon = emqx_pmon:new()}}. {ok, #state{pool = Pool, id = Id, submap = #{}, submon = emqx_pmon:new()}}.
handle_call(Req, _From, State) -> handle_call(Req, _From, State) ->
emqx_logger:error("[Broker] unexpected call: ~p", [Req]), emqx_logger:error("[Broker] unexpected call: ~p", [Req]),
{reply, ignored, State}. {reply, ignored, State}.
handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) -> handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) ->
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of Subscriber = {SubPid, SubId},
[] -> case ets:member(?SUBOPTION, {Topic, Subscriber}) of
Group = proplists:get_value(share, Options), false ->
true = do_subscribe(Group, Topic, Subscriber, Options), Group = maps:get(share, SubOpts, undefined),
emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)), true = do_subscribe(Group, Topic, Subscriber, SubOpts),
emqx_router:add_route(From, Topic, destination(Options)), emqx_shared_sub:subscribe(Group, Topic, SubPid),
emqx_router:add_route(From, Topic, dest(Group)),
{noreply, monitor_subscriber(Subscriber, State)}; {noreply, monitor_subscriber(Subscriber, State)};
[_] -> true ->
gen_server:reply(From, ok), gen_server:reply(From, ok),
{noreply, State} {noreply, State}
end; end;
handle_cast({From, {unsubscribe, Topic, Subscriber}}, State) -> handle_cast({From, #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId}}, State) ->
Subscriber = {SubPid, SubId},
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of
[{_, Options}] -> [{_, SubOpts}] ->
Group = proplists:get_value(share, Options), Group = maps:get(share, SubOpts, undefined),
true = do_unsubscribe(Group, Topic, Subscriber), true = do_unsubscribe(Group, Topic, Subscriber),
emqx_shared_sub:unsubscribe(Group, Topic, subpid(Subscriber)), emqx_shared_sub:unsubscribe(Group, Topic, SubPid),
case ets:member(?SUBSCRIBER, Topic) of case ets:member(?SUBSCRIBER, Topic) of
false -> emqx_router:del_route(From, Topic, destination(Options)); false -> emqx_router:del_route(From, Topic, dest(Group));
true -> gen_server:reply(From, ok) true -> gen_server:reply(From, ok)
end; end;
[] -> gen_server:reply(From, ok) [] -> gen_server:reply(From, ok)
@ -308,37 +344,22 @@ handle_cast(Msg, State) ->
emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]), emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]),
{noreply, State}. {noreply, State}.
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{submon = SubMon}) -> handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = SubMap}) ->
Subscriber = case SubMon:find(SubPid) of case maps:find(SubPid, SubMap) of
undefined -> SubPid; {ok, SubIds} ->
SubId -> {SubId, SubPid} lists:foreach(fun(SubId) -> subscriber_down({SubPid, SubId}) end, SubIds),
end,
Topics = lists:map(fun({_, {share, _, Topic}}) ->
Topic;
({_, Topic}) ->
Topic
end, ets:lookup(?SUBSCRIPTION, Subscriber)),
lists:foreach(
fun(Topic) ->
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of
[{_, Options}] ->
Group = proplists:get_value(share, Options),
true = do_unsubscribe(Group, Topic, Subscriber),
case ets:member(?SUBSCRIBER, Topic) of
false -> emqx_router:del_route(Topic, destination(Options));
true -> ok
end;
[] -> ok
end
end, Topics),
{noreply, demonitor_subscriber(SubPid, State)}; {noreply, demonitor_subscriber(SubPid, State)};
error ->
emqx_logger:error("unexpected 'DOWN': ~p, reason: ~p", [SubPid, Reason]),
{noreply, State}
end;
handle_info(Info, State) -> handle_info(Info, State) ->
emqx_logger:error("[Broker] unexpected info: ~p", [Info]), emqx_logger:error("[Broker] unexpected info: ~p", [Info]),
{noreply, State}. {noreply, State}.
terminate(_Reason, #state{pool = Pool, id = Id}) -> terminate(_Reason, #state{pool = Pool, id = Id}) ->
true = gproc_pool:disconnect_worker(Pool, {Pool, Id}). gproc_pool:disconnect_worker(Pool, {Pool, Id}).
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
@ -347,35 +368,44 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal functions %% Internal functions
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
do_subscribe(Group, Topic, Subscriber, Options) -> do_subscribe(Group, Topic, Subscriber, SubOpts) ->
ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}),
ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}),
ets:insert(?SUBOPTION, {{Topic, Subscriber}, Options}). ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}).
do_unsubscribe(Group, Topic, Subscriber) -> do_unsubscribe(Group, Topic, Subscriber) ->
ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}), ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}),
ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}), ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}),
ets:delete(?SUBOPTION, {Topic, Subscriber}). ets:delete(?SUBOPTION, {Topic, Subscriber}).
monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) -> subscriber_down(Subscriber) ->
State#state{submon = SubMon:monitor(SubPid)}; Topics = lists:map(fun({_, {share, _, Topic}}) ->
Topic;
({_, Topic}) ->
Topic
end, ets:lookup(?SUBSCRIPTION, Subscriber)),
lists:foreach(fun(Topic) ->
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of
[{_, SubOpts}] ->
Group = maps:get(share, SubOpts, undefined),
true = do_unsubscribe(Group, Topic, Subscriber),
ets:member(?SUBSCRIBER, Topic)
orelse emqx_router:del_route(Topic, dest(Group));
[] -> ok
end
end, Topics).
monitor_subscriber({SubId, SubPid}, State = #state{submon = SubMon}) -> monitor_subscriber({SubPid, SubId}, State = #state{submap = SubMap, submon = SubMon}) ->
State#state{submon = SubMon:monitor(SubPid, SubId)}. UpFun = fun(SubIds) -> lists:usort([SubId|SubIds]) end,
State#state{submap = maps:update_with(SubPid, UpFun, [SubId], SubMap),
submon = emqx_pmon:monitor(SubPid, SubMon)}.
demonitor_subscriber(SubPid, State = #state{submon = SubMon}) -> demonitor_subscriber(SubPid, State = #state{submap = SubMap, submon = SubMon}) ->
State#state{submon = SubMon:demonitor(SubPid)}. State#state{submap = maps:remove(SubPid, SubMap),
submon = emqx_pmon:demonitor(SubPid, SubMon)}.
destination(Options) -> dest(undefined) -> node();
case proplists:get_value(share, Options) of dest(Group) -> {Group, node()}.
undefined -> node();
Group -> {Group, node()}
end.
subpid(SubPid) when is_pid(SubPid) ->
SubPid;
subpid({_SubId, SubPid}) when is_pid(SubPid) ->
SubPid.
shared(undefined, Name) -> Name; shared(undefined, Name) -> Name;
shared(Group, Name) -> {share, Group, Name}. shared(Group, Name) -> {share, Group, Name}.

View File

@ -17,9 +17,7 @@
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/0]). -export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(HELPER, ?MODULE). -define(HELPER, ?MODULE).
@ -39,7 +37,7 @@ init([]) ->
handle_call(Req, _From, State) -> handle_call(Req, _From, State) ->
emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]), emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]),
{reply, ignore, State}. {reply, ignored, State}.
handle_cast(Msg, State) -> handle_cast(Msg, State) ->
emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]), emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]),

View File

@ -14,7 +14,7 @@
-module(emqx_cli). -module(emqx_cli).
-export([print/1, print/2, usage/1]). -export([print/1, print/2, usage/1, usage/2]).
print(Msg) -> print(Msg) ->
io:format(Msg). io:format(Msg).
@ -28,3 +28,5 @@ usage(CmdList) ->
io:format("~-48s# ~s~n", [Cmd, Descr]) io:format("~-48s# ~s~n", [Cmd, Descr])
end, CmdList). end, CmdList).
usage(Format, Args) ->
usage([{Format, Args}]).

View File

@ -69,6 +69,11 @@
-export_type([host/0, option/0]). -export_type([host/0, option/0]).
-record(mqtt_msg, {qos = ?QOS0, retain = false, dup = false,
packet_id, topic, props, payload}).
-type(mqtt_msg() :: #mqtt_msg{}).
-record(state, {name :: atom(), -record(state, {name :: atom(),
owner :: pid(), owner :: pid(),
host :: host(), host :: host(),
@ -89,7 +94,7 @@
force_ping :: boolean(), force_ping :: boolean(),
paused :: boolean(), paused :: boolean(),
will_flag :: boolean(), will_flag :: boolean(),
will_msg :: mqtt_message(), will_msg :: mqtt_msg(),
properties :: properties(), properties :: properties(),
pending_calls :: list(), pending_calls :: list(),
subscriptions :: map(), subscriptions :: map(),
@ -140,6 +145,9 @@
-define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). -define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}).
-define(WILL_MSG(QoS, Retain, Topic, Props, Payload),
#mqtt_msg{qos = QoS, retain = Retain, topic = Topic, props = Props, payload = Payload}).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% API %% API
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
@ -242,8 +250,7 @@ parse_subopt([{qos, QoS} | Opts], Rec) ->
-spec(publish(client(), topic(), payload()) -> ok | {error, term()}). -spec(publish(client(), topic(), payload()) -> ok | {error, term()}).
publish(Client, Topic, Payload) when is_binary(Topic) -> publish(Client, Topic, Payload) when is_binary(Topic) ->
publish(Client, #mqtt_message{topic = Topic, qos = ?QOS_0, publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}).
payload = iolist_to_binary(Payload)}).
-spec(publish(client(), topic(), payload(), qos() | [pubopt()]) -spec(publish(client(), topic(), payload(), qos() | [pubopt()])
-> ok | {ok, packet_id()} | {error, term()}). -> ok | {ok, packet_id()} | {error, term()}).
@ -261,15 +268,14 @@ publish(Client, Topic, Properties, Payload, Opts)
ok = emqx_mqtt_properties:validate(Properties), ok = emqx_mqtt_properties:validate(Properties),
Retain = proplists:get_bool(retain, Opts), Retain = proplists:get_bool(retain, Opts),
QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)),
publish(Client, #mqtt_message{qos = QoS, publish(Client, #mqtt_msg{qos = QoS,
retain = Retain, retain = Retain,
topic = Topic, topic = Topic,
properties = Properties, props = Properties,
payload = iolist_to_binary(Payload)}). payload = iolist_to_binary(Payload)}).
-spec(publish(client(), mqtt_message()) -spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}).
-> ok | {ok, packet_id()} | {error, term()}). publish(Client, Msg) when is_record(Msg, mqtt_msg) ->
publish(Client, Msg) when is_record(Msg, mqtt_message) ->
gen_statem:call(Client, {publish, Msg}). gen_statem:call(Client, {publish, Msg}).
-spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). -spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()).
@ -380,7 +386,7 @@ init([Options]) ->
force_ping = false, force_ping = false,
paused = false, paused = false,
will_flag = false, will_flag = false,
will_msg = #mqtt_message{}, will_msg = #mqtt_msg{},
pending_calls = [], pending_calls = [],
subscriptions = #{}, subscriptions = #{},
max_inflight = infinity, max_inflight = infinity,
@ -488,15 +494,15 @@ init([_Opt | Opts], State) ->
init(Opts, State). init(Opts, State).
init_will_msg({topic, Topic}, WillMsg) -> init_will_msg({topic, Topic}, WillMsg) ->
WillMsg#mqtt_message{topic = iolist_to_binary(Topic)}; WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)};
init_will_msg({props, Properties}, WillMsg) -> init_will_msg({props, Props}, WillMsg) ->
WillMsg#mqtt_message{properties = Properties}; WillMsg#mqtt_msg{props = Props};
init_will_msg({payload, Payload}, WillMsg) -> init_will_msg({payload, Payload}, WillMsg) ->
WillMsg#mqtt_message{payload = iolist_to_binary(Payload)}; WillMsg#mqtt_msg{payload = iolist_to_binary(Payload)};
init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) ->
WillMsg#mqtt_message{retain = Retain}; WillMsg#mqtt_msg{retain = Retain};
init_will_msg({qos, QoS}, WillMsg) -> init_will_msg({qos, QoS}, WillMsg) ->
WillMsg#mqtt_message{qos = ?QOS_I(QoS)}. WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}.
init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) ->
Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), Size = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE),
@ -534,15 +540,16 @@ mqtt_connect(State = #state{client_id = ClientId,
will_flag = WillFlag, will_flag = WillFlag,
will_msg = WillMsg, will_msg = WillMsg,
properties = Properties}) -> properties = Properties}) ->
?WILL_MSG(WillQos, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg,
ConnProps = emqx_mqtt_properties:filter(?CONNECT, maps:to_list(Properties)), ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties),
io:format("ConnProps: ~p~n", [ConnProps]),
send(?CONNECT_PACKET( send(?CONNECT_PACKET(
#mqtt_packet_connect{proto_ver = ProtoVer, #mqtt_packet_connect{proto_ver = ProtoVer,
proto_name = ProtoName, proto_name = ProtoName,
is_bridge = IsBridge, is_bridge = IsBridge,
clean_start = CleanStart, clean_start = CleanStart,
will_flag = WillFlag, will_flag = WillFlag,
will_qos = WillQos, will_qos = WillQoS,
will_retain = WillRetain, will_retain = WillRetain,
keepalive = KeepAlive, keepalive = KeepAlive,
properties = ConnProps, properties = ConnProps,
@ -624,7 +631,7 @@ connected({call, From}, SubReq = {subscribe, Properties, Topics},
{stop_and_reply, Reason, [{reply, From, Error}]} {stop_and_reply, Reason, [{reply, From, Error}]}
end; end;
connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) -> connected({call, From}, {publish, Msg = #mqtt_msg{qos = ?QOS_0}}, State) ->
case send(Msg, State) of case send(Msg, State) of
{ok, NewState} -> {ok, NewState} ->
{keep_state, NewState, [{reply, From, ok}]}; {keep_state, NewState, [{reply, From, ok}]};
@ -632,14 +639,14 @@ connected({call, From}, {publish, Msg = #mqtt_message{qos = ?QOS_0}}, State) ->
{stop_and_reply, Reason, [{reply, From, Error}]} {stop_and_reply, Reason, [{reply, From, Error}]}
end; end;
connected({call, From}, {publish, Msg = #mqtt_message{qos = Qos}}, connected({call, From}, {publish, Msg = #mqtt_msg{qos = QoS}},
State = #state{inflight = Inflight, last_packet_id = PacketId}) State = #state{inflight = Inflight, last_packet_id = PacketId})
when (Qos =:= ?QOS_1); (Qos =:= ?QOS_2) -> when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) ->
case emqx_inflight:is_full(Inflight) of case emqx_inflight:is_full(Inflight) of
true -> true ->
{keep_state, State, [{reply, From, {error, inflight_full}}]}; {keep_state, State, [{reply, From, {error, inflight_full}}]};
false -> false ->
Msg1 = Msg#mqtt_message{packet_id = PacketId}, Msg1 = Msg#mqtt_msg{packet_id = PacketId},
case send(Msg1, State) of case send(Msg1, State) of
{ok, NewState} -> {ok, NewState} ->
Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg1, os:timestamp()}, Inflight), Inflight1 = emqx_inflight:insert(PacketId, {publish, Msg1, os:timestamp()}, Inflight),
@ -690,7 +697,7 @@ connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) ->
send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State);
connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) ->
{keep_state, deliver_msg(packet_to_msg(Packet), State)}; {keep_state, deliver(packet_to_msg(Packet), State)};
connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) -> connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true}) ->
{keep_state, State}; {keep_state, State};
@ -698,7 +705,7 @@ connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), State = #state{paused = true})
connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId),
State = #state{auto_ack = AutoAck}) -> State = #state{auto_ack = AutoAck}) ->
_ = deliver_msg(packet_to_msg(Packet), State), _ = deliver(packet_to_msg(Packet), State),
case AutoAck of case AutoAck of
true -> send_puback(?PUBACK_PACKET(PacketId), State); true -> send_puback(?PUBACK_PACKET(PacketId), State);
false -> {keep_state, State} false -> {keep_state, State}
@ -716,7 +723,7 @@ connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId),
connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties), connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties),
State = #state{owner = Owner, inflight = Inflight}) -> State = #state{owner = Owner, inflight = Inflight}) ->
case emqx_inflight:lookup(PacketId, Inflight) of case emqx_inflight:lookup(PacketId, Inflight) of
{value, {publish, #mqtt_message{packet_id = PacketId}, _Ts}} -> {value, {publish, #mqtt_msg{packet_id = PacketId}, _Ts}} ->
Owner ! {puback, #{packet_id => PacketId, Owner ! {puback, #{packet_id => PacketId,
reason_code => ReasonCode, reason_code => ReasonCode,
properties => Properties}}, properties => Properties}},
@ -745,8 +752,7 @@ connected(cast, ?PUBREL_PACKET(PacketId),
State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) ->
case maps:take(PacketId, AwaitingRel) of case maps:take(PacketId, AwaitingRel) of
{Packet, AwaitingRel1} -> {Packet, AwaitingRel1} ->
NewState = deliver_msg(packet_to_msg(Packet), NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}),
State#state{awaiting_rel = AwaitingRel1}),
case AutoAck of case AutoAck of
true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState);
false -> {keep_state, NewState} false -> {keep_state, NewState}
@ -960,9 +966,9 @@ retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interv
false -> {keep_state, ensure_retry_timer(Interval - Diff, State)} false -> {keep_state, ensure_retry_timer(Interval - Diff, State)}
end. end.
retry_send(publish, Msg = #mqtt_message{qos = QoS, packet_id = PacketId}, retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId},
Now, State = #state{inflight = Inflight}) -> Now, State = #state{inflight = Inflight}) ->
Msg1 = Msg#mqtt_message{dup = (QoS =:= ?QOS1)}, Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS1)},
case send(Msg1, State) of case send(Msg1, State) of
{ok, NewState} -> {ok, NewState} ->
Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight), Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight),
@ -979,42 +985,36 @@ retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) ->
Error Error
end. end.
deliver_msg(#mqtt_message{qos = QoS, deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId,
dup = Dup, topic = Topic, props = Props, payload = Payload},
retain = Retain,
topic = Topic,
packet_id = PacketId,
properties = Properties,
payload = Payload},
State = #state{owner = Owner}) -> State = #state{owner = Owner}) ->
Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId,
packet_id => PacketId, topic => Topic, topic => Topic, properties => Props, payload => Payload,
properties => Properties, payload => Payload}}, client_pid => self()}},
State. State.
packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) -> packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
#mqtt_packet_header{qos = QoS, retain = R, dup = Dup} = Header,
#mqtt_message{qos = QoS, retain = R, dup = Dup,
packet_id = PacketId, topic = Topic,
properties = Properties, payload = Payload}.
msg_to_packet(#mqtt_message{qos = Qos,
dup = Dup, dup = Dup,
retain = Retain, qos = QoS,
topic = Topic, retain = R},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId, packet_id = PacketId,
properties = Properties, properties = Props},
payload = Payload}) -> payload = Payload}) ->
#mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId,
topic = Topic, props = Props, payload = Payload}.
msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId,
topic = Topic, props = Props, payload = Payload}) ->
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = Qos, qos = QoS,
retain = Retain, retain = Retain,
dup = Dup}, dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic, variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId, packet_id = PacketId,
properties = Properties}, properties = Props},
payload = Payload}. payload = Payload}.
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Socket Connect/Send %% Socket Connect/Send
@ -1040,7 +1040,7 @@ send_puback(Packet, State) ->
{error, Reason} -> {stop, Reason} {error, Reason} -> {stop, Reason}
end. end.
send(Msg, State) when is_record(Msg, mqtt_message) -> send(Msg, State) when is_record(Msg, mqtt_msg) ->
send(msg_to_packet(Msg), State); send(msg_to_packet(Msg), State);
send(Packet, State = #state{socket = Sock, proto_ver = Ver}) send(Packet, State = #state{socket = Sock, proto_ver = Ver})

View File

@ -84,7 +84,7 @@ unregister_client(CObj = {ClientId, ClientPid}) when is_binary(ClientId), is_pid
%% @doc Lookup client pid %% @doc Lookup client pid
-spec(lookup_client_pid(client_id()) -> pid() | undefined). -spec(lookup_client_pid(client_id()) -> pid() | undefined).
lookup_client_pid(ClientId) when is_binary(ClientId) -> lookup_client_pid(ClientId) when is_binary(ClientId) ->
case lookup_client_pid(ClientId) of case ets:lookup(?CLIENT, ClientId) of
[] -> undefined; [] -> undefined;
[{_, Pid}] -> Pid [{_, Pid}] -> Pid
end. end.

View File

@ -33,15 +33,15 @@
-define(APP, emqx). -define(APP, emqx).
%% @doc Get environment
-spec(get_env(Key :: atom()) -> term() | undefined).
get_env(Key) ->
get_env(Key, undefined).
-spec(get_env(Key :: atom(), Default :: term()) -> term()). -spec(get_env(Key :: atom(), Default :: term()) -> term()).
get_env(Key, Default) -> get_env(Key, Default) ->
application:get_env(?APP, Key, Default). application:get_env(?APP, Key, Default).
%% @doc Get environment
-spec(get_env(Key :: atom()) -> {ok, any()} | undefined).
get_env(Key) ->
application:get_env(?APP, Key).
%% TODO: %% TODO:
populate(_App) -> populate(_App) ->
ok. ok.

View File

@ -17,37 +17,40 @@
-behaviour(gen_server). -behaviour(gen_server).
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("emqx_misc.hrl"). -include("emqx_misc.hrl").
-import(proplists, [get_value/2, get_value/3]).
%% API Function Exports
-export([start_link/3]). -export([start_link/3]).
-export([info/1, stats/1, kick/1]).
%% Management and Monitor API
-export([info/1, stats/1, kick/1, clean_acl_cache/2]).
-export([set_rate_limit/2, get_rate_limit/1]).
%% SUB/UNSUB Asynchronously. Called by plugins.
-export([subscribe/2, unsubscribe/2]).
%% Get the session proc?
-export([session/1]). -export([session/1]).
-export([clean_acl_cache/1]).
-export([get_rate_limit/1, set_rate_limit/2]).
-export([get_pub_limit/1, set_pub_limit/2]).
%% gen_server Function Exports %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, -export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3,
terminate/2]). terminate/2]).
%% Unused fields: connname, peerhost, peerport -record(state, {
-record(state, {transport, socket, peername, conn_state, await_recv, transport, %% Network transport module
rate_limit, max_packet_size, proto_state, parse_state, socket, %% TCP or SSL Socket
keepalive, enable_stats, idle_timeout, force_gc_count}). peername, %% Peername of the socket
sockname, %% Sockname of the socket
conn_state, %% Connection state: running | blocked
await_recv, %% Awaiting recv
incoming, %% Incoming bytes and packets
pub_limit, %% Publish rate limit
rate_limit, %% Traffic rate limit
limit_timer, %% Rate limit timer
proto_state, %% MQTT protocol state
parser_state, %% MQTT parser state
keepalive, %% MQTT keepalive timer
enable_stats, %% Enable stats
stats_timer, %% Stats timer
idle_timeout %% Connection idle timeout
}).
-define(INFO_KEYS, [peername, conn_state, await_recv]). -define(INFO_KEYS, [peername, sockname, conn_state, await_recv, rate_limit, pub_limit]).
-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]).
@ -55,8 +58,12 @@
emqx_logger:Level("Client(~s): " ++ Format, emqx_logger:Level("Client(~s): " ++ Format,
[esockd_net:format(State#state.peername) | Args])). [esockd_net:format(State#state.peername) | Args])).
start_link(Transport, Sock, Env) -> start_link(Transport, Socket, Options) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Env]])}. {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}.
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
info(CPid) -> info(CPid) ->
gen_server:call(CPid, info). gen_server:call(CPid, info).
@ -67,156 +74,141 @@ stats(CPid) ->
kick(CPid) -> kick(CPid) ->
gen_server:call(CPid, kick). gen_server:call(CPid, kick).
set_rate_limit(CPid, Rl) -> session(CPid) ->
gen_server:call(CPid, {set_rate_limit, Rl}). gen_server:call(CPid, session, infinity).
clean_acl_cache(CPid) ->
gen_server:call(CPid, clean_acl_cache).
get_rate_limit(CPid) -> get_rate_limit(CPid) ->
gen_server:call(CPid, get_rate_limit). gen_server:call(CPid, get_rate_limit).
subscribe(CPid, TopicTable) -> set_rate_limit(CPid, Rl = {_Rate, _Burst}) ->
CPid ! {subscribe, TopicTable}. gen_server:call(CPid, {set_rate_limit, Rl}).
unsubscribe(CPid, Topics) -> get_pub_limit(CPid) ->
CPid ! {unsubscribe, Topics}. gen_server:call(CPid, get_pub_limit).
session(CPid) -> set_pub_limit(CPid, Rl = {_Rate, _Burst}) ->
gen_server:call(CPid, session, infinity). gen_server:call(CPid, {set_pub_limit, Rl}).
clean_acl_cache(CPid, Topic) -> %%------------------------------------------------------------------------------
gen_server:call(CPid, {clean_acl_cache, Topic}).
%%--------------------------------------------------------------------
%% gen_server callbacks %% gen_server callbacks
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
init([Transport, Sock, Env]) -> init([Transport, RawSocket, Options]) ->
case Transport:wait(Sock) of case Transport:wait(RawSocket) of
{ok, NewSock} -> {ok, Socket} ->
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), Zone = proplists:get_value(zone, Options),
do_init(Transport, Sock, Peername, Env); {ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]),
{error, Reason} -> {ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]),
{stop, Reason} Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]),
end. PubLimit = rate_limit(emqx_zone:env(Zone, publish_limit)),
RateLimit = rate_limit(proplists:get_value(rate_limit, Options)),
do_init(Transport, Sock, Peername, Env) -> EnableStats = emqx_zone:env(Zone, enable_stats, true),
RateLimit = get_value(rate_limit, Env), IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000),
PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE), SendFun = send_fun(Transport, Socket, Peername),
SendFun = send_fun(Transport, Sock, Peername), ProtoState = emqx_protocol:init(#{peername => Peername,
ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Env), sockname => Sockname,
EnableStats = get_value(client_enable_stats, Env, false), peercert => Peercert,
IdleTimout = get_value(client_idle_timeout, Env, 30000), sendfun => SendFun}, Options),
ForceGcCount = emqx_gc:conn_max_gc_count(), ParserState = emqx_protocol:parser(ProtoState),
State = run_socket(#state{transport = Transport, State = run_socket(#state{transport = Transport,
socket = Sock, socket = Socket,
peername = Peername, peername = Peername,
await_recv = false, await_recv = false,
conn_state = running, conn_state = running,
rate_limit = RateLimit, rate_limit = RateLimit,
max_packet_size = PacketSize, pub_limit = PubLimit,
proto_state = ProtoState, proto_state = ProtoState,
parser_state = ParserState,
enable_stats = EnableStats, enable_stats = EnableStats,
idle_timeout = IdleTimout, idle_timeout = IdleTimout}),
force_gc_count = ForceGcCount}),
gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}],
init_parse_state(State), self(), IdleTimout). State, self(), IdleTimout);
{error, Reason} ->
{stop, Reason}
end.
send_fun(Transport, Sock, Peername) -> rate_limit(undefined) ->
Self = self(), undefined;
fun(Packet) -> rate_limit({Rate, Burst}) ->
Data = emqx_frame:serialize(Packet), esockd_rate_limit:new(Rate, Burst).
send_fun(Transport, Socket, Peername) ->
fun(Data) ->
try Transport:async_send(Socket, Data) of
ok ->
?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}), ?LOG(debug, "SEND ~p", [Data], #state{peername = Peername}),
emqx_metrics:inc('bytes/sent', iolist_size(Data)), emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok;
try Transport:async_send(Sock, Data) of Error -> Error
ok -> ok;
{error, Reason} -> Self ! {shutdown, Reason}
catch catch
error:Error -> Self ! {shutdown, Error} error:Error -> {error, Error}
end end
end. end.
init_parse_state(State = #state{max_packet_size = Size, proto_state = ProtoState}) -> handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) ->
Version = emqx_protocol:get(proto_ver, ProtoState),
State#state{parse_state = emqx_frame:initial_state(
#{max_packet_size => Size, version => Version})}.
handle_call(info, From, State = #state{proto_state = ProtoState}) ->
ProtoInfo = emqx_protocol:info(ProtoState), ProtoInfo = emqx_protocol:info(ProtoState),
ClientInfo = ?record_to_proplist(state, State, ?INFO_KEYS), ConnInfo = [{socktype, Transport:type(Socket)} | ?record_to_proplist(state, State, ?INFO_KEYS)],
{reply, Stats, _, _} = handle_call(stats, From, State), StatsInfo = element(2, handle_call(stats, From, State)),
reply(lists:append([ClientInfo, ProtoInfo, Stats]), State); {reply, lists:append([ConnInfo, StatsInfo, ProtoInfo]), State};
handle_call(stats, _From, State = #state{proto_state = ProtoState}) -> handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, proto_state = ProtoState}) ->
reply(lists:append([emqx_misc:proc_stats(), ProcStats = emqx_misc:proc_stats(),
emqx_protocol:stats(ProtoState), ProtoStats = emqx_protocol:stats(ProtoState),
sock_stats(State)]), State); SockStats = case Transport:getstat(Sock, ?SOCK_STATS) of
{ok, Ss} -> Ss;
{error, _} -> []
end,
{reply, lists:append([ProcStats, ProtoStats, SockStats]), State};
handle_call(kick, _From, State) -> handle_call(kick, _From, State) ->
{stop, {shutdown, kick}, ok, State}; {stop, {shutdown, kick}, ok, State};
handle_call({set_rate_limit, Rl}, _From, State) -> handle_call(session, _From, State = #state{proto_state = ProtoState}) ->
reply(ok, State#state{rate_limit = Rl}); {reply, emqx_protocol:session(ProtoState), State};
handle_call(clean_acl_cache, _From, State = #state{proto_state = ProtoState}) ->
{reply, ok, State#state{proto_state = emqx_protocol:clean_acl_cache(ProtoState)}};
handle_call(get_rate_limit, _From, State = #state{rate_limit = Rl}) -> handle_call(get_rate_limit, _From, State = #state{rate_limit = Rl}) ->
reply(Rl, State); {reply, esockd_rate_limit:info(Rl), State};
handle_call(session, _From, State = #state{proto_state = ProtoState}) -> handle_call({set_rate_limit, {Rate, Burst}}, _From, State) ->
reply(emqx_protocol:session(ProtoState), State); {reply, ok, State#state{rate_limit = esockd_rate_limit:new(Rate, Burst)}};
handle_call({clean_acl_cache, Topic}, _From, State) -> handle_call(get_publish_limit, _From, State = #state{pub_limit = Rl}) ->
erase({acl, publish, Topic}), {reply, esockd_rate_limit:info(Rl), State};
reply(ok, State);
handle_call({set_publish_limit, {Rate, Burst}}, _From, State) ->
{reply, ok, State#state{pub_limit = esockd_rate_limit:new(Rate, Burst)}};
handle_call(Req, _From, State) -> handle_call(Req, _From, State) ->
?LOG(error, "Unexpected Call: ~p", [Req], State), ?LOG(error, "unexpected call: ~p", [Req], State),
{reply, ignore, State}. {reply, ignored, State}.
handle_cast(Msg, State) -> handle_cast(Msg, State) ->
?LOG(error, "Unexpected Cast: ~p", [Msg], State), ?LOG(error, "unexpected cast: ~p", [Msg], State),
{noreply, State}. {noreply, State}.
handle_info({subscribe, TopicTable}, State) -> handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) ->
with_proto( case emqx_protocol:deliver(PubOrAck, ProtoState) of
fun(ProtoState) -> {ok, ProtoState1} ->
emqx_protocol:subscribe(TopicTable, ProtoState) {noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))};
end, State); {error, Reason} ->
shutdown(Reason, State);
{error, Reason, ProtoState1} ->
shutdown(Reason, State#state{proto_state = ProtoState1})
end;
handle_info({unsubscribe, Topics}, State) -> handle_info(emit_stats, State = #state{proto_state = ProtoState}) ->
with_proto( Stats = element(2, handle_call(stats, undefined, State)),
fun(ProtoState) -> emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats),
emqx_protocol:unsubscribe(Topics, ProtoState) {noreply, State#state{stats_timer = undefined}, hibernate};
end, State);
%% Asynchronous SUBACK
handle_info({suback, PacketId, GrantedQos}, State) ->
with_proto(
fun(ProtoState) ->
Packet = ?SUBACK_PACKET(PacketId, GrantedQos),
emqx_protocol:send(Packet, ProtoState)
end, State);
%% Fastlane
handle_info({dispatch, _Topic, Msg}, State) ->
handle_info({deliver, emqx_message:set_flag(qos, ?QOS_0, Msg)}, State);
handle_info({deliver, Message}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:send(Message, ProtoState)
end, State);
handle_info({redeliver, {?PUBREL, PacketId}}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:pubrel(PacketId, ProtoState)
end, State);
handle_info(emit_stats, State) ->
{noreply, emit_stats(State), hibernate};
handle_info(timeout, State) -> handle_info(timeout, State) ->
shutdown(idle_timeout, State); shutdown(idle_timeout, State);
%% Fix issue #535
handle_info({shutdown, Error}, State) -> handle_info({shutdown, Error}, State) ->
shutdown(Error, State); shutdown(Error, State);
@ -225,25 +217,25 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
shutdown(conflict, State); shutdown(conflict, State);
handle_info(activate_sock, State) -> handle_info(activate_sock, State) ->
{noreply, run_socket(State#state{conn_state = running})}; {noreply, run_socket(State#state{conn_state = running, limit_timer = undefined})};
handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) -> handle_info({inet_async, _Sock, _Ref, {ok, Data}}, State) ->
Size = iolist_size(Data), Size = iolist_size(Data),
?LOG(debug, "RECV ~p", [Data], State), ?LOG(debug, "RECV ~p", [Data], State),
emqx_metrics:inc('bytes/received', Size), emqx_metrics:inc('bytes/received', Size),
received(Data, rate_limit(Size, State#state{await_recv = false})); Incoming = #{bytes => Size, packets => 0},
handle_packet(Data, State#state{await_recv = false, incoming = Incoming});
handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) -> handle_info({inet_async, _Sock, _Ref, {error, Reason}}, State) ->
shutdown(Reason, State); shutdown(Reason, State);
handle_info({inet_reply, _Sock, ok}, State) -> handle_info({inet_reply, _Sock, ok}, State) ->
{noreply, gc(State)}; %% Tune GC {noreply, State};
handle_info({inet_reply, _Sock, {error, Reason}}, State) -> handle_info({inet_reply, _Sock, {error, Reason}}, State) ->
shutdown(Reason, State); shutdown(Reason, State);
handle_info({keepalive, start, Interval}, handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Sock}) ->
State = #state{transport = Transport, socket = Sock}) ->
?LOG(debug, "Keepalive at the interval of ~p", [Interval], State), ?LOG(debug, "Keepalive at the interval of ~p", [Interval], State),
StatFun = fun() -> StatFun = fun() ->
case Transport:getstat(Sock, [recv_oct]) of case Transport:getstat(Sock, [recv_oct]) of
@ -272,20 +264,18 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) ->
end; end;
handle_info(Info, State) -> handle_info(Info, State) ->
?LOG(error, "Unexpected Info: ~p", [Info], State), ?LOG(error, "unexpected info: ~p", [Info], State),
{noreply, State}. {noreply, State}.
terminate(Reason, State = #state{transport = Transport, terminate(Reason, State = #state{transport = Transport,
socket = Sock, socket = Sock,
keepalive = KeepAlive, keepalive = KeepAlive,
proto_state = ProtoState}) -> proto_state = ProtoState}) ->
?LOG(debug, "Terminated for ~p", [Reason], State), ?LOG(debug, "Terminated for ~p", [Reason], State),
Transport:fast_close(Sock), Transport:fast_close(Sock),
emqx_keepalive:cancel(KeepAlive), emqx_keepalive:cancel(KeepAlive),
case {ProtoState, Reason} of case {ProtoState, Reason} of
{undefined, _} -> {undefined, _} -> ok;
ok;
{_, {shutdown, Error}} -> {_, {shutdown, Error}} ->
emqx_protocol:shutdown(Error, ProtoState); emqx_protocol:shutdown(Error, ProtoState);
{_, Reason} -> {_, Reason} ->
@ -295,25 +285,29 @@ terminate(Reason, State = #state{transport = Transport,
code_change(_OldVsn, State, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Receive and Parse TCP Data %% Receive and parse data
received(<<>>, State) -> handle_packet(<<>>, State) ->
{noreply, gc(State)}; {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))};
received(Bytes, State = #state{parse_state = ParseState, handle_packet(Bytes, State = #state{incoming = Incoming,
parser_state = ParserState,
proto_state = ProtoState, proto_state = ProtoState,
idle_timeout = IdleTimeout}) -> idle_timeout = IdleTimeout}) ->
case catch emqx_frame:parse(Bytes, ParseState) of case catch emqx_frame:parse(Bytes, ParserState) of
{more, NewParseState} -> {more, NewParserState} ->
{noreply, State#state{parse_state = NewParseState}, IdleTimeout}; {noreply, State#state{parser_state = NewParserState}, IdleTimeout};
{ok, Packet, Rest} -> {ok, Packet = ?PACKET(Type), Rest} ->
emqx_metrics:received(Packet), emqx_metrics:received(Packet),
case emqx_protocol:received(Packet, ProtoState) of case emqx_protocol:received(Packet, ProtoState) of
{ok, ProtoState1} -> {ok, ProtoState1} ->
received(Rest, init_parse_state(State#state{proto_state = ProtoState1})); ParserState1 = emqx_protocol:parser(ProtoState1),
handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming),
proto_state = ProtoState1,
parser_state = ParserState1});
{error, Error} -> {error, Error} ->
?LOG(error, "Protocol error - ~p", [Error], State), ?LOG(error, "Protocol error - ~p", [Error], State),
shutdown(Error, State); shutdown(Error, State);
@ -326,21 +320,32 @@ received(Bytes, State = #state{parse_state = ParseState,
?LOG(error, "Framing error - ~p", [Error], State), ?LOG(error, "Framing error - ~p", [Error], State),
shutdown(Error, State); shutdown(Error, State);
{'EXIT', Reason} -> {'EXIT', Reason} ->
?LOG(error, "Parser failed for ~p", [Reason], State), ?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Bytes], State),
?LOG(error, "Error data: ~p", [Bytes], State), shutdown(parse_error, State)
shutdown(parser_error, State)
end. end.
rate_limit(_Size, State = #state{rate_limit = undefined}) -> count_packets(?PUBLISH, Incoming = #{packets := Num}) ->
Incoming#{packets := Num + 1};
count_packets(?SUBSCRIBE, Incoming = #{packets := Num}) ->
Incoming#{packets := Num + 1};
count_packets(_Type, Incoming) ->
Incoming.
ensure_rate_limit(State = #state{rate_limit = Rl, pub_limit = Pl,
incoming = #{bytes := Bytes, packets := Pkts}}) ->
ensure_rate_limit([{Pl, #state.pub_limit, Pkts}, {Rl, #state.rate_limit, Bytes}], State).
ensure_rate_limit([], State) ->
run_socket(State); run_socket(State);
rate_limit(Size, State = #state{rate_limit = Rl}) -> ensure_rate_limit([{undefined, _Pos, _Num}|Limiters], State) ->
case Rl:check(Size) of ensure_rate_limit(Limiters, State);
ensure_rate_limit([{Rl, Pos, Num}|Limiters], State) ->
case esockd_rate_limit:check(Num, Rl) of
{0, Rl1} -> {0, Rl1} ->
run_socket(State#state{conn_state = running, rate_limit = Rl1}); ensure_rate_limit(Limiters, setelement(Pos, State, Rl1));
{Pause, Rl1} -> {Pause, Rl1} ->
?LOG(warning, "Rate limiter pause for ~p", [Pause], State), TRef = erlang:send_after(Pause, self(), activate_sock),
erlang:send_after(Pause, self(), activate_sock), setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1)
State#state{conn_state = blocked, rate_limit = Rl1}
end. end.
run_socket(State = #state{conn_state = blocked}) -> run_socket(State = #state{conn_state = blocked}) ->
@ -351,38 +356,21 @@ run_socket(State = #state{transport = Transport, socket = Sock}) ->
Transport:async_recv(Sock, 0, infinity), Transport:async_recv(Sock, 0, infinity),
State#state{await_recv = true}. State#state{await_recv = true}.
with_proto(Fun, State = #state{proto_state = ProtoState}) -> ensure_stats_timer(State = #state{enable_stats = true,
{ok, ProtoState1} = Fun(ProtoState), stats_timer = undefined,
{noreply, State#state{proto_state = ProtoState1}}. idle_timeout = IdleTimeout}) ->
State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)};
emit_stats(State = #state{proto_state = ProtoState}) -> ensure_stats_timer(State) ->
emit_stats(emqx_protocol:clientid(ProtoState), State).
emit_stats(_ClientId, State = #state{enable_stats = false}) ->
State;
emit_stats(undefined, State) ->
State;
emit_stats(ClientId, State) ->
{reply, Stats, _, _} = handle_call(stats, undefined, State),
emqx_cm:set_client_stats(ClientId, Stats),
State. State.
sock_stats(#state{transport = Transport, socket = Sock}) ->
case Transport:getstat(Sock, ?SOCK_STATS) of
{ok, Ss} -> Ss;
_Error -> []
end.
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
shutdown(Reason, State) -> shutdown(Reason, State) ->
stop({shutdown, Reason}, State). stop({shutdown, Reason}, State).
stop(Reason, State) -> stop(Reason, State) ->
{stop, Reason, State}. {stop, Reason, State}.
gc(State = #state{transport = Transport, socket = Sock}) -> maybe_gc(State) ->
Cb = fun() -> Transport:gc(Sock), emit_stats(State) end, State. %% TODO:...
emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb). %%Cb = fun() -> Transport:gc(Sock), end,
%%emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb).

View File

@ -18,7 +18,7 @@
-export([start_link/0]). -export([start_link/0]).
-export([register_command/2, register_command/3, unregister_command/1]). -export([register_command/2, register_command/3, unregister_command/1]).
-export([run_command/2, lookup_command/1]). -export([run_command/1, run_command/2, lookup_command/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]). code_change/3]).
@ -48,7 +48,21 @@ unregister_command(Cmd) when is_atom(Cmd) ->
cast(Msg) -> cast(Msg) ->
gen_server:cast(?SERVER, Msg). gen_server:cast(?SERVER, Msg).
run_command([]) ->
run_command(help, []);
run_command([Cmd | Args]) ->
run_command(list_to_atom(Cmd), Args).
-spec(run_command(cmd(), [string()]) -> ok | {error, term()}). -spec(run_command(cmd(), [string()]) -> ok | {error, term()}).
run_command(set, []) ->
emqx_mgmt_cli_cfg:set_usage(), ok;
run_command(set, Args) ->
emqx_mgmt_cli_cfg:run(["config" | Args]), ok;
run_command(show, Args) ->
emqx_mgmt_cli_cfg:run(["config" | Args]), ok;
run_command(help, []) -> run_command(help, []) ->
usage(); usage();
run_command(Cmd, Args) when is_atom(Cmd) -> run_command(Cmd, Args) when is_atom(Cmd) ->

View File

@ -121,7 +121,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) ->
<<UsernameFlag : 1, <<UsernameFlag : 1,
PasswordFlag : 1, PasswordFlag : 1,
WillRetain : 1, WillRetain : 1,
WillQos : 2, WillQoS : 2,
WillFlag : 1, WillFlag : 1,
CleanStart : 1, CleanStart : 1,
_Reserved : 1, _Reserved : 1,
@ -138,7 +138,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) ->
is_bridge = (BridgeTag =:= 8), is_bridge = (BridgeTag =:= 8),
clean_start = bool(CleanStart), clean_start = bool(CleanStart),
will_flag = bool(WillFlag), will_flag = bool(WillFlag),
will_qos = WillQos, will_qos = WillQoS,
will_retain = bool(WillRetain), will_retain = bool(WillRetain),
keepalive = KeepAlive, keepalive = KeepAlive,
properties = Properties, properties = Properties,
@ -162,7 +162,6 @@ parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin,
?QOS_0 -> {undefined, Rest}; ?QOS_0 -> {undefined, Rest};
_ -> parse_packet_id(Rest) _ -> parse_packet_id(Rest)
end, end,
io:format("Rest1: ~p~n", [Rest1]),
{Properties, Payload} = parse_properties(Rest1, Ver), {Properties, Payload} = parse_properties(Rest1, Ver),
{#mqtt_packet_publish{topic_name = TopicName, {#mqtt_packet_publish{topic_name = TopicName,
packet_id = PacketId, packet_id = PacketId,
@ -242,6 +241,9 @@ parse_packet_id(<<PacketId:16/big, Rest/binary>>) ->
parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 -> parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 ->
{undefined, Bin}; {undefined, Bin};
%% TODO: version mess?
parse_properties(<<>>, ?MQTT_PROTO_V5) ->
{#{}, <<>>};
parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) -> parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) ->
{#{}, Rest}; {#{}, Rest};
parse_properties(Bin, ?MQTT_PROTO_V5) -> parse_properties(Bin, ?MQTT_PROTO_V5) ->
@ -382,7 +384,7 @@ serialize_variable(#mqtt_packet_connect{
is_bridge = IsBridge, is_bridge = IsBridge,
clean_start = CleanStart, clean_start = CleanStart,
will_flag = WillFlag, will_flag = WillFlag,
will_qos = WillQos, will_qos = WillQoS,
will_retain = WillRetain, will_retain = WillRetain,
keepalive = KeepAlive, keepalive = KeepAlive,
properties = Properties, properties = Properties,
@ -400,7 +402,7 @@ serialize_variable(#mqtt_packet_connect{
(flag(Username)):1, (flag(Username)):1,
(flag(Password)):1, (flag(Password)):1,
(flag(WillRetain)):1, (flag(WillRetain)):1,
WillQos:2, WillQoS:2,
(flag(WillFlag)):1, (flag(WillFlag)):1,
(flag(CleanStart)):1, (flag(CleanStart)):1,
0:1, 0:1,

View File

@ -27,8 +27,8 @@
-spec(conn_max_gc_count() -> integer()). -spec(conn_max_gc_count() -> integer()).
conn_max_gc_count() -> conn_max_gc_count() ->
case emqx_config:get_env(conn_force_gc_count) of case emqx_config:get_env(conn_force_gc_count) of
{ok, I} when I > 0 -> I + rand:uniform(I); I when is_integer(I), I > 0 -> I + rand:uniform(I);
{ok, I} when I =< 0 -> undefined; I when is_integer(I), I =< 0 -> undefined;
undefined -> undefined undefined -> undefined
end. end.

View File

@ -129,5 +129,6 @@ to_base62(<<I:128>>) ->
emqx_base62:encode(I). emqx_base62:encode(I).
from_base62(S) -> from_base62(S) ->
I = emqx_base62:decode(S), <<I:128>>. I = emqx_base62:decode(S, integer),
<<I:128>>.

View File

@ -26,11 +26,12 @@ start_link() ->
init([]) -> init([]) ->
{ok, {{one_for_one, 10, 100}, {ok, {{one_for_one, 10, 100},
[child_spec(emqx_pool, supervisor), [child_spec(emqx_pool, supervisor),
child_spec(emqx_alarm, worker), child_spec(emqx_alarm_mgr, worker),
child_spec(emqx_hooks, worker), child_spec(emqx_hooks, worker),
child_spec(emqx_stats, worker), child_spec(emqx_stats, worker),
child_spec(emqx_metrics, worker), child_spec(emqx_metrics, worker),
child_spec(emqx_ctl, worker), child_spec(emqx_ctl, worker),
child_spec(emqx_zone, worker),
child_spec(emqx_tracer, worker)]}}. child_spec(emqx_tracer, worker)]}}.
child_spec(M, worker) -> child_spec(M, worker) ->

View File

@ -1,84 +0,0 @@
%% Copyright (c) 2018 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_lager_backend).
-behaviour(gen_event).
-include_lib("lager/include/lager.hrl").
-export([init/1, handle_call/2, handle_event/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {level :: {'mask', integer()},
formatter :: atom(),
format_config :: any()}).
-define(DEFAULT_FORMAT, [time, " ", pid, " [",severity, "] ", message]).
init([Level]) when is_atom(Level) ->
init(Level);
init(Level) when is_atom(Level) ->
init([Level,{lager_default_formatter, ?DEFAULT_FORMAT}]);
init([Level,{Formatter, FormatterConfig}]) when is_atom(Formatter) ->
Levels = lager_util:config_to_mask(Level),
{ok, #state{level = Levels, formatter = Formatter,
format_config = FormatterConfig}}.
handle_call(get_loglevel, #state{level = Level} = State) ->
{ok, Level, State};
handle_call({set_loglevel, Level}, State) ->
try lager_util:config_to_mask(Level) of
Levels -> {ok, ok, State#state{level = Levels}}
catch
_:_ -> {ok, {error, bad_log_level}, State}
end;
handle_call(_Request, State) ->
{ok, ok, State}.
handle_event({log, Message}, State = #state{level = L}) ->
case lager_util:is_loggable(Message, L, ?MODULE) of
true ->
publish_log(Message, State);
false ->
{ok, State}
end;
handle_event(_Event, State) ->
{ok, State}.
handle_info(_Info, State) ->
{ok, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
publish_log(Message, State = #state{formatter = Formatter,
format_config = FormatConfig}) ->
Severity = lager_msg:severity(Message),
Payload = Formatter:format(Message, FormatConfig),
Msg = emqx_message:make(log, topic(Severity), iolist_to_binary(Payload)),
emqx:publish(emqx_message:set_flag(sys, Msg)),
{ok, State}.
topic(Severity) ->
emqx_topic:systop(list_to_binary(lists:concat(['logs/', Severity]))).

View File

@ -12,76 +12,98 @@
%% See the License for the specific language governing permissions and %% See the License for the specific language governing permissions and
%% limitations under the License. %% limitations under the License.
%% @doc start/stop MQTT listeners. %% @doc Start/Stop MQTT listeners.
-module(emqx_listeners). -module(emqx_listeners).
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-export([start_all/0, restart_all/0, stop_all/0]). -export([start/0, restart/0, stop/0]).
-export([start_listener/1, stop_listener/1, restart_listener/1]). -export([start_listener/1, stop_listener/1, restart_listener/1]).
-type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}). -type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}).
%% @doc Start all listeners %% @doc Start all listeners
-spec(start_all() -> ok). -spec(start() -> ok).
start_all() -> start() ->
lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])). lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])).
%% Start MQTT/TCP listener %% Start MQTT/TCP listener
-spec(start_listener(listener()) -> {ok, pid()} | {error, term()}). -spec(start_listener(listener()) -> {ok, pid()} | {error, term()}).
start_listener({tcp, ListenOn, Options}) -> start_listener({tcp, ListenOn, Options}) ->
start_mqtt_listener('mqtt:tcp', ListenOn, Options); start_mqtt_listener('mqtt:tcp', ListenOn, Options);
%% Start MQTT/TLS listener %% Start MQTT/TLS listener
start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls -> start_listener({Proto, ListenOn, Options}) when Proto == ssl; Proto == tls ->
start_mqtt_listener('mqtt:tls', ListenOn, Options); start_mqtt_listener('mqtt:ssl', ListenOn, Options);
%% Start MQTT/WS listener %% Start MQTT/WS listener
start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws -> start_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws ->
start_http_listener('mqtt:ws', ListenOn, Options); Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws_connection, Options}]}]),
NumAcceptors = proplists:get_value(acceptors, Options, 4),
MaxConnections = proplists:get_value(max_connections, Options, 1024),
TcpOptions = proplists:get_value(tcp_options, Options, []),
RanchOpts = [{num_acceptors, NumAcceptors},
{max_connections, MaxConnections} | TcpOptions],
cowboy:start_clear('mqtt:ws', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}});
%% Start MQTT/WSS listener %% Start MQTT/WSS listener
start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss -> start_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss ->
start_http_listener('mqtt:wss', ListenOn, Options). Dispatch = cowboy_router:compile([{'_', [{"/mqtt", emqx_ws, []}]}]),
NumAcceptors = proplists:get_value(acceptors, Options, 4),
MaxConnections = proplists:get_value(max_clients, Options, 1024),
TcpOptions = proplists:get_value(tcp_options, Options, []),
SslOptions = proplists:get_value(ssl_options, Options, []),
RanchOpts = [{num_acceptors, NumAcceptors},
{max_connections, MaxConnections} | TcpOptions ++ SslOptions],
cowboy:start_tls('mqtt:wss', with_port(ListenOn, RanchOpts), #{env => #{dispatch => Dispatch}}).
start_mqtt_listener(Name, ListenOn, Options) -> start_mqtt_listener(Name, ListenOn, Options) ->
{ok, _} = esockd:open(Name, ListenOn, merge_sockopts(Options), {emqx_connection, start_link, []}). SockOpts = esockd:parse_opt(Options),
MFA = {emqx_connection, start_link, [Options -- SockOpts]},
{ok, _} = esockd:open(Name, ListenOn, merge_default(SockOpts), MFA).
start_http_listener(Name, ListenOn, Options) -> with_port(Port, Opts) when is_integer(Port) ->
{ok, _} = mochiweb:start_http(Name, ListenOn, Options, {emqx_ws, handle_request, []}). [{port, Port}|Opts];
with_port({Addr, Port}, Opts) ->
[{ip, Addr}, {port, Port}|Opts].
%% @doc Restart all listeners %% @doc Restart all listeners
-spec(restart_all() -> ok). -spec(restart() -> ok).
restart_all() -> restart() ->
lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])). lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])).
-spec(restart_listener(listener()) -> any()). -spec(restart_listener(listener()) -> any()).
restart_listener({tcp, ListenOn, _Opts}) -> restart_listener({tcp, ListenOn, _Options}) ->
esockd:reopen('mqtt:tcp', ListenOn); esockd:reopen('mqtt:tcp', ListenOn);
restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> restart_listener({Proto, ListenOn, _Options}) when Proto == ssl; Proto == tls ->
esockd:reopen('mqtt:tls', ListenOn); esockd:reopen('mqtt:ssl', ListenOn);
restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> restart_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws ->
mochiweb:restart_http('mqtt:ws', ListenOn); cowboy:stop_listener('mqtt:ws'),
restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> start_listener({Proto, ListenOn, Options});
mochiweb:restart_http('mqtt:wss', ListenOn); restart_listener({Proto, ListenOn, Options}) when Proto == https; Proto == wss ->
cowboy:stop_listener('mqtt:wss'),
start_listener({Proto, ListenOn, Options});
restart_listener({Proto, ListenOn, _Opts}) -> restart_listener({Proto, ListenOn, _Opts}) ->
esockd:reopen(Proto, ListenOn). esockd:reopen(Proto, ListenOn).
%% @doc Stop all listeners %% @doc Stop all listeners
-spec(stop_all() -> ok). -spec(stop() -> ok).
stop_all() -> stop() ->
lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])). lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])).
-spec(stop_listener(listener()) -> ok | {error, any()}). -spec(stop_listener(listener()) -> ok | {error, any()}).
stop_listener({tcp, ListenOn, _Opts}) -> stop_listener({tcp, ListenOn, _Opts}) ->
esockd:close('mqtt:tcp', ListenOn); esockd:close('mqtt:tcp', ListenOn);
stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls -> stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls ->
esockd:close('mqtt:tls', ListenOn); esockd:close('mqtt:ssl', ListenOn);
stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws -> stop_listener({Proto, _ListenOn, _Opts}) when Proto == http; Proto == ws ->
mochiweb:stop_http('mqtt:ws', ListenOn); cowboy:stop_listener('mqtt:ws');
stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss -> stop_listener({Proto, _ListenOn, _Opts}) when Proto == https; Proto == wss ->
mochiweb:stop_http('mqtt:wss', ListenOn); cowboy:stop_listener('mqtt:wss');
stop_listener({Proto, ListenOn, _Opts}) -> stop_listener({Proto, ListenOn, _Opts}) ->
esockd:close(Proto, ListenOn). esockd:close(Proto, ListenOn).
merge_sockopts(Options) -> merge_default(Options) ->
case lists:keytake(tcp_options, 1, Options) of case lists:keytake(tcp_options, 1, Options) of
{value, {tcp_options, TcpOpts}, Options1} -> {value, {tcp_options, TcpOpts}, Options1} ->
[{tcp_options, emqx_misc:merge_opts(?MQTT_SOCKOPTS, TcpOpts)} | Options1]; [{tcp_options, emqx_misc:merge_opts(?MQTT_SOCKOPTS, TcpOpts)} | Options1];
@ -89,11 +111,3 @@ merge_sockopts(Options) ->
[{tcp_options, ?MQTT_SOCKOPTS} | Options] [{tcp_options, ?MQTT_SOCKOPTS} | Options]
end. end.
%% all() ->
%% [Listener || Listener = {{Proto, _}, _Pid} <- esockd:listeners(), is_mqtt(Proto)].
%%is_mqtt('mqtt:tcp') -> true;
%%is_mqtt('mqtt:tls') -> true;
%%is_mqtt('mqtt:ws') -> true;
%%is_mqtt('mqtt:wss') -> true;
%%is_mqtt(_Proto) -> false.

View File

@ -17,44 +17,43 @@
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-export([new/2, new/3, new/4, new/5]). -export([make/2, make/3, make/4]).
-export([set_flags/2]).
-export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]). -export([get_flag/2, get_flag/3, set_flag/2, set_flag/3, unset_flag/2]).
-export([set_headers/2]).
-export([get_header/2, get_header/3, set_header/3]). -export([get_header/2, get_header/3, set_header/3]).
-export([get_user_property/2, get_user_property/3, set_user_property/3]).
-spec(new(topic(), payload()) -> message()). -spec(make(topic(), payload()) -> message()).
new(Topic, Payload) -> make(Topic, Payload) ->
new(undefined, Topic, Payload). make(undefined, Topic, Payload).
-spec(new(atom() | client(), topic(), payload()) -> message()). -spec(make(atom() | client_id(), topic(), payload()) -> message()).
new(From, Topic, Payload) when is_atom(From); is_record(From, client) -> make(From, Topic, Payload) ->
new(From, #{qos => ?QOS0}, Topic, Payload). make(From, ?QOS0, Topic, Payload).
-spec(new(atom() | client(), message_flags(), topic(), payload()) -> message()). -spec(make(atom() | client_id(), qos(), topic(), payload()) -> message()).
new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) -> make(From, QoS, Topic, Payload) ->
new(From, Flags, #{}, Topic, Payload). #message{id = msgid(QoS),
qos = QoS,
-spec(new(atom() | client(), message_flags(), message_headers(), topic(), payload()) -> message()).
new(From, Flags, Headers, Topic, Payload) when is_atom(From); is_record(From, client) ->
#message{id = msgid(),
from = From, from = From,
sender = self(), flags = #{dup => false},
flags = Flags,
headers = Headers,
topic = Topic, topic = Topic,
properties = #{},
payload = Payload, payload = Payload,
timestamp = os:timestamp()}. timestamp = os:timestamp()}.
msgid() -> emqx_guid:gen(). msgid(?QOS0) -> undefined;
msgid(_QoS) -> emqx_guid:gen().
set_flags(Flags, Msg = #message{flags = undefined}) when is_map(Flags) ->
Msg#message{flags = Flags};
set_flags(New, Msg = #message{flags = Old}) when is_map(New) ->
Msg#message{flags = maps:merge(Old, New)}.
%% @doc Get flag
get_flag(Flag, Msg) -> get_flag(Flag, Msg) ->
get_flag(Flag, Msg, false). get_flag(Flag, Msg, false).
get_flag(Flag, #message{flags = Flags}, Default) -> get_flag(Flag, #message{flags = Flags}, Default) ->
maps:get(Flag, Flags, Default). maps:get(Flag, Flags, Default).
%% @doc Set flag
-spec(set_flag(message_flag(), message()) -> message()). -spec(set_flag(message_flag(), message()) -> message()).
set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) -> set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) ->
Msg#message{flags = maps:put(Flag, true, Flags)}. Msg#message{flags = maps:put(Flag, true, Flags)}.
@ -63,27 +62,24 @@ set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) ->
set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) -> set_flag(Flag, Val, Msg = #message{flags = Flags}) when is_atom(Flag) ->
Msg#message{flags = maps:put(Flag, Val, Flags)}. Msg#message{flags = maps:put(Flag, Val, Flags)}.
%% @doc Unset flag
-spec(unset_flag(message_flag(), message()) -> message()). -spec(unset_flag(message_flag(), message()) -> message()).
unset_flag(Flag, Msg = #message{flags = Flags}) -> unset_flag(Flag, Msg = #message{flags = Flags}) ->
Msg#message{flags = maps:remove(Flag, Flags)}. Msg#message{flags = maps:remove(Flag, Flags)}.
%% @doc Get header set_headers(Headers, Msg = #message{headers = undefined}) when is_map(Headers) ->
Msg#message{headers = Headers};
set_headers(New, Msg = #message{headers = Old}) when is_map(New) ->
Msg#message{headers = maps:merge(Old, New)};
set_headers(_, Msg) ->
Msg.
get_header(Hdr, Msg) -> get_header(Hdr, Msg) ->
get_header(Hdr, Msg, undefined). get_header(Hdr, Msg, undefined).
get_header(Hdr, #message{headers = Headers}, Default) -> get_header(Hdr, #message{headers = Headers}, Default) ->
maps:get(Hdr, Headers, Default). maps:get(Hdr, Headers, Default).
%% @doc Set header set_header(Hdr, Val, Msg = #message{headers = undefined}) ->
Msg#message{headers = #{Hdr => Val}};
set_header(Hdr, Val, Msg = #message{headers = Headers}) -> set_header(Hdr, Val, Msg = #message{headers = Headers}) ->
Msg#message{headers = maps:put(Hdr, Val, Headers)}. Msg#message{headers = maps:put(Hdr, Val, Headers)}.
%% @doc Get user property
get_user_property(Key, Msg) ->
get_user_property(Key, Msg, undefined).
get_user_property(Key, #message{properties = Props}, Default) ->
maps:get(Key, Props, Default).
set_user_property(Key, Val, Msg = #message{properties = Props}) ->
Msg#message{properties = maps:put(Key, Val, Props)}.

View File

@ -171,10 +171,10 @@ update_counter(Key, UpOp) ->
received(Packet) -> received(Packet) ->
inc('packets/received'), inc('packets/received'),
received1(Packet). received1(Packet).
received1(?PUBLISH_PACKET(Qos, _PktId)) -> received1(?PUBLISH_PACKET(QoS, _PktId)) ->
inc('packets/publish/received'), inc('packets/publish/received'),
inc('messages/received'), inc('messages/received'),
qos_received(Qos); qos_received(QoS);
received1(?PACKET(Type)) -> received1(?PACKET(Type)) ->
received2(Type). received2(Type).
received2(?CONNECT) -> received2(?CONNECT) ->
@ -206,15 +206,15 @@ qos_received(?QOS_2) ->
%% @doc Count packets received. Will not count $SYS PUBLISH. %% @doc Count packets received. Will not count $SYS PUBLISH.
-spec(sent(mqtt_packet()) -> ignore | non_neg_integer()). -spec(sent(mqtt_packet()) -> ignore | non_neg_integer()).
sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) -> sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) ->
ignore; ignore;
sent(Packet) -> sent(Packet) ->
inc('packets/sent'), inc('packets/sent'),
sent1(Packet). sent1(Packet).
sent1(?PUBLISH_PACKET(Qos, _PktId)) -> sent1(?PUBLISH_PACKET(QoS, _PktId)) ->
inc('packets/publish/sent'), inc('packets/publish/sent'),
inc('messages/sent'), inc('messages/sent'),
qos_sent(Qos); qos_sent(QoS);
sent1(?PACKET(Type)) -> sent1(?PACKET(Type)) ->
sent2(Type). sent2(Type).
sent2(?CONNACK) -> sent2(?CONNACK) ->

View File

@ -39,8 +39,7 @@ on_client_connected(ConnAck, Client = #client{id = ClientId,
{connack, ConnAck}, {connack, ConnAck},
{ts, emqx_time:now_secs()}]) of {ts, emqx_time:now_secs()}]) of
{ok, Payload} -> {ok, Payload} ->
Msg = message(qos(Env), topic(connected, ClientId), Payload), emqx:publish(message(qos(Env), topic(connected, ClientId), Payload));
emqx:publish(emqx_message:set_flag(sys, Msg));
{error, Reason} -> {error, Reason} ->
emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) emqx_logger:error("[Presence Module] Json error: ~p", [Reason])
end, end,
@ -52,8 +51,7 @@ on_client_disconnected(Reason, #client{id = ClientId, username = Username}, Env)
{reason, reason(Reason)}, {reason, reason(Reason)},
{ts, emqx_time:now_secs()}]) of {ts, emqx_time:now_secs()}]) of
{ok, Payload} -> {ok, Payload} ->
Msg = message(qos(Env), topic(disconnected, ClientId), Payload), emqx_broker:publish(message(qos(Env), topic(disconnected, ClientId), Payload));
emqx:publish(emqx_message:set_flag(sys, Msg));
{error, Reason} -> {error, Reason} ->
emqx_logger:error("[Presence Module] Json error: ~p", [Reason]) emqx_logger:error("[Presence Module] Json error: ~p", [Reason])
end, ok. end, ok.
@ -62,9 +60,9 @@ unload(_Env) ->
emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3), emqx:unhook('client.connected', fun ?MODULE:on_client_connected/3),
emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3). emqx:unhook('client.disconnected', fun ?MODULE:on_client_disconnected/3).
message(Qos, Topic, Payload) -> message(QoS, Topic, Payload) ->
Msg = emqx_message:make(?MODULE, Topic, iolist_to_binary(Payload)), Msg = emqx_message:make(?MODULE, QoS, Topic, iolist_to_binary(Payload)),
emqx_message:set_header(qos, Qos, Msg). emqx_message:set_flags(#{sys => true}, Msg).
topic(connected, ClientId) -> topic(connected, ClientId) ->
emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"])); emqx_topic:systop(iolist_to_binary(["clients/", ClientId, "/connected"]));

View File

@ -34,7 +34,7 @@ load(Topics) ->
on_client_connected(RC, Client = #client{id = ClientId, pid = ClientPid, username = Username}, Topics) on_client_connected(RC, Client = #client{id = ClientId, pid = ClientPid, username = Username}, Topics)
when RC < 16#80 -> when RC < 16#80 ->
Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end, Replace = fun(Topic) -> rep(<<"%u">>, Username, rep(<<"%c">>, ClientId, Topic)) end,
TopicTable = [{Replace(Topic), Qos} || {Topic, Qos} <- Topics], TopicTable = [{Replace(Topic), QoS} || {Topic, QoS} <- Topics],
ClientPid ! {subscribe, TopicTable}, ClientPid ! {subscribe, TopicTable},
{ok, Client}; {ok, Client};

View File

@ -104,17 +104,20 @@ id('Wildcard-Subscription-Available') -> 16#28;
id('Subscription-Identifier-Available') -> 16#29; id('Subscription-Identifier-Available') -> 16#29;
id('Shared-Subscription-Available') -> 16#2A. id('Shared-Subscription-Available') -> 16#2A.
filter(Packet, Props) when ?CONNECT =< Packet, Packet =< ?AUTH -> filter(PacketType, Props) when is_map(Props) ->
Fun = fun(Name) -> maps:from_list(filter(PacketType, maps:to_list(Props)));
filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) ->
Filter = fun(Name) ->
case maps:find(id(Name), ?PROPS_TABLE) of case maps:find(id(Name), ?PROPS_TABLE) of
{ok, {Name, _Type, 'ALL'}} -> {ok, {Name, _Type, 'ALL'}} ->
true; true;
{ok, {Name, _Type, Packets}} -> {ok, {Name, _Type, AllowedTypes}} ->
lists:member(Packet, Packets); lists:member(PacketType, AllowedTypes);
error -> false error -> false
end end
end, end,
[Prop || Prop = {Name, _} <- Props, Fun(Name)]. [Prop || Prop = {Name, _} <- Props, Filter(Name)].
validate(Props) when is_map(Props) -> validate(Props) when is_map(Props) ->
lists:foreach(fun validate_prop/1, maps:to_list(Props)). lists:foreach(fun validate_prop/1, maps:to_list(Props)).

View File

@ -5,8 +5,7 @@
%% You may obtain a copy of the License at %% You may obtain a copy of the License at
%% %%
%% http://www.apache.org/licenses/LICENSE-2.0 %% http://www.apache.org/licenses/LICENSE-2.0
%% %%%% Unless required by applicable law or agreed to in writing, software
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS, %% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and %% See the License for the specific language governing permissions and
@ -40,33 +39,25 @@
%% %%
%% @end %% @end
%% TODO: ...
-module(emqx_mqueue). -module(emqx_mqueue).
%% TODO: XYZ
%%
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-import(proplists, [get_value/3]). -import(proplists, [get_value/3]).
-export([new/3, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1, -export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1]).
dropped/1, stats/1]). -export([dropped/1, stats/1]).
-define(LOW_WM, 0.2).
-define(HIGH_WM, 0.6).
-define(PQUEUE, emqx_pqueue). -define(PQUEUE, emqx_pqueue).
-type(priority() :: {iolist(), pos_integer()}). -type(priority() :: {iolist(), pos_integer()}).
-type(option() :: {type, simple | priority} -type(options() :: #{type => simple | priority,
| {max_length, non_neg_integer()} %% Max queue length max_len => non_neg_integer(),
| {priority, list(priority())} priority => list(priority()),
| {low_watermark, float()} %% Low watermark store_qos0 => boolean()}).
| {high_watermark, float()} %% High watermark
| {store_qos0, boolean()}). %% Queue Qos0?
-type(stat() :: {max_len, non_neg_integer()} -type(stat() :: {max_len, non_neg_integer()}
| {len, non_neg_integer()} | {len, non_neg_integer()}
@ -78,31 +69,22 @@
pseq = 0, priorities = [], pseq = 0, priorities = [],
%% len of simple queue %% len of simple queue
len = 0, max_len = 0, len = 0, max_len = 0,
low_wm = ?LOW_WM, high_wm = ?HIGH_WM, qos0 = false, dropped = 0}).
qos0 = false, dropped = 0,
alarm_fun}).
-type(mqueue() :: #mqueue{}). -type(mqueue() :: #mqueue{}).
-export_type([mqueue/0, priority/0, option/0]). -export_type([mqueue/0, priority/0, options/0]).
%% @doc New Queue. -spec(new(iolist(), options()) -> mqueue()).
-spec(new(iolist(), list(option()), fun()) -> mqueue()). new(Name, #{type := Type, max_len := MaxLen, store_qos0 := StoreQos0}) ->
new(Name, Opts, AlarmFun) ->
Type = get_value(type, Opts, simple),
MaxLen = get_value(max_length, Opts, 0),
init_q(#mqueue{type = Type, name = iolist_to_binary(Name), init_q(#mqueue{type = Type, name = iolist_to_binary(Name),
len = 0, max_len = MaxLen, len = 0, max_len = MaxLen, qos0 = StoreQos0}).
low_wm = low_wm(MaxLen, Opts),
high_wm = high_wm(MaxLen, Opts),
qos0 = get_value(store_qos0, Opts, false),
alarm_fun = AlarmFun}, Opts).
init_q(MQ = #mqueue{type = simple}, _Opts) -> init_q(MQ = #mqueue{type = simple}) ->
MQ#mqueue{q = queue:new()}; MQ#mqueue{q = queue:new()};
init_q(MQ = #mqueue{type = priority}, Opts) -> init_q(MQ = #mqueue{type = priority}) ->
Priorities = get_value(priority, Opts, []), %%Priorities = get_value(priority, Opts, []),
init_p(Priorities, MQ#mqueue{q = ?PQUEUE:new()}). init_p([], MQ#mqueue{q = ?PQUEUE:new()}).
init_p([], MQ) -> init_p([], MQ) ->
MQ; MQ;
@ -114,16 +96,6 @@ insert_p(Topic, P, MQ = #mqueue{priorities = Tab, pseq = Seq}) ->
<<PInt:48>> = <<P:8, (erlang:phash2(Topic)):32, Seq:8>>, <<PInt:48>> = <<P:8, (erlang:phash2(Topic)):32, Seq:8>>,
{PInt, MQ#mqueue{priorities = [{Topic, PInt} | Tab], pseq = Seq + 1}}. {PInt, MQ#mqueue{priorities = [{Topic, PInt} | Tab], pseq = Seq + 1}}.
low_wm(0, _Opts) ->
undefined;
low_wm(MaxLen, Opts) ->
round(MaxLen * get_value(low_watermark, Opts, ?LOW_WM)).
high_wm(0, _Opts) ->
undefined;
high_wm(MaxLen, Opts) ->
round(MaxLen * get_value(high_watermark, Opts, ?HIGH_WM)).
-spec(name(mqueue()) -> iolist()). -spec(name(mqueue()) -> iolist()).
name(#mqueue{name = Name}) -> name(#mqueue{name = Name}) ->
Name. Name.
@ -163,7 +135,7 @@ in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = MaxLen, dropped
{{value, _Old}, Q2} = queue:out(Q), {{value, _Old}, Q2} = queue:out(Q),
MQ#mqueue{q = queue:in(Msg, Q2), dropped = Dropped +1}; MQ#mqueue{q = queue:in(Msg, Q2), dropped = Dropped +1};
in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) -> in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len}) ->
maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}); MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1};
in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q, in(Msg = #message{topic = Topic}, MQ = #mqueue{type = priority, q = Q,
priorities = Priorities, priorities = Priorities,
@ -199,28 +171,8 @@ out(MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) ->
{R, MQ#mqueue{q = Q2, len = Len - 1}}; {R, MQ#mqueue{q = Q2, len = Len - 1}};
out(MQ = #mqueue{type = simple, q = Q, len = Len}) -> out(MQ = #mqueue{type = simple, q = Q, len = Len}) ->
{R, Q2} = queue:out(Q), {R, Q2} = queue:out(Q),
{R, maybe_clear_alarm(MQ#mqueue{q = Q2, len = Len - 1})}; {R, MQ#mqueue{q = Q2, len = Len - 1}};
out(MQ = #mqueue{type = priority, q = Q}) -> out(MQ = #mqueue{type = priority, q = Q}) ->
{R, Q2} = ?PQUEUE:out(Q), {R, Q2} = ?PQUEUE:out(Q),
{R, MQ#mqueue{q = Q2}}. {R, MQ#mqueue{q = Q2}}.
maybe_set_alarm(MQ = #mqueue{high_wm = undefined}) ->
MQ;
maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm_fun = AlarmFun})
when Len > HighWM ->
Alarm = #alarm{id = iolist_to_binary(["queue_high_watermark.", Name]),
severity = warning,
title = io_lib:format("Queue ~s high-water mark", [Name]),
summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])},
MQ#mqueue{alarm_fun = AlarmFun(alert, Alarm)};
maybe_set_alarm(MQ) ->
MQ.
maybe_clear_alarm(MQ = #mqueue{low_wm = undefined}) ->
MQ;
maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm_fun = AlarmFun})
when Len < LowWM ->
MQ#mqueue{alarm_fun = AlarmFun(clear, list_to_binary(["queue_high_watermark.", Name]))};
maybe_clear_alarm(MQ) ->
MQ.

View File

@ -15,12 +15,11 @@
-module(emqx_packet). -module(emqx_packet).
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-export([protocol_name/1, type_name/1]). -export([protocol_name/1, type_name/1]).
-export([format/1]). -export([format/1]).
-export([to_message/1, from_message/1]). -export([to_message/2, from_message/2]).
%% @doc Protocol name of version %% @doc Protocol name of version
-spec(protocol_name(mqtt_version()) -> binary()). -spec(protocol_name(mqtt_version()) -> binary()).
@ -34,43 +33,40 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH ->
lists:nth(Type, ?TYPE_NAMES). lists:nth(Type, ?TYPE_NAMES).
%% @doc From Message to Packet %% @doc From Message to Packet
-spec(from_message(message()) -> mqtt_packet()). -spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()).
from_message(Msg = #message{topic = Topic, payload = Payload}) -> from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) ->
Qos = emqx_message:get_flag(qos, Msg, 0),
Dup = emqx_message:get_flag(dup, Msg, false), Dup = emqx_message:get_flag(dup, Msg, false),
Retain = emqx_message:get_flag(retain, Msg, false), Retain = emqx_message:get_flag(retain, Msg, false),
PacketId = emqx_message:get_header(packet_id, Msg),
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
qos = Qos, qos = QoS,
retain = Retain, retain = Retain,
dup = Dup}, dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId},
payload = Payload}.
%% @doc Message from Packet
-spec(to_message(mqtt_packet()) -> message()).
to_message(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
retain = Retain,
qos = Qos,
dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic, variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId, packet_id = PacketId,
properties = Properties}, properties = #{}}, %%TODO:
payload = Payload}) -> payload = Payload}.
Flags = #{dup => Dup, retain => Retain, qos => Qos},
Msg = emqx_message:new(undefined, Flags, #{packet_id => PacketId}, Topic, Payload),
Msg#message{properties = Properties};
to_message(#mqtt_packet_connect{will_flag = false}) -> %% @doc Message from Packet
-spec(to_message(client_id(), mqtt_packet()) -> message()).
to_message(ClientId, #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
retain = Retain,
qos = QoS,
dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic,
properties = Props},
payload = Payload}) ->
Msg = emqx_message:make(ClientId, QoS, Topic, Payload),
Msg#message{flags = #{dup => Dup, retain => Retain}, headers = Props};
to_message(_ClientId, #mqtt_packet_connect{will_flag = false}) ->
undefined; undefined;
to_message(#mqtt_packet_connect{will_retain = Retain, to_message(ClientId, #mqtt_packet_connect{will_retain = Retain,
will_qos = Qos, will_qos = QoS,
will_topic = Topic, will_topic = Topic,
will_props = Props, will_props = Props,
will_payload = Payload}) -> will_payload = Payload}) ->
Msg = emqx_message:new(undefined, #{qos => Qos, retain => Retain}, Topic, Payload), Msg = emqx_message:make(ClientId, QoS, Topic, Payload),
Msg#message{properties = Props}. Msg#message{flags = #{qos => QoS, retain => Retain}, headers = Props}.
%% @doc Format packet %% @doc Format packet
-spec(format(mqtt_packet()) -> iolist()). -spec(format(mqtt_packet()) -> iolist()).
@ -118,7 +114,7 @@ format_variable(#mqtt_packet_connect{
format_variable(#mqtt_packet_connack{ack_flags = AckFlags, format_variable(#mqtt_packet_connack{ack_flags = AckFlags,
reason_code = ReasonCode}) -> reason_code = ReasonCode}) ->
io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReasonCode]); io_lib:format("AckFlags=~p, ReasonCode=~p", [AckFlags, ReasonCode]);
format_variable(#mqtt_packet_publish{topic_name = TopicName, format_variable(#mqtt_packet_publish{topic_name = TopicName,
packet_id = PacketId}) -> packet_id = PacketId}) ->
@ -153,3 +149,4 @@ format_password(_Password) -> '******'.
i(true) -> 1; i(true) -> 1;
i(false) -> 0; i(false) -> 0;
i(I) when is_integer(I) -> I. i(I) when is_integer(I) -> I.

View File

@ -32,12 +32,11 @@
-spec(init() -> ok). -spec(init() -> ok).
init() -> init() ->
case emqx_config:get_env(plugins_etc_dir) of case emqx_config:get_env(plugins_etc_dir) of
{ok, PluginsEtc} -> undefined -> ok;
PluginsEtc ->
CfgFiles = [filename:join(PluginsEtc, File) || CfgFiles = [filename:join(PluginsEtc, File) ||
File <- filelib:wildcard("*.config", PluginsEtc)], File <- filelib:wildcard("*.config", PluginsEtc)],
lists:foreach(fun init_config/1, CfgFiles); lists:foreach(fun init_config/1, CfgFiles)
undefined ->
ok
end. end.
init_config(CfgFile) -> init_config(CfgFile) ->
@ -51,25 +50,24 @@ init_config(CfgFile) ->
load() -> load() ->
load_expand_plugins(), load_expand_plugins(),
case emqx_config:get_env(plugins_loaded_file) of case emqx_config:get_env(plugins_loaded_file) of
{ok, File} -> undefined -> %% No plugins available
ignore;
File ->
ensure_file(File), ensure_file(File),
with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end); with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end)
undefined ->
%% No plugins available
ignore
end. end.
load_expand_plugins() -> load_expand_plugins() ->
case emqx_config:get_env(expand_plugins_dir) of case emqx_config:get_env(expand_plugins_dir) of
{ok, Dir} -> undefined -> ok;
Dir ->
PluginsDir = filelib:wildcard("*", Dir), PluginsDir = filelib:wildcard("*", Dir),
lists:foreach(fun(PluginDir) -> lists:foreach(fun(PluginDir) ->
case filelib:is_dir(Dir ++ PluginDir) of case filelib:is_dir(Dir ++ PluginDir) of
true -> load_expand_plugin(Dir ++ PluginDir); true -> load_expand_plugin(Dir ++ PluginDir);
false -> ok false -> ok
end end
end, PluginsDir); end, PluginsDir)
_ -> ok
end. end.
load_expand_plugin(PluginDir) -> load_expand_plugin(PluginDir) ->
@ -102,7 +100,8 @@ init_expand_plugin_config(PluginDir) ->
get_expand_plugin_config() -> get_expand_plugin_config() ->
case emqx_config:get_env(expand_plugins_dir) of case emqx_config:get_env(expand_plugins_dir) of
{ok, Dir} -> undefined -> ok;
Dir ->
PluginsDir = filelib:wildcard("*", Dir), PluginsDir = filelib:wildcard("*", Dir),
lists:foldl(fun(PluginDir, Acc) -> lists:foldl(fun(PluginDir, Acc) ->
case filelib:is_dir(Dir ++ PluginDir) of case filelib:is_dir(Dir ++ PluginDir) of
@ -115,11 +114,9 @@ get_expand_plugin_config() ->
false -> false ->
Acc Acc
end end
end, [], PluginsDir); end, [], PluginsDir)
_ -> ok
end. end.
ensure_file(File) -> ensure_file(File) ->
case filelib:is_file(File) of false -> write_loaded([]); true -> ok end. case filelib:is_file(File) of false -> write_loaded([]); true -> ok end.
@ -145,10 +142,10 @@ load_plugins(Names, Persistent) ->
-spec(unload() -> list() | {error, term()}). -spec(unload() -> list() | {error, term()}).
unload() -> unload() ->
case emqx_config:get_env(plugins_loaded_file) of case emqx_config:get_env(plugins_loaded_file) of
{ok, File} ->
with_loaded_file(File, fun stop_plugins/1);
undefined -> undefined ->
ignore ignore;
File ->
with_loaded_file(File, fun stop_plugins/1)
end. end.
%% stop plugins %% stop plugins
@ -159,7 +156,9 @@ stop_plugins(Names) ->
-spec(list() -> [plugin()]). -spec(list() -> [plugin()]).
list() -> list() ->
case emqx_config:get_env(plugins_etc_dir) of case emqx_config:get_env(plugins_etc_dir) of
{ok, PluginsEtc} -> undefined ->
[];
PluginsEtc ->
CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(), CfgFiles = filelib:wildcard("*.{conf,config}", PluginsEtc) ++ get_expand_plugin_config(),
Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles], Plugins = [plugin(CfgFile) || CfgFile <- CfgFiles],
StartedApps = names(started_app), StartedApps = names(started_app),
@ -168,9 +167,7 @@ list() ->
true -> Plugin#plugin{active = true}; true -> Plugin#plugin{active = true};
false -> Plugin false -> Plugin
end end
end, Plugins); end, Plugins)
undefined ->
[]
end. end.
plugin(CfgFile) -> plugin(CfgFile) ->
@ -314,14 +311,14 @@ plugin_unloaded(Name, true) ->
read_loaded() -> read_loaded() ->
case emqx_config:get_env(plugins_loaded_file) of case emqx_config:get_env(plugins_loaded_file) of
{ok, File} -> read_loaded(File); undefined -> {error, not_found};
undefined -> {error, not_found} File -> read_loaded(File)
end. end.
read_loaded(File) -> file:consult(File). read_loaded(File) -> file:consult(File).
write_loaded(AppNames) -> write_loaded(AppNames) ->
{ok, File} = emqx_config:get_env(plugins_loaded_file), File = emqx_config:get_env(plugins_loaded_file),
case file:open(File, [binary, write]) of case file:open(File, [binary, write]) of
{ok, Fd} -> {ok, Fd} ->
lists:foreach(fun(Name) -> lists:foreach(fun(Name) ->

View File

@ -31,7 +31,7 @@ spec(ChildId, Args) ->
-spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}). -spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}).
start_link(Pool, Type, MFA) -> start_link(Pool, Type, MFA) ->
start_link(Pool, Type, emqx_vm:schedulers(schedulers), MFA). start_link(Pool, Type, emqx_vm:schedulers(), MFA).
-spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}). -spec(start_link(atom() | tuple(), atom(), pos_integer(), mfa()) -> {ok, pid()} | {error, term()}).
start_link(Pool, Type, Size, MFA) when is_atom(Pool) -> start_link(Pool, Type, Size, MFA) when is_atom(Pool) ->

View File

@ -1,138 +1,107 @@
%%%=================================================================== %% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. %%
%%% %% Licensed under the Apache License, Version 2.0 (the "License");
%%% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License.
%%% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at
%%% You may obtain a copy of the License at %%
%%% %% http://www.apache.org/licenses/LICENSE-2.0
%%% http://www.apache.org/licenses/LICENSE-2.0 %%
%%% %% Unless required by applicable law or agreed to in writing, software
%%% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS,
%%% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and
%%% See the License for the specific language governing permissions and %% limitations under the License.
%%% limitations under the License.
%%%===================================================================
-module(emqx_protocol). -module(emqx_protocol).
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("emqx_misc.hrl"). -include("emqx_misc.hrl").
-import(proplists, [get_value/2, get_value/3]). -export([init/2, info/1, stats/1, clientid/1, session/1]).
%%-export([capabilities/1]).
%% API -export([parser/1]).
-export([init/3, init/5, get/2, info/1, stats/1, clientid/1, client/1, session/1]). -export([received/2, process/2, deliver/2, send/2]).
-export([shutdown/2]).
-export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]).
-export([received/2, send/2]).
-export([process/2]).
-ifdef(TEST). -ifdef(TEST).
-compile(export_all). -compile(export_all).
-endif. -endif.
-record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0, -define(CAPABILITIES, [{max_packet_size, ?MAX_PACKET_SIZE},
send_pkt = 0, send_msg = 0}). {max_clientid_len, ?MAX_CLIENTID_LEN},
{max_topic_alias, 0},
{max_qos_allowed, ?QOS2},
{retain_available, true},
{shared_subscription, true},
{wildcard_subscription, true}]).
%% Protocol State -record(proto_state, {zone, sockprops, capabilities, connected, client_id, client_pid,
%% ws_initial_headers: Headers from first HTTP request for WebSocket Client. clean_start, proto_ver, proto_name, username, connprops,
-record(proto_state, {peername, sendfun, connected = false, client_id, client_pid, is_superuser, will_msg, keepalive, keepalive_backoff, session,
clean_start, proto_ver, proto_name, username, is_superuser, recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0,
will_msg, keepalive, keepalive_backoff, max_clientid_len, mountpoint, is_bridge, connected_at}).
session, stats_data, mountpoint, ws_initial_headers,
peercert_username, is_bridge, connected_at}).
-type(proto_state() :: #proto_state{}). -define(INFO_KEYS, [capabilities, connected, client_id, clean_start, username, proto_ver, proto_name,
keepalive, will_msg, mountpoint, is_bridge, connected_at]).
-define(INFO_KEYS, [client_id, username, clean_start, proto_ver, proto_name,
keepalive, will_msg, ws_initial_headers, mountpoint,
peercert_username, connected_at]).
-define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]). -define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]).
-define(LOG(Level, Format, Args, State), -define(LOG(Level, Format, Args, State),
emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format,
[State#proto_state.client_id, esockd_net:format(State#proto_state.peername) | Args])). [State#proto_state.client_id,
esockd_net:format(maps:get(peername, State#proto_state.sockprops)) | Args])).
%% @doc Init protocol -type(proto_state() :: #proto_state{}).
init(Peername, SendFun, Opts) ->
Backoff = get_value(keepalive_backoff, Opts, 0.75), -export_type([proto_state/0]).
EnableStats = get_value(client_enable_stats, Opts, false),
MaxLen = get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), init(SockProps = #{peercert := Peercert}, Options) ->
WsInitialHeaders = get_value(ws_initial_headers, Opts), Zone = proplists:get_value(zone, Options),
#proto_state{peername = Peername, MountPoint = emqx_zone:env(Zone, mountpoint),
sendfun = SendFun, Backoff = emqx_zone:env(Zone, keepalive_backoff, 0.75),
max_clientid_len = MaxLen, Username = case proplists:get_value(peer_cert_as_username, Options) of
is_superuser = false, cn -> esockd_peercert:common_name(Peercert);
dn -> esockd_peercert:subject(Peercert);
_ -> undefined
end,
#proto_state{zone = Zone,
sockprops = SockProps,
capabilities = capabilities(Zone),
connected = false,
clean_start = true,
client_pid = self(), client_pid = self(),
peercert_username = undefined, proto_ver = ?MQTT_PROTO_V4,
ws_initial_headers = WsInitialHeaders, proto_name = <<"MQTT">>,
username = Username,
is_superuser = false,
keepalive_backoff = Backoff, keepalive_backoff = Backoff,
stats_data = #proto_stats{enable_stats = EnableStats}}. mountpoint = MountPoint,
is_bridge = false,
recv_pkt = 0,
recv_msg = 0,
send_pkt = 0,
send_msg = 0}.
init(_Transport, _Sock, Peername, SendFun, Opts) -> capabilities(Zone) ->
init(Peername, SendFun, Opts). Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []),
%%enrich_opt(Conn:opts(), Conn, ). maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)).
enrich_opt([], _Conn, State) -> parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) ->
State; emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}).
enrich_opt([{mountpoint, MountPoint} | ConnOpts], Conn, State) ->
enrich_opt(ConnOpts, Conn, State#proto_state{mountpoint = MountPoint});
enrich_opt([{peer_cert_as_username, N} | ConnOpts], Conn, State) ->
enrich_opt(ConnOpts, Conn, State#proto_state{peercert_username = peercert_username(N, Conn)});
enrich_opt([_ | ConnOpts], Conn, State) ->
enrich_opt(ConnOpts, Conn, State).
peercert_username(cn, Conn) ->
Conn:peer_cert_common_name();
peercert_username(dn, Conn) ->
Conn:peer_cert_subject().
repl_username_with_peercert(State = #proto_state{peercert_username = undefined}) ->
State;
repl_username_with_peercert(State = #proto_state{peercert_username = PeerCert}) ->
State#proto_state{username = PeerCert}.
%%TODO::
get(proto_ver, #proto_state{proto_ver = Ver}) ->
Ver;
get(_, _ProtoState) ->
undefined.
info(ProtoState) -> info(ProtoState) ->
?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS). ?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS).
stats(#proto_state{stats_data = Stats}) -> stats(ProtoState) ->
tl(?record_to_proplist(proto_stats, Stats)). ?record_to_proplist(proto_state, ProtoState, ?STATS_KEYS).
clientid(#proto_state{client_id = ClientId}) -> clientid(#proto_state{client_id = ClientId}) ->
ClientId. ClientId.
client(#proto_state{client_id = ClientId, client(#proto_state{sockprops = #{peername := Peername},
client_pid = ClientPid, client_id = ClientId, client_pid = ClientPid, username = Username}) ->
peername = Peername, #client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}.
username = Username,
clean_start = CleanStart,
proto_ver = ProtoVer,
keepalive = Keepalive,
will_msg = WillMsg,
ws_initial_headers = WsInitialHeaders,
mountpoint = MountPoint,
connected_at = Time}) ->
WillTopic = if
WillMsg =:= undefined -> undefined;
true -> WillMsg#message.topic
end,
#client{id = ClientId,
pid = ClientPid,
username = Username,
peername = Peername}.
session(#proto_state{session = Session}) -> session(#proto_state{session = Session}) ->
Session. Session.
@ -141,117 +110,96 @@ session(#proto_state{session = Session}) ->
%% A Client can only send the CONNECT Packet once over a Network Connection. %% A Client can only send the CONNECT Packet once over a Network Connection.
-spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}). -spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}).
received(Packet = ?PACKET(?CONNECT), received(Packet = ?PACKET(?CONNECT), ProtoState = #proto_state{connected = false}) ->
State = #proto_state{connected = false, stats_data = Stats}) -> trace(recv, Packet, ProtoState),
trace(recv, Packet, State), Stats1 = inc_stats(recv, ?CONNECT, Stats), process(Packet, inc_stats(recv, ?CONNECT, ProtoState#proto_state{connected = true}));
process(Packet, State#proto_state{connected = true, stats_data = Stats1});
received(?PACKET(?CONNECT), State = #proto_state{connected = true}) -> received(?PACKET(?CONNECT), State = #proto_state{connected = true}) ->
{error, protocol_bad_connect, State}; {error, protocol_bad_connect, State};
%% Received other packets when CONNECT not arrived. %% Received other packets when CONNECT not arrived.
received(_Packet, State = #proto_state{connected = false}) -> received(_Packet, ProtoState = #proto_state{connected = false}) ->
{error, protocol_not_connected, State}; {error, protocol_not_connected, ProtoState};
received(Packet = ?PACKET(Type), State = #proto_state{stats_data = Stats}) -> received(Packet = ?PACKET(Type), ProtoState) ->
trace(recv, Packet, State), Stats1 = inc_stats(recv, Type, Stats), trace(recv, Packet, ProtoState),
case validate_packet(Packet) of case validate_packet(Packet) of
ok -> ok ->
process(Packet, State#proto_state{stats_data = Stats1}); process(Packet, inc_stats(recv, Type, ProtoState));
{error, Reason} -> {error, Reason} ->
{error, Reason, State} {error, Reason, ProtoState}
end. end.
subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId, process(?CONNECT_PACKET(Var), ProtoState = #proto_state{zone = Zone,
username = Username, username = Username0,
session = Session}) -> client_pid = ClientPid}) ->
TopicTable = parse_topic_table(RawTopicTable), #mqtt_packet_connect{proto_name = ProtoName,
case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of proto_ver = ProtoVer,
{ok, TopicTable1} ->
emqx_session:subscribe(Session, TopicTable1);
{stop, _} ->
ok
end,
{ok, ProtoState}.
unsubscribe(RawTopics, ProtoState = #proto_state{client_id = ClientId,
username = Username,
session = Session}) ->
case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of
{ok, TopicTable} ->
emqx_session:unsubscribe(Session, TopicTable);
{stop, _} ->
ok
end,
{ok, ProtoState}.
%% @doc Send PUBREL
pubrel(PacketId, State) -> send(?PUBREL_PACKET(PacketId), State).
process(?CONNECT_PACKET(Var), State0) ->
#mqtt_packet_connect{proto_ver = ProtoVer,
proto_name = ProtoName,
username = Username,
password = Password,
clean_start= CleanStart,
keepalive = KeepAlive,
client_id = ClientId,
is_bridge = IsBridge} = Var,
State1 = repl_username_with_peercert(
State0#proto_state{proto_ver = ProtoVer,
proto_name = ProtoName,
username = Username,
client_id = ClientId,
clean_start = CleanStart,
keepalive = KeepAlive,
will_msg = willmsg(Var, State0),
is_bridge = IsBridge, is_bridge = IsBridge,
connected_at = os:timestamp()}), clean_start = CleanStart,
keepalive = Keepalive,
properties = ConnProps,
client_id = ClientId,
username = Username,
password = Password} = Var,
ProtoState1 = ProtoState#proto_state{proto_ver = ProtoVer,
proto_name = ProtoName,
username = if Username0 == undefined ->
Username;
true -> Username0
end, %% TODO: fixme later.
client_id = ClientId,
clean_start = CleanStart,
keepalive = Keepalive,
connprops = ConnProps,
will_msg = willmsg(Var, ProtoState),
is_bridge = IsBridge,
connected_at = os:timestamp()},
{ReturnCode1, SessPresent, State3} = {ReturnCode1, SessPresent, ProtoState3} =
case validate_connect(Var, State1) of case validate_connect(Var, ProtoState1) of
?RC_SUCCESS -> ?RC_SUCCESS ->
case authenticate(client(State1), Password) of case authenticate(client(ProtoState1), Password) of
{ok, IsSuperuser} -> {ok, IsSuperuser} ->
%% Generate clientId if null %% Generate clientId if null
State2 = maybe_set_clientid(State1), ProtoState2 = maybe_set_clientid(ProtoState1),
%% Open session
%% Start session case emqx_sm:open_session(#{zone => Zone,
case emqx_sm:open_session(#{clean_start => CleanStart, clean_start => CleanStart,
client_id => clientid(State2), client_id => clientid(ProtoState2),
username => Username, username => Username,
client_pid => self()}) of client_pid => ClientPid}) of
{ok, Session} -> %% TODO:... {ok, Session} -> %% TODO:...
SP = true, %% TODO:... SP = true, %% TODO:...
%% TODO: Register the client %% TODO: Register the client
emqx_cm:register_client(clientid(State2)), emqx_cm:register_client(clientid(ProtoState2)),
%%emqx_cm:reg(client(State2)), %%emqx_cm:reg(client(State2)),
%% Start keepalive %% Start keepalive
start_keepalive(KeepAlive, State2), start_keepalive(Keepalive, ProtoState2),
%% Emit Stats %% Emit Stats
self() ! emit_stats, %% self() ! emit_stats,
%% ACCEPT %% ACCEPT
{?RC_SUCCESS, SP, State2#proto_state{session = Session, is_superuser = IsSuperuser}}; {?RC_SUCCESS, SP, ProtoState2#proto_state{session = Session, is_superuser = IsSuperuser}};
{error, Error} -> {error, Error} ->
{stop, {shutdown, Error}, State2} ?LOG(error, "Failed to open session: ~p", [Error], ProtoState2),
{?RC_UNSPECIFIED_ERROR, false, ProtoState2} %% TODO: the error reason???
end; end;
{error, Reason}-> {error, Reason}->
?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], State1), ?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], ProtoState1),
{?RC_BAD_USER_NAME_OR_PASSWORD, false, State1} {?RC_BAD_USER_NAME_OR_PASSWORD, false, ProtoState1}
end; end;
ReturnCode -> ReturnCode ->
{ReturnCode, false, State1} {ReturnCode, false, ProtoState1}
end, end,
%% Run hooks %% Run hooks
emqx_hooks:run('client.connected', [ReturnCode1], client(State3)), emqx_hooks:run('client.connected', [ReturnCode1], client(ProtoState3)),
%%TODO: Send Connack %%TODO: Send Connack
send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3), send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), ProtoState3),
%% stop if authentication failure %% stop if authentication failure
stop_if_auth_failure(ReturnCode1, State3); stop_if_auth_failure(ReturnCode1, ProtoState3);
process(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), State = #proto_state{is_superuser = IsSuper}) -> process(Packet = ?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload),
State = #proto_state{is_superuser = IsSuper}) ->
case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of case IsSuper orelse allow == check_acl(publish, Topic, client(State)) of
true -> publish(Packet, State); true -> publish(Packet, State);
false -> ?LOG(error, "Cannot publish to ~s for ACL Deny", [Topic], State) false -> ?LOG(error, "Cannot publish to ~s for ACL Deny", [Topic], State)
@ -278,28 +226,28 @@ process(?SUBSCRIBE_PACKET(PacketId, []), State) ->
send(?SUBACK_PACKET(PacketId, []), State); send(?SUBACK_PACKET(PacketId, []), State);
%% TODO: refactor later... %% TODO: refactor later...
process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) ->
State = #proto_state{client_id = ClientId, #proto_state{client_id = ClientId,
username = Username, username = Username,
is_superuser = IsSuperuser, is_superuser = IsSuperuser,
mountpoint = MountPoint, mountpoint = MountPoint,
session = Session}) -> session = Session} = State,
Client = client(State), TopicTable = parse_topic_table(RawTopicTable), Client = client(State),
TopicFilters = parse_topic_filters(RawTopicFilters),
AllowDenies = if AllowDenies = if
IsSuperuser -> []; IsSuperuser -> [];
true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicTable] true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicFilters]
end, end,
case lists:member(deny, AllowDenies) of case lists:member(deny, AllowDenies) of
true -> true ->
?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicTable], State), ?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicFilters], State),
send(?SUBACK_PACKET(PacketId, [16#80 || _ <- TopicTable]), State); send(?SUBACK_PACKET(PacketId, [?RC_NOT_AUTHORIZED || _ <- TopicFilters]), State);
false -> false ->
case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicFilters) of
{ok, TopicTable1} -> {ok, TopicFilters1} ->
emqx_session:subscribe(Session, PacketId, mount(replvar(MountPoint, State), TopicTable1)), ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}),
{ok, State}; {ok, State};
{stop, _} -> {stop, _} -> {ok, State}
{ok, State}
end end
end; end;
@ -307,97 +255,109 @@ process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable),
process(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> process(?UNSUBSCRIBE_PACKET(PacketId, []), State) ->
send(?UNSUBACK_PACKET(PacketId), State); send(?UNSUBACK_PACKET(PacketId), State);
process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), process(?UNSUBSCRIBE_PACKET(PacketId, Properties, RawTopics),
State = #proto_state{client_id = ClientId, State = #proto_state{client_id = ClientId,
username = Username, username = Username,
mountpoint = MountPoint, mountpoint = MountPoint,
session = Session}) -> session = Session}) ->
case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of
{ok, TopicTable} -> {ok, TopicTable} ->
emqx_session:unsubscribe(Session, mount(replvar(MountPoint, State), TopicTable)); emqx_session:unsubscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicTable)});
{stop, _} -> {stop, _} ->
ok ok
end, end,
send(?UNSUBACK_PACKET(PacketId), State); send(?UNSUBACK_PACKET(PacketId), State);
process(?PACKET(?PINGREQ), State) -> process(?PACKET(?PINGREQ), ProtoState) ->
send(?PACKET(?PINGRESP), State); send(?PACKET(?PINGRESP), ProtoState);
process(?PACKET(?DISCONNECT), State) -> process(?PACKET(?DISCONNECT), ProtoState) ->
% Clean willmsg % Clean willmsg
{stop, normal, State#proto_state{will_msg = undefined}}. {stop, normal, ProtoState#proto_state{will_msg = undefined}}.
publish(Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), deliver({publish, PacketId, Msg},
State = #proto_state{client_id = ClientId,
username = Username,
mountpoint = MountPoint,
is_bridge = IsBridge}) ->
emqx_hooks:run('message.delivered', [ClientId],
emqx_message:set_header(username, Username, Msg)),
Msg1 = unmount(MountPoint, clean_retain(IsBridge, Msg)),
send(emqx_packet:from_message(PacketId, Msg1), State);
deliver({pubrel, PacketId}, State) ->
send(?PUBREL_PACKET(PacketId), State);
deliver({suback, PacketId, ReasonCodes}, ProtoState) ->
send(?SUBACK_PACKET(PacketId, ReasonCodes), ProtoState);
deliver({unsuback, PacketId, ReasonCodes}, ProtoState) ->
send(?UNSUBACK_PACKET(PacketId, ReasonCodes), ProtoState).
publish(Packet = ?PUBLISH_PACKET(?QOS_0, PacketId),
State = #proto_state{client_id = ClientId, State = #proto_state{client_id = ClientId,
username = Username, username = Username,
mountpoint = MountPoint, mountpoint = MountPoint,
session = Session}) -> session = Session}) ->
Msg = emqx_packet:to_message(Packet), Msg = emqx_message:set_header(username, Username,
Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, emqx_packet:to_message(ClientId, Packet)),
emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)); emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg));
publish(Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> publish(Packet = ?PUBLISH_PACKET(?QOS_1), State) ->
with_puback(?PUBACK, Packet, State); with_puback(?PUBACK, Packet, State);
publish(Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> publish(Packet = ?PUBLISH_PACKET(?QOS_2), State) ->
with_puback(?PUBREC, Packet, State). with_puback(?PUBREC, Packet, State).
with_puback(Type, Packet = ?PUBLISH_PACKET(_Qos, PacketId), with_puback(Type, Packet = ?PUBLISH_PACKET(_QoS, PacketId),
State = #proto_state{client_id = ClientId, State = #proto_state{client_id = ClientId,
username = Username, username = Username,
mountpoint = MountPoint, mountpoint = MountPoint,
session = Session}) -> session = Session}) ->
%% TODO: ... Msg = emqx_message:set_header(username, Username,
Msg = emqx_packet:to_message(Packet), emqx_packet:to_message(ClientId, Packet)),
Msg1 = Msg#message{from = #client{id = ClientId, username = Username}}, case emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)) of
case emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1)) of
ok ->
case Type of
?PUBACK -> send(?PUBACK_PACKET(PacketId), State);
?PUBREC -> send(?PUBREC_PACKET(PacketId), State)
end;
{error, Error} -> {error, Error} ->
?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State) ?LOG(error, "PUBLISH ~p error: ~p", [PacketId, Error], State);
_Delivery -> send({Type, PacketId}, State) %% TODO:
end. end.
-spec(send(message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}). -spec(send({mqtt_packet_type(), mqtt_packet_id()} |
send(Msg, State = #proto_state{client_id = ClientId, {mqtt_packet_id(), message()} |
username = Username, mqtt_packet(), proto_state()) -> {ok, proto_state()}).
mountpoint = MountPoint, send({?PUBACK, PacketId}, State) ->
is_bridge = IsBridge}) send(?PUBACK_PACKET(PacketId), State);
when is_record(Msg, message) ->
emqx_hooks:run('message.delivered', [ClientId, Username], Msg),
send(emqx_packet:from_message(unmount(MountPoint, clean_retain(IsBridge, Msg))), State);
send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) -> send({?PUBREC, PacketId}, State) ->
trace(send, Packet, State), send(?PUBREC_PACKET(PacketId), State);
emqx_metrics:sent(Packet),
SendFun(Packet), send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver,
{ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}. sockprops = #{sendfun := SendFun}}) ->
Data = emqx_frame:serialize(Packet, #{version => Ver}),
case SendFun(Data) of
{error, Reason} ->
{error, Reason};
_ -> emqx_metrics:sent(Packet),
trace(send, Packet, ProtoState),
{ok, inc_stats(send, Type, ProtoState)}
end.
trace(recv, Packet, ProtoState) -> trace(recv, Packet, ProtoState) ->
?LOG(info, "RECV ~s", [emqx_packet:format(Packet)], ProtoState); ?LOG(debug, "RECV ~s", [emqx_packet:format(Packet)], ProtoState);
trace(send, Packet, ProtoState) -> trace(send, Packet, ProtoState) ->
?LOG(info, "SEND ~s", [emqx_packet:format(Packet)], ProtoState). ?LOG(debug, "SEND ~s", [emqx_packet:format(Packet)], ProtoState).
inc_stats(_Direct, _Type, Stats = #proto_stats{enable_stats = false}) -> inc_stats(recv, Type, ProtoState = #proto_state{recv_pkt = PktCnt, recv_msg = MsgCnt}) ->
Stats; ProtoState#proto_state{recv_pkt = PktCnt + 1,
recv_msg = if Type =:= ?PUBLISH -> MsgCnt + 1;
inc_stats(recv, Type, Stats) -> true -> MsgCnt
#proto_stats{recv_pkt = PktCnt, recv_msg = MsgCnt} = Stats, end};
inc_stats(Type, #proto_stats.recv_pkt, PktCnt, #proto_stats.recv_msg, MsgCnt, Stats); inc_stats(send, Type, ProtoState = #proto_state{send_pkt = PktCnt, send_msg = MsgCnt}) ->
ProtoState#proto_state{send_pkt = PktCnt + 1,
inc_stats(send, Type, Stats) -> send_msg = if Type =:= ?PUBLISH -> MsgCnt + 1;
#proto_stats{send_pkt = PktCnt, send_msg = MsgCnt} = Stats, true -> MsgCnt
inc_stats(Type, #proto_stats.send_pkt, PktCnt, #proto_stats.send_msg, MsgCnt, Stats). end}.
inc_stats(Type, PktPos, PktCnt, MsgPos, MsgCnt, Stats) ->
Stats1 = setelement(PktPos, Stats, PktCnt + 1),
case Type =:= ?PUBLISH of
true -> setelement(MsgPos, Stats1, MsgCnt + 1);
false -> Stats1
end.
stop_if_auth_failure(?RC_SUCCESS, State) -> stop_if_auth_failure(?RC_SUCCESS, State) ->
{ok, State}; {ok, State};
@ -415,19 +375,18 @@ shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) ->
shutdown(Error, State = #proto_state{client_id = ClientId, shutdown(Error, State = #proto_state{client_id = ClientId,
will_msg = WillMsg}) -> will_msg = WillMsg}) ->
?LOG(info, "Shutdown for ~p", [Error], State), ?LOG(info, "Shutdown for ~p", [Error], State),
Client = client(State),
%% Auth failure not publish the will message %% Auth failure not publish the will message
case Error =:= auth_failure of case Error =:= auth_failure of
true -> ok; true -> ok;
false -> send_willmsg(Client, WillMsg) false -> send_willmsg(ClientId, WillMsg)
end, end,
emqx_hooks:run('client.disconnected', [Error], Client), emqx_hooks:run('client.disconnected', [Error], client(State)),
emqx_cm:unregister_client(ClientId), emqx_cm:unregister_client(ClientId),
ok. ok.
willmsg(Packet, State = #proto_state{mountpoint = MountPoint}) willmsg(Packet, State = #proto_state{client_id = ClientId, mountpoint = MountPoint})
when is_record(Packet, mqtt_packet_connect) -> when is_record(Packet, mqtt_packet_connect) ->
case emqx_packet:to_message(Packet) of case emqx_packet:to_message(ClientId, Packet) of
undefined -> undefined; undefined -> undefined;
Msg -> mount(replvar(MountPoint, State), Msg) Msg -> mount(replvar(MountPoint, State), Msg)
end. end.
@ -442,10 +401,10 @@ maybe_set_clientid(State = #proto_state{client_id = NullId})
maybe_set_clientid(State) -> maybe_set_clientid(State) ->
State. State.
send_willmsg(_Client, undefined) -> send_willmsg(_ClientId, undefined) ->
ignore; ignore;
send_willmsg(Client, WillMsg) -> send_willmsg(ClientId, WillMsg) ->
emqx_broker:publish(WillMsg#message{from = Client}). emqx_broker:publish(WillMsg#message{from = ClientId}).
start_keepalive(0, _State) -> ignore; start_keepalive(0, _State) -> ignore;
@ -471,7 +430,7 @@ validate_protocol(#mqtt_packet_connect{proto_ver = Ver, proto_name = Name}) ->
lists:member({Ver, Name}, ?PROTOCOL_NAMES). lists:member({Ver, Name}, ?PROTOCOL_NAMES).
validate_clientid(#mqtt_packet_connect{client_id = ClientId}, validate_clientid(#mqtt_packet_connect{client_id = ClientId},
#proto_state{max_clientid_len = MaxLen}) #proto_state{capabilities = #{max_clientid_len := MaxLen}})
when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) -> when (byte_size(ClientId) >= 1) andalso (byte_size(ClientId) =< MaxLen) ->
true; true;
@ -493,7 +452,7 @@ validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer,
[ProtoVer, CleanStart], ProtoState), [ProtoVer, CleanStart], ProtoState),
false. false.
validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) -> validate_packet(?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload)) ->
case emqx_topic:validate({name, Topic}) of case emqx_topic:validate({name, Topic}) of
true -> ok; true -> ok;
false -> {error, badtopic} false -> {error, badtopic}
@ -513,11 +472,11 @@ validate_topics(_Type, []) ->
validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_]) validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_])
when Type =:= name orelse Type =:= filter -> when Type =:= name orelse Type =:= filter ->
Valid = fun(Topic, Qos) -> Valid = fun(Topic, QoS) ->
emqx_topic:validate({Type, Topic}) and validate_qos(Qos) emqx_topic:validate({Type, Topic}) and validate_qos(QoS)
end, end,
case [Topic || {Topic, SubOpts} <- TopicTable, case [Topic || {Topic, SubOpts} <- TopicTable,
not Valid(Topic, proplists:get_value(qos, SubOpts))] of not Valid(Topic, SubOpts#mqtt_subopts.qos)] of
[] -> ok; [] -> ok;
_ -> {error, badtopic} _ -> {error, badtopic}
end; end;
@ -530,17 +489,16 @@ validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) ->
validate_qos(undefined) -> validate_qos(undefined) ->
true; true;
validate_qos(Qos) when ?IS_QOS(Qos) -> validate_qos(QoS) when ?IS_QOS(QoS) ->
true; true;
validate_qos(_) -> validate_qos(_) ->
false. false.
parse_topic_table(TopicTable) -> parse_topic_filters(TopicFilters) ->
lists:map(fun({Topic0, SubOpts}) -> [begin
{Topic, Opts} = emqx_topic:parse(Topic0), {Topic, Opts} = emqx_topic:parse(RawTopic),
%%TODO: {Topic, maps:merge(?record_to_map(mqtt_subopts, SubOpts), Opts)}
{Topic, lists:usort(lists:umerge(Opts, SubOpts))} end || {RawTopic, SubOpts} <- TopicFilters].
end, TopicTable).
parse_topics(Topics) -> parse_topics(Topics) ->
[emqx_topic:parse(Topic) || Topic <- Topics]. [emqx_topic:parse(Topic) || Topic <- Topics].

View File

@ -33,15 +33,12 @@
-export([del_route/1, del_route/2, del_route/3]). -export([del_route/1, del_route/2, del_route/3]).
-export([has_routes/1, match_routes/1, print_routes/1]). -export([has_routes/1, match_routes/1, print_routes/1]).
-export([topics/0]). -export([topics/0]).
%% gen_server callbacks
%% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-type(destination() :: node() | {binary(), node()}). -type(destination() :: node() | {binary(), node()}).
-record(batch, {enabled, timer, pending}). -record(batch, {enabled, timer, pending}).
-record(state, {pool, id, batch :: #batch{}}). -record(state, {pool, id, batch :: #batch{}}).
-define(ROUTE, emqx_route). -define(ROUTE, emqx_route).

View File

@ -1,73 +1,64 @@
%%%=================================================================== %% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. %%
%%% %% Licensed under the Apache License, Version 2.0 (the "License");
%%% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License.
%%% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at
%%% You may obtain a copy of the License at %%
%%% %% http://www.apache.org/licenses/LICENSE-2.0
%%% http://www.apache.org/licenses/LICENSE-2.0 %%
%%% %% Unless required by applicable law or agreed to in writing, software
%%% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS,
%%% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and
%%% See the License for the specific language governing permissions and %% limitations under the License.
%%% limitations under the License. %%
%%%=================================================================== %% @doc
%% A stateful interaction between a Client and a Server. Some Sessions
%% last only as long as the Network Connection, others can span multiple
%% consecutive Network Connections between a Client and a Server.
%%
%% The Session State in the Server consists of:
%%
%% The existence of a Session, even if the rest of the Session State is empty.
%%
%% The Clients subscriptions, including any Subscription Identifiers.
%%
%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not
%% been completely acknowledged.
%%
%% QoS 1 and QoS 2 messages pending transmission to the Client and OPTIONALLY
%% QoS 0 messages pending transmission to the Client.
%%
%% QoS 2 messages which have been received from the Client, but have not been
%% completely acknowledged.The Will Message and the Will Delay Interval
%%
%% If the Session is currently not connected, the time at which the Session
%% will end and Session State will be discarded.
%% @end
-module(emqx_session). -module(emqx_session).
-behaviour(gen_server). -behaviour(gen_server).
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("emqx_misc.hrl"). -include("emqx_misc.hrl").
-import(emqx_misc, [start_timer/2]). -export([start_link/1, close/1]).
-export([info/1, stats/1]).
-export([resume/2, discard/2]).
-export([subscribe/2]).%%, subscribe/3]).
-export([publish/3]).
-export([puback/2, puback/3]).
-export([pubrec/2, pubrec/3]).
-export([pubrel/2, pubcomp/2]).
-export([unsubscribe/2]).
-import(proplists, [get_value/2, get_value/3]). %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
%% Session API -record(state, {
-export([start_link/1, resume/2, discard/2]). %% Clean Start Flag
%% Management and Monitor API
-export([state/1, info/1, stats/1]).
%% PubSub API
-export([subscribe/2, subscribe/3, publish/2, puback/2, pubrec/2,
pubrel/2, pubcomp/2, unsubscribe/2]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(MQueue, emqx_mqueue).
%% A stateful interaction between a Client and a Server. Some Sessions
%% last only as long as the Network Connection, others can span multiple
%% consecutive Network Connections between a Client and a Server.
%%
%% The Session state in the Server consists of:
%%
%% The existence of a Session, even if the rest of the Session state is empty.
%%
%% The Clients subscriptions.
%%
%% QoS 1 and QoS 2 messages which have been sent to the Client, but have not
%% been completely acknowledged.
%%
%% QoS 1 and QoS 2 messages pending transmission to the Client.
%%
%% QoS 2 messages which have been received from the Client, but have not
%% been completely acknowledged.
%%
%% Optionally, QoS 0 messages pending transmission to the Client.
%%
%% If the session is currently disconnected, the time at which the Session state
%% will be deleted.
-record(state,
{ %% Clean Start Flag
clean_start = false :: boolean(), clean_start = false :: boolean(),
%% Client Binding: local | remote %% Client Binding: local | remote
@ -79,21 +70,25 @@
%% Username %% Username
username :: binary() | undefined, username :: binary() | undefined,
%% Client Pid binding with session %% Client pid binding with session
client_pid :: pid(), client_pid :: pid(),
%% Old Client Pid that has been kickout %% Old client Pid that has been kickout
old_client_pid :: pid(), old_client_pid :: pid(),
%% Next message id of the session %% Pending sub/unsub requests
next_msg_id = 1 :: mqtt_packet_id(), requests :: map(),
%% Next packet id of the session
next_pkt_id = 1 :: mqtt_packet_id(),
%% Max subscriptions
max_subscriptions :: non_neg_integer(), max_subscriptions :: non_neg_integer(),
%% Clients subscriptions. %% Clients Subscriptions.
subscriptions :: map(), subscriptions :: map(),
%% Upgrade Qos? %% Upgrade QoS?
upgrade_qos = false :: boolean(), upgrade_qos = false :: boolean(),
%% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked. %% Client <- Broker: Inflight QoS1, QoS2 messages sent to the client but unacked.
@ -112,18 +107,18 @@
%% QoS 1 and QoS 2 messages pending transmission to the Client. %% QoS 1 and QoS 2 messages pending transmission to the Client.
%% %%
%% Optionally, QoS 0 messages pending transmission to the Client. %% Optionally, QoS 0 messages pending transmission to the Client.
mqueue :: ?MQueue:mqueue(), mqueue :: emqx_mqueue:mqueue(),
%% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel. %% Client -> Broker: Inflight QoS2 messages received from client and waiting for pubrel.
awaiting_rel :: map(), awaiting_rel :: map(),
%% Max Packets that Awaiting PUBREL %% Max Packets Awaiting PUBREL
max_awaiting_rel = 100 :: non_neg_integer(), max_awaiting_rel = 100 :: non_neg_integer(),
%% Awaiting PUBREL timeout %% Awaiting PUBREL Timeout
await_rel_timeout = 20000 :: timeout(), await_rel_timeout = 20000 :: timeout(),
%% Awaiting PUBREL timer %% Awaiting PUBREL Timer
await_rel_timer :: reference() | undefined, await_rel_timer :: reference() | undefined,
%% Session Expiry Interval %% Session Expiry Interval
@ -141,15 +136,18 @@
%% Ignore loop deliver? %% Ignore loop deliver?
ignore_loop_deliver = false :: boolean(), ignore_loop_deliver = false :: boolean(),
%% Created at
created_at :: erlang:timestamp() created_at :: erlang:timestamp()
}). }).
-define(TIMEOUT, 60000). -define(TIMEOUT, 60000).
-define(DEFAULT_SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}).
-define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]). -define(INFO_KEYS, [clean_start, client_id, username, client_pid, binding, created_at]).
-define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid, -define(STATE_KEYS, [clean_start, client_id, username, binding, client_pid, old_client_pid,
next_msg_id, max_subscriptions, subscriptions, upgrade_qos, inflight, next_pkt_id, max_subscriptions, subscriptions, upgrade_qos, inflight,
max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel, max_inflight, retry_interval, mqueue, awaiting_rel, max_awaiting_rel,
await_rel_timeout, expiry_interval, enable_stats, force_gc_count, await_rel_timeout, expiry_interval, enable_stats, force_gc_count,
created_at]). created_at]).
@ -158,82 +156,82 @@
emqx_logger:Level([{client, State#state.client_id}], emqx_logger:Level([{client, State#state.client_id}],
"Session(~s): " ++ Format, [State#state.client_id | Args])). "Session(~s): " ++ Format, [State#state.client_id | Args])).
%% @doc Start a Session %% @doc Start a session
-spec(start_link(map()) -> {ok, pid()} | {error, term()}). -spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}).
start_link(Attrs) -> start_link(SessAttrs) ->
gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, 30000}]).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% PubSub API %% PubSub API
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% @doc Subscribe topics -spec(subscribe(pid(), list({topic(), map()}) |
-spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok).
subscribe(SessionPid, TopicTable) -> %%TODO: the ack function??... %% internal call
gen_server:cast(SessionPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). subscribe(SPid, TopicFilters) when is_list(TopicFilters) ->
%%TODO: Parse the topic filters?
subscribe(SPid, {undefined, #{}, TopicFilters});
%% for mqtt 5.0
subscribe(SPid, SubReq = {PacketId, Props, TopicFilters}) ->
gen_server:cast(SPid, {subscribe, self(), SubReq}).
-spec(subscribe(pid(), mqtt_packet_id(), [{binary(), [emqx_topic:option()]}]) -> ok). -spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}).
subscribe(SessionPid, PacketId, TopicTable) -> %%TODO: the ack function??... publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) ->
From = self(), %% Publish QoS0 message to broker directly
AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, emqx_broker:publish(Msg);
gen_server:cast(SessionPid, {subscribe, From, TopicTable, AckFun}).
%% @doc Publish Message publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) ->
-spec(publish(pid(), message()) -> ok | {error, term()}). %% Publish QoS1 message to broker directly
publish(_SessionPid, Msg = #message{qos = ?QOS_0}) -> emqx_broker:publish(Msg);
%% Publish QoS0 Directly
emqx_broker:publish(Msg), ok;
publish(_SessionPid, Msg = #message{qos = ?QOS_1}) -> publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) ->
%% Publish QoS1 message directly for client will PubAck automatically %% Publish QoS2 message to session
emqx_broker:publish(Msg), ok; gen_server:call(SPid, {publish, PacketId, Msg}, infinity).
publish(SessionPid, Msg = #message{qos = ?QOS_2}) ->
%% Publish QoS2 to Session
gen_server:call(SessionPid, {publish, Msg}, ?TIMEOUT).
%% @doc PubAck Message
-spec(puback(pid(), mqtt_packet_id()) -> ok). -spec(puback(pid(), mqtt_packet_id()) -> ok).
puback(SessionPid, PacketId) -> puback(SPid, PacketId) ->
gen_server:cast(SessionPid, {puback, PacketId}). gen_server:cast(SPid, {puback, PacketId}).
puback(SPid, PacketId, {ReasonCode, Props}) ->
gen_server:cast(SPid, {puback, PacketId, {ReasonCode, Props}}).
-spec(pubrec(pid(), mqtt_packet_id()) -> ok). -spec(pubrec(pid(), mqtt_packet_id()) -> ok).
pubrec(SessionPid, PacketId) -> pubrec(SPid, PacketId) ->
gen_server:cast(SessionPid, {pubrec, PacketId}). gen_server:cast(SPid, {pubrec, PacketId}).
pubrec(SPid, PacketId, {ReasonCode, Props}) ->
gen_server:cast(SPid, {pubrec, PacketId, {ReasonCode, Props}}).
-spec(pubrel(pid(), mqtt_packet_id()) -> ok). -spec(pubrel(pid(), mqtt_packet_id()) -> ok).
pubrel(SessionPid, PacketId) -> pubrel(SPid, PacketId) ->
gen_server:cast(SessionPid, {pubrel, PacketId}). gen_server:cast(SPid, {pubrel, PacketId}).
-spec(pubcomp(pid(), mqtt_packet_id()) -> ok). -spec(pubcomp(pid(), mqtt_packet_id()) -> ok).
pubcomp(SessionPid, PacketId) -> pubcomp(SPid, PacketId) ->
gen_server:cast(SessionPid, {pubcomp, PacketId}). gen_server:cast(SPid, {pubcomp, PacketId}).
%% @doc Unsubscribe the topics -spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok).
-spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) ->
unsubscribe(SessionPid, TopicTable) -> %%TODO: Parse the topic filters?
gen_server:cast(SessionPid, {unsubscribe, self(), TopicTable}). unsubscribe(SPid, {undefined, #{}, TopicFilters});
unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) ->
gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}).
%% @doc Resume the session
-spec(resume(pid(), pid()) -> ok). -spec(resume(pid(), pid()) -> ok).
resume(SessionPid, ClientPid) -> resume(SPid, ClientPid) ->
gen_server:cast(SessionPid, {resume, ClientPid}). gen_server:cast(SPid, {resume, ClientPid}).
%% @doc Get session state
state(SessionPid) when is_pid(SessionPid) ->
gen_server:call(SessionPid, state).
%% @doc Get session info %% @doc Get session info
-spec(info(pid() | #state{}) -> list(tuple())). -spec(info(pid() | #state{}) -> list(tuple())).
info(SessionPid) when is_pid(SessionPid) -> info(SPid) when is_pid(SPid) ->
gen_server:call(SessionPid, info); gen_server:call(SPid, info);
info(State) when is_record(State, state) -> info(State) when is_record(State, state) ->
?record_to_proplist(state, State, ?INFO_KEYS). ?record_to_proplist(state, State, ?INFO_KEYS).
-spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})). -spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})).
stats(SessionPid) when is_pid(SessionPid) -> stats(SPid) when is_pid(SPid) ->
gen_server:call(SessionPid, stats); gen_server:call(SPid, stats);
stats(#state{max_subscriptions = MaxSubscriptions, stats(#state{max_subscriptions = MaxSubscriptions,
subscriptions = Subscriptions, subscriptions = Subscriptions,
@ -247,9 +245,9 @@ stats(#state{max_subscriptions = MaxSubscriptions,
{subscriptions, maps:size(Subscriptions)}, {subscriptions, maps:size(Subscriptions)},
{max_inflight, MaxInflight}, {max_inflight, MaxInflight},
{inflight_len, emqx_inflight:size(Inflight)}, {inflight_len, emqx_inflight:size(Inflight)},
{max_mqueue, ?MQueue:max_len(MQueue)}, {max_mqueue, emqx_mqueue:max_len(MQueue)},
{mqueue_len, ?MQueue:len(MQueue)}, {mqueue_len, emqx_mqueue:len(MQueue)},
{mqueue_dropped, ?MQueue:dropped(MQueue)}, {mqueue_dropped, emqx_mqueue:dropped(MQueue)},
{max_awaiting_rel, MaxAwaitingRel}, {max_awaiting_rel, MaxAwaitingRel},
{awaiting_rel_len, maps:size(AwaitingRel)}, {awaiting_rel_len, maps:size(AwaitingRel)},
{deliver_msg, get(deliver_msg)}, {deliver_msg, get(deliver_msg)},
@ -257,50 +255,54 @@ stats(#state{max_subscriptions = MaxSubscriptions,
%% @doc Discard the session %% @doc Discard the session
-spec(discard(pid(), client_id()) -> ok). -spec(discard(pid(), client_id()) -> ok).
discard(SessionPid, ClientId) -> discard(SPid, ClientId) ->
gen_server:call(SessionPid, {discard, ClientId}). gen_server:call(SPid, {discard, ClientId}, infinity).
%%-------------------------------------------------------------------- -spec(close(pid()) -> ok).
%% gen_server Callbacks close(SPid) ->
%%-------------------------------------------------------------------- gen_server:call(SPid, close, infinity).
init(#{clean_start := CleanStart, %%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init(#{zone := Zone,
client_id := ClientId, client_id := ClientId,
username := Username, client_pid := ClientPid,
client_pid := ClientPid}) -> clean_start := CleanStart,
username := Username}) ->
process_flag(trap_exit, true), process_flag(trap_exit, true),
true = link(ClientPid), true = link(ClientPid),
init_stats([deliver_msg, enqueue_msg]), init_stats([deliver_msg, enqueue_msg]),
{ok, Env} = emqx_config:get_env(session), MaxInflight = emqx_zone:env(Zone, max_inflight),
{ok, QEnv} = emqx_config:get_env(mqueue),
MaxInflight = get_value(max_inflight, Env, 0),
EnableStats = get_value(enable_stats, Env, false),
IgnoreLoopDeliver = get_value(ignore_loop_deliver, Env, false),
MQueue = ?MQueue:new(ClientId, QEnv, emqx_alarm:alarm_fun()),
State = #state{clean_start = CleanStart, State = #state{clean_start = CleanStart,
binding = binding(ClientPid), binding = binding(ClientPid),
client_id = ClientId, client_id = ClientId,
client_pid = ClientPid, client_pid = ClientPid,
username = Username, username = Username,
subscriptions = #{}, subscriptions = #{},
max_subscriptions = get_value(max_subscriptions, Env, 0), max_subscriptions = emqx_zone:env(Zone, max_subscriptions, 0),
upgrade_qos = get_value(upgrade_qos, Env, false), upgrade_qos = emqx_zone:env(Zone, upgrade_qos, false),
max_inflight = MaxInflight, max_inflight = MaxInflight,
inflight = emqx_inflight:new(MaxInflight), inflight = emqx_inflight:new(MaxInflight),
mqueue = MQueue, mqueue = init_mqueue(Zone, ClientId),
retry_interval = get_value(retry_interval, Env), retry_interval = emqx_zone:env(Zone, retry_interval, 0),
awaiting_rel = #{}, awaiting_rel = #{},
await_rel_timeout = get_value(await_rel_timeout, Env), await_rel_timeout = emqx_zone:env(Zone, await_rel_timeout),
max_awaiting_rel = get_value(max_awaiting_rel, Env), max_awaiting_rel = emqx_zone:env(Zone, max_awaiting_rel),
expiry_interval = get_value(expiry_interval, Env), expiry_interval = emqx_zone:env(Zone, session_expiry_interval),
enable_stats = EnableStats, enable_stats = emqx_zone:env(Zone, enable_stats, true),
ignore_loop_deliver = IgnoreLoopDeliver, ignore_loop_deliver = emqx_zone:env(Zone, ignore_loop_deliver, true),
created_at = os:timestamp()}, created_at = os:timestamp()},
emqx_sm:register_session(ClientId, info(State)), emqx_sm:register_session(ClientId, info(State)),
emqx_hooks:run('session.created', [ClientId, Username]), emqx_hooks:run('session.created', [ClientId]),
io:format("Session started: ~p~n", [self()]),
{ok, emit_stats(State), hibernate}. {ok, emit_stats(State), hibernate}.
init_mqueue(Zone, ClientId) ->
emqx_mqueue:new(ClientId, #{type => simple,
max_len => emqx_zone:env(Zone, max_mqueue_len),
store_qos0 => emqx_zone:env(Zone, mqueue_store_qos0)}).
init_stats(Keys) -> init_stats(Keys) ->
lists:foreach(fun(K) -> put(K, 0) end, Keys). lists:foreach(fun(K) -> put(K, 0) end, Keys).
@ -315,19 +317,19 @@ handle_call({discard, ClientPid}, _From, State = #state{client_pid = OldClientPi
?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State), ?LOG(warning, " ~p kickout ~p", [ClientPid, OldClientPid], State),
{stop, {shutdown, conflict}, ok, State}; {stop, {shutdown, conflict}, ok, State};
handle_call({publish, Msg = #message{qos = ?QOS_2, headers = #{packet_id := PacketId}}}, _From, handle_call({publish, PacketId, Msg = #message{qos = ?QOS_2}}, _From,
State = #state{awaiting_rel = AwaitingRel, State = #state{awaiting_rel = AwaitingRel,
await_rel_timer = Timer, await_rel_timer = Timer,
await_rel_timeout = Timeout}) -> await_rel_timeout = Timeout}) ->
case is_awaiting_full(State) of case is_awaiting_full(State) of
false -> false ->
State1 = case Timer == undefined of State1 = case Timer == undefined of
true -> State#state{await_rel_timer = start_timer(Timeout, check_awaiting_rel)}; true -> State#state{await_rel_timer = emqx_misc:start_timer(Timeout, check_awaiting_rel)};
false -> State false -> State
end, end,
reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)}); reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)});
true -> true ->
?LOG(warning, "Dropped Qos2 Message for too many awaiting_rel: ~p", [Msg], State), ?LOG(warning, "Dropped QoS2 Message for too many awaiting_rel: ~p", [Msg], State),
emqx_metrics:inc('messages/qos2/dropped'), emqx_metrics:inc('messages/qos2/dropped'),
reply({error, dropped}, State) reply({error, dropped}, State)
end; end;
@ -338,69 +340,53 @@ handle_call(info, _From, State) ->
handle_call(stats, _From, State) -> handle_call(stats, _From, State) ->
reply(stats(State), State); reply(stats(State), State);
handle_call(state, _From, State) -> handle_call(close, _From, State) ->
reply(?record_to_proplist(state, State, ?STATE_KEYS), State); {stop, normal, State};
handle_call(Req, _From, State) -> handle_call(Req, _From, State) ->
emqx_logger:error("[Session] Unexpected request: ~p", [Req]), emqx_logger:error("[Session] unexpected call: ~p", [Req]),
{reply, ignore, State}. {reply, ignored, State}.
handle_cast({subscribe, From, TopicTable, AckFun}, handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}},
State = #state{client_id = ClientId, State = #state{client_id = ClientId, subscriptions = Subscriptions}) ->
username = Username, ?LOG(info, "Subscribe ~p", [TopicFilters], State),
subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} =
?LOG(info, "Subscribe ~p", [TopicTable], State), lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) ->
{GrantedQos, Subscriptions1} = {[QoS|RcAcc],
lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) ->
io:format("SubOpts: ~p~n", [Opts]),
Fastlane = lists:member(fastlane, Opts),
NewQos = if Fastlane == true -> ?QOS_0; true -> get_value(qos, Opts) end,
SubMap1 =
case maps:find(Topic, SubMap) of case maps:find(Topic, SubMap) of
{ok, NewQos} -> {ok, SubOpts} ->
?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State),
SubMap; SubMap;
{ok, OldQos} -> {ok, OldOpts} ->
emqx_broker:setopts(Topic, ClientId, [{qos, NewQos}]), emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts),
emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]),
?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w", ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State),
[Topic, OldQos, NewQos], State), maps:put(Topic, SubOpts, SubMap);
maps:put(Topic, NewQos, SubMap);
error -> error ->
case Fastlane of emqx_broker:subscribe(Topic, ClientId, SubOpts),
true -> emqx:subscribe(Topic, From, Opts); emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]),
false -> emqx:subscribe(Topic, ClientId, Opts) maps:put(Topic, SubOpts, SubMap)
end, end}
emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), end, {[], Subscriptions}, TopicFilters),
maps:put(Topic, NewQos, SubMap) suback(From, PacketId, lists:reverse(ReasonCodes)),
end, {noreply, emit_stats(State#state{subscriptions = Subscriptions1})};
{[NewQos|QosAcc], SubMap1}
end, {[], Subscriptions}, TopicTable),
io:format("GrantedQos: ~p~n", [GrantedQos]),
AckFun(lists:reverse(GrantedQos)),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate};
handle_cast({unsubscribe, From, TopicTable}, handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}},
State = #state{client_id = ClientId, State = #state{client_id = ClientId, subscriptions = Subscriptions}) ->
username = Username, ?LOG(info, "Unsubscribe ~p", [TopicFilters], State),
subscriptions = Subscriptions}) -> {ReasonCodes, Subscriptions1} =
?LOG(info, "Unsubscribe ~p", [TopicTable], State), lists:foldl(fun(Topic, {RcAcc, SubMap}) ->
Subscriptions1 =
lists:foldl(fun({Topic, Opts}, SubMap) ->
Fastlane = lists:member(fastlane, Opts),
case maps:find(Topic, SubMap) of case maps:find(Topic, SubMap) of
{ok, _Qos} -> {ok, SubOpts} ->
case Fastlane of emqx_broker:unsubscribe(Topic, ClientId),
true -> emqx:unsubscribe(Topic, From); emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]),
false -> emqx:unsubscribe(Topic, ClientId) {[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)};
end,
emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}),
maps:remove(Topic, SubMap);
error -> error ->
SubMap {[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap}
end end
end, Subscriptions, TopicTable), end, {[], Subscriptions}, TopicFilters),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; unsuback(From, PacketId, lists:reverse(ReasonCodes)),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1})};
%% PUBACK: %% PUBACK:
handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) -> handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) ->
@ -501,11 +487,16 @@ handle_cast({resume, ClientPid},
{noreply, emit_stats(dequeue(retry_delivery(true, State1)))}; {noreply, emit_stats(dequeue(retry_delivery(true, State1)))};
handle_cast(Msg, State) -> handle_cast(Msg, State) ->
emqx_logger:error("[Session] Unexpected msg: ~p", [Msg]), emqx_logger:error("[Session] unexpected cast: ~p", [Msg]),
{noreply, State}. {noreply, State}.
%% Ignore Messages delivered by self handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) ->
handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, {noreply, lists:foldl(fun(Msg, NewState) ->
element(2, handle_info({dispatch, Topic, Msg}, NewState))
end, State, Msgs)};
%% Ignore messages delivered by self
handle_info({dispatch, _Topic, #message{from = ClientId}},
State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> State = #state{client_id = ClientId, ignore_loop_deliver = true}) ->
{noreply, State}; {noreply, State};
@ -536,7 +527,7 @@ handle_info({'EXIT', ClientPid, Reason},
client_pid = ClientPid, client_pid = ClientPid,
expiry_interval = Interval}) -> expiry_interval = Interval}) ->
?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State),
ExpireTimer = start_timer(Interval, expired), ExpireTimer = emqx_misc:start_timer(Interval, expired),
State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer},
{noreply, emit_stats(State1), hibernate}; {noreply, emit_stats(State1), hibernate};
@ -545,26 +536,37 @@ handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) ->
{noreply, State, hibernate}; {noreply, State, hibernate};
handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) -> handle_info({'EXIT', Pid, Reason}, State = #state{client_pid = ClientPid}) ->
?LOG(error, "unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p",
?LOG(error, "Unexpected EXIT: client_pid=~p, exit_pid=~p, reason=~p",
[ClientPid, Pid, Reason], State), [ClientPid, Pid, Reason], State),
{noreply, State, hibernate}; {noreply, State, hibernate};
handle_info(Info, State) -> handle_info(Info, State) ->
emqx_logger:error("[Session] Unexpected info: ~p", [Info]), emqx_logger:error("[Session] unexpected info: ~p", [Info]),
{noreply, State}. {noreply, State}.
terminate(Reason, #state{client_id = ClientId, username = Username}) -> terminate(Reason, #state{client_id = ClientId, username = Username}) ->
emqx_hooks:run('session.terminated', [ClientId, Username, Reason]), emqx_hooks:run('session.terminated', [ClientId, Username, Reason]),
emqx_sm:unregister_session(ClientId). emqx_sm:unregister_session(ClientId).
code_change(_OldVsn, Session, _Extra) -> code_change(_OldVsn, State, _Extra) ->
{ok, Session}. {ok, State}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
suback(_From, undefined, _ReasonCodes) ->
ignore;
suback(From, PacketId, ReasonCodes) ->
From ! {deliver, {suback, PacketId, ReasonCodes}}.
unsuback(_From, undefined, _ReasonCodes) ->
ignore;
unsuback(From, PacketId, ReasonCodes) ->
From ! {deliver, {unsuback, PacketId, ReasonCodes}}.
%%------------------------------------------------------------------------------
%% Kickout old client %% Kickout old client
%%--------------------------------------------------------------------
kick(_ClientId, undefined, _Pid) -> kick(_ClientId, undefined, _Pid) ->
ignore; ignore;
@ -576,32 +578,32 @@ kick(ClientId, OldPid, Pid) ->
%% Clean noproc %% Clean noproc
receive {'EXIT', OldPid, _} -> ok after 0 -> ok end. receive {'EXIT', OldPid, _} -> ok after 0 -> ok end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Replay or Retry Delivery %% Replay or Retry Delivery
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Redeliver at once if Force is true %% Redeliver at once if force is true
retry_delivery(Force, State = #state{inflight = Inflight}) -> retry_delivery(Force, State = #state{inflight = Inflight}) ->
case emqx_inflight:is_empty(Inflight) of case emqx_inflight:is_empty(Inflight) of
true -> State; true ->
false -> Msgs = lists:sort(sortfun(inflight), State;
emqx_inflight:values(Inflight)), false ->
Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)),
retry_delivery(Force, Msgs, os:timestamp(), State) retry_delivery(Force, Msgs, os:timestamp(), State)
end. end.
retry_delivery(_Force, [], _Now, State = #state{retry_interval = Interval}) -> retry_delivery(_Force, [], _Now, State = #state{retry_interval = Interval}) ->
State#state{retry_timer = start_timer(Interval, retry_delivery)}; State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)};
retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now, retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now,
State = #state{inflight = Inflight, State = #state{inflight = Inflight, retry_interval = Interval}) ->
retry_interval = Interval}) ->
Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms
if if
Force orelse (Diff >= Interval) -> Force orelse (Diff >= Interval) ->
case {Type, Msg} of case {Type, Msg0} of
{publish, Msg = #message{headers = #{packet_id := PacketId}}} -> {publish, {PacketId, Msg}} ->
redeliver(Msg, State), redeliver({PacketId, Msg}, State),
Inflight1 = emqx_inflight:update(PacketId, {publish, Msg, Now}, Inflight), Inflight1 = emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight),
retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}); retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1});
{pubrel, PacketId} -> {pubrel, PacketId} ->
redeliver({pubrel, PacketId}, State), redeliver({pubrel, PacketId}, State),
@ -609,12 +611,12 @@ retry_delivery(Force, [{Type, Msg, Ts} | Msgs], Now,
retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1}) retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1})
end; end;
true -> true ->
State#state{retry_timer = start_timer(Interval - Diff, retry_delivery)} State#state{retry_timer = emqx_misc:start_timer(Interval - Diff, retry_delivery)}
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Expire Awaiting Rel %% Expire Awaiting Rel
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) -> expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) ->
case maps:size(AwaitingRel) of case maps:size(AwaitingRel) of
@ -635,12 +637,12 @@ expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs],
emqx_metrics:inc('messages/qos2/dropped'), emqx_metrics:inc('messages/qos2/dropped'),
expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}); expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)});
Diff -> Diff ->
State#state{await_rel_timer = start_timer(Timeout - Diff, check_awaiting_rel)} State#state{await_rel_timer = emqx_misc:start_timer(Timeout - Diff, check_awaiting_rel)}
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Sort Inflight, AwaitingRel %% Sort Inflight, AwaitingRel
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
sortfun(inflight) -> sortfun(inflight) ->
fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end; fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end;
@ -651,18 +653,18 @@ sortfun(awaiting_rel) ->
Ts1 < Ts2 Ts1 < Ts2
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Check awaiting rel %% Check awaiting rel
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
is_awaiting_full(#state{max_awaiting_rel = 0}) -> is_awaiting_full(#state{max_awaiting_rel = 0}) ->
false; false;
is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) -> is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) ->
maps:size(AwaitingRel) >= MaxLen. maps:size(AwaitingRel) >= MaxLen.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Dispatch Messages %% Dispatch Messages
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Enqueue message if the client has been disconnected %% Enqueue message if the client has been disconnected
dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) -> dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) ->
@ -673,53 +675,50 @@ dispatch(Msg, State = #state{client_id = ClientId, client_pid = undefined}) ->
%% Deliver qos0 message directly to client %% Deliver qos0 message directly to client
dispatch(Msg = #message{qos = ?QOS0}, State) -> dispatch(Msg = #message{qos = ?QOS0}, State) ->
deliver(Msg, State), State; deliver(undefined, Msg, State), State;
dispatch(Msg = #message{qos = QoS}, dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight})
State = #state{next_msg_id = MsgId, inflight = Inflight})
when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 -> when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 ->
case emqx_inflight:is_full(Inflight) of case emqx_inflight:is_full(Inflight) of
true -> true ->
enqueue_msg(Msg, State); enqueue_msg(Msg, State);
false -> false ->
Msg1 = emqx_message:set_header(packet_id, MsgId, Msg), deliver(PacketId, Msg, State),
deliver(Msg1, State), await(PacketId, Msg, next_pkt_id(State))
await(Msg1, next_msg_id(State))
end. end.
enqueue_msg(Msg, State = #state{mqueue = Q}) -> enqueue_msg(Msg, State = #state{mqueue = Q}) ->
inc_stats(enqueue_msg), inc_stats(enqueue_msg),
State#state{mqueue = ?MQueue:in(Msg, Q)}. State#state{mqueue = emqx_mqueue:in(Msg, Q)}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Deliver %% Deliver
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
redeliver(Msg = #message{qos = QoS}, State) -> redeliver({PacketId, Msg = #message{qos = QoS}}, State) ->
deliver(if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State); deliver(PacketId, if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State);
redeliver({pubrel, PacketId}, #state{client_pid = Pid}) -> redeliver({pubrel, PacketId}, #state{client_pid = Pid}) ->
Pid ! {redeliver, {?PUBREL, PacketId}}. Pid ! {deliver, {pubrel, PacketId}}.
deliver(Msg, #state{client_pid = Pid, binding = local}) -> deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) ->
inc_stats(deliver_msg), Pid ! {deliver, Msg}; inc_stats(deliver_msg), Pid ! {deliver, {publish, PacketId, Msg}};
deliver(Msg, #state{client_pid = Pid, binding = remote}) -> deliver(PacketId, Msg, #state{client_pid = Pid, binding = remote}) ->
inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, Msg}]). inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Awaiting ACK for QoS1/QoS2 Messages %% Awaiting ACK for QoS1/QoS2 Messages
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
await(Msg = #message{headers = #{packet_id := PacketId}}, await(PacketId, Msg, State = #state{inflight = Inflight,
State = #state{inflight = Inflight,
retry_timer = RetryTimer, retry_timer = RetryTimer,
retry_interval = Interval}) -> retry_interval = Interval}) ->
%% Start retry timer if the Inflight is still empty %% Start retry timer if the Inflight is still empty
State1 = case RetryTimer == undefined of State1 = case RetryTimer == undefined of
true -> State#state{retry_timer = start_timer(Interval, retry_delivery)}; true -> State#state{retry_timer = emqx_misc:start_timer(Interval, retry_delivery)};
false -> State false -> State
end, end,
State1#state{inflight = emqx_inflight:insert(PacketId, {publish, Msg, os:timestamp()}, Inflight)}. State1#state{inflight = emqx_inflight:insert(PacketId, {publish, {PacketId, Msg}, os:timestamp()}, Inflight)}.
acked(puback, PacketId, State = #state{client_id = ClientId, acked(puback, PacketId, State = #state{client_id = ClientId,
username = Username, username = Username,
@ -751,9 +750,9 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId,
acked(pubcomp, PacketId, State = #state{inflight = Inflight}) -> acked(pubcomp, PacketId, State = #state{inflight = Inflight}) ->
State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}. State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Dequeue %% Dequeue
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Do nothing if client is disconnected %% Do nothing if client is disconnected
dequeue(State = #state{client_pid = undefined}) -> dequeue(State = #state{client_pid = undefined}) ->
@ -766,7 +765,7 @@ dequeue(State = #state{inflight = Inflight}) ->
end. end.
dequeue2(State = #state{mqueue = Q}) -> dequeue2(State = #state{mqueue = Q}) ->
case ?MQueue:out(Q) of case emqx_mqueue:out(Q) of
{empty, _Q} -> {empty, _Q} ->
State; State;
{{value, Msg}, Q1} -> {{value, Msg}, Q1} ->
@ -774,43 +773,37 @@ dequeue2(State = #state{mqueue = Q}) ->
dequeue(dispatch(Msg, State#state{mqueue = Q1})) dequeue(dispatch(Msg, State#state{mqueue = Q1}))
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Tune QoS %% Tune QoS
%%--------------------------------------------------------------------
tune_qos(Topic, Msg = #message{qos = PubQoS}, tune_qos(Topic, Msg = #message{qos = PubQoS},
#state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) -> #state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) ->
case maps:find(Topic, SubMap) of case maps:find(Topic, SubMap) of
{ok, SubQoS} when UpgradeQoS andalso (SubQoS > PubQoS) -> {ok, #{qos := SubQoS}} when UpgradeQoS andalso (SubQoS > PubQoS) ->
Msg#message{qos = SubQoS}; Msg#message{qos = SubQoS};
{ok, SubQoS} when (not UpgradeQoS) andalso (SubQoS < PubQoS) -> {ok, #{qos := SubQoS}} when (not UpgradeQoS) andalso (SubQoS < PubQoS) ->
Msg#message{qos = SubQoS}; Msg#message{qos = SubQoS};
{ok, _} -> {ok, _} -> Msg;
Msg; error -> Msg
error ->
Msg
end. end.
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Reset Dup %% Reset Dup
%%--------------------------------------------------------------------
reset_dup(Msg) -> reset_dup(Msg) ->
emqx_message:unset_flag(dup, Msg). emqx_message:unset_flag(dup, Msg).
%%-------------------------------------------------------------------- %%------------------------------------------------------------------------------
%% Next Msg Id %% Next Msg Id
%%--------------------------------------------------------------------
next_msg_id(State = #state{next_msg_id = 16#FFFF}) -> next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) ->
State#state{next_msg_id = 1}; State#state{next_pkt_id = 1};
next_msg_id(State = #state{next_msg_id = Id}) -> next_pkt_id(State = #state{next_pkt_id = Id}) ->
State#state{next_msg_id = Id + 1}. State#state{next_pkt_id = Id + 1}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Emit session stats %% Emit session stats
%%--------------------------------------------------------------------
emit_stats(State = #state{enable_stats = false}) -> emit_stats(State = #state{enable_stats = false}) ->
State; State;
@ -822,7 +815,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Helper functions %% Helper functions
%%--------------------------------------------------------------------
reply(Reply, State) -> reply(Reply, State) ->
{reply, Reply, State, hibernate}. {reply, Reply, State, hibernate}.

View File

@ -38,7 +38,7 @@
-define(TAB, emqx_shared_subscription). -define(TAB, emqx_shared_subscription).
-record(state, {pmon}). -record(state, {pmon}).
-record(shared_subscription, {group, topic, subpid}). -record(emqx_shared_subscription, {group, topic, subpid}).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Mnesia bootstrap %% Mnesia bootstrap
@ -48,8 +48,8 @@ mnesia(boot) ->
ok = ekka_mnesia:create_table(?TAB, [ ok = ekka_mnesia:create_table(?TAB, [
{type, bag}, {type, bag},
{ram_copies, [node()]}, {ram_copies, [node()]},
{record_name, shared_subscription}, {record_name, emqx_shared_subscription},
{attributes, record_info(fields, shared_subscription)}]); {attributes, record_info(fields, emqx_shared_subscription)}]);
mnesia(copy) -> mnesia(copy) ->
ok = ekka_mnesia:copy_table(?TAB). ok = ekka_mnesia:copy_table(?TAB).
@ -78,7 +78,7 @@ unsubscribe(Group, Topic, SubPid) when is_pid(SubPid) ->
mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)). mnesia:dirty_delete_object(?TAB, record(Group, Topic, SubPid)).
record(Group, Topic, SubPid) -> record(Group, Topic, SubPid) ->
#shared_subscription{group = Group, topic = Topic, subpid = SubPid}. #emqx_shared_subscription{group = Group, topic = Topic, subpid = SubPid}.
%% TODO: dispatch strategy, ensure the delivery... %% TODO: dispatch strategy, ensure the delivery...
dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) -> dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) ->
@ -110,7 +110,7 @@ init([]) ->
init_monitors() -> init_monitors() ->
mnesia:foldl( mnesia:foldl(
fun(#shared_subscription{subpid = SubPid}, Mon) -> fun(#emqx_shared_subscription{subpid = SubPid}, Mon) ->
emqx_pmon:monitor(SubPid, Mon) emqx_pmon:monitor(SubPid, Mon)
end, emqx_pmon:new(), ?TAB). end, emqx_pmon:new(), ?TAB).
@ -126,11 +126,11 @@ handle_cast(Msg, State) ->
{noreply, State}. {noreply, State}.
handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) -> handle_info({mnesia_table_event, {write, NewRecord, _}}, State = #state{pmon = PMon}) ->
#shared_subscription{subpid = SubPid} = NewRecord, #emqx_shared_subscription{subpid = SubPid} = NewRecord,
{noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})}; {noreply, update_stats(State#state{pmon = emqx_pmon:monitor(SubPid, PMon)})};
handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) -> handle_info({mnesia_table_event, {delete_object, OldRecord, _}}, State = #state{pmon = PMon}) ->
#shared_subscription{subpid = SubPid} = OldRecord, #emqx_shared_subscription{subpid = SubPid} = OldRecord,
{noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})}; {noreply, update_stats(State#state{pmon = emqx_pmon:demonitor(SubPid, PMon)})};
handle_info({mnesia_table_event, _Event}, State) -> handle_info({mnesia_table_event, _Event}, State) ->
@ -138,7 +138,7 @@ handle_info({mnesia_table_event, _Event}, State) ->
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) -> handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{pmon = PMon}) ->
emqx_logger:info("[SharedSub] shared subscriber down: ~p", [SubPid]), emqx_logger:info("[SharedSub] shared subscriber down: ~p", [SubPid]),
mnesia:async_dirty(fun cleanup_down/1, [SubPid]), cleanup_down(SubPid),
{noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})}; {noreply, update_stats(State#state{pmon = emqx_pmon:erase(SubPid, PMon)})};
handle_info(Info, State) -> handle_info(Info, State) ->
@ -156,8 +156,10 @@ code_change(_OldVsn, State, _Extra) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
cleanup_down(SubPid) -> cleanup_down(SubPid) ->
lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end, lists:foreach(
mnesia:match_object(#shared_subscription{_ = '_', subpid = SubPid})). fun(Record) ->
mnesia:dirty_delete_object(?TAB, Record)
end,mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})).
update_stats(State) -> update_stats(State) ->
emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State. emqx_stats:setstat('subscriptions/shared/count', 'subscriptions/shared/max', ets:info(?TAB, size)), State.

View File

@ -20,7 +20,7 @@
-export([start_link/0]). -export([start_link/0]).
-export([open_session/1, lookup_session/1, close_session/1]). -export([open_session/1, lookup_session/1, close_session/1, lookup_session_pid/1]).
-export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]). -export([resume_session/1, resume_session/2, discard_session/1, discard_session/2]).
-export([register_session/2, get_session_attrs/1, unregister_session/1]). -export([register_session/2, get_session_attrs/1, unregister_session/1]).
-export([get_session_stats/1, set_session_stats/2]). -export([get_session_stats/1, set_session_stats/2]).
@ -46,7 +46,7 @@ start_link() ->
gen_server:start_link({local, ?SM}, ?MODULE, [], []). gen_server:start_link({local, ?SM}, ?MODULE, [], []).
%% @doc Open a session. %% @doc Open a session.
-spec(open_session(map()) -> {ok, pid()} | {error, term()}). -spec(open_session(map()) -> {ok, pid(), boolean()} | {error, term()}).
open_session(Attrs = #{clean_start := true, open_session(Attrs = #{clean_start := true,
client_id := ClientId, client_id := ClientId,
client_pid := ClientPid}) -> client_pid := ClientPid}) ->

View File

@ -22,7 +22,8 @@
-export([is_enabled/0]). -export([is_enabled/0]).
-export([register_session/1, lookup_session/1, unregister_session/1]). -export([register_session/1, lookup_session/1, unregister_session/1]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-define(REGISTRY, ?MODULE). -define(REGISTRY, ?MODULE).
-define(TAB, emqx_session_registry). -define(TAB, emqx_session_registry).

View File

@ -1,18 +1,16 @@
%%%=================================================================== %% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. %%
%%% %% Licensed under the Apache License, Version 2.0 (the "License");
%%% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License.
%%% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at
%%% You may obtain a copy of the License at %%
%%% %% http://www.apache.org/licenses/LICENSE-2.0
%%% http://www.apache.org/licenses/LICENSE-2.0 %%
%%% %% Unless required by applicable law or agreed to in writing, software
%%% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS,
%%% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and
%%% See the License for the specific language governing permissions and %% limitations under the License.
%%% limitations under the License.
%%%===================================================================
-module(emqx_sup). -module(emqx_sup).
@ -65,6 +63,7 @@ init([]) ->
BrokerSup = supervisor_spec(emqx_broker_sup), BrokerSup = supervisor_spec(emqx_broker_sup),
%% BridgeSup %% BridgeSup
BridgeSup = supervisor_spec(emqx_bridge_sup_sup), BridgeSup = supervisor_spec(emqx_bridge_sup_sup),
BridgeSup1 = supervisor_spec(emqx_bridge1_sup),
%% AccessControl %% AccessControl
AccessControl = worker_spec(emqx_access_control), AccessControl = worker_spec(emqx_access_control),
%% Session Manager %% Session Manager
@ -73,8 +72,6 @@ init([]) ->
SessionSup = supervisor_spec(emqx_session_sup), SessionSup = supervisor_spec(emqx_session_sup),
%% Connection Manager %% Connection Manager
CMSup = supervisor_spec(emqx_cm_sup), CMSup = supervisor_spec(emqx_cm_sup),
%% WebSocket Connection Sup
WSConnSup = supervisor_spec(emqx_ws_connection_sup),
%% Sys Sup %% Sys Sup
SysSup = supervisor_spec(emqx_sys_sup), SysSup = supervisor_spec(emqx_sys_sup),
{ok, {{one_for_all, 0, 1}, {ok, {{one_for_all, 0, 1},
@ -82,11 +79,11 @@ init([]) ->
RouterSup, RouterSup,
BrokerSup, BrokerSup,
BridgeSup, BridgeSup,
BridgeSup1,
AccessControl, AccessControl,
SMSup, SMSup,
SessionSup, SessionSup,
CMSup, CMSup,
WSConnSup,
SysSup]}}. SysSup]}}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -171,6 +171,8 @@ publish(metrics, Metrics) ->
safe_publish(Topic, Payload) -> safe_publish(Topic, Payload) ->
safe_publish(Topic, #{}, Payload). safe_publish(Topic, #{}, Payload).
safe_publish(Topic, Flags, Payload) -> safe_publish(Topic, Flags, Payload) ->
Flags1 = maps:merge(#{sys => true, qos => 0}, Flags), emqx_broker:safe_publish(
emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))). emqx_message:set_flags(
maps:merge(#{sys => true}, Flags),
emqx_message:make(?SYS, Topic, iolist_to_binary(Payload)))).

View File

@ -43,7 +43,7 @@ init([Opts]) ->
{ok, start_timer(#state{events = []})}. {ok, start_timer(#state{events = []})}.
start_timer(State) -> start_timer(State) ->
State#state{timer = emqx_misc:start_timer(timer:seconds(2), tick)}. State#state{timer = emqx_misc:start_timer(timer:seconds(2), reset)}.
parse_opt(Opts) -> parse_opt(Opts) ->
parse_opt(Opts, []). parse_opt(Opts, []).
@ -126,7 +126,7 @@ handle_info({monitor, SusPid, busy_dist_port, Port}, State) ->
safe_publish(busy_dist_port, WarnMsg) safe_publish(busy_dist_port, WarnMsg)
end, State); end, State);
handle_info(reset, State) -> handle_info({timeout, _Ref, reset}, State) ->
{noreply, State#state{events = []}, hibernate}; {noreply, State#state{events = []}, hibernate};
handle_info(Info, State) -> handle_info(Info, State) ->
@ -158,5 +158,5 @@ safe_publish(Event, WarnMsg) ->
emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))). emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))).
sysmon_msg(Topic, Payload) -> sysmon_msg(Topic, Payload) ->
emqx_message:new(?SYSMON, #{sys => true, qos => 0}, Topic, Payload). emqx_message:make(?SYSMON, #{sys => true}, Topic, Payload).

View File

@ -31,8 +31,7 @@ init([]) ->
type => worker, type => worker,
modules => [emqx_sys]}, modules => [emqx_sys]},
Sysmon = #{id => sys_mon, Sysmon = #{id => sys_mon,
start => {emqx_sys_mon, start_link, start => {emqx_sys_mon, start_link, [emqx_config:get_env(sysmon, [])]},
[emqx_config:get_env(sysmon, [])]},
restart => permanent, restart => permanent,
shutdown => 5000, shutdown => 5000,
type => worker, type => worker,

View File

@ -14,23 +14,16 @@
-module(emqx_time). -module(emqx_time).
-export([seed/0, now_secs/0, now_secs/1, now_ms/0, now_ms/1, ts_from_ms/1]). -export([seed/0, now_secs/0, now_ms/0, now_ms/1]).
seed() -> seed() ->
rand:seed(exsplus, erlang:timestamp()). rand:seed(exsplus, erlang:timestamp()).
now_secs() ->
erlang:system_time(second).
now_ms() -> now_ms() ->
now_ms(os:timestamp()). erlang:system_time(millisecond).
now_ms({MegaSecs, Secs, MicroSecs}) -> now_ms({MegaSecs, Secs, MicroSecs}) ->
(MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000). (MegaSecs * 1000000 + Secs) * 1000 + round(MicroSecs/1000).
now_secs() ->
now_secs(os:timestamp()).
now_secs({MegaSecs, Secs, _MicroSecs}) ->
MegaSecs * 1000000 + Secs.
ts_from_ms(Ms) ->
{Ms div 1000000, Ms rem 1000000, 0}.

View File

@ -25,10 +25,9 @@
-type(word() :: '' | '+' | '#' | binary()). -type(word() :: '' | '+' | '#' | binary()).
-type(words() :: list(word())). -type(words() :: list(word())).
-type(option() :: {qos, mqtt_qos()} | {share, '$queue' | binary()}).
-type(triple() :: {root | binary(), word(), binary()}). -type(triple() :: {root | binary(), word(), binary()}).
-export_type([option/0, word/0, triple/0]). -export_type([word/0, triple/0]).
-define(MAX_TOPIC_LEN, 4096). -define(MAX_TOPIC_LEN, 4096).
@ -163,20 +162,21 @@ join(Words) ->
end, {true, <<>>}, [bin(W) || W <- Words]), end, {true, <<>>}, [bin(W) || W <- Words]),
Bin. Bin.
-spec(parse(topic()) -> {topic(), [option()]}). -spec(parse(topic()) -> {topic(), #{}}).
parse(Topic) when is_binary(Topic) -> parse(Topic) when is_binary(Topic) ->
parse(Topic, []). parse(Topic, #{}).
parse(Topic = <<"$queue/", Topic1/binary>>, Options) -> parse(Topic = <<"$queue/", Topic1/binary>>, Options) ->
case lists:keyfind(share, 1, Options) of case maps:find(share, Options) of
{share, _} -> error({invalid_topic, Topic}); {ok, _} -> error({invalid_topic, Topic});
false -> parse(Topic1, [{share, '$queue'} | Options]) error -> parse(Topic1, maps:put(share, '$queue', Options))
end; end;
parse(Topic = <<"$share/", Topic1/binary>>, Options) -> parse(Topic = <<"$share/", Topic1/binary>>, Options) ->
case lists:keyfind(share, 1, Options) of case maps:find(share, Options) of
{share, _} -> error({invalid_topic, Topic}); {ok, _} -> error({invalid_topic, Topic});
false -> [Group, Topic2] = binary:split(Topic1, <<"/">>), error -> [Group, Topic2] = binary:split(Topic1, <<"/">>),
{Topic2, [{share, Group} | Options]} {Topic2, maps:put(share, Group, Options)}
end; end;
parse(Topic, Options) -> {Topic, Options}. parse(Topic, Options) ->
{Topic, Options}.

View File

@ -19,6 +19,7 @@
-include("emqx.hrl"). -include("emqx.hrl").
-export([start_link/0]). -export([start_link/0]).
-export([trace/2]).
-export([start_trace/2, lookup_traces/0, stop_trace/1]). -export([start_trace/2, lookup_traces/0, stop_trace/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
@ -31,14 +32,17 @@
-define(TRACER, ?MODULE). -define(TRACER, ?MODULE).
-define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]). -define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]).
%%------------------------------------------------------------------------------
%% Start the tracer
%%------------------------------------------------------------------------------
-spec(start_link() -> {ok, pid()} | ignore | {error, term()}). -spec(start_link() -> {ok, pid()} | ignore | {error, term()}).
start_link() -> start_link() ->
gen_server:start_link({local, ?TRACER}, ?MODULE, [], []). gen_server:start_link({local, ?TRACER}, ?MODULE, [], []).
trace(publish, #message{topic = <<"$SYS/", _/binary>>}) ->
%% Dont' trace '$SYS' publish
ignore;
trace(publish, #message{from = From, topic = Topic, payload = Payload})
when is_binary(From); is_atom(From) ->
emqx_logger:info([{client, From}, {topic, Topic}], "~s PUBLISH to ~s: ~p", [From, Topic, Payload]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
%% Start/Stop trace %% Start/Stop trace
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -17,21 +17,15 @@
-module(emqx_vm). -module(emqx_vm).
-export([schedulers/0]). -export([schedulers/0]).
-export([microsecs/0]). -export([microsecs/0]).
-export([loads/0, get_system_info/0, get_system_info/1, mem_info/0, scheduler_usage/1]). -export([loads/0, get_system_info/0, get_system_info/1, mem_info/0, scheduler_usage/1]).
-export([get_memory/0]). -export([get_memory/0]).
-export([get_process_list/0, get_process_info/0, get_process_info/1, -export([get_process_list/0, get_process_info/0, get_process_info/1,
get_process_gc/0, get_process_gc/1, get_process_gc/0, get_process_gc/1,
get_process_group_leader_info/1, get_process_group_leader_info/1,
get_process_limit/0]). get_process_limit/0]).
-export([get_ets_list/0, get_ets_info/0, get_ets_info/1, -export([get_ets_list/0, get_ets_info/0, get_ets_info/1,
get_ets_object/0, get_ets_object/1]). get_ets_object/0, get_ets_object/1]).
-export([get_port_types/0, get_port_info/0, get_port_info/1]). -export([get_port_types/0, get_port_info/0, get_port_info/1]).
-define(UTIL_ALLOCATORS, [temp_alloc, -define(UTIL_ALLOCATORS, [temp_alloc,
@ -205,7 +199,7 @@ mem_info() ->
{used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}]. {used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}].
ftos(F) -> ftos(F) ->
[S] = io_lib:format("~.2f", [F]), S. S = io_lib:format("~.2f", [F]), S.
%%%% erlang vm scheduler_usage fun copied from recon %%%% erlang vm scheduler_usage fun copied from recon
scheduler_usage(Interval) when is_integer(Interval) -> scheduler_usage(Interval) when is_integer(Interval) ->

View File

@ -1,119 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Inc. 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_ws).
-include("emqx_mqtt.hrl").
-import(proplists, [get_value/3]).
-export([handle_request/1, ws_loop/3]).
%% WebSocket Loop State
-record(wsocket_state, {peername, client_pid, max_packet_size, parser}).
-define(WSLOG(Level, Format, Args, State),
emqx_logger:Level("WsClient(~s): " ++ Format,
[esockd_net:format(State#wsocket_state.peername) | Args])).
handle_request(Req) ->
handle_request(Req:get(method), Req:get(path), Req).
%%--------------------------------------------------------------------
%% MQTT Over WebSocket
%%--------------------------------------------------------------------
handle_request('GET', "/mqtt", Req) ->
emqx_logger:debug("WebSocket Connection from: ~s", [Req:get(peer)]),
Upgrade = Req:get_header_value("Upgrade"),
Proto = check_protocol_header(Req),
case {is_websocket(Upgrade), Proto} of
{true, "mqtt" ++ _Vsn} ->
case Req:get(peername) of
{ok, Peername} ->
{ok, ProtoEnv} = emqx_config:get_env(protocol),
PacketSize = get_value(max_packet_size, ProtoEnv, ?MAX_PACKET_SIZE),
Parser = emqx_parser:initial_state(PacketSize),
%% Upgrade WebSocket.
{ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection(Req, fun ?MODULE:ws_loop/3),
{ok, ClientPid} = emqx_ws_conn_sup:start_connection(self(), Req, ReplyChannel),
ReentryWs(#wsocket_state{peername = Peername,
parser = Parser,
max_packet_size = PacketSize,
client_pid = ClientPid});
{error, Reason} ->
emqx_logger:error("Get peername with error ~s", [Reason]),
Req:respond({400, [], <<"Bad Request">>})
end;
{false, _} ->
emqx_logger:error("Not WebSocket: Upgrade = ~s", [Upgrade]),
Req:respond({400, [], <<"Bad Request">>});
{_, Proto} ->
emqx_logger:error("WebSocket with error Protocol: ~s", [Proto]),
Req:respond({400, [], <<"Bad WebSocket Protocol">>})
end;
handle_request(Method, Path, Req) ->
emqx_logger:error("Unexpected WS Request: ~s ~s", [Method, Path]),
Req:not_found().
is_websocket(Upgrade) ->
(not emqx_config:get_env(websocket_check_upgrade_header, true)) orelse
(Upgrade =/= undefined andalso string:to_lower(Upgrade) =:= "websocket").
check_protocol_header(Req) ->
case emqx_config:get_env(websocket_protocol_header, false) of
true -> get_protocol_header(Req);
false -> "mqtt-v3.1.1"
end.
get_protocol_header(Req) ->
case Req:get_header_value("EMQ-WebSocket-Protocol") of
undefined -> Req:get_header_value("Sec-WebSocket-Protocol");
Proto -> Proto
end.
%%--------------------------------------------------------------------
%% Receive Loop
%%--------------------------------------------------------------------
%% @doc WebSocket frame receive loop.
ws_loop(<<>>, State, _ReplyChannel) ->
State;
ws_loop([<<>>], State, _ReplyChannel) ->
State;
ws_loop(Data, State = #wsocket_state{client_pid = ClientPid, parser = Parser}, ReplyChannel) ->
?WSLOG(debug, "RECV ~p", [Data], State),
emqx_metrics:inc('bytes/received', iolist_size(Data)),
case catch emqx_parser:parse(iolist_to_binary(Data), Parser) of
{more, NewParser} ->
State#wsocket_state{parser = NewParser};
{ok, Packet, Rest} ->
gen_server:cast(ClientPid, {received, Packet}),
ws_loop(Rest, reset_parser(State), ReplyChannel);
{error, Error} ->
?WSLOG(error, "Frame error: ~p", [Error], State),
exit({shutdown, Error});
{'EXIT', Reason} ->
?WSLOG(error, "Frame error: ~p", [Reason], State),
?WSLOG(error, "Error data: ~p", [Data], State),
exit({shutdown, parser_error})
end.
reset_parser(State = #wsocket_state{max_packet_size = PacketSize}) ->
State#wsocket_state{parser = emqx_parser:initial_state(PacketSize)}.

View File

@ -1,228 +1,224 @@
%%%=================================================================== %% Copyright (c) 2018 EMQ Technologies Co., Ltd. All Rights Reserved.
%%% Copyright (c) 2013-2018 EMQ Inc. All rights reserved. %%
%%% %% Licensed under the Apache License, Version 2.0 (the "License");
%%% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License.
%%% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at
%%% You may obtain a copy of the License at %%
%%% %% http://www.apache.org/licenses/LICENSE-2.0
%%% http://www.apache.org/licenses/LICENSE-2.0 %%
%%% %% Unless required by applicable law or agreed to in writing, software
%%% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS,
%%% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and
%%% See the License for the specific language governing permissions and %% limitations under the License.
%%% limitations under the License.
%%%===================================================================
-module(emqx_ws_connection). -module(emqx_ws_connection).
-behaviour(gen_server).
-include("emqx.hrl"). -include("emqx.hrl").
-include("emqx_mqtt.hrl"). -include("emqx_mqtt.hrl").
-include("emqx_misc.hrl").
-import(proplists, [get_value/2, get_value/3]). -export([info/1]).
-export([stats/1]).
%% API Exports -export([kick/1]).
-export([start_link/4]).
%% Management and Monitor API
-export([info/1, stats/1, kick/1, clean_acl_cache/2]).
%% SUB/UNSUB Asynchronously
-export([subscribe/2, unsubscribe/2]).
%% Get the session proc?
-export([session/1]). -export([session/1]).
%% gen_server Function Exports %% websocket callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/2]).
terminate/2, code_change/3]). -export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).
-export([terminate/3]).
%% TODO: remove ... -record(state, {
-export([handle_pre_hibernate/1]). request,
options,
peername,
sockname,
proto_state,
parser_state,
keepalive,
enable_stats,
stats_timer,
idle_timeout,
shutdown_reason
}).
%% WebSocket Client State -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt]).
-record(wsclient_state, {ws_pid, transport, socket, peername,
proto_state, keepalive, enable_stats,
force_gc_count}).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). -define(INFO_KEYS, [peername, sockname]).
-define(WSLOG(Level, Format, Args, State), -define(WSLOG(Level, Format, Args, State),
emqx_logger:Level("WsClient(~s): " ++ Format, lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])).
[esockd_net:format(State#wsclient_state.peername) | Args])).
%% @doc Start WebSocket Client. %%------------------------------------------------------------------------------
start_link(Env, WsPid, Req, ReplyChannel) -> %% API
gen_server:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel], %%------------------------------------------------------------------------------
[[{hibernate_after, 10000}]]).
info(CPid) -> info(WSPid) ->
gen_server:call(CPid, info). call(WSPid, info).
stats(CPid) -> stats(WSPid) ->
gen_server:call(CPid, stats). call(WSPid, stats).
kick(CPid) -> kick(WSPid) ->
gen_server:call(CPid, kick). call(WSPid, kick).
subscribe(CPid, TopicTable) -> session(WSPid) ->
CPid ! {subscribe, TopicTable}. call(WSPid, session).
unsubscribe(CPid, Topics) -> call(WSPid, Req) ->
CPid ! {unsubscribe, Topics}. Mref = erlang:monitor(process, WSPid),
WSPid ! {call, {self(), Mref}, Req},
session(CPid) -> receive
gen_server:call(CPid, session). {Mref, Reply} ->
erlang:demonitor(Mref, [flush]),
clean_acl_cache(CPid, Topic) -> Reply;
gen_server:call(CPid, {clean_acl_cache, Topic}). {'DOWN', Mref, _, _, Reason} ->
exit(Reason)
%%-------------------------------------------------------------------- after 5000 ->
%% gen_server Callbacks erlang:demonitor(Mref, [flush]),
%%-------------------------------------------------------------------- exit(timeout)
init([Env, WsPid, Req, ReplyChannel]) ->
process_flag(trap_exit, true),
true = link(WsPid),
Transport = mochiweb_request:get(transport, Req),
Sock = mochiweb_request:get(socket, Req),
case mochiweb_request:get(peername, Req) of
{ok, Peername} ->
Headers = mochiweb_headers:to_list(mochiweb_request:get(headers, Req)),
ProtoState = emqx_protocol:init(Transport, Sock, Peername, send_fun(ReplyChannel),
[{ws_initial_headers, Headers} | Env]),
IdleTimeout = get_value(client_idle_timeout, Env, 30000),
EnableStats = get_value(client_enable_stats, Env, false),
ForceGcCount = emqx_gc:conn_max_gc_count(),
{ok, #wsclient_state{transport = Transport,
socket = Sock,
ws_pid = WsPid,
peername = Peername,
proto_state = ProtoState,
enable_stats = EnableStats,
force_gc_count = ForceGcCount},
IdleTimeout, {backoff, 2000, 2000, 20000}, ?MODULE};
{error, enotconn} -> Transport:fast_close(Sock),
exit(WsPid, normal),
exit(normal);
{error, Reason} -> Transport:fast_close(Sock),
exit(WsPid, normal),
exit({shutdown, Reason})
end. end.
handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) -> %%------------------------------------------------------------------------------
erlang:garbage_collect(WsPid), %% WebSocket callbacks
{hibernate, emqx_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}. %%------------------------------------------------------------------------------
handle_call(info, From, State = #wsclient_state{peername = Peername, init(Req, Opts) ->
io:format("Opts: ~p~n", [Opts]),
case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of
undefined ->
{cowboy_websocket, Req, #state{}};
Subprotocols ->
case lists:member(<<"mqtt">>, Subprotocols) of
true ->
Resp = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"mqtt">>, Req),
{cowboy_websocket, Resp, #state{request = Req, options = Opts}, #{idle_timeout => 86400000}};
false ->
{ok, cowboy_req:reply(400, Req), #state{}}
end
end.
websocket_init(#state{request = Req, options = Options}) ->
Peername = cowboy_req:peer(Req),
Sockname = cowboy_req:sock(Req),
Peercert = cowboy_req:cert(Req),
ProtoState = emqx_protocol:init(#{peername => Peername,
sockname => Sockname,
peercert => Peercert,
sendfun => send_fun(self())}, Options),
ParserState = emqx_protocol:parser(ProtoState),
Zone = proplists:get_value(zone, Options),
EnableStats = emqx_zone:env(Zone, enable_stats, true),
IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000),
lists:foreach(fun(Stat) -> put(Stat, 0) end, ?SOCK_STATS),
{ok, #state{peername = Peername,
sockname = Sockname,
parser_state = ParserState,
proto_state = ProtoState,
enable_stats = EnableStats,
idle_timeout = IdleTimout}}.
send_fun(WsPid) ->
fun(Data) ->
BinSize = iolist_size(Data),
emqx_metrics:inc('bytes/sent', BinSize),
put(send_oct, get(send_oct) + BinSize),
put(send_cnt, get(send_cnt) + 1),
WsPid ! {binary, iolist_to_binary(Data)}
end.
stat_fun() ->
fun() -> {ok, get(recv_oct)} end.
websocket_handle({binary, <<>>}, State) ->
{ok, State};
websocket_handle({binary, [<<>>]}, State) ->
{ok, State};
websocket_handle({binary, Data}, State = #state{parser_state = ParserState,
proto_state = ProtoState}) -> proto_state = ProtoState}) ->
Info = [{websocket, true}, {peername, Peername} | emqx_protocol:info(ProtoState)], BinSize = iolist_size(Data),
{reply, Stats, _, _} = handle_call(stats, From, State), put(recv_oct, get(recv_oct) + BinSize),
reply(lists:append(Info, Stats), State); ?WSLOG(debug, "RECV ~p", [Data], State),
emqx_metrics:inc('bytes/received', BinSize),
handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) -> case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of
reply(lists:append([emqx_misc:proc_stats(), {more, NewParserState} ->
wsock_stats(State), {ok, State#state{parser_state = NewParserState}};
emqx_protocol:stats(ProtoState)]), State); {ok, Packet, Rest} ->
handle_call(kick, _From, State) ->
{stop, {shutdown, kick}, ok, State};
handle_call(session, _From, State = #wsclient_state{proto_state = ProtoState}) ->
reply(emqx_protocol:session(ProtoState), State);
handle_call({clean_acl_cache, Topic}, _From, State) ->
erase({acl, publish, Topic}),
reply(ok, State);
handle_call(Req, _From, State) ->
?WSLOG(error, "Unexpected request: ~p", [Req], State),
reply({error, unexpected_request}, State).
handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState}) ->
emqx_metrics:received(Packet), emqx_metrics:received(Packet),
put(recv_cnt, get(recv_cnt) + 1),
case emqx_protocol:received(Packet, ProtoState) of case emqx_protocol:received(Packet, ProtoState) of
{ok, ProtoState1} -> {ok, ProtoState1} ->
{noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate}; websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1}));
{error, Error} -> {error, Error} ->
?WSLOG(error, "Protocol error - ~p", [Error], State), ?WSLOG(error, "Protocol error - ~p", [Error], State),
shutdown(Error, State); {stop, State};
{error, Error, ProtoState1} -> {error, Error, ProtoState1} ->
shutdown(Error, State#wsclient_state{proto_state = ProtoState1}); shutdown(Error, State#state{proto_state = ProtoState1});
{stop, Reason, ProtoState1} -> {stop, Reason, ProtoState1} ->
stop(Reason, State#wsclient_state{proto_state = ProtoState1}) shutdown(Reason, State#state{proto_state = ProtoState1})
end;
{error, Error} ->
?WSLOG(error, "Frame error: ~p", [Error], State),
{stop, State};
{'EXIT', Reason} ->
?WSLOG(error, "Frame error:~p~nFrame data: ~p", [Reason, Data], State),
{stop, State}
end.
websocket_info({call, From, info}, State = #state{peername = Peername,
sockname = Sockname,
proto_state = ProtoState}) ->
ProtoInfo = emqx_protocol:info(ProtoState),
ConnInfo = [{socktype, websocket}, {conn_state, running},
{peername, Peername}, {sockname, Sockname}],
gen_server:reply(From, lists:append([ConnInfo, ProtoInfo])),
{ok, State};
websocket_info({call, From, stats}, State = #state{proto_state = ProtoState}) ->
Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(), emqx_protocol:stats(ProtoState)]),
gen_server:reply(From, Stats),
{ok, State};
websocket_info({call, From, kick}, State) ->
gen_server:reply(From, ok),
shutdown(kick, State);
websocket_info({call, From, session}, State = #state{proto_state = ProtoState}) ->
gen_server:reply(From, emqx_protocol:session(ProtoState)),
{ok, State};
websocket_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) ->
case emqx_protocol:deliver(PubOrAck, ProtoState) of
{ok, ProtoState1} ->
{ok, ensure_stats_timer(State#state{proto_state = ProtoState1})};
{error, Reason} ->
shutdown(Reason, State);
{error, Reason, ProtoState1} ->
shutdown(Reason, State#state{proto_state = ProtoState1})
end; end;
handle_cast(Msg, State) -> websocket_info(emit_stats, State = #state{proto_state = ProtoState}) ->
?WSLOG(error, "Unexpected Msg: ~p", [Msg], State), Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(),
{noreply, State, hibernate}. emqx_protocol:stats(ProtoState)]),
emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats),
{ok, State#state{stats_timer = undefined}, hibernate};
handle_info({subscribe, TopicTable}, State) -> websocket_info({keepalive, start, Interval}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:subscribe(TopicTable, ProtoState)
end, State);
handle_info({unsubscribe, Topics}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:unsubscribe(Topics, ProtoState)
end, State);
handle_info({suback, PacketId, GrantedQos}, State) ->
with_proto(
fun(ProtoState) ->
Packet = ?SUBACK_PACKET(PacketId, GrantedQos),
emqx_protocol:send(Packet, ProtoState)
end, State);
%% Fastlane
handle_info({dispatch, _Topic, Message}, State) ->
handle_info({deliver, Message#message{qos = ?QOS_0}}, State);
handle_info({deliver, Message}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:send(Message, ProtoState)
end, gc(State));
handle_info({redeliver, {?PUBREL, PacketId}}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:pubrel(PacketId, ProtoState)
end, State);
handle_info(emit_stats, State) ->
{noreply, emit_stats(State), hibernate};
handle_info(timeout, State) ->
shutdown(idle_timeout, State);
handle_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State),
shutdown(conflict, State);
handle_info({shutdown, Reason}, State) ->
shutdown(Reason, State);
handle_info({keepalive, start, Interval},
State = #wsclient_state{transport = Transport, socket =Sock}) ->
?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State), ?WSLOG(debug, "Keepalive at the interval of ~p", [Interval], State),
case emqx_keepalive:start(stat_fun(Transport, Sock), Interval, {keepalive, check}) of case emqx_keepalive:start(stat_fun(), Interval, {keepalive, check}) of
{ok, KeepAlive} -> {ok, KeepAlive} ->
{noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate}; {ok, State#state{keepalive = KeepAlive}};
{error, Error} -> {error, Error} ->
?WSLOG(warning, "Keepalive error - ~p", [Error], State), ?WSLOG(warning, "Keepalive error - ~p", [Error], State),
shutdown(Error, State) shutdown(Error, State)
end; end;
handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) -> websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) ->
case emqx_keepalive:check(KeepAlive) of case emqx_keepalive:check(KeepAlive) of
{ok, KeepAlive1} -> {ok, KeepAlive1} ->
{noreply, emit_stats(State#wsclient_state{keepalive = KeepAlive1}), hibernate}; {ok, State#state{keepalive = KeepAlive1}};
{error, timeout} -> {error, timeout} ->
?WSLOG(debug, "Keepalive Timeout!", [], State), ?WSLOG(debug, "Keepalive Timeout!", [], State),
shutdown(keepalive_timeout, State); shutdown(keepalive_timeout, State);
@ -231,92 +227,46 @@ handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive})
shutdown(keepalive_error, State) shutdown(keepalive_error, State)
end; end;
handle_info({'EXIT', WsPid, normal}, State = #wsclient_state{ws_pid = WsPid}) -> websocket_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
stop(normal, State); ?WSLOG(warning, "clientid '~s' conflict with ~p", [ClientId, NewPid], State),
shutdown(conflict, State);
handle_info({'EXIT', WsPid, Reason}, State = #wsclient_state{ws_pid = WsPid}) -> websocket_info({binary, Data}, State) ->
?WSLOG(error, "shutdown: ~p",[Reason], State), {reply, {binary, Data}, State};
websocket_info({shutdown, Reason}, State) ->
shutdown(Reason, State); shutdown(Reason, State);
%% The session process exited unexpectedly. websocket_info(Info, State) ->
handle_info({'EXIT', Pid, Reason}, State = #wsclient_state{proto_state = ProtoState}) -> ?WSLOG(error, "unexpected info: ~p", [Info], State),
case emqx_protocol:session(ProtoState) of
Pid -> stop(Reason, State);
_ -> ?WSLOG(error, "Unexpected EXIT: ~p, Reason: ~p", [Pid, Reason], State),
{noreply, State, hibernate}
end;
handle_info(Info, State) ->
?WSLOG(error, "Unexpected Info: ~p", [Info], State),
{noreply, State, hibernate}.
terminate(Reason, #wsclient_state{proto_state = ProtoState, keepalive = KeepAlive}) ->
emqx_keepalive:cancel(KeepAlive),
case Reason of
{shutdown, Error} ->
emqx_protocol:shutdown(Error, ProtoState);
_ ->
emqx_protocol:shutdown(Reason, ProtoState)
end.
code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
%%-------------------------------------------------------------------- terminate(SockError, _Req, #state{keepalive = Keepalive,
%% Internal functions proto_state = ProtoState,
%%-------------------------------------------------------------------- shutdown_reason = Reason}) ->
emqx_keepalive:cancel(Keepalive),
send_fun(ReplyChannel) -> io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]),
Self = self(), case Reason of
fun(Packet) -> undefined ->
Data = emqx_frame:serialize(Packet), ok;
emqx_metrics:inc('bytes/sent', iolist_size(Data)), %%emqx_protocol:shutdown(SockError, ProtoState);
case ReplyChannel({binary, Data}) of _ ->
ok -> ok; ok%%emqx_protocol:shutdown(Reason, ProtoState)
{error, Reason} -> Self ! {shutdown, Reason}
end
end. end.
stat_fun(Transport, Sock) -> reset_parser(State = #state{proto_state = ProtoState}) ->
fun() -> State#state{parser_state = emqx_protocol:parser(ProtoState)}.
case Transport:getstat(Sock, [recv_oct]) of
{ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
{error, Error} -> {error, Error}
end
end.
emit_stats(State = #wsclient_state{proto_state = ProtoState}) -> ensure_stats_timer(State = #state{enable_stats = true,
emit_stats(emqx_protocol:clientid(ProtoState), State). stats_timer = undefined,
idle_timeout = Timeout}) ->
emit_stats(_ClientId, State = #wsclient_state{enable_stats = false}) -> State#state{stats_timer = erlang:send_after(Timeout, self(), emit_stats)};
State; ensure_stats_timer(State) ->
emit_stats(undefined, State) ->
State;
emit_stats(ClientId, State) ->
{reply, Stats, _, _} = handle_call(stats, undefined, State),
emqx_cm:set_client_stats(ClientId, Stats),
State. State.
wsock_stats(#wsclient_state{transport = Transport, socket = Sock}) ->
case Transport:getstat(Sock, ?SOCK_STATS) of
{ok, Ss} -> Ss;
{error, _} -> []
end.
with_proto(Fun, State = #wsclient_state{proto_state = ProtoState}) ->
{ok, ProtoState1} = Fun(ProtoState),
{noreply, State#wsclient_state{proto_state = ProtoState1}, hibernate}.
reply(Reply, State) ->
{reply, Reply, State, hibernate}.
shutdown(Reason, State) -> shutdown(Reason, State) ->
stop({shutdown, Reason}, State). {stop, State#state{shutdown_reason = Reason}}.
stop(Reason, State) -> wsock_stats() ->
{stop, Reason, State}. [{Key, get(Key)} || Key <- ?SOCK_STATS].
gc(State) ->
Cb = fun() -> emit_stats(State) end,
emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb).

View File

@ -1,44 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2013-2018 EMQ Inc. 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_ws_connection_sup).
-behavior(supervisor).
-export([start_link/0, start_connection/3]).
-export([init/1]).
-spec(start_link() -> {ok, pid()}).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% @doc Start a MQTT/WebSocket Connection.
-spec(start_connection(pid(), mochiweb_request:request(), fun()) -> {ok, pid()}).
start_connection(WsPid, Req, ReplyChannel) ->
supervisor:start_child(?MODULE, [WsPid, Req, ReplyChannel]).
%%--------------------------------------------------------------------
%% Supervisor callbacks
%%--------------------------------------------------------------------
init([]) ->
%%TODO: Cannot upgrade the environments, Use zone?
Env = lists:append(emqx_config:get_env(client, []), emqx_config:get_env(protocol, [])),
{ok, {{simple_one_for_one, 0, 1},
[{ws_connection, {emqx_ws_connection, start_link, [Env]},
temporary, 5000, worker, [emqx_ws_connection]}]}}.

86
src/emqx_zone.erl Normal file
View File

@ -0,0 +1,86 @@
%% Copyright (c) 2018 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_zone).
-behaviour(gen_server).
-export([start_link/0]).
-export([env/2, env/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-record(state, {timer}).
-define(TAB, ?MODULE).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
env(undefined, Par) ->
emqx_config:get_env(Par);
env(Zone, Par) ->
env(Zone, Par, undefined).
env(undefined, Par, Default) ->
emqx_config:get_env(Par, Default);
env(Zone, Par, Default) ->
try ets:lookup_element(?TAB, {Zone, Par}, 2)
catch error:badarg ->
emqx_config:get_env(Par, Default)
end.
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init([]) ->
_ = emqx_tables:new(?TAB, [set, {read_concurrency, true}]),
{ok, element(2, handle_info(reload, #state{}))}.
handle_call(Req, _From, State) ->
emqx_logger:error("[Zone] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast(Msg, State) ->
emqx_logger:error("[Zone] unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(reload, State) ->
lists:foreach(
fun({Zone, Opts}) ->
[ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Opts]
end, emqx_config:get_env(zones, [])),
{noreply, ensure_reload_timer(State), hibernate};
handle_info(Info, State) ->
emqx_logger:error("[Zone] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%------------------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------------------
ensure_reload_timer(State) ->
State#state{timer = erlang:send_after(5000, self(), reload)}.

View File

@ -26,11 +26,14 @@
all() -> [t_base62_encode]. all() -> [t_base62_encode].
t_base62_encode(_) -> t_base62_encode(_) ->
10 = ?BASE62:decode(?BASE62:encode(10)), <<"10">> = ?BASE62:decode(?BASE62:encode(<<"10">>)),
100 = ?BASE62:decode(?BASE62:encode(100)), <<"100">> = ?BASE62:decode(?BASE62:encode(<<"100">>)),
9999 = ?BASE62:decode(?BASE62:encode(9999)), <<"9999">> = ?BASE62:decode(?BASE62:encode(<<"9999">>)),
65535 = ?BASE62:decode(?BASE62:encode(65535)), <<"65535">> = ?BASE62:decode(?BASE62:encode(<<"65535">>)),
<<X:128/unsigned-big-integer>> = emqx_guid:gen(), <<X:128/unsigned-big-integer>> = emqx_guid:gen(),
<<Y:128/unsigned-big-integer>> = emqx_guid:gen(), <<Y:128/unsigned-big-integer>> = emqx_guid:gen(),
X = ?BASE62:decode(?BASE62:encode(X)), X = ?BASE62:decode(?BASE62:encode(X), integer),
Y = ?BASE62:decode(?BASE62:encode(Y)). Y = ?BASE62:decode(?BASE62:encode(Y), integer),
<<"helloworld">> = ?BASE62:decode(?BASE62:encode("helloworld")),
"helloworld" = ?BASE62:decode(?BASE62:encode("helloworld", string), string).