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
otp_release:
- 20.0
- 20.1
- 21.0
- 21.0.4
script:
- make

View File

@ -4,19 +4,15 @@ PROJECT = emqx
PROJECT_DESCRIPTION = EMQ X Broker
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_getopt = git https://github.com/jcomellas/getopt v0.8.2
dep_lager = git https://github.com/basho/lager master
dep_lager_syslog = git https://github.com/basho/lager_syslog
dep_esockd = git https://github.com/emqtt/esockd emqx30
dep_ekka = git https://github.com/emqtt/ekka develop
dep_mochiweb = git https://github.com/emqtt/mochiweb emqx30
dep_pbkdf2 = git https://github.com/emqtt/pbkdf2 2.0.1
dep_bcrypt = git https://github.com/smarkets/erlang-bcrypt master
dep_gproc = git https://github.com/uwiger/gproc 0.8.0
dep_gen_rpc = git https://github.com/emqx/gen_rpc 2.1.1
dep_lager = git https://github.com/erlang-lager/lager 3.6.4
dep_esockd = git https://github.com/emqx/esockd emqx30
dep_ekka = git https://github.com/emqx/ekka emqx30
dep_cowboy = git https://github.com/ninenines/cowboy 2.4.0
dep_clique = git https://github.com/emqx/clique
NO_AUTOPATCH = gen_rpc cuttlefish
@ -25,7 +21,7 @@ ERLC_OPTS += +debug_info
ERLC_OPTS += +'{parse_transform, lager_transform}'
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
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
DIALYZER_DIRS := ebin/
DIALYZER_OPTS := --verbose --statistics -Werror_handling \
-Wrace_conditions #-Wunmatched_returns
DIALYZER_OPTS := --verbose --statistics -Werror_handling -Wrace_conditions #-Wunmatched_returns
include erlang.mk

4
TODO
View File

@ -1,5 +1,7 @@
1. Update the README.md
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, "#"}]}.
{allow, all}.

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -78,18 +78,17 @@
%% Maximum ClientId Length.
%%--------------------------------------------------------------------
-define(MAX_CLIENTID_LEN, 1024).
-define(MAX_CLIENTID_LEN, 65535).
%%--------------------------------------------------------------------
%% MQTT Client
%%--------------------------------------------------------------------
-record(mqtt_client,
{ client_id :: binary() | undefined,
-record(mqtt_client, {
client_id :: binary() | undefined,
client_pid :: pid(),
username :: binary() | undefined,
peername :: {inet:ip_address(), inet:port_number()},
clean_sess :: boolean(),
clean_start :: boolean(),
proto_ver :: mqtt_version(),
keepalive = 0 :: non_neg_integer(),
will_topic :: undefined | binary(),
@ -207,8 +206,8 @@
%% MQTT Packet Fixed Header
%%--------------------------------------------------------------------
-record(mqtt_packet_header,
{ type = ?RESERVED :: mqtt_packet_type(),
-record(mqtt_packet_header, {
type = ?RESERVED :: mqtt_packet_type(),
dup = false :: boolean(),
qos = ?QOS_0 :: mqtt_qos(),
retain = false :: boolean()
@ -235,8 +234,8 @@
-type(mqtt_subopts() :: #mqtt_subopts{}).
-record(mqtt_packet_connect,
{ proto_name = <<"MQTT">> :: binary(),
-record(mqtt_packet_connect, {
proto_name = <<"MQTT">> :: binary(),
proto_ver = ?MQTT_PROTO_V4 :: mqtt_version(),
is_bridge = false :: boolean(),
clean_start = true :: boolean(),
@ -253,55 +252,55 @@
password = undefined :: undefined | binary()
}).
-record(mqtt_packet_connack,
{ ack_flags :: 0 | 1,
-record(mqtt_packet_connack, {
ack_flags :: 0 | 1,
reason_code :: mqtt_reason_code(),
properties :: mqtt_properties()
}).
-record(mqtt_packet_publish,
{ topic_name :: mqtt_topic(),
-record(mqtt_packet_publish, {
topic_name :: mqtt_topic(),
packet_id :: mqtt_packet_id(),
properties :: mqtt_properties()
}).
-record(mqtt_packet_puback,
{ packet_id :: mqtt_packet_id(),
-record(mqtt_packet_puback, {
packet_id :: mqtt_packet_id(),
reason_code :: mqtt_reason_code(),
properties :: mqtt_properties()
}).
-record(mqtt_packet_subscribe,
{ packet_id :: mqtt_packet_id(),
-record(mqtt_packet_subscribe, {
packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(),
topic_filters :: [{mqtt_topic(), mqtt_subopts()}]
}).
-record(mqtt_packet_suback,
{ packet_id :: mqtt_packet_id(),
-record(mqtt_packet_suback, {
packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(),
reason_codes :: list(mqtt_reason_code())
}).
-record(mqtt_packet_unsubscribe,
{ packet_id :: mqtt_packet_id(),
-record(mqtt_packet_unsubscribe, {
packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(),
topic_filters :: [mqtt_topic()]
}).
-record(mqtt_packet_unsuback,
{ packet_id :: mqtt_packet_id(),
-record(mqtt_packet_unsuback, {
packet_id :: mqtt_packet_id(),
properties :: mqtt_properties(),
reason_codes :: list(mqtt_reason_code())
}).
-record(mqtt_packet_disconnect,
{ reason_code :: mqtt_reason_code(),
-record(mqtt_packet_disconnect, {
reason_code :: mqtt_reason_code(),
properties :: mqtt_properties()
}).
-record(mqtt_packet_auth,
{ reason_code :: mqtt_reason_code(),
-record(mqtt_packet_auth, {
reason_code :: mqtt_reason_code(),
properties :: mqtt_properties()
}).
@ -309,8 +308,8 @@
%% MQTT Control Packet
%%--------------------------------------------------------------------
-record(mqtt_packet,
{ header :: #mqtt_packet_header{},
-record(mqtt_packet, {
header :: #mqtt_packet_header{},
variable :: #mqtt_packet_connect{}
| #mqtt_packet_connack{}
| #mqtt_packet_publish{}
@ -364,9 +363,12 @@
variable = #mqtt_packet_auth{reason_code = ReasonCode,
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,
qos = Qos},
qos = QoS},
variable = #mqtt_packet_publish{packet_id = PacketId}}).
-define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload),
@ -464,6 +466,11 @@
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
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),
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
variable = #mqtt_packet_unsuback{packet_id = PacketId,
@ -486,43 +493,3 @@
-define(PACKET(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"},
{modules,[]},
{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,[]},
{mod,{emqx_app,[]}},
{maintainers,["Feng Lee <feng@emqx.io>"]},

View File

@ -74,7 +74,7 @@ subscribe(Topic) ->
subscribe(Topic, 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) ->
emqx_broker:subscribe(iolist_to_binary(Topic), list_to_subid(Subscriber), Options).
@ -95,11 +95,11 @@ unsubscribe(Topic, Subscriber) ->
%% PubSub management API
%%--------------------------------------------------------------------
-spec(get_subopts(topic() | string(), subscriber()) -> [suboption()]).
-spec(get_subopts(topic() | string(), subscriber()) -> subopts()).
get_subopts(Topic, 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) ->
emqx_broker:set_subopts(iolist_to_binary(Topic), list_to_subid(Subscriber), Options).
@ -110,7 +110,7 @@ topics() -> emqx_router:topics().
subscribers(Topic) ->
emqx_broker:subscribers(iolist_to_binary(Topic)).
-spec(subscriptions(subscriber() | string()) -> [{topic(), list(suboption())}]).
-spec(subscriptions(subscriber() | string()) -> [{topic(), subopts()}]).
subscriptions(Subscriber) ->
emqx_broker:subscriptions(list_to_subid(Subscriber)).
@ -166,8 +166,8 @@ shutdown() ->
shutdown(Reason) ->
emqx_logger:error("emqx shutdown for ~s", [Reason]),
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() ->
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,
register_mod/3, register_mod/4, unregister_mod/2, stop/0]).
-export([clean_acl_cache/1, clean_acl_cache/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
@ -50,9 +52,9 @@ start_link() ->
register_default_mod() ->
case emqx_config:get_env(acl_file) of
{ok, File} ->
emqx_access_control:register_mod(acl, emqx_acl_internal, [File]);
undefined -> ok
undefined -> ok;
File ->
emqx_access_control:register_mod(acl, emqx_acl_internal, [File])
end.
%% @doc Authenticate Client.
@ -127,6 +129,12 @@ tab_key(acl) -> acl_modules.
stop() ->
gen_server:stop(?MODULE, normal, infinity).
%%TODO: Support ACL cache...
clean_acl_cache(_ClientId) ->
ok.
clean_acl_cache(_ClientId, _Topic) ->
ok.
%%--------------------------------------------------------------------
%% 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}) ->
case encode_alarm(Alarm) of
{ok, Json} ->
emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json));
ok = emqx_broker:safe_publish(alarm_msg(alert, AlarmId, Json));
{error, Reason} ->
emqx_logger:error("[AlarmMgr] Failed to encode alarm: ~p", [Reason])
end,
@ -131,7 +131,9 @@ encode_alarm(#alarm{id = AlarmId, severity = Severity, title = Title,
{ts, emqx_time:now_secs(Ts)}]).
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) ->
emqx_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>);

View File

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

View File

@ -16,10 +16,6 @@
-include("emqx.hrl").
-export([passwd_hash/2]).
-type(hash_type() :: plain | md5 | sha | sha256 | pbkdf2 | bcrypt).
%%--------------------------------------------------------------------
%% Authentication behavihour
%%--------------------------------------------------------------------
@ -46,33 +42,3 @@ behaviour_info(_Other) ->
-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).
-export([encode/1, decode/1]).
-export([encode/1,
encode/2,
decode/1,
decode/2]).
%% @doc Encode an integer to base62 string
-spec(encode(non_neg_integer()) -> binary()).
encode(I) when is_integer(I) andalso I > 0 ->
list_to_binary(encode(I, [])).
%% @doc Encode any data to base62 binary
-spec encode(string()
| integer()
| 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 ->
[char(I) | Acc];
encode(I, Acc) ->
encode(I div 62, [char(I rem 62) | Acc]).
%% encode(D, string) ->
%% binary_to_list(encode(D)).
char(I) when I < 10 ->
$0 + I;
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()).
%% @doc Decode base62 binary to origin data binary
decode(L) when is_list(L) ->
decode(list_to_binary(L));
decode(B) when is_binary(B) ->
decode(binary_to_list(B));
decode(S) when is_list(S) ->
decode(S, 0).
decode(B, <<>>).
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 ->
C - $A + 10;
byte(C) when $a =< C andalso C =< $z ->
C - $a + 36.
%%====================================================================
%% Internal functions
%%====================================================================
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}]),
State = parse_opts(Options, #state{node = Node, subtopic = Topic}),
%%TODO: queue....
MQueue = emqx_mqueue:new(qname(Node, Topic),
[{max_len, State#state.max_queue_len}],
emqx_alarm:alarm_fun()),
MQueue = emqx_mqueue:new(qname(Node, Topic), [{max_len, State#state.max_queue_len}]),
{ok, State#state{pool = Pool, id = Id, mqueue = MQueue}};
false ->
{stop, {cannot_connect_node, Node}}
@ -74,8 +72,8 @@ init([Pool, Id, Node, Topic, Options]) ->
parse_opts([], State) ->
State;
parse_opts([{qos, Qos} | Opts], State) ->
parse_opts(Opts, State#state{qos = Qos});
parse_opts([{qos, QoS} | Opts], State) ->
parse_opts(Opts, State#state{qos = QoS});
parse_opts([{topic_suffix, Suffix} | Opts], State) ->
parse_opts(Opts, State#state{topic_suffix= Suffix});
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([subscribe/1, subscribe/2, subscribe/3, subscribe/4]).
-export([publish/1, publish/2, safe_publish/1]).
-export([unsubscribe/1, unsubscribe/2]).
-export([multi_subscribe/1, multi_subscribe/2, multi_subscribe/3]).
-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([subscriptions/1, subscribers/1, subscribed/2]).
-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,
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(TIMEOUT, 120000).
%% ETS tables
-define(SUBOPTION, emqx_suboption).
-define(SUBSCRIBER, emqx_subscriber).
-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()}).
start_link(Pool, Id) ->
gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)},
?MODULE, [Pool, Id], [{hibernate_after, 2000}]).
gen_server:start_link({local, emqx_misc:proc_name(?MODULE, Id)}, ?MODULE,
[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, self()).
-spec(subscribe(topic(), subscriber()) -> ok | {error, term()}).
subscribe(Topic, Subscriber) when is_binary(Topic) ->
subscribe(Topic, Subscriber, []).
-spec(subscribe(topic(), pid() | subid()) -> ok).
subscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) ->
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()}).
subscribe(Topic, Subscriber, Options) when is_binary(Topic) ->
subscribe(Topic, Subscriber, Options, ?TIMEOUT).
-spec(subscribe(topic(), pid() | subid(), subid() | subopts()) -> ok).
subscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
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())
-> ok | {error, term()}).
subscribe(Topic, Subscriber, Options, Timeout) ->
{Topic1, Options1} = emqx_topic:parse(Topic, Options),
SubReq = {subscribe, Topic1, with_subpid(Subscriber), Options1},
async_call(pick(Subscriber), SubReq, Timeout).
-spec(subscribe(topic(), pid(), subid(), subopts()) -> ok).
subscribe(Topic, SubPid, SubId, SubOpts) when is_binary(Topic), is_pid(SubPid),
?is_subid(SubId), is_map(SubOpts) ->
Broker = pick(SubPid),
SubReq = #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts},
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, self()).
-spec(unsubscribe(topic(), subscriber()) -> ok | {error, term()}).
unsubscribe(Topic, Subscriber) when is_binary(Topic) ->
unsubscribe(Topic, Subscriber, ?TIMEOUT).
-spec(unsubscribe(topic(), pid() | subid()) -> ok).
unsubscribe(Topic, SubPid) when is_binary(Topic), is_pid(SubPid) ->
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()}).
unsubscribe(Topic, Subscriber, Timeout) ->
{Topic1, _} = emqx_topic:parse(Topic),
UnsubReq = {unsubscribe, Topic1, with_subpid(Subscriber)},
async_call(pick(Subscriber), UnsubReq, Timeout).
-spec(unsubscribe(topic(), pid(), subid()) -> ok).
unsubscribe(Topic, SubPid, SubId) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
Broker = pick(SubPid),
UnsubReq = #unsubscribe{topic = Topic, subpid = SubPid, subid = SubId},
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
%%------------------------------------------------------------------------------
-spec(publish(topic(), payload()) -> delivery() | stopped).
publish(Topic, Payload) when is_binary(Topic), is_binary(Payload) ->
publish(emqx_message:make(Topic, Payload)).
-spec(publish(message()) -> {ok, delivery()} | {error, stopped}).
publish(Msg = #message{from = From}) ->
%% Hook to trace?
_ = trace(publish, From, Msg),
-spec(publish(message()) -> delivery()).
publish(Msg) when is_record(Msg, message) ->
_ = emqx_tracer:trace(publish, Msg),
case emqx_hooks:run('message.publish', [], Msg) of
{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} ->
emqx_logger:warning("Stop publishing: ~s", [emqx_message:format(Msg1)]),
{error, stopped}
emqx_logger:warning("Stop publishing: ~p", [Msg]), delivery(Msg1)
end.
%% called internally
safe_publish(Msg) ->
%% Called internally
safe_publish(Msg) when is_record(Msg, message) ->
try
publish(Msg)
catch
_:Error:Stacktrace ->
emqx_logger:error("[Broker] publish error: ~p~n~p~n~p", [Error, Msg, Stacktrace])
after
ok
end.
%%------------------------------------------------------------------------------
%% Trace
%%------------------------------------------------------------------------------
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]).
delivery(Msg) ->
#delivery{sender = self(), message = Msg, flows = []}.
%%------------------------------------------------------------------------------
%% Route
@ -186,12 +227,8 @@ dispatch(Topic, Delivery = #delivery{message = Msg, flows = Flows}) ->
Delivery#delivery{flows = [{dispatch, Topic, Count}|Flows]}
end.
dispatch(SubPid, Topic, Msg) when is_pid(SubPid) ->
dispatch({SubPid, _SubId}, Topic, Msg) when is_pid(SubPid) ->
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) ->
ignored.
@ -200,12 +237,11 @@ dropped(<<"$SYS/", _/binary>>) ->
dropped(_Topic) ->
emqx_metrics:inc('messages/dropped').
delivery(Msg) ->
#delivery{node = node(), message = Msg, flows = []}.
-spec(subscribers(topic()) -> [subscriber()]).
subscribers(Topic) ->
try ets:lookup_element(?SUBSCRIBER, Topic, 2) catch error:badarg -> [] end.
-spec(subscriptions(subscriber()) -> [{topic(), subopts()}]).
subscriptions(Subscriber) ->
lists:map(fun({_, {share, _Group, Topic}}) ->
subscription(Topic, Subscriber);
@ -216,51 +252,49 @@ subscriptions(Subscriber) ->
subscription(Topic, Subscriber) ->
{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) ->
ets:member(?SUBOPTION, {Topic, SubPid});
subscribed(Topic, SubId) when is_binary(Topic), is_binary(SubId) ->
length(ets:match_object(?SUBOPTION, {{Topic, {SubId, '_'}}, '_'}, 1)) == 1;
subscribed(Topic, {SubId, SubPid}) when is_binary(Topic), is_binary(SubId), is_pid(SubPid) ->
ets:member(?SUBOPTION, {Topic, {SubId, SubPid}}).
length(ets:match_object(?SUBOPTION, {{Topic, {SubPid, '_'}}, '_'}, 1)) == 1;
subscribed(Topic, SubId) when is_binary(Topic), ?is_subid(SubId) ->
length(ets:match_object(?SUBOPTION, {{Topic, {'_', SubId}}, '_'}, 1)) == 1;
subscribed(Topic, {SubPid, SubId}) when is_binary(Topic), is_pid(SubPid), ?is_subid(SubId) ->
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) ->
try ets:lookup_element(?SUBOPTION, {Topic, Subscriber}, 2)
catch error:badarg -> []
end.
-spec(set_subopts(topic(), subscriber(), [suboption()]) -> boolean()).
set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_list(Opts) ->
-spec(set_subopts(topic(), subscriber(), subopts()) -> boolean()).
set_subopts(Topic, Subscriber, Opts) when is_binary(Topic), is_map(Opts) ->
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of
[{_, OldOpts}] ->
Opts1 = lists:usort(lists:umerge(Opts, OldOpts)),
ets:insert(?SUBOPTION, {{Topic, Subscriber}, Opts1});
ets:insert(?SUBOPTION, {{Topic, Subscriber}, maps:merge(OldOpts, Opts)});
[] -> false
end.
with_subpid(SubPid) when is_pid(SubPid) ->
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) ->
async_call(Broker, Req) ->
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
{Tag, Reply} -> Reply
after Timeout ->
{error, timeout}
exit(timeout)
end.
%% Pick a broker
pick(SubPid) when is_pid(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).
gproc_pool:pick_worker(broker, SubPid).
-spec(topics() -> [topic()]).
topics() -> emqx_router:topics().
@ -271,33 +305,35 @@ topics() -> emqx_router:topics().
init([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) ->
emqx_logger:error("[Broker] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast({From, {subscribe, Topic, Subscriber, Options}}, State) ->
case ets:lookup(?SUBOPTION, {Topic, Subscriber}) of
[] ->
Group = proplists:get_value(share, Options),
true = do_subscribe(Group, Topic, Subscriber, Options),
emqx_shared_sub:subscribe(Group, Topic, subpid(Subscriber)),
emqx_router:add_route(From, Topic, destination(Options)),
handle_cast({From, #subscribe{topic = Topic, subpid = SubPid, subid = SubId, subopts = SubOpts}}, State) ->
Subscriber = {SubPid, SubId},
case ets:member(?SUBOPTION, {Topic, Subscriber}) of
false ->
Group = maps:get(share, SubOpts, undefined),
true = do_subscribe(Group, Topic, Subscriber, SubOpts),
emqx_shared_sub:subscribe(Group, Topic, SubPid),
emqx_router:add_route(From, Topic, dest(Group)),
{noreply, monitor_subscriber(Subscriber, State)};
[_] ->
true ->
gen_server:reply(From, ok),
{noreply, State}
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
[{_, Options}] ->
Group = proplists:get_value(share, Options),
[{_, SubOpts}] ->
Group = maps:get(share, SubOpts, undefined),
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
false -> emqx_router:del_route(From, Topic, destination(Options));
false -> emqx_router:del_route(From, Topic, dest(Group));
true -> gen_server:reply(From, ok)
end;
[] -> gen_server:reply(From, ok)
@ -308,37 +344,22 @@ handle_cast(Msg, State) ->
emqx_logger:error("[Broker] unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info({'DOWN', _MRef, process, SubPid, _Reason}, State = #state{submon = SubMon}) ->
Subscriber = case SubMon:find(SubPid) of
undefined -> SubPid;
SubId -> {SubId, SubPid}
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),
handle_info({'DOWN', _MRef, process, SubPid, Reason}, State = #state{submap = SubMap}) ->
case maps:find(SubPid, SubMap) of
{ok, SubIds} ->
lists:foreach(fun(SubId) -> subscriber_down({SubPid, SubId}) end, SubIds),
{noreply, demonitor_subscriber(SubPid, State)};
error ->
emqx_logger:error("unexpected 'DOWN': ~p, reason: ~p", [SubPid, Reason]),
{noreply, State}
end;
handle_info(Info, State) ->
emqx_logger:error("[Broker] unexpected info: ~p", [Info]),
{noreply, State}.
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) ->
{ok, State}.
@ -347,35 +368,44 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal functions
%%------------------------------------------------------------------------------
do_subscribe(Group, Topic, Subscriber, Options) ->
do_subscribe(Group, Topic, Subscriber, SubOpts) ->
ets:insert(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}),
ets:insert(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}),
ets:insert(?SUBOPTION, {{Topic, Subscriber}, Options}).
ets:insert(?SUBOPTION, {{Topic, Subscriber}, SubOpts}).
do_unsubscribe(Group, Topic, Subscriber) ->
ets:delete_object(?SUBSCRIPTION, {Subscriber, shared(Group, Topic)}),
ets:delete_object(?SUBSCRIBER, {Topic, shared(Group, Subscriber)}),
ets:delete(?SUBOPTION, {Topic, Subscriber}).
monitor_subscriber(SubPid, State = #state{submon = SubMon}) when is_pid(SubPid) ->
State#state{submon = SubMon:monitor(SubPid)};
subscriber_down(Subscriber) ->
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}) ->
State#state{submon = SubMon:monitor(SubPid, SubId)}.
monitor_subscriber({SubPid, SubId}, State = #state{submap = SubMap, submon = SubMon}) ->
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}) ->
State#state{submon = SubMon:demonitor(SubPid)}.
demonitor_subscriber(SubPid, State = #state{submap = SubMap, submon = SubMon}) ->
State#state{submap = maps:remove(SubPid, SubMap),
submon = emqx_pmon:demonitor(SubPid, SubMon)}.
destination(Options) ->
case proplists:get_value(share, Options) of
undefined -> node();
Group -> {Group, node()}
end.
subpid(SubPid) when is_pid(SubPid) ->
SubPid;
subpid({_SubId, SubPid}) when is_pid(SubPid) ->
SubPid.
dest(undefined) -> node();
dest(Group) -> {Group, node()}.
shared(undefined, Name) -> Name;
shared(Group, Name) -> {share, Group, Name}.

View File

@ -17,9 +17,7 @@
-behaviour(gen_server).
-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).
@ -39,7 +37,7 @@ init([]) ->
handle_call(Req, _From, State) ->
emqx_logger:error("[BrokerHelper] unexpected call: ~p", [Req]),
{reply, ignore, State}.
{reply, ignored, State}.
handle_cast(Msg, State) ->
emqx_logger:error("[BrokerHelper] unexpected cast: ~p", [Msg]),

View File

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

View File

@ -69,6 +69,11 @@
-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(),
owner :: pid(),
host :: host(),
@ -89,7 +94,7 @@
force_ping :: boolean(),
paused :: boolean(),
will_flag :: boolean(),
will_msg :: mqtt_message(),
will_msg :: mqtt_msg(),
properties :: properties(),
pending_calls :: list(),
subscriptions :: map(),
@ -140,6 +145,9 @@
-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
%%------------------------------------------------------------------------------
@ -242,8 +250,7 @@ parse_subopt([{qos, QoS} | Opts], Rec) ->
-spec(publish(client(), topic(), payload()) -> ok | {error, term()}).
publish(Client, Topic, Payload) when is_binary(Topic) ->
publish(Client, #mqtt_message{topic = Topic, qos = ?QOS_0,
payload = iolist_to_binary(Payload)}).
publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}).
-spec(publish(client(), topic(), payload(), qos() | [pubopt()])
-> ok | {ok, packet_id()} | {error, term()}).
@ -261,15 +268,14 @@ publish(Client, Topic, Properties, Payload, Opts)
ok = emqx_mqtt_properties:validate(Properties),
Retain = proplists:get_bool(retain, Opts),
QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)),
publish(Client, #mqtt_message{qos = QoS,
publish(Client, #mqtt_msg{qos = QoS,
retain = Retain,
topic = Topic,
properties = Properties,
props = Properties,
payload = iolist_to_binary(Payload)}).
-spec(publish(client(), mqtt_message())
-> ok | {ok, packet_id()} | {error, term()}).
publish(Client, Msg) when is_record(Msg, mqtt_message) ->
-spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}).
publish(Client, Msg) when is_record(Msg, mqtt_msg) ->
gen_statem:call(Client, {publish, Msg}).
-spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()).
@ -380,7 +386,7 @@ init([Options]) ->
force_ping = false,
paused = false,
will_flag = false,
will_msg = #mqtt_message{},
will_msg = #mqtt_msg{},
pending_calls = [],
subscriptions = #{},
max_inflight = infinity,
@ -488,15 +494,15 @@ init([_Opt | Opts], State) ->
init(Opts, State).
init_will_msg({topic, Topic}, WillMsg) ->
WillMsg#mqtt_message{topic = iolist_to_binary(Topic)};
init_will_msg({props, Properties}, WillMsg) ->
WillMsg#mqtt_message{properties = Properties};
WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)};
init_will_msg({props, Props}, WillMsg) ->
WillMsg#mqtt_msg{props = Props};
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) ->
WillMsg#mqtt_message{retain = Retain};
WillMsg#mqtt_msg{retain = Retain};
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}) ->
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_msg = WillMsg,
properties = Properties}) ->
?WILL_MSG(WillQos, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg,
ConnProps = emqx_mqtt_properties:filter(?CONNECT, maps:to_list(Properties)),
?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg,
ConnProps = emqx_mqtt_properties:filter(?CONNECT, Properties),
io:format("ConnProps: ~p~n", [ConnProps]),
send(?CONNECT_PACKET(
#mqtt_packet_connect{proto_ver = ProtoVer,
proto_name = ProtoName,
is_bridge = IsBridge,
clean_start = CleanStart,
will_flag = WillFlag,
will_qos = WillQos,
will_qos = WillQoS,
will_retain = WillRetain,
keepalive = KeepAlive,
properties = ConnProps,
@ -624,7 +631,7 @@ connected({call, From}, SubReq = {subscribe, Properties, Topics},
{stop_and_reply, Reason, [{reply, From, Error}]}
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
{ok, NewState} ->
{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}]}
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})
when (Qos =:= ?QOS_1); (Qos =:= ?QOS_2) ->
when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) ->
case emqx_inflight:is_full(Inflight) of
true ->
{keep_state, State, [{reply, From, {error, inflight_full}}]};
false ->
Msg1 = Msg#mqtt_message{packet_id = PacketId},
Msg1 = Msg#mqtt_msg{packet_id = PacketId},
case send(Msg1, State) of
{ok, NewState} ->
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);
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}) ->
{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),
State = #state{auto_ack = AutoAck}) ->
_ = deliver_msg(packet_to_msg(Packet), State),
_ = deliver(packet_to_msg(Packet), State),
case AutoAck of
true -> send_puback(?PUBACK_PACKET(PacketId), State);
false -> {keep_state, State}
@ -716,7 +723,7 @@ connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId),
connected(cast, ?PUBACK_PACKET(PacketId, ReasonCode, Properties),
State = #state{owner = Owner, inflight = Inflight}) ->
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,
reason_code => ReasonCode,
properties => Properties}},
@ -745,8 +752,7 @@ connected(cast, ?PUBREL_PACKET(PacketId),
State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) ->
case maps:take(PacketId, AwaitingRel) of
{Packet, AwaitingRel1} ->
NewState = deliver_msg(packet_to_msg(Packet),
State#state{awaiting_rel = AwaitingRel1}),
NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}),
case AutoAck of
true -> send_puback(?PUBCOMP_PACKET(PacketId), 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)}
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}) ->
Msg1 = Msg#mqtt_message{dup = (QoS =:= ?QOS1)},
Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS1)},
case send(Msg1, State) of
{ok, NewState} ->
Inflight1 = emqx_inflight:update(PacketId, {publish, Msg1, Now}, Inflight),
@ -979,42 +985,36 @@ retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) ->
Error
end.
deliver_msg(#mqtt_message{qos = QoS,
dup = Dup,
retain = Retain,
topic = Topic,
packet_id = PacketId,
properties = Properties,
payload = Payload},
deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId,
topic = Topic, props = Props, payload = Payload},
State = #state{owner = Owner}) ->
Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain,
packet_id => PacketId, topic => Topic,
properties => Properties, payload => Payload}},
Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId,
topic => Topic, properties => Props, payload => Payload,
client_pid => self()}},
State.
packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) ->
#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,
packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
dup = Dup,
retain = Retain,
topic = Topic,
qos = QoS,
retain = R},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId,
properties = Properties,
properties = Props},
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,
qos = Qos,
qos = QoS,
retain = Retain,
dup = Dup},
variable = #mqtt_packet_publish{topic_name = Topic,
packet_id = PacketId,
properties = Properties},
properties = Props},
payload = Payload}.
%%------------------------------------------------------------------------------
%% Socket Connect/Send
@ -1040,7 +1040,7 @@ send_puback(Packet, State) ->
{error, Reason} -> {stop, Reason}
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(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
-spec(lookup_client_pid(client_id()) -> pid() | undefined).
lookup_client_pid(ClientId) when is_binary(ClientId) ->
case lookup_client_pid(ClientId) of
case ets:lookup(?CLIENT, ClientId) of
[] -> undefined;
[{_, Pid}] -> Pid
end.

View File

@ -33,15 +33,15 @@
-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()).
get_env(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:
populate(_App) ->
ok.

View File

@ -17,37 +17,40 @@
-behaviour(gen_server).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include("emqx_misc.hrl").
-import(proplists, [get_value/2, get_value/3]).
%% API Function Exports
-export([start_link/3]).
%% 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([info/1, stats/1, kick/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,
terminate/2]).
%% Unused fields: connname, peerhost, peerport
-record(state, {transport, socket, peername, conn_state, await_recv,
rate_limit, max_packet_size, proto_state, parse_state,
keepalive, enable_stats, idle_timeout, force_gc_count}).
-record(state, {
transport, %% Network transport module
socket, %% TCP or SSL Socket
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]).
@ -55,8 +58,12 @@
emqx_logger:Level("Client(~s): " ++ Format,
[esockd_net:format(State#state.peername) | Args])).
start_link(Transport, Sock, Env) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Env]])}.
start_link(Transport, Socket, Options) ->
{ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}.
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
info(CPid) ->
gen_server:call(CPid, info).
@ -67,156 +74,141 @@ stats(CPid) ->
kick(CPid) ->
gen_server:call(CPid, kick).
set_rate_limit(CPid, Rl) ->
gen_server:call(CPid, {set_rate_limit, Rl}).
session(CPid) ->
gen_server:call(CPid, session, infinity).
clean_acl_cache(CPid) ->
gen_server:call(CPid, clean_acl_cache).
get_rate_limit(CPid) ->
gen_server:call(CPid, get_rate_limit).
subscribe(CPid, TopicTable) ->
CPid ! {subscribe, TopicTable}.
set_rate_limit(CPid, Rl = {_Rate, _Burst}) ->
gen_server:call(CPid, {set_rate_limit, Rl}).
unsubscribe(CPid, Topics) ->
CPid ! {unsubscribe, Topics}.
get_pub_limit(CPid) ->
gen_server:call(CPid, get_pub_limit).
session(CPid) ->
gen_server:call(CPid, session, infinity).
set_pub_limit(CPid, Rl = {_Rate, _Burst}) ->
gen_server:call(CPid, {set_pub_limit, Rl}).
clean_acl_cache(CPid, Topic) ->
gen_server:call(CPid, {clean_acl_cache, Topic}).
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
init([Transport, Sock, Env]) ->
case Transport:wait(Sock) of
{ok, NewSock} ->
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]),
do_init(Transport, Sock, Peername, Env);
{error, Reason} ->
{stop, Reason}
end.
do_init(Transport, Sock, Peername, Env) ->
RateLimit = get_value(rate_limit, Env),
PacketSize = get_value(max_packet_size, Env, ?MAX_PACKET_SIZE),
SendFun = send_fun(Transport, Sock, Peername),
ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Env),
EnableStats = get_value(client_enable_stats, Env, false),
IdleTimout = get_value(client_idle_timeout, Env, 30000),
ForceGcCount = emqx_gc:conn_max_gc_count(),
init([Transport, RawSocket, Options]) ->
case Transport:wait(RawSocket) of
{ok, Socket} ->
Zone = proplists:get_value(zone, Options),
{ok, Peername} = Transport:ensure_ok_or_exit(peername, [Socket]),
{ok, Sockname} = Transport:ensure_ok_or_exit(sockname, [Socket]),
Peercert = Transport:ensure_ok_or_exit(peercert, [Socket]),
PubLimit = rate_limit(emqx_zone:env(Zone, publish_limit)),
RateLimit = rate_limit(proplists:get_value(rate_limit, Options)),
EnableStats = emqx_zone:env(Zone, enable_stats, true),
IdleTimout = emqx_zone:env(Zone, idle_timeout, 30000),
SendFun = send_fun(Transport, Socket, Peername),
ProtoState = emqx_protocol:init(#{peername => Peername,
sockname => Sockname,
peercert => Peercert,
sendfun => SendFun}, Options),
ParserState = emqx_protocol:parser(ProtoState),
State = run_socket(#state{transport = Transport,
socket = Sock,
socket = Socket,
peername = Peername,
await_recv = false,
conn_state = running,
rate_limit = RateLimit,
max_packet_size = PacketSize,
pub_limit = PubLimit,
proto_state = ProtoState,
parser_state = ParserState,
enable_stats = EnableStats,
idle_timeout = IdleTimout,
force_gc_count = ForceGcCount}),
idle_timeout = 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) ->
Self = self(),
fun(Packet) ->
Data = emqx_frame:serialize(Packet),
rate_limit(undefined) ->
undefined;
rate_limit({Rate, Burst}) ->
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}),
emqx_metrics:inc('bytes/sent', iolist_size(Data)),
try Transport:async_send(Sock, Data) of
ok -> ok;
{error, Reason} -> Self ! {shutdown, Reason}
emqx_metrics:inc('bytes/sent', iolist_size(Data)), ok;
Error -> Error
catch
error:Error -> Self ! {shutdown, Error}
error:Error -> {error, Error}
end
end.
init_parse_state(State = #state{max_packet_size = Size, 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}) ->
handle_call(info, From, State = #state{transport = Transport, socket = Socket, proto_state = ProtoState}) ->
ProtoInfo = emqx_protocol:info(ProtoState),
ClientInfo = ?record_to_proplist(state, State, ?INFO_KEYS),
{reply, Stats, _, _} = handle_call(stats, From, State),
reply(lists:append([ClientInfo, ProtoInfo, Stats]), State);
ConnInfo = [{socktype, Transport:type(Socket)} | ?record_to_proplist(state, State, ?INFO_KEYS)],
StatsInfo = element(2, handle_call(stats, From, State)),
{reply, lists:append([ConnInfo, StatsInfo, ProtoInfo]), State};
handle_call(stats, _From, State = #state{proto_state = ProtoState}) ->
reply(lists:append([emqx_misc:proc_stats(),
emqx_protocol:stats(ProtoState),
sock_stats(State)]), State);
handle_call(stats, _From, State = #state{transport = Transport, socket = Sock, proto_state = ProtoState}) ->
ProcStats = emqx_misc:proc_stats(),
ProtoStats = emqx_protocol:stats(ProtoState),
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) ->
{stop, {shutdown, kick}, ok, State};
handle_call({set_rate_limit, Rl}, _From, State) ->
reply(ok, State#state{rate_limit = Rl});
handle_call(session, _From, State = #state{proto_state = ProtoState}) ->
{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}) ->
reply(Rl, State);
{reply, esockd_rate_limit:info(Rl), State};
handle_call(session, _From, State = #state{proto_state = ProtoState}) ->
reply(emqx_protocol:session(ProtoState), State);
handle_call({set_rate_limit, {Rate, Burst}}, _From, State) ->
{reply, ok, State#state{rate_limit = esockd_rate_limit:new(Rate, Burst)}};
handle_call({clean_acl_cache, Topic}, _From, State) ->
erase({acl, publish, Topic}),
reply(ok, State);
handle_call(get_publish_limit, _From, State = #state{pub_limit = Rl}) ->
{reply, esockd_rate_limit:info(Rl), 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) ->
?LOG(error, "Unexpected Call: ~p", [Req], State),
{reply, ignore, State}.
?LOG(error, "unexpected call: ~p", [Req], State),
{reply, ignored, State}.
handle_cast(Msg, State) ->
?LOG(error, "Unexpected Cast: ~p", [Msg], State),
?LOG(error, "unexpected cast: ~p", [Msg], State),
{noreply, State}.
handle_info({subscribe, TopicTable}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:subscribe(TopicTable, ProtoState)
end, State);
handle_info({deliver, PubOrAck}, State = #state{proto_state = ProtoState}) ->
case emqx_protocol:deliver(PubOrAck, ProtoState) of
{ok, ProtoState1} ->
{noreply, maybe_gc(ensure_stats_timer(State#state{proto_state = ProtoState1}))};
{error, Reason} ->
shutdown(Reason, State);
{error, Reason, ProtoState1} ->
shutdown(Reason, State#state{proto_state = ProtoState1})
end;
handle_info({unsubscribe, Topics}, State) ->
with_proto(
fun(ProtoState) ->
emqx_protocol:unsubscribe(Topics, ProtoState)
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(emit_stats, State = #state{proto_state = ProtoState}) ->
Stats = element(2, handle_call(stats, undefined, State)),
emqx_cm:set_client_stats(emqx_protocol:clientid(ProtoState), Stats),
{noreply, State#state{stats_timer = undefined}, hibernate};
handle_info(timeout, State) ->
shutdown(idle_timeout, State);
%% Fix issue #535
handle_info({shutdown, Error}, State) ->
shutdown(Error, State);
@ -225,25 +217,25 @@ handle_info({shutdown, conflict, {ClientId, NewPid}}, State) ->
shutdown(conflict, 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) ->
Size = iolist_size(Data),
?LOG(debug, "RECV ~p", [Data], State),
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) ->
shutdown(Reason, State);
handle_info({inet_reply, _Sock, ok}, State) ->
{noreply, gc(State)}; %% Tune GC
{noreply, State};
handle_info({inet_reply, _Sock, {error, Reason}}, State) ->
shutdown(Reason, State);
handle_info({keepalive, start, Interval},
State = #state{transport = Transport, socket = Sock}) ->
handle_info({keepalive, start, Interval}, State = #state{transport = Transport, socket = Sock}) ->
?LOG(debug, "Keepalive at the interval of ~p", [Interval], State),
StatFun = fun() ->
case Transport:getstat(Sock, [recv_oct]) of
@ -272,20 +264,18 @@ handle_info({keepalive, check}, State = #state{keepalive = KeepAlive}) ->
end;
handle_info(Info, State) ->
?LOG(error, "Unexpected Info: ~p", [Info], State),
?LOG(error, "unexpected info: ~p", [Info], State),
{noreply, State}.
terminate(Reason, State = #state{transport = Transport,
socket = Sock,
keepalive = KeepAlive,
proto_state = ProtoState}) ->
?LOG(debug, "Terminated for ~p", [Reason], State),
Transport:fast_close(Sock),
emqx_keepalive:cancel(KeepAlive),
case {ProtoState, Reason} of
{undefined, _} ->
ok;
{undefined, _} -> ok;
{_, {shutdown, Error}} ->
emqx_protocol:shutdown(Error, ProtoState);
{_, Reason} ->
@ -295,25 +285,29 @@ terminate(Reason, State = #state{transport = Transport,
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Receive and Parse TCP Data
received(<<>>, State) ->
{noreply, gc(State)};
%% Receive and parse data
handle_packet(<<>>, 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,
idle_timeout = IdleTimeout}) ->
case catch emqx_frame:parse(Bytes, ParseState) of
{more, NewParseState} ->
{noreply, State#state{parse_state = NewParseState}, IdleTimeout};
{ok, Packet, Rest} ->
case catch emqx_frame:parse(Bytes, ParserState) of
{more, NewParserState} ->
{noreply, State#state{parser_state = NewParserState}, IdleTimeout};
{ok, Packet = ?PACKET(Type), Rest} ->
emqx_metrics:received(Packet),
case emqx_protocol:received(Packet, ProtoState) of
{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} ->
?LOG(error, "Protocol error - ~p", [Error], State),
shutdown(Error, State);
@ -326,21 +320,32 @@ received(Bytes, State = #state{parse_state = ParseState,
?LOG(error, "Framing error - ~p", [Error], State),
shutdown(Error, State);
{'EXIT', Reason} ->
?LOG(error, "Parser failed for ~p", [Reason], State),
?LOG(error, "Error data: ~p", [Bytes], State),
shutdown(parser_error, State)
?LOG(error, "Parse failed for ~p~nError data:~p", [Reason, Bytes], State),
shutdown(parse_error, State)
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);
rate_limit(Size, State = #state{rate_limit = Rl}) ->
case Rl:check(Size) of
ensure_rate_limit([{undefined, _Pos, _Num}|Limiters], State) ->
ensure_rate_limit(Limiters, State);
ensure_rate_limit([{Rl, Pos, Num}|Limiters], State) ->
case esockd_rate_limit:check(Num, Rl) of
{0, Rl1} ->
run_socket(State#state{conn_state = running, rate_limit = Rl1});
ensure_rate_limit(Limiters, setelement(Pos, State, Rl1));
{Pause, Rl1} ->
?LOG(warning, "Rate limiter pause for ~p", [Pause], State),
erlang:send_after(Pause, self(), activate_sock),
State#state{conn_state = blocked, rate_limit = Rl1}
TRef = erlang:send_after(Pause, self(), activate_sock),
setelement(Pos, State#state{conn_state = blocked, limit_timer = TRef}, Rl1)
end.
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),
State#state{await_recv = true}.
with_proto(Fun, State = #state{proto_state = ProtoState}) ->
{ok, ProtoState1} = Fun(ProtoState),
{noreply, State#state{proto_state = ProtoState1}}.
emit_stats(State = #state{proto_state = ProtoState}) ->
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),
ensure_stats_timer(State = #state{enable_stats = true,
stats_timer = undefined,
idle_timeout = IdleTimeout}) ->
State#state{stats_timer = erlang:send_after(IdleTimeout, self(), emit_stats)};
ensure_stats_timer(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) ->
stop({shutdown, Reason}, State).
stop(Reason, State) ->
{stop, Reason, State}.
gc(State = #state{transport = Transport, socket = Sock}) ->
Cb = fun() -> Transport:gc(Sock), emit_stats(State) end,
emqx_gc:maybe_force_gc(#state.force_gc_count, State, Cb).
maybe_gc(State) ->
State. %% TODO:...
%%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([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,
code_change/3]).
@ -48,7 +48,21 @@ unregister_command(Cmd) when is_atom(Cmd) ->
cast(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()}).
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, []) ->
usage();
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,
PasswordFlag : 1,
WillRetain : 1,
WillQos : 2,
WillQoS : 2,
WillFlag : 1,
CleanStart : 1,
_Reserved : 1,
@ -138,7 +138,7 @@ parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) ->
is_bridge = (BridgeTag =:= 8),
clean_start = bool(CleanStart),
will_flag = bool(WillFlag),
will_qos = WillQos,
will_qos = WillQoS,
will_retain = bool(WillRetain),
keepalive = KeepAlive,
properties = Properties,
@ -162,7 +162,6 @@ parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin,
?QOS_0 -> {undefined, Rest};
_ -> parse_packet_id(Rest)
end,
io:format("Rest1: ~p~n", [Rest1]),
{Properties, Payload} = parse_properties(Rest1, Ver),
{#mqtt_packet_publish{topic_name = TopicName,
packet_id = PacketId,
@ -242,6 +241,9 @@ parse_packet_id(<<PacketId:16/big, Rest/binary>>) ->
parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 ->
{undefined, Bin};
%% TODO: version mess?
parse_properties(<<>>, ?MQTT_PROTO_V5) ->
{#{}, <<>>};
parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) ->
{#{}, Rest};
parse_properties(Bin, ?MQTT_PROTO_V5) ->
@ -382,7 +384,7 @@ serialize_variable(#mqtt_packet_connect{
is_bridge = IsBridge,
clean_start = CleanStart,
will_flag = WillFlag,
will_qos = WillQos,
will_qos = WillQoS,
will_retain = WillRetain,
keepalive = KeepAlive,
properties = Properties,
@ -400,7 +402,7 @@ serialize_variable(#mqtt_packet_connect{
(flag(Username)):1,
(flag(Password)):1,
(flag(WillRetain)):1,
WillQos:2,
WillQoS:2,
(flag(WillFlag)):1,
(flag(CleanStart)):1,
0:1,

View File

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

View File

@ -129,5 +129,6 @@ to_base62(<<I:128>>) ->
emqx_base62:encode(I).
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([]) ->
{ok, {{one_for_one, 10, 100},
[child_spec(emqx_pool, supervisor),
child_spec(emqx_alarm, worker),
child_spec(emqx_alarm_mgr, worker),
child_spec(emqx_hooks, worker),
child_spec(emqx_stats, worker),
child_spec(emqx_metrics, worker),
child_spec(emqx_ctl, worker),
child_spec(emqx_zone, worker),
child_spec(emqx_tracer, 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
%% limitations under the License.
%% @doc start/stop MQTT listeners.
%% @doc Start/Stop MQTT listeners.
-module(emqx_listeners).
-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]).
-type(listener() :: {atom(), esockd:listen_on(), [esockd:option()]}).
%% @doc Start all listeners
-spec(start_all() -> ok).
start_all() ->
-spec(start() -> ok).
start() ->
lists:foreach(fun start_listener/1, emqx_config:get_env(listeners, [])).
%% Start MQTT/TCP listener
-spec(start_listener(listener()) -> {ok, pid()} | {error, term()}).
start_listener({tcp, ListenOn, Options}) ->
start_mqtt_listener('mqtt:tcp', ListenOn, Options);
%% Start MQTT/TLS listener
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_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_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) ->
{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) ->
{ok, _} = mochiweb:start_http(Name, ListenOn, Options, {emqx_ws, handle_request, []}).
with_port(Port, Opts) when is_integer(Port) ->
[{port, Port}|Opts];
with_port({Addr, Port}, Opts) ->
[{ip, Addr}, {port, Port}|Opts].
%% @doc Restart all listeners
-spec(restart_all() -> ok).
restart_all() ->
-spec(restart() -> ok).
restart() ->
lists:foreach(fun restart_listener/1, emqx_config:get_env(listeners, [])).
-spec(restart_listener(listener()) -> any()).
restart_listener({tcp, ListenOn, _Opts}) ->
restart_listener({tcp, ListenOn, _Options}) ->
esockd:reopen('mqtt:tcp', ListenOn);
restart_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls ->
esockd:reopen('mqtt:tls', ListenOn);
restart_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws ->
mochiweb:restart_http('mqtt:ws', ListenOn);
restart_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss ->
mochiweb:restart_http('mqtt:wss', ListenOn);
restart_listener({Proto, ListenOn, _Options}) when Proto == ssl; Proto == tls ->
esockd:reopen('mqtt:ssl', ListenOn);
restart_listener({Proto, ListenOn, Options}) when Proto == http; Proto == ws ->
cowboy:stop_listener('mqtt:ws'),
start_listener({Proto, ListenOn, Options});
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}) ->
esockd:reopen(Proto, ListenOn).
%% @doc Stop all listeners
-spec(stop_all() -> ok).
stop_all() ->
-spec(stop() -> ok).
stop() ->
lists:foreach(fun stop_listener/1, emqx_config:get_env(listeners, [])).
-spec(stop_listener(listener()) -> ok | {error, any()}).
stop_listener({tcp, ListenOn, _Opts}) ->
esockd:close('mqtt:tcp', ListenOn);
stop_listener({Proto, ListenOn, _Opts}) when Proto == ssl; Proto == tls ->
esockd:close('mqtt:tls', ListenOn);
stop_listener({Proto, ListenOn, _Opts}) when Proto == http; Proto == ws ->
mochiweb:stop_http('mqtt:ws', ListenOn);
stop_listener({Proto, ListenOn, _Opts}) when Proto == https; Proto == wss ->
mochiweb:stop_http('mqtt:wss', ListenOn);
esockd:close('mqtt:ssl', ListenOn);
stop_listener({Proto, _ListenOn, _Opts}) when Proto == http; Proto == ws ->
cowboy:stop_listener('mqtt:ws');
stop_listener({Proto, _ListenOn, _Opts}) when Proto == https; Proto == wss ->
cowboy:stop_listener('mqtt:wss');
stop_listener({Proto, ListenOn, _Opts}) ->
esockd:close(Proto, ListenOn).
merge_sockopts(Options) ->
merge_default(Options) ->
case lists:keytake(tcp_options, 1, Options) of
{value, {tcp_options, TcpOpts}, Options1} ->
[{tcp_options, emqx_misc:merge_opts(?MQTT_SOCKOPTS, TcpOpts)} | Options1];
@ -89,11 +111,3 @@ merge_sockopts(Options) ->
[{tcp_options, ?MQTT_SOCKOPTS} | Options]
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_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([set_headers/2]).
-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()).
new(Topic, Payload) ->
new(undefined, Topic, Payload).
-spec(make(topic(), payload()) -> message()).
make(Topic, Payload) ->
make(undefined, Topic, Payload).
-spec(new(atom() | client(), topic(), payload()) -> message()).
new(From, Topic, Payload) when is_atom(From); is_record(From, client) ->
new(From, #{qos => ?QOS0}, Topic, Payload).
-spec(make(atom() | client_id(), topic(), payload()) -> message()).
make(From, Topic, Payload) ->
make(From, ?QOS0, Topic, Payload).
-spec(new(atom() | client(), message_flags(), topic(), payload()) -> message()).
new(From, Flags, Topic, Payload) when is_atom(From); is_record(From, client) ->
new(From, Flags, #{}, Topic, Payload).
-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(),
-spec(make(atom() | client_id(), qos(), topic(), payload()) -> message()).
make(From, QoS, Topic, Payload) ->
#message{id = msgid(QoS),
qos = QoS,
from = From,
sender = self(),
flags = Flags,
headers = Headers,
flags = #{dup => false},
topic = Topic,
properties = #{},
payload = Payload,
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, false).
get_flag(Flag, #message{flags = Flags}, Default) ->
maps:get(Flag, Flags, Default).
%% @doc Set flag
-spec(set_flag(message_flag(), message()) -> message()).
set_flag(Flag, Msg = #message{flags = Flags}) when is_atom(Flag) ->
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) ->
Msg#message{flags = maps:put(Flag, Val, Flags)}.
%% @doc Unset flag
-spec(unset_flag(message_flag(), message()) -> message()).
unset_flag(Flag, Msg = #message{flags = 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, undefined).
get_header(Hdr, #message{headers = 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}) ->
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) ->
inc('packets/received'),
received1(Packet).
received1(?PUBLISH_PACKET(Qos, _PktId)) ->
received1(?PUBLISH_PACKET(QoS, _PktId)) ->
inc('packets/publish/received'),
inc('messages/received'),
qos_received(Qos);
qos_received(QoS);
received1(?PACKET(Type)) ->
received2(Type).
received2(?CONNECT) ->
@ -206,15 +206,15 @@ qos_received(?QOS_2) ->
%% @doc Count packets received. Will not count $SYS PUBLISH.
-spec(sent(mqtt_packet()) -> ignore | non_neg_integer()).
sent(?PUBLISH_PACKET(_Qos, <<"$SYS/", _/binary>>, _, _)) ->
sent(?PUBLISH_PACKET(_QoS, <<"$SYS/", _/binary>>, _, _)) ->
ignore;
sent(Packet) ->
inc('packets/sent'),
sent1(Packet).
sent1(?PUBLISH_PACKET(Qos, _PktId)) ->
sent1(?PUBLISH_PACKET(QoS, _PktId)) ->
inc('packets/publish/sent'),
inc('messages/sent'),
qos_sent(Qos);
qos_sent(QoS);
sent1(?PACKET(Type)) ->
sent2(Type).
sent2(?CONNACK) ->

View File

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

View File

@ -104,17 +104,20 @@ id('Wildcard-Subscription-Available') -> 16#28;
id('Subscription-Identifier-Available') -> 16#29;
id('Shared-Subscription-Available') -> 16#2A.
filter(Packet, Props) when ?CONNECT =< Packet, Packet =< ?AUTH ->
Fun = fun(Name) ->
filter(PacketType, Props) when is_map(Props) ->
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
{ok, {Name, _Type, 'ALL'}} ->
true;
{ok, {Name, _Type, Packets}} ->
lists:member(Packet, Packets);
{ok, {Name, _Type, AllowedTypes}} ->
lists:member(PacketType, AllowedTypes);
error -> false
end
end,
[Prop || Prop = {Name, _} <- Props, Fun(Name)].
[Prop || Prop = {Name, _} <- Props, Filter(Name)].
validate(Props) when is_map(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
%%
%% 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,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
@ -40,33 +39,25 @@
%%
%% @end
%% TODO: ...
-module(emqx_mqueue).
%% TODO: XYZ
%%
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-import(proplists, [get_value/3]).
-export([new/3, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1,
dropped/1, stats/1]).
-define(LOW_WM, 0.2).
-define(HIGH_WM, 0.6).
-export([new/2, type/1, name/1, is_empty/1, len/1, max_len/1, in/2, out/1]).
-export([dropped/1, stats/1]).
-define(PQUEUE, emqx_pqueue).
-type(priority() :: {iolist(), pos_integer()}).
-type(option() :: {type, simple | priority}
| {max_length, non_neg_integer()} %% Max queue length
| {priority, list(priority())}
| {low_watermark, float()} %% Low watermark
| {high_watermark, float()} %% High watermark
| {store_qos0, boolean()}). %% Queue Qos0?
-type(options() :: #{type => simple | priority,
max_len => non_neg_integer(),
priority => list(priority()),
store_qos0 => boolean()}).
-type(stat() :: {max_len, non_neg_integer()}
| {len, non_neg_integer()}
@ -78,31 +69,22 @@
pseq = 0, priorities = [],
%% len of simple queue
len = 0, max_len = 0,
low_wm = ?LOW_WM, high_wm = ?HIGH_WM,
qos0 = false, dropped = 0,
alarm_fun}).
qos0 = false, dropped = 0}).
-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(), list(option()), fun()) -> mqueue()).
new(Name, Opts, AlarmFun) ->
Type = get_value(type, Opts, simple),
MaxLen = get_value(max_length, Opts, 0),
-spec(new(iolist(), options()) -> mqueue()).
new(Name, #{type := Type, max_len := MaxLen, store_qos0 := StoreQos0}) ->
init_q(#mqueue{type = Type, name = iolist_to_binary(Name),
len = 0, max_len = MaxLen,
low_wm = low_wm(MaxLen, Opts),
high_wm = high_wm(MaxLen, Opts),
qos0 = get_value(store_qos0, Opts, false),
alarm_fun = AlarmFun}, Opts).
len = 0, max_len = MaxLen, qos0 = StoreQos0}).
init_q(MQ = #mqueue{type = simple}, _Opts) ->
init_q(MQ = #mqueue{type = simple}) ->
MQ#mqueue{q = queue:new()};
init_q(MQ = #mqueue{type = priority}, Opts) ->
Priorities = get_value(priority, Opts, []),
init_p(Priorities, MQ#mqueue{q = ?PQUEUE:new()}).
init_q(MQ = #mqueue{type = priority}) ->
%%Priorities = get_value(priority, Opts, []),
init_p([], MQ#mqueue{q = ?PQUEUE:new()}).
init_p([], 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, 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()).
name(#mqueue{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),
MQ#mqueue{q = queue:in(Msg, Q2), dropped = Dropped +1};
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,
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}};
out(MQ = #mqueue{type = simple, q = Q, len = Len}) ->
{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}) ->
{R, Q2} = ?PQUEUE:out(Q),
{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).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-export([protocol_name/1, type_name/1]).
-export([format/1]).
-export([to_message/1, from_message/1]).
-export([to_message/2, from_message/2]).
%% @doc Protocol name of version
-spec(protocol_name(mqtt_version()) -> binary()).
@ -34,43 +33,40 @@ type_name(Type) when Type > ?RESERVED andalso Type =< ?AUTH ->
lists:nth(Type, ?TYPE_NAMES).
%% @doc From Message to Packet
-spec(from_message(message()) -> mqtt_packet()).
from_message(Msg = #message{topic = Topic, payload = Payload}) ->
Qos = emqx_message:get_flag(qos, Msg, 0),
-spec(from_message(mqtt_packet_id(), message()) -> mqtt_packet()).
from_message(PacketId, Msg = #message{qos = QoS, topic = Topic, payload = Payload}) ->
Dup = emqx_message:get_flag(dup, 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,
qos = Qos,
qos = QoS,
retain = Retain,
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,
packet_id = PacketId,
properties = Properties},
payload = Payload}) ->
Flags = #{dup => Dup, retain => Retain, qos => Qos},
Msg = emqx_message:new(undefined, Flags, #{packet_id => PacketId}, Topic, Payload),
Msg#message{properties = Properties};
properties = #{}}, %%TODO:
payload = Payload}.
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;
to_message(#mqtt_packet_connect{will_retain = Retain,
will_qos = Qos,
to_message(ClientId, #mqtt_packet_connect{will_retain = Retain,
will_qos = QoS,
will_topic = Topic,
will_props = Props,
will_payload = Payload}) ->
Msg = emqx_message:new(undefined, #{qos => Qos, retain => Retain}, Topic, Payload),
Msg#message{properties = Props}.
Msg = emqx_message:make(ClientId, QoS, Topic, Payload),
Msg#message{flags = #{qos => QoS, retain => Retain}, headers = Props}.
%% @doc Format packet
-spec(format(mqtt_packet()) -> iolist()).
@ -118,7 +114,7 @@ format_variable(#mqtt_packet_connect{
format_variable(#mqtt_packet_connack{ack_flags = AckFlags,
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,
packet_id = PacketId}) ->
@ -153,3 +149,4 @@ format_password(_Password) -> '******'.
i(true) -> 1;
i(false) -> 0;
i(I) when is_integer(I) -> I.

View File

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

View File

@ -31,7 +31,7 @@ spec(ChildId, Args) ->
-spec(start_link(atom() | tuple(), atom(), mfa()) -> {ok, pid()} | {error, term()}).
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()}).
start_link(Pool, Type, Size, MFA) when is_atom(Pool) ->

View File

@ -1,138 +1,107 @@
%%%===================================================================
%%% 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.
%%%===================================================================
%% 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_protocol).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include("emqx_misc.hrl").
-import(proplists, [get_value/2, get_value/3]).
%% API
-export([init/3, init/5, get/2, info/1, stats/1, clientid/1, client/1, session/1]).
-export([subscribe/2, unsubscribe/2, pubrel/2, shutdown/2]).
-export([received/2, send/2]).
-export([process/2]).
-export([init/2, info/1, stats/1, clientid/1, session/1]).
%%-export([capabilities/1]).
-export([parser/1]).
-export([received/2, process/2, deliver/2, send/2]).
-export([shutdown/2]).
-ifdef(TEST).
-compile(export_all).
-endif.
-record(proto_stats, {enable_stats = false, recv_pkt = 0, recv_msg = 0,
send_pkt = 0, send_msg = 0}).
-define(CAPABILITIES, [{max_packet_size, ?MAX_PACKET_SIZE},
{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
%% ws_initial_headers: Headers from first HTTP request for WebSocket Client.
-record(proto_state, {peername, sendfun, connected = false, client_id, client_pid,
clean_start, proto_ver, proto_name, username, is_superuser,
will_msg, keepalive, keepalive_backoff, max_clientid_len,
session, stats_data, mountpoint, ws_initial_headers,
peercert_username, is_bridge, connected_at}).
-record(proto_state, {zone, sockprops, capabilities, connected, client_id, client_pid,
clean_start, proto_ver, proto_name, username, connprops,
is_superuser, will_msg, keepalive, keepalive_backoff, session,
recv_pkt = 0, recv_msg = 0, send_pkt = 0, send_msg = 0,
mountpoint, is_bridge, connected_at}).
-type(proto_state() :: #proto_state{}).
-define(INFO_KEYS, [client_id, username, clean_start, proto_ver, proto_name,
keepalive, will_msg, ws_initial_headers, mountpoint,
peercert_username, connected_at]).
-define(INFO_KEYS, [capabilities, connected, client_id, clean_start, username, proto_ver, proto_name,
keepalive, will_msg, mountpoint, is_bridge, connected_at]).
-define(STATS_KEYS, [recv_pkt, recv_msg, send_pkt, send_msg]).
-define(LOG(Level, Format, Args, State),
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
init(Peername, SendFun, Opts) ->
Backoff = get_value(keepalive_backoff, Opts, 0.75),
EnableStats = get_value(client_enable_stats, Opts, false),
MaxLen = get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN),
WsInitialHeaders = get_value(ws_initial_headers, Opts),
#proto_state{peername = Peername,
sendfun = SendFun,
max_clientid_len = MaxLen,
is_superuser = false,
-type(proto_state() :: #proto_state{}).
-export_type([proto_state/0]).
init(SockProps = #{peercert := Peercert}, Options) ->
Zone = proplists:get_value(zone, Options),
MountPoint = emqx_zone:env(Zone, mountpoint),
Backoff = emqx_zone:env(Zone, keepalive_backoff, 0.75),
Username = case proplists:get_value(peer_cert_as_username, Options) of
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(),
peercert_username = undefined,
ws_initial_headers = WsInitialHeaders,
proto_ver = ?MQTT_PROTO_V4,
proto_name = <<"MQTT">>,
username = Username,
is_superuser = false,
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) ->
init(Peername, SendFun, Opts).
%%enrich_opt(Conn:opts(), Conn, ).
capabilities(Zone) ->
Capabilities = emqx_zone:env(Zone, mqtt_capabilities, []),
maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)).
enrich_opt([], _Conn, State) ->
State;
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.
parser(#proto_state{capabilities = #{max_packet_size := Size}, proto_ver = Ver}) ->
emqx_frame:initial_state(#{max_packet_size => Size, version => Ver}).
info(ProtoState) ->
?record_to_proplist(proto_state, ProtoState, ?INFO_KEYS).
stats(#proto_state{stats_data = Stats}) ->
tl(?record_to_proplist(proto_stats, Stats)).
stats(ProtoState) ->
?record_to_proplist(proto_state, ProtoState, ?STATS_KEYS).
clientid(#proto_state{client_id = ClientId}) ->
ClientId.
client(#proto_state{client_id = ClientId,
client_pid = ClientPid,
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}.
client(#proto_state{sockprops = #{peername := Peername},
client_id = ClientId, client_pid = ClientPid, username = Username}) ->
#client{id = ClientId, pid = ClientPid, username = Username, peername = Peername}.
session(#proto_state{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.
-spec(received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, term()}).
received(Packet = ?PACKET(?CONNECT),
State = #proto_state{connected = false, stats_data = Stats}) ->
trace(recv, Packet, State), Stats1 = inc_stats(recv, ?CONNECT, Stats),
process(Packet, State#proto_state{connected = true, stats_data = Stats1});
received(Packet = ?PACKET(?CONNECT), ProtoState = #proto_state{connected = false}) ->
trace(recv, Packet, ProtoState),
process(Packet, inc_stats(recv, ?CONNECT, ProtoState#proto_state{connected = true}));
received(?PACKET(?CONNECT), State = #proto_state{connected = true}) ->
{error, protocol_bad_connect, State};
%% Received other packets when CONNECT not arrived.
received(_Packet, State = #proto_state{connected = false}) ->
{error, protocol_not_connected, State};
received(_Packet, ProtoState = #proto_state{connected = false}) ->
{error, protocol_not_connected, ProtoState};
received(Packet = ?PACKET(Type), State = #proto_state{stats_data = Stats}) ->
trace(recv, Packet, State), Stats1 = inc_stats(recv, Type, Stats),
received(Packet = ?PACKET(Type), ProtoState) ->
trace(recv, Packet, ProtoState),
case validate_packet(Packet) of
ok ->
process(Packet, State#proto_state{stats_data = Stats1});
process(Packet, inc_stats(recv, Type, ProtoState));
{error, Reason} ->
{error, Reason, State}
{error, Reason, ProtoState}
end.
subscribe(RawTopicTable, ProtoState = #proto_state{client_id = ClientId,
username = Username,
session = Session}) ->
TopicTable = parse_topic_table(RawTopicTable),
case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of
{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),
process(?CONNECT_PACKET(Var), ProtoState = #proto_state{zone = Zone,
username = Username0,
client_pid = ClientPid}) ->
#mqtt_packet_connect{proto_name = ProtoName,
proto_ver = ProtoVer,
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} =
case validate_connect(Var, State1) of
{ReturnCode1, SessPresent, ProtoState3} =
case validate_connect(Var, ProtoState1) of
?RC_SUCCESS ->
case authenticate(client(State1), Password) of
case authenticate(client(ProtoState1), Password) of
{ok, IsSuperuser} ->
%% Generate clientId if null
State2 = maybe_set_clientid(State1),
%% Start session
case emqx_sm:open_session(#{clean_start => CleanStart,
client_id => clientid(State2),
ProtoState2 = maybe_set_clientid(ProtoState1),
%% Open session
case emqx_sm:open_session(#{zone => Zone,
clean_start => CleanStart,
client_id => clientid(ProtoState2),
username => Username,
client_pid => self()}) of
client_pid => ClientPid}) of
{ok, Session} -> %% TODO:...
SP = true, %% TODO:...
%% TODO: Register the client
emqx_cm:register_client(clientid(State2)),
emqx_cm:register_client(clientid(ProtoState2)),
%%emqx_cm:reg(client(State2)),
%% Start keepalive
start_keepalive(KeepAlive, State2),
start_keepalive(Keepalive, ProtoState2),
%% Emit Stats
self() ! emit_stats,
%% self() ! emit_stats,
%% 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} ->
{stop, {shutdown, Error}, State2}
?LOG(error, "Failed to open session: ~p", [Error], ProtoState2),
{?RC_UNSPECIFIED_ERROR, false, ProtoState2} %% TODO: the error reason???
end;
{error, Reason}->
?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], State1),
{?RC_BAD_USER_NAME_OR_PASSWORD, false, State1}
?LOG(error, "Username '~s' login failed for ~p", [Username, Reason], ProtoState1),
{?RC_BAD_USER_NAME_OR_PASSWORD, false, ProtoState1}
end;
ReturnCode ->
{ReturnCode, false, State1}
{ReturnCode, false, ProtoState1}
end,
%% Run hooks
emqx_hooks:run('client.connected', [ReturnCode1], client(State3)),
emqx_hooks:run('client.connected', [ReturnCode1], client(ProtoState3)),
%%TODO: Send Connack
send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), State3),
send(?CONNACK_PACKET(ReturnCode1, sp(SessPresent)), ProtoState3),
%% 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
true -> publish(Packet, 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);
%% TODO: refactor later...
process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable),
State = #proto_state{client_id = ClientId,
process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) ->
#proto_state{client_id = ClientId,
username = Username,
is_superuser = IsSuperuser,
mountpoint = MountPoint,
session = Session}) ->
Client = client(State), TopicTable = parse_topic_table(RawTopicTable),
session = Session} = State,
Client = client(State),
TopicFilters = parse_topic_filters(RawTopicFilters),
AllowDenies = if
IsSuperuser -> [];
true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicTable]
true -> [check_acl(subscribe, Topic, Client) || {Topic, _Opts} <- TopicFilters]
end,
case lists:member(deny, AllowDenies) of
true ->
?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicTable], State),
send(?SUBACK_PACKET(PacketId, [16#80 || _ <- TopicTable]), State);
?LOG(error, "Cannot SUBSCRIBE ~p for ACL Deny", [TopicFilters], State),
send(?SUBACK_PACKET(PacketId, [?RC_NOT_AUTHORIZED || _ <- TopicFilters]), State);
false ->
case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of
{ok, TopicTable1} ->
emqx_session:subscribe(Session, PacketId, mount(replvar(MountPoint, State), TopicTable1)),
case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicFilters) of
{ok, TopicFilters1} ->
ok = emqx_session:subscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicFilters1)}),
{ok, State};
{stop, _} ->
{ok, State}
{stop, _} -> {ok, State}
end
end;
@ -307,97 +255,109 @@ process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable),
process(?UNSUBSCRIBE_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,
username = Username,
mountpoint = MountPoint,
session = Session}) ->
case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of
{ok, TopicTable} ->
emqx_session:unsubscribe(Session, mount(replvar(MountPoint, State), TopicTable));
emqx_session:unsubscribe(Session, {PacketId, Properties, mount(replvar(MountPoint, State), TopicTable)});
{stop, _} ->
ok
end,
send(?UNSUBACK_PACKET(PacketId), State);
process(?PACKET(?PINGREQ), State) ->
send(?PACKET(?PINGRESP), State);
process(?PACKET(?PINGREQ), ProtoState) ->
send(?PACKET(?PINGRESP), ProtoState);
process(?PACKET(?DISCONNECT), State) ->
process(?PACKET(?DISCONNECT), ProtoState) ->
% 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,
username = Username,
mountpoint = MountPoint,
session = Session}) ->
Msg = emqx_packet:to_message(Packet),
Msg1 = Msg#message{from = #client{id = ClientId, username = Username}},
emqx_session:publish(Session, mount(replvar(MountPoint, State), Msg1));
Msg = emqx_message:set_header(username, Username,
emqx_packet:to_message(ClientId, Packet)),
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);
publish(Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) ->
publish(Packet = ?PUBLISH_PACKET(?QOS_2), 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,
username = Username,
mountpoint = MountPoint,
session = Session}) ->
%% TODO: ...
Msg = emqx_packet:to_message(Packet),
Msg1 = Msg#message{from = #client{id = ClientId, username = Username}},
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;
Msg = emqx_message:set_header(username, Username,
emqx_packet:to_message(ClientId, Packet)),
case emqx_session:publish(Session, PacketId, mount(replvar(MountPoint, State), Msg)) of
{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.
-spec(send(message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}).
send(Msg, State = #proto_state{client_id = ClientId,
username = Username,
mountpoint = MountPoint,
is_bridge = IsBridge})
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);
-spec(send({mqtt_packet_type(), mqtt_packet_id()} |
{mqtt_packet_id(), message()} |
mqtt_packet(), proto_state()) -> {ok, proto_state()}).
send({?PUBACK, PacketId}, State) ->
send(?PUBACK_PACKET(PacketId), State);
send(Packet = ?PACKET(Type), State = #proto_state{sendfun = SendFun, stats_data = Stats}) ->
trace(send, Packet, State),
emqx_metrics:sent(Packet),
SendFun(Packet),
{ok, State#proto_state{stats_data = inc_stats(send, Type, Stats)}}.
send({?PUBREC, PacketId}, State) ->
send(?PUBREC_PACKET(PacketId), State);
send(Packet = ?PACKET(Type), ProtoState = #proto_state{proto_ver = Ver,
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) ->
?LOG(info, "RECV ~s", [emqx_packet:format(Packet)], ProtoState);
?LOG(debug, "RECV ~s", [emqx_packet:format(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}) ->
Stats;
inc_stats(recv, Type, Stats) ->
#proto_stats{recv_pkt = PktCnt, recv_msg = MsgCnt} = Stats,
inc_stats(Type, #proto_stats.recv_pkt, PktCnt, #proto_stats.recv_msg, MsgCnt, Stats);
inc_stats(send, Type, Stats) ->
#proto_stats{send_pkt = PktCnt, send_msg = MsgCnt} = Stats,
inc_stats(Type, #proto_stats.send_pkt, PktCnt, #proto_stats.send_msg, MsgCnt, Stats).
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.
inc_stats(recv, Type, ProtoState = #proto_state{recv_pkt = PktCnt, recv_msg = MsgCnt}) ->
ProtoState#proto_state{recv_pkt = PktCnt + 1,
recv_msg = if Type =:= ?PUBLISH -> MsgCnt + 1;
true -> MsgCnt
end};
inc_stats(send, Type, ProtoState = #proto_state{send_pkt = PktCnt, send_msg = MsgCnt}) ->
ProtoState#proto_state{send_pkt = PktCnt + 1,
send_msg = if Type =:= ?PUBLISH -> MsgCnt + 1;
true -> MsgCnt
end}.
stop_if_auth_failure(?RC_SUCCESS, State) ->
{ok, State};
@ -415,19 +375,18 @@ shutdown(mnesia_conflict, _State = #proto_state{client_id = ClientId}) ->
shutdown(Error, State = #proto_state{client_id = ClientId,
will_msg = WillMsg}) ->
?LOG(info, "Shutdown for ~p", [Error], State),
Client = client(State),
%% Auth failure not publish the will message
case Error =:= auth_failure of
true -> ok;
false -> send_willmsg(Client, WillMsg)
false -> send_willmsg(ClientId, WillMsg)
end,
emqx_hooks:run('client.disconnected', [Error], Client),
emqx_hooks:run('client.disconnected', [Error], client(State)),
emqx_cm:unregister_client(ClientId),
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) ->
case emqx_packet:to_message(Packet) of
case emqx_packet:to_message(ClientId, Packet) of
undefined -> undefined;
Msg -> mount(replvar(MountPoint, State), Msg)
end.
@ -442,10 +401,10 @@ maybe_set_clientid(State = #proto_state{client_id = NullId})
maybe_set_clientid(State) ->
State.
send_willmsg(_Client, undefined) ->
send_willmsg(_ClientId, undefined) ->
ignore;
send_willmsg(Client, WillMsg) ->
emqx_broker:publish(WillMsg#message{from = Client}).
send_willmsg(ClientId, WillMsg) ->
emqx_broker:publish(WillMsg#message{from = ClientId}).
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).
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) ->
true;
@ -493,7 +452,7 @@ validate_clientid(#mqtt_packet_connect{proto_ver = ProtoVer,
[ProtoVer, CleanStart], ProtoState),
false.
validate_packet(?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload)) ->
validate_packet(?PUBLISH_PACKET(_QoS, Topic, _PacketId, _Payload)) ->
case emqx_topic:validate({name, Topic}) of
true -> ok;
false -> {error, badtopic}
@ -513,11 +472,11 @@ validate_topics(_Type, []) ->
validate_topics(Type, TopicTable = [{_Topic, _SubOpts}|_])
when Type =:= name orelse Type =:= filter ->
Valid = fun(Topic, Qos) ->
emqx_topic:validate({Type, Topic}) and validate_qos(Qos)
Valid = fun(Topic, QoS) ->
emqx_topic:validate({Type, Topic}) and validate_qos(QoS)
end,
case [Topic || {Topic, SubOpts} <- TopicTable,
not Valid(Topic, proplists:get_value(qos, SubOpts))] of
not Valid(Topic, SubOpts#mqtt_subopts.qos)] of
[] -> ok;
_ -> {error, badtopic}
end;
@ -530,17 +489,16 @@ validate_topics(Type, Topics = [Topic0|_]) when is_binary(Topic0) ->
validate_qos(undefined) ->
true;
validate_qos(Qos) when ?IS_QOS(Qos) ->
validate_qos(QoS) when ?IS_QOS(QoS) ->
true;
validate_qos(_) ->
false.
parse_topic_table(TopicTable) ->
lists:map(fun({Topic0, SubOpts}) ->
{Topic, Opts} = emqx_topic:parse(Topic0),
%%TODO:
{Topic, lists:usort(lists:umerge(Opts, SubOpts))}
end, TopicTable).
parse_topic_filters(TopicFilters) ->
[begin
{Topic, Opts} = emqx_topic:parse(RawTopic),
{Topic, maps:merge(?record_to_map(mqtt_subopts, SubOpts), Opts)}
end || {RawTopic, SubOpts} <- TopicFilters].
parse_topics(Topics) ->
[emqx_topic:parse(Topic) || Topic <- Topics].

View File

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

View File

@ -1,73 +1,64 @@
%%%===================================================================
%%% 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.
%%%===================================================================
%% 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.
%%
%% @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).
-behaviour(gen_server).
-include("emqx.hrl").
-include("emqx_mqtt.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
-export([start_link/1, resume/2, discard/2]).
%% 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
-record(state, {
%% Clean Start Flag
clean_start = false :: boolean(),
%% Client Binding: local | remote
@ -79,21 +70,25 @@
%% Username
username :: binary() | undefined,
%% Client Pid binding with session
%% Client pid binding with session
client_pid :: pid(),
%% Old Client Pid that has been kickout
%% Old client Pid that has been kickout
old_client_pid :: pid(),
%% Next message id of the session
next_msg_id = 1 :: mqtt_packet_id(),
%% Pending sub/unsub requests
requests :: map(),
%% Next packet id of the session
next_pkt_id = 1 :: mqtt_packet_id(),
%% Max subscriptions
max_subscriptions :: non_neg_integer(),
%% Clients subscriptions.
%% Clients Subscriptions.
subscriptions :: map(),
%% Upgrade Qos?
%% Upgrade QoS?
upgrade_qos = false :: boolean(),
%% 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.
%%
%% 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.
awaiting_rel :: map(),
%% Max Packets that Awaiting PUBREL
%% Max Packets Awaiting PUBREL
max_awaiting_rel = 100 :: non_neg_integer(),
%% Awaiting PUBREL timeout
%% Awaiting PUBREL Timeout
await_rel_timeout = 20000 :: timeout(),
%% Awaiting PUBREL timer
%% Awaiting PUBREL Timer
await_rel_timer :: reference() | undefined,
%% Session Expiry Interval
@ -141,15 +136,18 @@
%% Ignore loop deliver?
ignore_loop_deliver = false :: boolean(),
%% Created at
created_at :: erlang:timestamp()
}).
-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(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,
await_rel_timeout, expiry_interval, enable_stats, force_gc_count,
created_at]).
@ -158,82 +156,82 @@
emqx_logger:Level([{client, State#state.client_id}],
"Session(~s): " ++ Format, [State#state.client_id | Args])).
%% @doc Start a Session
-spec(start_link(map()) -> {ok, pid()} | {error, term()}).
start_link(Attrs) ->
gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]).
%% @doc Start a session
-spec(start_link(SessAttrs :: map()) -> {ok, pid()} | {error, term()}).
start_link(SessAttrs) ->
gen_server:start_link(?MODULE, SessAttrs, [{hibernate_after, 30000}]).
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% PubSub API
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% @doc Subscribe topics
-spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok).
subscribe(SessionPid, TopicTable) -> %%TODO: the ack function??...
gen_server:cast(SessionPid, {subscribe, self(), TopicTable, fun(_) -> ok end}).
-spec(subscribe(pid(), list({topic(), map()}) |
{mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok).
%% internal call
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).
subscribe(SessionPid, PacketId, TopicTable) -> %%TODO: the ack function??...
From = self(),
AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end,
gen_server:cast(SessionPid, {subscribe, From, TopicTable, AckFun}).
-spec(publish(pid(), mqtt_packet_id(), message()) -> {ok, delivery()} | {error, term()}).
publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_0}) ->
%% Publish QoS0 message to broker directly
emqx_broker:publish(Msg);
%% @doc Publish Message
-spec(publish(pid(), message()) -> ok | {error, term()}).
publish(_SessionPid, Msg = #message{qos = ?QOS_0}) ->
%% Publish QoS0 Directly
emqx_broker:publish(Msg), ok;
publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) ->
%% Publish QoS1 message to broker directly
emqx_broker:publish(Msg);
publish(_SessionPid, Msg = #message{qos = ?QOS_1}) ->
%% Publish QoS1 message directly for client will PubAck automatically
emqx_broker:publish(Msg), ok;
publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) ->
%% Publish QoS2 message to session
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).
puback(SessionPid, PacketId) ->
gen_server:cast(SessionPid, {puback, PacketId}).
puback(SPid, 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).
pubrec(SessionPid, PacketId) ->
gen_server:cast(SessionPid, {pubrec, PacketId}).
pubrec(SPid, 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).
pubrel(SessionPid, PacketId) ->
gen_server:cast(SessionPid, {pubrel, PacketId}).
pubrel(SPid, PacketId) ->
gen_server:cast(SPid, {pubrel, PacketId}).
-spec(pubcomp(pid(), mqtt_packet_id()) -> ok).
pubcomp(SessionPid, PacketId) ->
gen_server:cast(SessionPid, {pubcomp, PacketId}).
pubcomp(SPid, PacketId) ->
gen_server:cast(SPid, {pubcomp, PacketId}).
%% @doc Unsubscribe the topics
-spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok).
unsubscribe(SessionPid, TopicTable) ->
gen_server:cast(SessionPid, {unsubscribe, self(), TopicTable}).
-spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok).
unsubscribe(SPid, TopicFilters) when is_list(TopicFilters) ->
%%TODO: Parse the topic filters?
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).
resume(SessionPid, ClientPid) ->
gen_server:cast(SessionPid, {resume, ClientPid}).
%% @doc Get session state
state(SessionPid) when is_pid(SessionPid) ->
gen_server:call(SessionPid, state).
resume(SPid, ClientPid) ->
gen_server:cast(SPid, {resume, ClientPid}).
%% @doc Get session info
-spec(info(pid() | #state{}) -> list(tuple())).
info(SessionPid) when is_pid(SessionPid) ->
gen_server:call(SessionPid, info);
info(SPid) when is_pid(SPid) ->
gen_server:call(SPid, info);
info(State) when is_record(State, state) ->
?record_to_proplist(state, State, ?INFO_KEYS).
-spec(stats(pid() | #state{}) -> list({atom(), non_neg_integer()})).
stats(SessionPid) when is_pid(SessionPid) ->
gen_server:call(SessionPid, stats);
stats(SPid) when is_pid(SPid) ->
gen_server:call(SPid, stats);
stats(#state{max_subscriptions = MaxSubscriptions,
subscriptions = Subscriptions,
@ -247,9 +245,9 @@ stats(#state{max_subscriptions = MaxSubscriptions,
{subscriptions, maps:size(Subscriptions)},
{max_inflight, MaxInflight},
{inflight_len, emqx_inflight:size(Inflight)},
{max_mqueue, ?MQueue:max_len(MQueue)},
{mqueue_len, ?MQueue:len(MQueue)},
{mqueue_dropped, ?MQueue:dropped(MQueue)},
{max_mqueue, emqx_mqueue:max_len(MQueue)},
{mqueue_len, emqx_mqueue:len(MQueue)},
{mqueue_dropped, emqx_mqueue:dropped(MQueue)},
{max_awaiting_rel, MaxAwaitingRel},
{awaiting_rel_len, maps:size(AwaitingRel)},
{deliver_msg, get(deliver_msg)},
@ -257,50 +255,54 @@ stats(#state{max_subscriptions = MaxSubscriptions,
%% @doc Discard the session
-spec(discard(pid(), client_id()) -> ok).
discard(SessionPid, ClientId) ->
gen_server:call(SessionPid, {discard, ClientId}).
discard(SPid, ClientId) ->
gen_server:call(SPid, {discard, ClientId}, infinity).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
-spec(close(pid()) -> ok).
close(SPid) ->
gen_server:call(SPid, close, infinity).
init(#{clean_start := CleanStart,
%%------------------------------------------------------------------------------
%% gen_server callbacks
%%------------------------------------------------------------------------------
init(#{zone := Zone,
client_id := ClientId,
username := Username,
client_pid := ClientPid}) ->
client_pid := ClientPid,
clean_start := CleanStart,
username := Username}) ->
process_flag(trap_exit, true),
true = link(ClientPid),
init_stats([deliver_msg, enqueue_msg]),
{ok, Env} = emqx_config:get_env(session),
{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()),
MaxInflight = emqx_zone:env(Zone, max_inflight),
State = #state{clean_start = CleanStart,
binding = binding(ClientPid),
client_id = ClientId,
client_pid = ClientPid,
username = Username,
subscriptions = #{},
max_subscriptions = get_value(max_subscriptions, Env, 0),
upgrade_qos = get_value(upgrade_qos, Env, false),
max_subscriptions = emqx_zone:env(Zone, max_subscriptions, 0),
upgrade_qos = emqx_zone:env(Zone, upgrade_qos, false),
max_inflight = MaxInflight,
inflight = emqx_inflight:new(MaxInflight),
mqueue = MQueue,
retry_interval = get_value(retry_interval, Env),
mqueue = init_mqueue(Zone, ClientId),
retry_interval = emqx_zone:env(Zone, retry_interval, 0),
awaiting_rel = #{},
await_rel_timeout = get_value(await_rel_timeout, Env),
max_awaiting_rel = get_value(max_awaiting_rel, Env),
expiry_interval = get_value(expiry_interval, Env),
enable_stats = EnableStats,
ignore_loop_deliver = IgnoreLoopDeliver,
await_rel_timeout = emqx_zone:env(Zone, await_rel_timeout),
max_awaiting_rel = emqx_zone:env(Zone, max_awaiting_rel),
expiry_interval = emqx_zone:env(Zone, session_expiry_interval),
enable_stats = emqx_zone:env(Zone, enable_stats, true),
ignore_loop_deliver = emqx_zone:env(Zone, ignore_loop_deliver, true),
created_at = os:timestamp()},
emqx_sm:register_session(ClientId, info(State)),
emqx_hooks:run('session.created', [ClientId, Username]),
io:format("Session started: ~p~n", [self()]),
emqx_hooks:run('session.created', [ClientId]),
{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) ->
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),
{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,
await_rel_timer = Timer,
await_rel_timeout = Timeout}) ->
case is_awaiting_full(State) of
false ->
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
end,
reply(ok, State1#state{awaiting_rel = maps:put(PacketId, Msg, AwaitingRel)});
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'),
reply({error, dropped}, State)
end;
@ -338,69 +340,53 @@ handle_call(info, _From, State) ->
handle_call(stats, _From, State) ->
reply(stats(State), State);
handle_call(state, _From, State) ->
reply(?record_to_proplist(state, State, ?STATE_KEYS), State);
handle_call(close, _From, State) ->
{stop, normal, State};
handle_call(Req, _From, State) ->
emqx_logger:error("[Session] Unexpected request: ~p", [Req]),
{reply, ignore, State}.
emqx_logger:error("[Session] unexpected call: ~p", [Req]),
{reply, ignored, State}.
handle_cast({subscribe, From, TopicTable, AckFun},
State = #state{client_id = ClientId,
username = Username,
subscriptions = Subscriptions}) ->
?LOG(info, "Subscribe ~p", [TopicTable], State),
{GrantedQos, Subscriptions1} =
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 =
handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}},
State = #state{client_id = ClientId, subscriptions = Subscriptions}) ->
?LOG(info, "Subscribe ~p", [TopicFilters], State),
{ReasonCodes, Subscriptions1} =
lists:foldl(fun({Topic, SubOpts = #{qos := QoS}}, {RcAcc, SubMap}) ->
{[QoS|RcAcc],
case maps:find(Topic, SubMap) of
{ok, NewQos} ->
?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State),
{ok, SubOpts} ->
?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State),
SubMap;
{ok, OldQos} ->
emqx_broker:setopts(Topic, ClientId, [{qos, NewQos}]),
emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}),
?LOG(warning, "Duplicated subscribe ~s, old_qos=~w, new_qos=~w",
[Topic, OldQos, NewQos], State),
maps:put(Topic, NewQos, SubMap);
{ok, OldOpts} ->
emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts),
emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]),
?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State),
maps:put(Topic, SubOpts, SubMap);
error ->
case Fastlane of
true -> emqx:subscribe(Topic, From, Opts);
false -> emqx:subscribe(Topic, ClientId, Opts)
end,
emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}),
maps:put(Topic, NewQos, SubMap)
end,
{[NewQos|QosAcc], SubMap1}
end, {[], Subscriptions}, TopicTable),
io:format("GrantedQos: ~p~n", [GrantedQos]),
AckFun(lists:reverse(GrantedQos)),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate};
emqx_broker:subscribe(Topic, ClientId, SubOpts),
emqx_hooks:run('session.subscribed', [ClientId, Topic, SubOpts]),
maps:put(Topic, SubOpts, SubMap)
end}
end, {[], Subscriptions}, TopicFilters),
suback(From, PacketId, lists:reverse(ReasonCodes)),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1})};
handle_cast({unsubscribe, From, TopicTable},
State = #state{client_id = ClientId,
username = Username,
subscriptions = Subscriptions}) ->
?LOG(info, "Unsubscribe ~p", [TopicTable], State),
Subscriptions1 =
lists:foldl(fun({Topic, Opts}, SubMap) ->
Fastlane = lists:member(fastlane, Opts),
handle_cast({unsubscribe, From, {PacketId, _Properties, TopicFilters}},
State = #state{client_id = ClientId, subscriptions = Subscriptions}) ->
?LOG(info, "Unsubscribe ~p", [TopicFilters], State),
{ReasonCodes, Subscriptions1} =
lists:foldl(fun(Topic, {RcAcc, SubMap}) ->
case maps:find(Topic, SubMap) of
{ok, _Qos} ->
case Fastlane of
true -> emqx:unsubscribe(Topic, From);
false -> emqx:unsubscribe(Topic, ClientId)
end,
emqx_hooks:run('session.unsubscribed', [ClientId, Username], {Topic, Opts}),
maps:remove(Topic, SubMap);
{ok, SubOpts} ->
emqx_broker:unsubscribe(Topic, ClientId),
emqx_hooks:run('session.unsubscribed', [ClientId, Topic, SubOpts]),
{[?RC_SUCCESS|RcAcc], maps:remove(Topic, SubMap)};
error ->
SubMap
{[?RC_NO_SUBSCRIPTION_EXISTED|RcAcc], SubMap}
end
end, Subscriptions, TopicTable),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate};
end, {[], Subscriptions}, TopicFilters),
unsuback(From, PacketId, lists:reverse(ReasonCodes)),
{noreply, emit_stats(State#state{subscriptions = Subscriptions1})};
%% PUBACK:
handle_cast({puback, PacketId}, State = #state{inflight = Inflight}) ->
@ -501,11 +487,16 @@ handle_cast({resume, ClientPid},
{noreply, emit_stats(dequeue(retry_delivery(true, State1)))};
handle_cast(Msg, State) ->
emqx_logger:error("[Session] Unexpected msg: ~p", [Msg]),
emqx_logger:error("[Session] unexpected cast: ~p", [Msg]),
{noreply, State}.
%% Ignore Messages delivered by self
handle_info({dispatch, _Topic, #message{from = {ClientId, _}}},
handle_info({dispatch, Topic, Msgs}, State) when is_list(Msgs) ->
{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}) ->
{noreply, State};
@ -536,7 +527,7 @@ handle_info({'EXIT', ClientPid, Reason},
client_pid = ClientPid,
expiry_interval = Interval}) ->
?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},
{noreply, emit_stats(State1), hibernate};
@ -545,26 +536,37 @@ handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) ->
{noreply, State, hibernate};
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),
{noreply, State, hibernate};
handle_info(Info, State) ->
emqx_logger:error("[Session] Unexpected info: ~p", [Info]),
emqx_logger:error("[Session] unexpected info: ~p", [Info]),
{noreply, State}.
terminate(Reason, #state{client_id = ClientId, username = Username}) ->
emqx_hooks:run('session.terminated', [ClientId, Username, Reason]),
emqx_sm:unregister_session(ClientId).
code_change(_OldVsn, Session, _Extra) ->
{ok, Session}.
code_change(_OldVsn, State, _Extra) ->
{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
%%--------------------------------------------------------------------
kick(_ClientId, undefined, _Pid) ->
ignore;
@ -576,32 +578,32 @@ kick(ClientId, OldPid, Pid) ->
%% Clean noproc
receive {'EXIT', OldPid, _} -> ok after 0 -> ok end.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% 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}) ->
case emqx_inflight:is_empty(Inflight) of
true -> State;
false -> Msgs = lists:sort(sortfun(inflight),
emqx_inflight:values(Inflight)),
true ->
State;
false ->
Msgs = lists:sort(sortfun(inflight), emqx_inflight:values(Inflight)),
retry_delivery(Force, Msgs, os:timestamp(), State)
end.
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,
State = #state{inflight = Inflight,
retry_interval = Interval}) ->
retry_delivery(Force, [{Type, Msg0, Ts} | Msgs], Now,
State = #state{inflight = Inflight, retry_interval = Interval}) ->
Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms
if
Force orelse (Diff >= Interval) ->
case {Type, Msg} of
{publish, Msg = #message{headers = #{packet_id := PacketId}}} ->
redeliver(Msg, State),
Inflight1 = emqx_inflight:update(PacketId, {publish, Msg, Now}, Inflight),
case {Type, Msg0} of
{publish, {PacketId, Msg}} ->
redeliver({PacketId, Msg}, State),
Inflight1 = emqx_inflight:update(PacketId, {publish, {PacketId, Msg}, Now}, Inflight),
retry_delivery(Force, Msgs, Now, State#state{inflight = Inflight1});
{pubrel, PacketId} ->
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})
end;
true ->
State#state{retry_timer = start_timer(Interval - Diff, retry_delivery)}
State#state{retry_timer = emqx_misc:start_timer(Interval - Diff, retry_delivery)}
end.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Expire Awaiting Rel
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
expire_awaiting_rel(State = #state{awaiting_rel = AwaitingRel}) ->
case maps:size(AwaitingRel) of
@ -635,12 +637,12 @@ expire_awaiting_rel([{PacketId, Msg = #message{timestamp = TS}} | Msgs],
emqx_metrics:inc('messages/qos2/dropped'),
expire_awaiting_rel(Msgs, Now, State#state{awaiting_rel = maps:remove(PacketId, AwaitingRel)});
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.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Sort Inflight, AwaitingRel
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
sortfun(inflight) ->
fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end;
@ -651,18 +653,18 @@ sortfun(awaiting_rel) ->
Ts1 < Ts2
end.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Check awaiting rel
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
is_awaiting_full(#state{max_awaiting_rel = 0}) ->
false;
is_awaiting_full(#state{awaiting_rel = AwaitingRel, max_awaiting_rel = MaxLen}) ->
maps:size(AwaitingRel) >= MaxLen.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Dispatch Messages
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Enqueue message if the client has been disconnected
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
dispatch(Msg = #message{qos = ?QOS0}, State) ->
deliver(Msg, State), State;
deliver(undefined, Msg, State), State;
dispatch(Msg = #message{qos = QoS},
State = #state{next_msg_id = MsgId, inflight = Inflight})
dispatch(Msg = #message{qos = QoS}, State = #state{next_pkt_id = PacketId, inflight = Inflight})
when QoS =:= ?QOS1 orelse QoS =:= ?QOS2 ->
case emqx_inflight:is_full(Inflight) of
true ->
enqueue_msg(Msg, State);
false ->
Msg1 = emqx_message:set_header(packet_id, MsgId, Msg),
deliver(Msg1, State),
await(Msg1, next_msg_id(State))
deliver(PacketId, Msg, State),
await(PacketId, Msg, next_pkt_id(State))
end.
enqueue_msg(Msg, State = #state{mqueue = Q}) ->
inc_stats(enqueue_msg),
State#state{mqueue = ?MQueue:in(Msg, Q)}.
State#state{mqueue = emqx_mqueue:in(Msg, Q)}.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Deliver
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
redeliver(Msg = #message{qos = QoS}, State) ->
deliver(if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State);
redeliver({PacketId, Msg = #message{qos = QoS}}, State) ->
deliver(PacketId, if QoS =:= ?QOS2 -> Msg; true -> emqx_message:set_flag(dup, Msg) end, State);
redeliver({pubrel, PacketId}, #state{client_pid = Pid}) ->
Pid ! {redeliver, {?PUBREL, PacketId}}.
Pid ! {deliver, {pubrel, PacketId}}.
deliver(Msg, #state{client_pid = Pid, binding = local}) ->
inc_stats(deliver_msg), Pid ! {deliver, Msg};
deliver(Msg, #state{client_pid = Pid, binding = remote}) ->
inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, Msg}]).
deliver(PacketId, Msg, #state{client_pid = Pid, binding = local}) ->
inc_stats(deliver_msg), Pid ! {deliver, {publish, PacketId, Msg}};
deliver(PacketId, Msg, #state{client_pid = Pid, binding = remote}) ->
inc_stats(deliver_msg), emqx_rpc:cast(node(Pid), erlang, send, [Pid, {deliver, PacketId, Msg}]).
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Awaiting ACK for QoS1/QoS2 Messages
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
await(Msg = #message{headers = #{packet_id := PacketId}},
State = #state{inflight = Inflight,
await(PacketId, Msg, State = #state{inflight = Inflight,
retry_timer = RetryTimer,
retry_interval = Interval}) ->
%% Start retry timer if the Inflight is still empty
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
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,
username = Username,
@ -751,9 +750,9 @@ acked(pubrec, PacketId, State = #state{client_id = ClientId,
acked(pubcomp, PacketId, State = #state{inflight = Inflight}) ->
State#state{inflight = emqx_inflight:delete(PacketId, Inflight)}.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Dequeue
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Do nothing if client is disconnected
dequeue(State = #state{client_pid = undefined}) ->
@ -766,7 +765,7 @@ dequeue(State = #state{inflight = Inflight}) ->
end.
dequeue2(State = #state{mqueue = Q}) ->
case ?MQueue:out(Q) of
case emqx_mqueue:out(Q) of
{empty, _Q} ->
State;
{{value, Msg}, Q1} ->
@ -774,43 +773,37 @@ dequeue2(State = #state{mqueue = Q}) ->
dequeue(dispatch(Msg, State#state{mqueue = Q1}))
end.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Tune QoS
%%--------------------------------------------------------------------
tune_qos(Topic, Msg = #message{qos = PubQoS},
#state{subscriptions = SubMap, upgrade_qos = UpgradeQoS}) ->
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};
{ok, SubQoS} when (not UpgradeQoS) andalso (SubQoS < PubQoS) ->
{ok, #{qos := SubQoS}} when (not UpgradeQoS) andalso (SubQoS < PubQoS) ->
Msg#message{qos = SubQoS};
{ok, _} ->
Msg;
error ->
Msg
{ok, _} -> Msg;
error -> Msg
end.
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Reset Dup
%%--------------------------------------------------------------------
reset_dup(Msg) ->
emqx_message:unset_flag(dup, Msg).
%%--------------------------------------------------------------------
%%------------------------------------------------------------------------------
%% Next Msg Id
%%--------------------------------------------------------------------
next_msg_id(State = #state{next_msg_id = 16#FFFF}) ->
State#state{next_msg_id = 1};
next_pkt_id(State = #state{next_pkt_id = 16#FFFF}) ->
State#state{next_pkt_id = 1};
next_msg_id(State = #state{next_msg_id = Id}) ->
State#state{next_msg_id = Id + 1}.
next_pkt_id(State = #state{next_pkt_id = Id}) ->
State#state{next_pkt_id = Id + 1}.
%%--------------------------------------------------------------------
%% Emit session stats
%%--------------------------------------------------------------------
emit_stats(State = #state{enable_stats = false}) ->
State;
@ -822,7 +815,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1).
%%--------------------------------------------------------------------
%% Helper functions
%%--------------------------------------------------------------------
reply(Reply, State) ->
{reply, Reply, State, hibernate}.

View File

@ -38,7 +38,7 @@
-define(TAB, emqx_shared_subscription).
-record(state, {pmon}).
-record(shared_subscription, {group, topic, subpid}).
-record(emqx_shared_subscription, {group, topic, subpid}).
%%------------------------------------------------------------------------------
%% Mnesia bootstrap
@ -48,8 +48,8 @@ mnesia(boot) ->
ok = ekka_mnesia:create_table(?TAB, [
{type, bag},
{ram_copies, [node()]},
{record_name, shared_subscription},
{attributes, record_info(fields, shared_subscription)}]);
{record_name, emqx_shared_subscription},
{attributes, record_info(fields, emqx_shared_subscription)}]);
mnesia(copy) ->
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)).
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...
dispatch(Group, Topic, Delivery = #delivery{message = Msg, flows = Flows}) ->
@ -110,7 +110,7 @@ init([]) ->
init_monitors() ->
mnesia:foldl(
fun(#shared_subscription{subpid = SubPid}, Mon) ->
fun(#emqx_shared_subscription{subpid = SubPid}, Mon) ->
emqx_pmon:monitor(SubPid, Mon)
end, emqx_pmon:new(), ?TAB).
@ -126,11 +126,11 @@ handle_cast(Msg, State) ->
{noreply, State}.
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)})};
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)})};
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}) ->
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)})};
handle_info(Info, State) ->
@ -156,8 +156,10 @@ code_change(_OldVsn, State, _Extra) ->
%%--------------------------------------------------------------------
cleanup_down(SubPid) ->
lists:foreach(fun(Record) -> mnesia:delete_object(?TAB, Record) end,
mnesia:match_object(#shared_subscription{_ = '_', subpid = SubPid})).
lists:foreach(
fun(Record) ->
mnesia:dirty_delete_object(?TAB, Record)
end,mnesia:dirty_match_object(#emqx_shared_subscription{_ = '_', subpid = SubPid})).
update_stats(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([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([register_session/2, get_session_attrs/1, unregister_session/1]).
-export([get_session_stats/1, set_session_stats/2]).
@ -46,7 +46,7 @@ start_link() ->
gen_server:start_link({local, ?SM}, ?MODULE, [], []).
%% @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,
client_id := ClientId,
client_pid := ClientPid}) ->

View File

@ -22,7 +22,8 @@
-export([is_enabled/0]).
-export([register_session/1, lookup_session/1, unregister_session/1]).
%% 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(TAB, emqx_session_registry).

View File

@ -1,18 +1,16 @@
%%%===================================================================
%%% 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.
%%%===================================================================
%% 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_sup).
@ -65,6 +63,7 @@ init([]) ->
BrokerSup = supervisor_spec(emqx_broker_sup),
%% BridgeSup
BridgeSup = supervisor_spec(emqx_bridge_sup_sup),
BridgeSup1 = supervisor_spec(emqx_bridge1_sup),
%% AccessControl
AccessControl = worker_spec(emqx_access_control),
%% Session Manager
@ -73,8 +72,6 @@ init([]) ->
SessionSup = supervisor_spec(emqx_session_sup),
%% Connection Manager
CMSup = supervisor_spec(emqx_cm_sup),
%% WebSocket Connection Sup
WSConnSup = supervisor_spec(emqx_ws_connection_sup),
%% Sys Sup
SysSup = supervisor_spec(emqx_sys_sup),
{ok, {{one_for_all, 0, 1},
@ -82,11 +79,11 @@ init([]) ->
RouterSup,
BrokerSup,
BridgeSup,
BridgeSup1,
AccessControl,
SMSup,
SessionSup,
CMSup,
WSConnSup,
SysSup]}}.
%%--------------------------------------------------------------------

View File

@ -171,6 +171,8 @@ publish(metrics, Metrics) ->
safe_publish(Topic, Payload) ->
safe_publish(Topic, #{}, Payload).
safe_publish(Topic, Flags, Payload) ->
Flags1 = maps:merge(#{sys => true, qos => 0}, Flags),
emqx_broker:safe_publish(emqx_message:new(?SYS, Flags1, Topic, iolist_to_binary(Payload))).
emqx_broker:safe_publish(
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 = []})}.
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, []).
@ -126,7 +126,7 @@ handle_info({monitor, SusPid, busy_dist_port, Port}, State) ->
safe_publish(busy_dist_port, WarnMsg)
end, State);
handle_info(reset, State) ->
handle_info({timeout, _Ref, reset}, State) ->
{noreply, State#state{events = []}, hibernate};
handle_info(Info, State) ->
@ -158,5 +158,5 @@ safe_publish(Event, WarnMsg) ->
emqx_broker:safe_publish(sysmon_msg(Topic, iolist_to_binary(WarnMsg))).
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,
modules => [emqx_sys]},
Sysmon = #{id => sys_mon,
start => {emqx_sys_mon, start_link,
[emqx_config:get_env(sysmon, [])]},
start => {emqx_sys_mon, start_link, [emqx_config:get_env(sysmon, [])]},
restart => permanent,
shutdown => 5000,
type => worker,

View File

@ -14,23 +14,16 @@
-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() ->
rand:seed(exsplus, erlang:timestamp()).
now_secs() ->
erlang:system_time(second).
now_ms() ->
now_ms(os:timestamp()).
erlang:system_time(millisecond).
now_ms({MegaSecs, Secs, MicroSecs}) ->
(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(words() :: list(word())).
-type(option() :: {qos, mqtt_qos()} | {share, '$queue' | 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).
@ -163,20 +162,21 @@ join(Words) ->
end, {true, <<>>}, [bin(W) || W <- Words]),
Bin.
-spec(parse(topic()) -> {topic(), [option()]}).
-spec(parse(topic()) -> {topic(), #{}}).
parse(Topic) when is_binary(Topic) ->
parse(Topic, []).
parse(Topic, #{}).
parse(Topic = <<"$queue/", Topic1/binary>>, Options) ->
case lists:keyfind(share, 1, Options) of
{share, _} -> error({invalid_topic, Topic});
false -> parse(Topic1, [{share, '$queue'} | Options])
case maps:find(share, Options) of
{ok, _} -> error({invalid_topic, Topic});
error -> parse(Topic1, maps:put(share, '$queue', Options))
end;
parse(Topic = <<"$share/", Topic1/binary>>, Options) ->
case lists:keyfind(share, 1, Options) of
{share, _} -> error({invalid_topic, Topic});
false -> [Group, Topic2] = binary:split(Topic1, <<"/">>),
{Topic2, [{share, Group} | Options]}
case maps:find(share, Options) of
{ok, _} -> error({invalid_topic, Topic});
error -> [Group, Topic2] = binary:split(Topic1, <<"/">>),
{Topic2, maps:put(share, Group, Options)}
end;
parse(Topic, Options) -> {Topic, Options}.
parse(Topic, Options) ->
{Topic, Options}.

View File

@ -19,6 +19,7 @@
-include("emqx.hrl").
-export([start_link/0]).
-export([trace/2]).
-export([start_trace/2, lookup_traces/0, stop_trace/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
@ -31,14 +32,17 @@
-define(TRACER, ?MODULE).
-define(OPTIONS, [{formatter_config, [time, " [",severity,"] ", message, "\n"]}]).
%%------------------------------------------------------------------------------
%% Start the tracer
%%------------------------------------------------------------------------------
-spec(start_link() -> {ok, pid()} | ignore | {error, term()}).
start_link() ->
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
%%------------------------------------------------------------------------------

View File

@ -17,21 +17,15 @@
-module(emqx_vm).
-export([schedulers/0]).
-export([microsecs/0]).
-export([loads/0, get_system_info/0, get_system_info/1, mem_info/0, scheduler_usage/1]).
-export([get_memory/0]).
-export([get_process_list/0, get_process_info/0, get_process_info/1,
get_process_gc/0, get_process_gc/1,
get_process_group_leader_info/1,
get_process_limit/0]).
-export([get_ets_list/0, get_ets_info/0, get_ets_info/1,
get_ets_object/0, get_ets_object/1]).
-export([get_port_types/0, get_port_info/0, get_port_info/1]).
-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)}].
ftos(F) ->
[S] = io_lib:format("~.2f", [F]), S.
S = io_lib:format("~.2f", [F]), S.
%%%% erlang vm scheduler_usage fun copied from recon
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) 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.
%%%===================================================================
%% 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_ws_connection).
-behaviour(gen_server).
-include("emqx.hrl").
-include("emqx_mqtt.hrl").
-include("emqx_misc.hrl").
-import(proplists, [get_value/2, get_value/3]).
%% API Exports
-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([info/1]).
-export([stats/1]).
-export([kick/1]).
-export([session/1]).
%% gen_server Function Exports
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
%% websocket callbacks
-export([init/2]).
-export([websocket_init/1]).
-export([websocket_handle/2]).
-export([websocket_info/2]).
-export([terminate/3]).
%% TODO: remove ...
-export([handle_pre_hibernate/1]).
-record(state, {
request,
options,
peername,
sockname,
proto_state,
parser_state,
keepalive,
enable_stats,
stats_timer,
idle_timeout,
shutdown_reason
}).
%% WebSocket Client State
-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]).
-define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]).
-define(INFO_KEYS, [peername, sockname]).
-define(WSLOG(Level, Format, Args, State),
emqx_logger:Level("WsClient(~s): " ++ Format,
[esockd_net:format(State#wsclient_state.peername) | Args])).
lager:Level("WsClient(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])).
%% @doc Start WebSocket Client.
start_link(Env, WsPid, Req, ReplyChannel) ->
gen_server:start_link(?MODULE, [Env, WsPid, Req, ReplyChannel],
[[{hibernate_after, 10000}]]).
%%------------------------------------------------------------------------------
%% API
%%------------------------------------------------------------------------------
info(CPid) ->
gen_server:call(CPid, info).
info(WSPid) ->
call(WSPid, info).
stats(CPid) ->
gen_server:call(CPid, stats).
stats(WSPid) ->
call(WSPid, stats).
kick(CPid) ->
gen_server:call(CPid, kick).
kick(WSPid) ->
call(WSPid, kick).
subscribe(CPid, TopicTable) ->
CPid ! {subscribe, TopicTable}.
session(WSPid) ->
call(WSPid, session).
unsubscribe(CPid, Topics) ->
CPid ! {unsubscribe, Topics}.
session(CPid) ->
gen_server:call(CPid, session).
clean_acl_cache(CPid, Topic) ->
gen_server:call(CPid, {clean_acl_cache, Topic}).
%%--------------------------------------------------------------------
%% gen_server Callbacks
%%--------------------------------------------------------------------
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})
call(WSPid, Req) ->
Mref = erlang:monitor(process, WSPid),
WSPid ! {call, {self(), Mref}, Req},
receive
{Mref, Reply} ->
erlang:demonitor(Mref, [flush]),
Reply;
{'DOWN', Mref, _, _, Reason} ->
exit(Reason)
after 5000 ->
erlang:demonitor(Mref, [flush]),
exit(timeout)
end.
handle_pre_hibernate(State = #wsclient_state{ws_pid = WsPid}) ->
erlang:garbage_collect(WsPid),
{hibernate, emqx_gc:reset_conn_gc_count(#wsclient_state.force_gc_count, emit_stats(State))}.
%%------------------------------------------------------------------------------
%% WebSocket callbacks
%%------------------------------------------------------------------------------
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}) ->
Info = [{websocket, true}, {peername, Peername} | emqx_protocol:info(ProtoState)],
{reply, Stats, _, _} = handle_call(stats, From, State),
reply(lists:append(Info, Stats), State);
handle_call(stats, _From, State = #wsclient_state{proto_state = ProtoState}) ->
reply(lists:append([emqx_misc:proc_stats(),
wsock_stats(State),
emqx_protocol:stats(ProtoState)]), State);
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}) ->
BinSize = iolist_size(Data),
put(recv_oct, get(recv_oct) + BinSize),
?WSLOG(debug, "RECV ~p", [Data], State),
emqx_metrics:inc('bytes/received', BinSize),
case catch emqx_frame:parse(iolist_to_binary(Data), ParserState) of
{more, NewParserState} ->
{ok, State#state{parser_state = NewParserState}};
{ok, Packet, Rest} ->
emqx_metrics:received(Packet),
put(recv_cnt, get(recv_cnt) + 1),
case emqx_protocol:received(Packet, ProtoState) of
{ok, ProtoState1} ->
{noreply, gc(State#wsclient_state{proto_state = ProtoState1}), hibernate};
websocket_handle({binary, Rest}, reset_parser(State#state{proto_state = ProtoState1}));
{error, Error} ->
?WSLOG(error, "Protocol error - ~p", [Error], State),
shutdown(Error, State);
{stop, State};
{error, Error, ProtoState1} ->
shutdown(Error, State#wsclient_state{proto_state = ProtoState1});
shutdown(Error, State#state{proto_state = 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;
handle_cast(Msg, State) ->
?WSLOG(error, "Unexpected Msg: ~p", [Msg], State),
{noreply, State, hibernate}.
websocket_info(emit_stats, State = #state{proto_state = ProtoState}) ->
Stats = lists:append([wsock_stats(), emqx_misc:proc_stats(),
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) ->
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}) ->
websocket_info({keepalive, start, 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} ->
{noreply, State#wsclient_state{keepalive = KeepAlive}, hibernate};
{ok, State#state{keepalive = KeepAlive}};
{error, Error} ->
?WSLOG(warning, "Keepalive error - ~p", [Error], State),
shutdown(Error, State)
end;
handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive}) ->
websocket_info({keepalive, check}, State = #state{keepalive = KeepAlive}) ->
case emqx_keepalive:check(KeepAlive) of
{ok, KeepAlive1} ->
{noreply, emit_stats(State#wsclient_state{keepalive = KeepAlive1}), hibernate};
{ok, State#state{keepalive = KeepAlive1}};
{error, timeout} ->
?WSLOG(debug, "Keepalive Timeout!", [], State),
shutdown(keepalive_timeout, State);
@ -231,92 +227,46 @@ handle_info({keepalive, check}, State = #wsclient_state{keepalive = KeepAlive})
shutdown(keepalive_error, State)
end;
handle_info({'EXIT', WsPid, normal}, State = #wsclient_state{ws_pid = WsPid}) ->
stop(normal, State);
websocket_info({shutdown, conflict, {ClientId, NewPid}}, 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}) ->
?WSLOG(error, "shutdown: ~p",[Reason], State),
websocket_info({binary, Data}, State) ->
{reply, {binary, Data}, State};
websocket_info({shutdown, Reason}, State) ->
shutdown(Reason, State);
%% The session process exited unexpectedly.
handle_info({'EXIT', Pid, Reason}, State = #wsclient_state{proto_state = ProtoState}) ->
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) ->
websocket_info(Info, State) ->
?WSLOG(error, "unexpected info: ~p", [Info], State),
{ok, State}.
%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
send_fun(ReplyChannel) ->
Self = self(),
fun(Packet) ->
Data = emqx_frame:serialize(Packet),
emqx_metrics:inc('bytes/sent', iolist_size(Data)),
case ReplyChannel({binary, Data}) of
ok -> ok;
{error, Reason} -> Self ! {shutdown, Reason}
end
terminate(SockError, _Req, #state{keepalive = Keepalive,
proto_state = ProtoState,
shutdown_reason = Reason}) ->
emqx_keepalive:cancel(Keepalive),
io:format("Websocket shutdown for ~p, sockerror: ~p~n", [Reason, SockError]),
case Reason of
undefined ->
ok;
%%emqx_protocol:shutdown(SockError, ProtoState);
_ ->
ok%%emqx_protocol:shutdown(Reason, ProtoState)
end.
stat_fun(Transport, Sock) ->
fun() ->
case Transport:getstat(Sock, [recv_oct]) of
{ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
{error, Error} -> {error, Error}
end
end.
reset_parser(State = #state{proto_state = ProtoState}) ->
State#state{parser_state = emqx_protocol:parser(ProtoState)}.
emit_stats(State = #wsclient_state{proto_state = ProtoState}) ->
emit_stats(emqx_protocol:clientid(ProtoState), State).
emit_stats(_ClientId, State = #wsclient_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),
ensure_stats_timer(State = #state{enable_stats = true,
stats_timer = undefined,
idle_timeout = Timeout}) ->
State#state{stats_timer = erlang:send_after(Timeout, self(), emit_stats)};
ensure_stats_timer(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) ->
stop({shutdown, Reason}, State).
{stop, State#state{shutdown_reason = Reason}}.
stop(Reason, State) ->
{stop, Reason, State}.
gc(State) ->
Cb = fun() -> emit_stats(State) end,
emqx_gc:maybe_force_gc(#wsclient_state.force_gc_count, State, Cb).
wsock_stats() ->
[{Key, get(Key)} || Key <- ?SOCK_STATS].

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].
t_base62_encode(_) ->
10 = ?BASE62:decode(?BASE62:encode(10)),
100 = ?BASE62:decode(?BASE62:encode(100)),
9999 = ?BASE62:decode(?BASE62:encode(9999)),
65535 = ?BASE62:decode(?BASE62:encode(65535)),
<<"10">> = ?BASE62:decode(?BASE62:encode(<<"10">>)),
<<"100">> = ?BASE62:decode(?BASE62:encode(<<"100">>)),
<<"9999">> = ?BASE62:decode(?BASE62:encode(<<"9999">>)),
<<"65535">> = ?BASE62:decode(?BASE62:encode(<<"65535">>)),
<<X:128/unsigned-big-integer>> = emqx_guid:gen(),
<<Y:128/unsigned-big-integer>> = emqx_guid:gen(),
X = ?BASE62:decode(?BASE62:encode(X)),
Y = ?BASE62:decode(?BASE62:encode(Y)).
X = ?BASE62:decode(?BASE62:encode(X), integer),
Y = ?BASE62:decode(?BASE62:encode(Y), integer),
<<"helloworld">> = ?BASE62:decode(?BASE62:encode("helloworld")),
"helloworld" = ?BASE62:decode(?BASE62:encode("helloworld", string), string).