Merge pull request #1738 from emqtt/emqx30-dev
Merge emqx30-dev to emqx30
This commit is contained in:
commit
cae09fb815
|
@ -1,8 +1,8 @@
|
|||
language: erlang
|
||||
|
||||
otp_release:
|
||||
- 20.0
|
||||
- 20.1
|
||||
- 21.0
|
||||
- 21.0.4
|
||||
|
||||
script:
|
||||
- make
|
||||
|
|
23
Makefile
23
Makefile
|
@ -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
4
TODO
|
@ -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.
|
@ -24,3 +24,4 @@
|
|||
|
||||
{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.
|
||||
|
||||
{allow, all}.
|
||||
|
|
1140
etc/emqx.conf
1140
etc/emqx.conf
File diff suppressed because it is too large
Load Diff
144
include/emqx.hrl
144
include/emqx.hrl
|
@ -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(),
|
||||
|
|
|
@ -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}).
|
||||
|
||||
|
|
1007
priv/emqx.schema
1007
priv/emqx.schema
File diff suppressed because it is too large
Load Diff
|
@ -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>"]},
|
||||
|
|
12
src/emqx.erl
12
src/emqx.erl
|
@ -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]).
|
||||
|
||||
|
|
|
@ -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
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -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">>);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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])).
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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).
|
|
@ -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]}.
|
|
@ -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}.
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -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}]).
|
|
@ -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})
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>>.
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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]))).
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)}.
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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"]));
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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)).
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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()).
|
||||
|
@ -110,15 +106,15 @@ format_variable(#mqtt_packet_connect{
|
|||
Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanStart=~s, KeepAlive=~p, Username=~s, Password=~s",
|
||||
Args = [ClientId, ProtoName, ProtoVer, CleanStart, KeepAlive, Username, format_password(Password)],
|
||||
{Format1, Args1} = if
|
||||
WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)",
|
||||
Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload] };
|
||||
WillFlag -> {Format ++ ", Will(Q~p, R~p, Topic=~s, Payload=~p)",
|
||||
Args ++ [WillQoS, i(WillRetain), WillTopic, WillPayload]};
|
||||
true -> {Format, Args}
|
||||
end,
|
||||
io_lib:format(Format1, Args1);
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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,
|
||||
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,
|
||||
clean_start = CleanStart,
|
||||
keepalive = Keepalive,
|
||||
properties = ConnProps,
|
||||
client_id = ClientId,
|
||||
is_bridge = IsBridge} = Var,
|
||||
|
||||
State1 = repl_username_with_peercert(
|
||||
State0#proto_state{proto_ver = ProtoVer,
|
||||
proto_name = ProtoName,
|
||||
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,
|
||||
will_msg = willmsg(Var, State0),
|
||||
keepalive = Keepalive,
|
||||
connprops = ConnProps,
|
||||
will_msg = willmsg(Var, ProtoState),
|
||||
is_bridge = IsBridge,
|
||||
connected_at = os:timestamp()}),
|
||||
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].
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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 Client’s 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(),
|
||||
|
||||
%% Client’s subscriptions.
|
||||
%% Client’s 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,35 +527,46 @@ 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};
|
||||
|
||||
handle_info({'EXIT', Pid, _Reason}, State = #state{old_client_pid = Pid}) ->
|
||||
%%ignore
|
||||
%% ignore
|
||||
{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}.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}) ->
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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]}}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
|
|
@ -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)))).
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}.
|
||||
|
||||
|
|
|
@ -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}.
|
||||
|
||||
|
|
|
@ -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
|
||||
%%------------------------------------------------------------------------------
|
||||
|
|
|
@ -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) ->
|
||||
|
|
119
src/emqx_ws.erl
119
src/emqx_ws.erl
|
@ -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)}.
|
||||
|
|
@ -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].
|
||||
|
||||
|
|
|
@ -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]}]}}.
|
||||
|
|
@ -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)}.
|
||||
|
|
@ -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).
|
||||
|
||||
|
|
Loading…
Reference in New Issue