From 7d0cba94273f81c69cea7cb8ae3f00d20869e590 Mon Sep 17 00:00:00 2001 From: Feng Lee Date: Mon, 6 Aug 2018 16:33:10 +0800 Subject: [PATCH] Add MQTT 5.0 supports for connection, protocol and session modules --- docs/mqtt-v5.0.pdf | Bin 2605289 -> 2895576 bytes etc/emqx.conf | 38 ++- etc/zone.conf | 126 +++++++++ include/emqx.hrl | 154 +++++------ include/emqx_mqtt.hrl | 109 +++----- priv/emqx.schema | 21 +- src/emqx.erl | 8 +- src/emqx_alarm_mgr.erl | 6 +- src/emqx_bridge.erl | 4 +- src/emqx_broker.erl | 326 ++++++++++++----------- src/emqx_broker_helper.erl | 6 +- src/emqx_client.erl | 109 ++++---- src/emqx_connection.erl | 365 ++++++++++++++------------ src/emqx_frame.erl | 13 +- src/emqx_kernel_sup.erl | 1 + src/emqx_listeners.erl | 2 +- src/emqx_message.erl | 63 ++--- src/emqx_metrics.erl | 10 +- src/emqx_mod_presence.erl | 12 +- src/emqx_mod_subscription.erl | 2 +- src/emqx_mqtt_properties.erl | 25 +- src/emqx_mqueue.erl | 2 +- src/emqx_packet.erl | 57 ++-- src/emqx_protocol.erl | 471 +++++++++++++++++----------------- src/emqx_router.erl | 6 +- src/emqx_session.erl | 415 +++++++++++++++--------------- src/emqx_sm.erl | 2 +- src/emqx_sm_registry.erl | 3 +- src/emqx_sys.erl | 6 +- src/emqx_sys_mon.erl | 6 +- src/emqx_topic.erl | 24 +- src/emqx_tracer.erl | 12 +- src/emqx_vm.erl | 14 +- src/emqx_ws_connection.erl | 43 ++-- src/emqx_zone.erl | 78 ++++++ 35 files changed, 1382 insertions(+), 1157 deletions(-) create mode 100644 etc/zone.conf create mode 100644 src/emqx_zone.erl diff --git a/docs/mqtt-v5.0.pdf b/docs/mqtt-v5.0.pdf index 6e5cd42056adbbc99e718a7f8700aa15491751fa..5b7e403f86ee30f1518b3400acdcc06f77243f1b 100644 GIT binary patch delta 139568 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 delta 1643 zcmZuxJ#Q324Amv%2unz~gz#~MupAsqLE-Uy*wfKKg+QX_572;+s9BI)Q?e2TNc;c< z2#LQ!gM^fdCK3%bbGwIbZhhUzAZgOzt-?|5nBK2czoo*SFiZhqWn90RZSddBidu0m`RkVLdEv0>#CR zVRnlvwzRlH#8A@zf{f8G?z$%Ioy}wQtTTta$UHHEejGV#*5yU8kQbcQix4ur^E|WM zxXnX2)QOl4+x+=e?bq`PmW=A;Pw2xCTIF)G3iWv2qut%jq@GbZyd&NJ$ CZc)zw diff --git a/etc/emqx.conf b/etc/emqx.conf index d13348908..555a6d63b 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -702,10 +702,15 @@ listener.tcp.external.acceptors = 16 ## Value: Number listener.tcp.external.max_clients = 102400 +## Maximum connection per second. +## +## Value: Number +listener.tcp.external.max_conn_rate = 1000 + ## Zone of the external MQTT/TCP listener belonged to. ## ## Value: String -listener.tcp.external.zone = devicebound +listener.tcp.external.zone = external ## Mountpoint of the MQTT/TCP Listener. All the topics of this ## listener will be prefixed with the mount point if this option @@ -831,10 +836,10 @@ listener.tcp.internal.acceptors = 4 ## Value: Number listener.tcp.internal.max_clients = 102400 -## TODO: Zone of the internal MQTT/TCP listener belonged to. +## Zone of the internal MQTT/TCP listener belonged to. ## ## Value: String -## listener.tcp.internal.zone = internal +listener.tcp.internal.zone = internal ## Mountpoint of the MQTT/TCP Listener. ## @@ -932,10 +937,15 @@ listener.ssl.external.acceptors = 16 ## Value: Number listener.ssl.external.max_clients = 102400 -## TODO: Zone of the external MQTT/SSL listener belonged to. +## Maximum MQTT/SSL connections per second. +## +## Value: Number +listener.ssl.external.max_conn_rate = 1000 + +## Zone of the external MQTT/SSL listener belonged to. ## ## Value: String -## listener.ssl.external.zone = external +listener.ssl.external.zone = external ## Mountpoint of the MQTT/SSL Listener. ## @@ -1166,10 +1176,15 @@ listener.ws.external.acceptors = 4 ## Value: Number listener.ws.external.max_clients = 102400 -## TODO: Zone of the external MQTT/WebSocket listener belonged to. +## Maximum MQTT/WebSocket connections per second. +## +## Value: Number +listener.ws.external.max_conn_rate = 1000 + +## Zone of the external MQTT/WebSocket listener belonged to. ## ## Value: String -## listener.ws.external.zone = external +listener.ws.external.zone = external ## Mountpoint of the MQTT/WebSocket Listener. ## @@ -1294,10 +1309,15 @@ listener.wss.external.acceptors = 4 ## Value: Number listener.wss.external.max_clients = 64 -## TODO: Zone of the external MQTT/WebSocket/SSL listener belonged to. +## Maximum MQTT/WebSocket/SSL connections per second. +## +## Value: Number +listener.wss.external.max_conn_rate = 1000 + +## Zone of the external MQTT/WebSocket/SSL listener belonged to. ## ## Value: String -## listener.wss.external.zone = external +listener.wss.external.zone = external ## Mountpoint of the MQTT/WebSocket/SSL Listener. ## diff --git a/etc/zone.conf b/etc/zone.conf new file mode 100644 index 000000000..5c546fe46 --- /dev/null +++ b/etc/zone.conf @@ -0,0 +1,126 @@ + +## Limits and Capabilities + +##-------------------------------------------------------------------- +## Connection + +zone.${name}.idle_timeout = 30s + +zone.${name}.rate_limit = 10,100 + +## 10 messages per second, with a bucket 100 messages. +zone.${name}.publish_limit = 10,100 + +## Enable stats +zone.${name}.enable_stats = on + +## zone.${name}.shutdown_policy = ??? + +##-------------------------------------------------------------------- +## Protocol + +## Capabilities: + +## Maximum length of MQTT clientId allowed. +## +## Value: Number [23-65535] +zone.${name}.max_clientid_len = 1024 + +## Maximum MQTT packet size allowed. +## +## Value: Bytes +## +## Default: 64K +zone.${name}.max_packet_size = 64K +zone.${name}.max_topic_alias = 0 +zone.${name}.max_qos_allowed = 2 +zone.${name}.retain_available = on +zone.${name}.wildcard_subscription = on +zone.${name}.shared_subscription = off + +## The backoff for MQTT keepalive timeout. +## EMQ will kick a MQTT connection out until 'Keepalive * backoff * 2' timeout. +## +## Value: Float > 0.5 +zone.${name}.keepalive_backoff = 0.75 + +##-------------------------------------------------------------------- +## Authentication + +zone.${name}.allow_anonymous = true + +##-------------------------------------------------------------------- +## Session + +zone.${name}.max_subscriptions = 0 +zone.${name}.upgrade_qos = off + +## Maximum size of the Inflight Window storing QoS1/2 messages delivered but unacked. +## +## Value: Number +zone.${name}.max_inflight = 32 + +## Retry interval for QoS1/2 message delivering. +## +## Value: Duration +zone.${name}.retry_interval = 20s + +## Maximum QoS2 packets (Client -> Broker) awaiting PUBREL, 0 means no limit. +## +## Value: Number +zone.${name}.max_awaiting_rel = 100 + +## The QoS2 messages (Client -> Broker) will be dropped if awaiting PUBREL timeout. +## +## Value: Duration +zone.${name}.await_rel_timeout = 30s + +## Whether to ignore loop delivery of messages. +## +## Value: true | false +## +## Default: false +zone.${name}.ignore_loop_deliver = false + +## Max session expiration time. +## +## Value: Duration +## -d: day +## -h: hour +## -m: minute +## -s: second +## +## Default: 2h, 2 hours +zone.${name}.session_expiry_interval = 2h + +##-------------------------------------------------------------------- +## Queue + +## Message queue type. +## +## Value: simple | priority +zone.${name}.mqueue_type = simple + +## Topic priority. Default is 0. +## +## Value: Number [0-255] +## +## zone.${name}.mqueue_priority = topic/1=10,topic/2=8 + +## Maximum queue length. Enqueued messages when persistent client disconnected, +## or inflight window is full. 0 means no limit. +## +## Value: Number >= 0 +zone.${name}.max_mqueue_len = 100 + +## Whether to enqueue Qos0 messages. +## +## Value: false | true +zone.${name}.mqueue_store_qos0 = true + +##-------------------------------------------------------------------- +## General + +zone.${name}.enable_stats = on + + diff --git a/include/emqx.hrl b/include/emqx.hrl index f0b2b46d1..7340cf835 100644 --- a/include/emqx.hrl +++ b/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(), - topic :: topic(), - subopts :: list(suboption())}). +-type(subopts() :: #{qos => integer(), share => '$queue' | binary(), atom() => term()}). + +-record(subscription, { + topic :: topic(), + 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(), - pid :: pid(), - zone :: zone(), - peername :: peername(), - username :: username(), - protocol :: protocol(), - attributes :: #{atom() => term()}, - connected_at :: erlang:timestamp()}). +-record(client, { + id :: client_id(), + pid :: pid(), + zone :: zone(), + protocol :: protocol(), + peername :: peername(), + peercert :: nossl | binary(), + username :: username(), + clean_start :: boolean(), + attributes :: map() + }). -type(client() :: #client{}). @@ -90,63 +100,53 @@ -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(), - dest :: node() | {binary(), node()} - }). +-record(route, { + topic :: topic(), + dest :: node() | {binary(), node()} + }). -type(route() :: #route{}). @@ -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,8 +191,8 @@ %% Plugin %%-------------------------------------------------------------------- --record(plugin, - { name :: atom(), +-record(plugin, { + name :: atom(), version :: string(), dir :: string(), descr :: string(), @@ -207,8 +207,8 @@ %% Command %%-------------------------------------------------------------------- --record(command, - { name :: atom(), +-record(command, { + name :: atom(), action :: atom(), args = [] :: list(), opts = [] :: list(), diff --git a/include/emqx_mqtt.hrl b/include/emqx_mqtt.hrl index f5bb13604..35a8a5082 100644 --- a/include/emqx_mqtt.hrl +++ b/include/emqx_mqtt.hrl @@ -83,13 +83,12 @@ %%-------------------------------------------------------------------- %% 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), @@ -396,7 +398,7 @@ properties = Properties}}). -define(PUBREC_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, variable = #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}}). @@ -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}). - diff --git a/priv/emqx.schema b/priv/emqx.schema index a3d2cfa32..7ff5fd0a3 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -933,6 +933,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.tcp.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.tcp.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1024,6 +1028,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.ssl.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.ssl.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1165,8 +1173,8 @@ end}. {datatype, integer} ]}. -{mapping, "listener.ws.$name.rate_limit", "emqx.listeners", [ - {datatype, string} +{mapping, "listener.ws.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} ]}. {mapping, "listener.ws.$name.zone", "emqx.listeners", [ @@ -1261,6 +1269,10 @@ end}. {datatype, integer} ]}. +{mapping, "listener.wss.$name.max_conn_rate", "emqx.listeners", [ + {datatype, integer} +]}. + {mapping, "listener.wss.$name.zone", "emqx.listeners", [ {datatype, string} ]}. @@ -1404,7 +1416,7 @@ end}. AccOpts = fun(Prefix) -> case cuttlefish_variable:filter_by_prefix(Prefix ++ ".access", Conf) of [] -> []; - Rules -> [{access, [Access(Rule) || {_, Rule} <- Rules]}] + Rules -> [{access_rules, [Access(Rule) || {_, Rule} <- Rules]}] end end, @@ -1413,9 +1425,10 @@ end}. LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {max_clients, cuttlefish:conf_get(Prefix ++ ".max_clients", Conf)}, + {max_conn_rate, cuttlefish:conf_get(Prefix ++ ".max_conn_rate", Conf, undefined)}, {tune_buffer, cuttlefish:conf_get(Prefix ++ ".tune_buffer", Conf, undefined)}, {zone, Atom(cuttlefish:conf_get(Prefix ++ ".zone", Conf, undefined))}, - {rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, + %%{rate_limit, cuttlefish:conf_get(Prefix ++ ".rate_limit", Conf, undefined)}, {proxy_protocol, cuttlefish:conf_get(Prefix ++ ".proxy_protocol", Conf, undefined)}, {proxy_protocol_timeout, cuttlefish:conf_get(Prefix ++ ".proxy_protocol_timeout", Conf, undefined)}, {mountpoint, MountPoint(cuttlefish:conf_get(Prefix ++ ".mountpoint", Conf, undefined))}, diff --git a/src/emqx.erl b/src/emqx.erl index a539bbd42..cbe37d12e 100644 --- a/src/emqx.erl +++ b/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)). diff --git a/src/emqx_alarm_mgr.erl b/src/emqx_alarm_mgr.erl index e041341e2..41da4e705 100644 --- a/src/emqx_alarm_mgr.erl +++ b/src/emqx_alarm_mgr.erl @@ -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}, 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">>); diff --git a/src/emqx_bridge.erl b/src/emqx_bridge.erl index 517b32dcd..eef5d249b 100644 --- a/src/emqx_bridge.erl +++ b/src/emqx_bridge.erl @@ -72,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) -> diff --git a/src/emqx_broker.erl b/src/emqx_broker.erl index 43cc81155..9d332a5f2 100644 --- a/src/emqx_broker.erl +++ b/src/emqx_broker.erl @@ -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), - {noreply, demonitor_subscriber(SubPid, State)}; +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}. diff --git a/src/emqx_broker_helper.erl b/src/emqx_broker_helper.erl index 975b2bf0d..fecf98a7b 100644 --- a/src/emqx_broker_helper.erl +++ b/src/emqx_broker_helper.erl @@ -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]), diff --git a/src/emqx_client.erl b/src/emqx_client.erl index 87b1e2bf3..8c742fbfd 100644 --- a/src/emqx_client.erl +++ b/src/emqx_client.erl @@ -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, - retain = Retain, - topic = Topic, - properties = Properties, - payload = iolist_to_binary(Payload)}). + publish(Client, #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + 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,29 @@ 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}, - State = #state{owner = Owner}) -> - Owner ! {publish, #{qos => QoS, dup => Dup, retain => Retain, - packet_id => PacketId, topic => Topic, - 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 => Props, payload => Payload}}, State. -packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Properties, Payload)) -> +packet_to_msg(?PUBLISH_PACKET(Header, Topic, PacketId, Props, 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}. + #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}. -msg_to_packet(#mqtt_message{qos = Qos, - dup = Dup, - retain = Retain, - topic = Topic, - packet_id = PacketId, - properties = Properties, - 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 +1033,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}) diff --git a/src/emqx_connection.erl b/src/emqx_connection.erl index cd71b1af4..7dc70e6ce 100644 --- a/src/emqx_connection.erl +++ b/src/emqx_connection.erl @@ -20,33 +20,51 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(proplists, [get_value/2, get_value/3]). - -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([session/1]). -%% gen_server Function Exports +-export([info/1, stats/1, kick/1]). +-export([get_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 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, %% Throughput rate limit + limit_timer, %% Rate limit timer + proto_state, %% MQTT protocol state + parse_state, %% MQTT parse state + keepalive, %% MQTT keepalive timer + enable_stats, %% Enable stats + stats_timer, %% Stats timer + idle_timeout %% Connection idle timeout + }). + +-define(INFO_KEYS, [peername, sockname, conn_state, await_recv, rate_limit, pub_limit]). --define(INFO_KEYS, [peername, conn_state, await_recv]). -define(SOCK_STATS, [recv_oct, recv_cnt, send_oct, send_cnt, send_pend]). --define(LOG(Level, Format, Args, State), - emqx_logger:Level("Conn(~s): " ++ Format, [esockd_net:format(State#state.peername) | Args])). -start_link(Transport, Sock, Options) -> - {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Sock, Options]])}. +-define(LOG(Level, Format, Args, State), + emqx_logger:Level("Conn(~s): " ++ Format, + [esockd_net:format(State#state.peername) | Args])). + +start_link(Transport, Socket, Options) -> + {ok, proc_lib:spawn_link(?MODULE, init, [[Transport, Socket, Options]])}. + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ info(CPid) -> gen_server:call(CPid, info). @@ -57,152 +75,151 @@ stats(CPid) -> kick(CPid) -> gen_server:call(CPid, kick). -set_rate_limit(CPid, Rl) -> - gen_server:call(CPid, {set_rate_limit, Rl}). +get_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). - -clean_acl_cache(CPid, Topic) -> - gen_server:call(CPid, {clean_acl_cache, Topic}). +set_pub_limit(CPid, Rl = {_Rate, _Burst}) -> + gen_server:call(CPid, {set_pub_limit, Rl}). %%------------------------------------------------------------------------------ %% gen_server callbacks %%------------------------------------------------------------------------------ -init([Transport, Sock, Options]) -> - case Transport:wait(Sock) of - {ok, NewSock} -> - {ok, Peername} = Transport:ensure_ok_or_exit(peername, [NewSock]), - do_init(Transport, Sock, Peername, Options); +init([Transport, RawSocket, Options]) -> + case Transport:wait(RawSocket) of + {ok, Socket} -> + {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]), + Zone = proplists:get_value(zone, Options), + RateLimit = init_rate_limit(emqx_zone:get_env(Zone, rate_limit)), + PubLimit = init_rate_limit(emqx_zone:get_env(Zone, publish_limit)), + EnableStats = emqx_zone:get_env(Zone, enable_stats, false), + IdleTimout = emqx_zone:get_env(Zone, idle_timeout, 30000), + SendFun = send_fun(Transport, Socket, Peername), + ProtoState = emqx_protocol:init(#{zone => Zone, + peername => Peername, + sockname => Sockname, + peercert => Peercert, + sendfun => SendFun}, Options), + ParseState = emqx_protocol:parser(ProtoState), + State = run_socket(#state{transport = Transport, + socket = Socket, + peername = Peername, + await_recv = false, + conn_state = running, + rate_limit = RateLimit, + pub_limit = PubLimit, + proto_state = ProtoState, + parse_state = ParseState, + enable_stats = EnableStats, + idle_timeout = IdleTimout}), + gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], + State, self(), IdleTimout); {error, Reason} -> {stop, Reason} end. -do_init(Transport, Sock, Peername, Options) -> - io:format("Options: ~p~n", [Options]), - RateLimit = get_value(rate_limit, Options), - PacketSize = get_value(max_packet_size, Options, ?MAX_PACKET_SIZE), - SendFun = send_fun(Transport, Sock, Peername), - ProtoState = emqx_protocol:init(Transport, Sock, Peername, SendFun, Options), - EnableStats = get_value(client_enable_stats, Options, false), - IdleTimout = get_value(client_idle_timeout, Options, 30000), - ForceGcCount = emqx_gc:conn_max_gc_count(), - State = run_socket(#state{transport = Transport, - socket = Sock, - peername = Peername, - await_recv = false, - conn_state = running, - rate_limit = RateLimit, - max_packet_size = PacketSize, - proto_state = ProtoState, - enable_stats = EnableStats, - idle_timeout = IdleTimout, - force_gc_count = ForceGcCount}), - gen_server:enter_loop(?MODULE, [{hibernate_after, IdleTimout}], - init_parse_state(State), self(), IdleTimout). +init_rate_limit(undefined) -> + undefined; +init_rate_limit({Rate, Burst}) -> + esockd_rate_limit:new(Rate, Burst). -send_fun(Transport, Sock, Peername) -> - Self = self(), - fun(Packet) -> - Data = emqx_frame:serialize(Packet), - ?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} +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)), 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{transport = Transport, socket = Socket, proto_state = ProtoState}) -> + ProtoInfo = emqx_protocol:info(ProtoState), + 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(info, From, State = #state{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); - -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(get_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) -> +handle_info(SubReq = {subscribe, _TopicTable}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:subscribe(TopicTable, ProtoState) + emqx_protocol:process(SubReq, ProtoState) end, State); -handle_info({unsubscribe, Topics}, State) -> +handle_info(UnsubReq = {unsubscribe, _Topics}, State) -> with_proto( fun(ProtoState) -> - emqx_protocol:unsubscribe(Topics, ProtoState) + emqx_protocol:process(UnsubReq, ProtoState) end, State); -%% Asynchronous SUBACK -handle_info({suback, PacketId, GrantedQos}, State) -> +handle_info({deliver, PubOrAck}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), - emqx_protocol:send(Packet, ProtoState) - end, State); + emqx_protocol:deliver(PubOrAck, ProtoState) + end, maybe_gc(ensure_stats_timer(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); @@ -211,25 +228,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 @@ -258,20 +275,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} -> @@ -281,25 +296,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 TCP data +handle_packet(<<>>, State) -> + {noreply, maybe_gc(ensure_stats_timer(ensure_rate_limit(State)))}; -received(Bytes, State = #state{parse_state = ParseState, - proto_state = ProtoState, - idle_timeout = IdleTimeout}) -> +handle_packet(Bytes, State = #state{incoming = Incoming, + parse_state = ParseState, + 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} -> + {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})); + ParseState1 = emqx_protocol:parser(ProtoState1), + handle_packet(Rest, State#state{incoming = count_packets(Type, Incoming), + proto_state = ProtoState1, + parse_state = ParseState1}); {error, Error} -> ?LOG(error, "Protocol error - ~p", [Error], State), shutdown(Error, State); @@ -312,22 +331,33 @@ 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 - {0, Rl1} -> - run_socket(State#state{conn_state = running, rate_limit = 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} - end. +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} -> + ensure_rate_limit(Limiters, setelement(Pos, State, Rl1)); + {Pause, 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}) -> State; @@ -338,29 +368,21 @@ run_socket(State = #state{transport = Transport, socket = Sock}) -> 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), - State. - -sock_stats(#state{transport = Transport, socket = Sock}) -> - case Transport:getstat(Sock, ?SOCK_STATS) of - {ok, Ss} -> Ss; - _Error -> [] + case Fun(ProtoState) of + {ok, ProtoState1} -> + {noreply, State#state{proto_state = ProtoState1}}; + {error, Reason} -> + shutdown(Reason, State); + {error, Reason, ProtoState1} -> + shutdown(Reason, State#state{proto_state = ProtoState1}) end. -reply(Reply, State) -> - {reply, Reply, State, hibernate}. +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. shutdown(Reason, State) -> stop({shutdown, Reason}, State). @@ -368,7 +390,8 @@ 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). diff --git a/src/emqx_frame.erl b/src/emqx_frame.erl index 0a261db6e..7385b7116 100644 --- a/src/emqx_frame.erl +++ b/src/emqx_frame.erl @@ -121,7 +121,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, @@ -242,6 +242,9 @@ parse_packet_id(<>) -> 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) -> @@ -328,7 +331,7 @@ parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> {Value + Len * Multiplier, Rest}. parse_topic_filters(subscribe, Bin) -> - [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} + [{Topic, #mqtt_subopts{rh = Rh, rap = Rap, nl = Nl, qos = QoS}} || <> <= Bin]; parse_topic_filters(unsubscribe, Bin) -> @@ -382,7 +385,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 +403,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, diff --git a/src/emqx_kernel_sup.erl b/src/emqx_kernel_sup.erl index 25e96b930..40ec7cfd7 100644 --- a/src/emqx_kernel_sup.erl +++ b/src/emqx_kernel_sup.erl @@ -31,6 +31,7 @@ init([]) -> 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) -> diff --git a/src/emqx_listeners.erl b/src/emqx_listeners.erl index c9697f0ca..9e8445414 100644 --- a/src/emqx_listeners.erl +++ b/src/emqx_listeners.erl @@ -33,7 +33,7 @@ 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); diff --git a/src/emqx_message.erl b/src/emqx_message.erl index 77dbfbf82..ae8670942 100644 --- a/src/emqx_message.erl +++ b/src/emqx_message.erl @@ -17,45 +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(), - qos = ?QOS0, +-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)}. @@ -64,27 +62,22 @@ 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)}. + 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)}. - diff --git a/src/emqx_metrics.erl b/src/emqx_metrics.erl index 519b96fe4..506ff2c0d 100644 --- a/src/emqx_metrics.erl +++ b/src/emqx_metrics.erl @@ -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) -> diff --git a/src/emqx_mod_presence.erl b/src/emqx_mod_presence.erl index e41bd0587..ef70dc28d 100644 --- a/src/emqx_mod_presence.erl +++ b/src/emqx_mod_presence.erl @@ -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"])); diff --git a/src/emqx_mod_subscription.erl b/src/emqx_mod_subscription.erl index 6db5e30f3..978b46a3b 100644 --- a/src/emqx_mod_subscription.erl +++ b/src/emqx_mod_subscription.erl @@ -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}; diff --git a/src/emqx_mqtt_properties.erl b/src/emqx_mqtt_properties.erl index 4634d5bdc..643156013 100644 --- a/src/emqx_mqtt_properties.erl +++ b/src/emqx_mqtt_properties.erl @@ -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) -> - case maps:find(id(Name), ?PROPS_TABLE) of - {ok, {Name, _Type, 'ALL'}} -> - true; - {ok, {Name, _Type, Packets}} -> - lists:member(Packet, Packets); - error -> false - end - end, - [Prop || Prop = {Name, _} <- Props, 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, AllowedTypes}} -> + lists:member(PacketType, AllowedTypes); + error -> false + end + end, + [Prop || Prop = {Name, _} <- Props, Filter(Name)]. validate(Props) when is_map(Props) -> lists:foreach(fun validate_prop/1, maps:to_list(Props)). diff --git a/src/emqx_mqueue.erl b/src/emqx_mqueue.erl index 43bb8654a..31811583f 100644 --- a/src/emqx_mqueue.erl +++ b/src/emqx_mqueue.erl @@ -150,7 +150,7 @@ stats(#mqueue{type = Type, q = Q, max_len = MaxLen, len = Len, dropped = Dropped %% @doc Enqueue a message. -spec(in(message(), mqueue()) -> mqueue()). -in(#message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> +in(#message{flags = #{qos := ?QOS_0}}, MQ = #mqueue{qos0 = false}) -> MQ; in(Msg, MQ = #mqueue{type = simple, q = Q, len = Len, max_len = 0}) -> MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}; diff --git a/src/emqx_packet.erl b/src/emqx_packet.erl index dc88d59d7..8baa6f088 100644 --- a/src/emqx_packet.erl +++ b/src/emqx_packet.erl @@ -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}, + packet_id = PacketId, + properties = #{}}, %%TODO: 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}; +-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(#mqtt_packet_connect{will_flag = false}) -> +to_message(_ClientId, #mqtt_packet_connect{will_flag = false}) -> undefined; -to_message(#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}. +to_message(ClientId, #mqtt_packet_connect{will_retain = Retain, + will_qos = QoS, + will_topic = Topic, + will_props = Props, + will_payload = Payload}) -> + 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,8 +106,8 @@ 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); @@ -153,3 +149,4 @@ format_password(_Password) -> '******'. i(true) -> 1; i(false) -> 0; i(I) when is_integer(I) -> I. + diff --git a/src/emqx_protocol.erl b/src/emqx_protocol.erl index b2e9965e8..30b2c0294 100644 --- a/src/emqx_protocol.erl +++ b/src/emqx_protocol.erl @@ -18,108 +18,86 @@ -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([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, {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])). + emqx_logger:Level([{client, State#proto_state.client_id}], "Client(~s@~s): " ++ Format, + [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 = #{zone := Zone, peercert := Peercert}, Options) -> + MountPoint = emqx_zone:get_env(Zone, mountpoint), + Backoff = emqx_zone:get_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{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, Options) -> - enrich_opt(Options, init(Peername, SendFun, Options)). +capabilities(Zone) -> + Capabilities = emqx_zone:get_env(Zone, mqtt_capabilities, []), + maps:from_list(lists:ukeymerge(1, ?CAPABILITIES, Capabilities)). -enrich_opt([], State) -> - State; -enrich_opt([{mountpoint, MountPoint} | ConnOpts], State) -> - enrich_opt(ConnOpts, State#proto_state{mountpoint = MountPoint}); -%%enrich_opt([{peer_cert_as_username, N} | ConnOpts], State) -> -%% enrich_opt(ConnOpts, State#proto_state{peercert_username = peercert_username(N, Conn)}); -enrich_opt([_ | ConnOpts], State) -> - enrich_opt(ConnOpts, 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(#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}) -> @@ -129,117 +107,93 @@ 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}. +process(?CONNECT_PACKET(Var), ProtoState = #proto_state{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, + username = Username, + password = Password} = Var, + ProtoState1 = ProtoState#proto_state{proto_ver = ProtoVer, + proto_name = ProtoName, + username = if Username0 == undefined -> + Username; + true -> Username0 + end, %% TODO: fixme later. + client_id = ClientId, + clean_start = CleanStart, + keepalive = Keepalive, + connprops = ConnProps, + will_msg = willmsg(Var, ProtoState), + is_bridge = IsBridge, + connected_at = os:timestamp()}, -unsubscribe(RawTopics, ProtoState = #proto_state{client_id = ClientId, - username = Username, - session = Session}) -> - case emqx_hooks:run('client.unsubscribe', [ClientId, Username], parse_topics(RawTopics)) of - {ok, TopicTable} -> - emqx_session:unsubscribe(Session, TopicTable); - {stop, _} -> - ok - end, - {ok, ProtoState}. - -%% @doc Send PUBREL -pubrel(PacketId, State) -> send(?PUBREL_PACKET(PacketId), State). - -process(?CONNECT_PACKET(Var), State0) -> - - #mqtt_packet_connect{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - password = Password, - clean_start= CleanStart, - keepalive = KeepAlive, - client_id = ClientId, - is_bridge = IsBridge} = Var, - - State1 = repl_username_with_peercert( - State0#proto_state{proto_ver = ProtoVer, - proto_name = ProtoName, - username = Username, - client_id = ClientId, - clean_start = CleanStart, - keepalive = KeepAlive, - will_msg = willmsg(Var, State0), - is_bridge = IsBridge, - 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 + ProtoState2 = maybe_set_clientid(ProtoState1), + %% Open session case emqx_sm:open_session(#{clean_start => CleanStart, - client_id => clientid(State2), + 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) @@ -266,36 +220,49 @@ process(?SUBSCRIBE_PACKET(PacketId, []), State) -> send(?SUBACK_PACKET(PacketId, []), State); %% TODO: refactor later... -process(?SUBSCRIBE_PACKET(PacketId, RawTopicTable), - State = #proto_state{client_id = ClientId, - username = Username, - is_superuser = IsSuperuser, - mountpoint = MountPoint, - session = Session}) -> - Client = client(State), TopicTable = parse_topic_table(RawTopicTable), +process(?SUBSCRIBE_PACKET(PacketId, Properties, RawTopicFilters), State) -> + #proto_state{client_id = ClientId, + username = Username, + is_superuser = IsSuperuser, + mountpoint = MountPoint, + 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} end end; +process({subscribe, RawTopicTable}, + State = #proto_state{client_id = ClientId, + username = Username, + session = Session}) -> + TopicTable = parse_topic_filters(RawTopicTable), + case emqx_hooks:run('client.subscribe', [ClientId, Username], TopicTable) of + {ok, TopicTable1} -> + emqx_session:subscribe(Session, TopicTable1); + {stop, _} -> ok + end, + {ok, State}; + %% Protect from empty topic list 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, @@ -308,84 +275,106 @@ process(?UNSUBSCRIBE_PACKET(PacketId, RawTopics), end, send(?UNSUBACK_PACKET(PacketId), State); -process(?PACKET(?PINGREQ), State) -> - send(?PACKET(?PINGRESP), State); +process({unsubscribe, RawTopics}, State = #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, State}; -process(?PACKET(?DISCONNECT), State) -> +process(?PACKET(?PINGREQ), ProtoState) -> + send(?PACKET(?PINGRESP), ProtoState); + +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 + ok -> emqx_metrics:sent(Packet), + trace(send, Packet, ProtoState), + {ok, inc_stats(send, Type, ProtoState)}; + {error, Reason} -> + {error, Reason} + 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}; @@ -403,19 +392,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. @@ -430,10 +418,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; @@ -459,7 +447,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; @@ -481,7 +469,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} @@ -501,11 +489,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; @@ -518,17 +506,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]. diff --git a/src/emqx_router.erl b/src/emqx_router.erl index 8f6375720..85a6a63ad 100644 --- a/src/emqx_router.erl +++ b/src/emqx_router.erl @@ -33,10 +33,8 @@ -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()}). diff --git a/src/emqx_session.erl b/src/emqx_session.erl index 02c7152b9..75e927a9c 100644 --- a/src/emqx_session.erl +++ b/src/emqx_session.erl @@ -11,29 +11,30 @@ %% 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 Session State in the Server consists of: %% -%% The existence of a Session, even if the rest of the Session state is empty. +%% The existence of a Session, even if the rest of the Session State is empty. %% -%% The Client’s subscriptions. +%% 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. +%% 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. +%% QoS 2 messages which have been received from the Client, but have not been +%% completely acknowledged.The Will Message and the Will Delay Interval %% -%% 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. +%% 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). @@ -42,26 +43,20 @@ -include("emqx_mqtt.hrl"). -include("emqx_misc.hrl"). --import(emqx_misc, [start_timer/2]). --import(proplists, [get_value/2, get_value/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]). --export([publish/2, puback/2, pubrec/2, pubrel/2, pubcomp/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, pubrec/2, pubrel/2, pubcomp/2]). -export([unsubscribe/2]). -%% gen_server Function Exports +%% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --define(MQueue, emqx_mqueue). - --record(state, - { %% Clean Start Flag +-record(state, { + %% Clean Start Flag clean_start = false :: boolean(), %% Client Binding: local | remote @@ -73,21 +68,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. @@ -106,18 +105,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 @@ -135,64 +134,63 @@ %% Ignore loop deliver? ignore_loop_deliver = false :: boolean(), + %% Created at created_at :: erlang:timestamp() }). -define(TIMEOUT, 60000). -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]). -define(LOG(Level, Format, Args, State), - emqx_logger:Level([{client, State#state.client_id}], - "Session(~s): " ++ Format, [State#state.client_id | Args])). + 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()}). +%% @doc Start a session +-spec(start_link(Attrs :: map()) -> {ok, pid()} | {error, term()}). start_link(Attrs) -> gen_server:start_link(?MODULE, Attrs, [{hibernate_after, 10000}]). -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ %% PubSub API -%%-------------------------------------------------------------------- +%%------------------------------------------------------------------------------ -%% @doc Subscribe topics --spec(subscribe(pid(), [{binary(), [emqx_topic:option()]}]) -> ok). -subscribe(SPid, TopicTable) -> %%TODO: the ack function??... - gen_server:cast(SPid, {subscribe, self(), TopicTable, fun(_) -> ok end}). +%% for mqtt 5.0 +-spec(subscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +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(SPid, PacketId, TopicTable) -> %%TODO: the ack function??... - From = self(), - AckFun = fun(GrantedQos) -> From ! {suback, PacketId, GrantedQos} end, - gen_server:cast(SPid, {subscribe, From, TopicTable, AckFun}). - -%% @doc Publish Message --spec(publish(pid(), message()) -> {ok, delivery()} | {error, term()}). -publish(_SPid, Msg = #message{qos = ?QOS_0}) -> - %% Publish QoS0 Directly +-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); -publish(_SPid, Msg = #message{qos = ?QOS_1}) -> - %% Publish QoS1 message directly for client will PubAck automatically +publish(_SPid, _PacketId, Msg = #message{qos = ?QOS_1}) -> + %% Publish QoS1 message to broker directly emqx_broker:publish(Msg); -publish(SPid, Msg = #message{qos = ?QOS_2}) -> - %% Publish QoS2 to Session - gen_server:call(SPid, {publish, Msg}, infinity). +publish(SPid, PacketId, Msg = #message{qos = ?QOS_2}) -> + %% Publish QoS2 message to session + gen_server:call(SPid, {publish, PacketId, Msg}, infinity). -%% @doc PubAck Message -spec(puback(pid(), mqtt_packet_id()) -> ok). 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(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(SPid, PacketId) -> gen_server:cast(SPid, {pubrel, PacketId}). @@ -201,20 +199,14 @@ pubrel(SPid, PacketId) -> pubcomp(SPid, PacketId) -> gen_server:cast(SPid, {pubcomp, PacketId}). -%% @doc Unsubscribe the topics --spec(unsubscribe(pid(), [{binary(), [suboption()]}]) -> ok). -unsubscribe(SPid, TopicTable) -> - gen_server:cast(SPid, {unsubscribe, self(), TopicTable}). +-spec(unsubscribe(pid(), {mqtt_packet_id(), mqtt_properties(), topic_table()}) -> ok). +unsubscribe(SPid, UnsubReq = {PacketId, Properties, TopicFilters}) -> + gen_server:cast(SPid, {unsubscribe, self(), UnsubReq}). -%% @doc Resume the session -spec(resume(pid(), pid()) -> ok). resume(SPid, ClientPid) -> gen_server:cast(SPid, {resume, ClientPid}). -%% @doc Get session state -state(SPid) when is_pid(SPid) -> - gen_server:call(SPid, state). - %% @doc Get session info -spec(info(pid() | #state{}) -> list(tuple())). info(SPid) when is_pid(SPid) -> @@ -239,9 +231,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)}, @@ -250,41 +242,42 @@ stats(#state{max_subscriptions = MaxSubscriptions, %% @doc Discard the session -spec(discard(pid(), client_id()) -> ok). discard(SPid, ClientId) -> - gen_server:call(SPid, {discard, 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, - client_id := ClientId, - username := Username, - client_pid := ClientPid}) -> +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init(#{clean_start := CleanStart, client_id := ClientId, username := Username, client_pid := ClientPid}) -> 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), + MaxInflight = proplists:get_value(max_inflight, Env, 0), + EnableStats = proplists:get_value(enable_stats, Env, false), + IgnoreLoopDeliver = proplists:get_value(ignore_loop_deliver, Env, false), + MQueue = emqx_mqueue:new(ClientId, QEnv), 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 = proplists:get_value(max_subscriptions, Env, 0), + upgrade_qos = proplists:get_value(upgrade_qos, Env, false), max_inflight = MaxInflight, inflight = emqx_inflight:new(MaxInflight), mqueue = MQueue, - retry_interval = get_value(retry_interval, Env), + retry_interval = proplists:get_value(retry_interval, Env), 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), + await_rel_timeout = proplists:get_value(await_rel_timeout, Env), + max_awaiting_rel = proplists:get_value(max_awaiting_rel, Env), + expiry_interval = proplists:get_value(expiry_interval, Env), enable_stats = EnableStats, ignore_loop_deliver = IgnoreLoopDeliver, created_at = os:timestamp()}, @@ -307,19 +300,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; @@ -330,62 +323,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 call: ~p", [Req]), {reply, ignored, State}. -handle_cast({subscribe, From, TopicTable, AckFun}, +handle_cast({subscribe, From, {PacketId, _Properties, TopicFilters}}, State = #state{client_id = ClientId, username = Username, subscriptions = Subscriptions}) -> - ?LOG(info, "Subscribe ~p", [TopicTable], State), - {GrantedQos, Subscriptions1} = - lists:foldl(fun({Topic, Opts}, {QosAcc, SubMap}) -> - NewQos = get_value(qos, Opts), - SubMap1 = - case maps:find(Topic, SubMap) of - {ok, NewQos} -> - ?LOG(warning, "Duplicated subscribe: ~s, qos = ~w", [Topic, NewQos], State), - SubMap; - {ok, OldQos} -> - %% TODO:.... - emqx_broker:set_subopts(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); - error -> - %% TODO:.... - emqx:subscribe(Topic, ClientId, Opts), - emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, Opts}), - maps:put(Topic, NewQos, SubMap) - end, - {[NewQos|QosAcc], SubMap1} - end, {[], Subscriptions}, TopicTable), - AckFun(lists:reverse(GrantedQos)), - {noreply, emit_stats(State#state{subscriptions = Subscriptions1}), hibernate}; + ?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, SubOpts} -> + ?LOG(warning, "Duplicated subscribe: ~s, subopts: ~p", [Topic, SubOpts], State), + SubMap; + {ok, OldOpts} -> + emqx_broker:set_subopts(Topic, {self(), ClientId}, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Username], {Topic, SubOpts}), + ?LOG(warning, "Duplicated subscribe ~s, old_opts: ~p, new_opts: ~p", [Topic, OldOpts, SubOpts], State), + maps:put(Topic, SubOpts, SubMap); + error -> + emqx_broker:subscribe(Topic, ClientId, SubOpts), + emqx_hooks:run('session.subscribed', [ClientId, Username], {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, username = Username, 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, Username], {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}) -> @@ -490,12 +474,12 @@ handle_cast(Msg, State) -> {noreply, State}. %% Ignore Messages delivered by self -handle_info({dispatch, _Topic, #message{from = {ClientId, _}}}, +handle_info({dispatch, _Topic, #message{from = ClientId}}, State = #state{client_id = ClientId, ignore_loop_deliver = true}) -> {noreply, State}; %% Dispatch Message -handle_info({dispatch, Topic, Msg}, State) when is_record(Msg, message) -> +handle_info({dispatch, Topic, Msg}, State) -> {noreply, gc(dispatch(tune_qos(Topic, reset_dup(Msg), State), State))}; %% Do nothing if the client has been disconnected. @@ -521,7 +505,7 @@ handle_info({'EXIT', ClientPid, Reason}, client_pid = ClientPid, expiry_interval = Interval}) -> ?LOG(info, "Client ~p EXIT for ~p", [ClientPid, Reason], State), - ExpireTimer = start_timer(Interval, expired), + ExpireTimer = emqx_misc:start_timer(Interval, expired), State1 = State#state{client_pid = undefined, expiry_timer = ExpireTimer}, {noreply, emit_stats(State1), hibernate}; @@ -543,12 +527,25 @@ 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; @@ -560,32 +557,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)), - retry_delivery(Force, Msgs, os:timestamp(), State) + 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), @@ -593,12 +590,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 @@ -619,12 +616,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; @@ -635,18 +632,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}) -> @@ -657,53 +654,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, - retry_timer = RetryTimer, - retry_interval = Interval}) -> +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, @@ -735,9 +729,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}) -> @@ -750,7 +744,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} -> @@ -758,9 +752,8 @@ 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}) -> @@ -775,26 +768,23 @@ tune_qos(Topic, Msg = #message{qos = PubQoS}, 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; @@ -806,7 +796,6 @@ inc_stats(Key) -> put(Key, get(Key) + 1). %%-------------------------------------------------------------------- %% Helper functions -%%-------------------------------------------------------------------- reply(Reply, State) -> {reply, Reply, State, hibernate}. diff --git a/src/emqx_sm.erl b/src/emqx_sm.erl index de16a2b0f..00f4bff3d 100644 --- a/src/emqx_sm.erl +++ b/src/emqx_sm.erl @@ -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}) -> diff --git a/src/emqx_sm_registry.erl b/src/emqx_sm_registry.erl index 4a9be13c0..701b9ae4e 100644 --- a/src/emqx_sm_registry.erl +++ b/src/emqx_sm_registry.erl @@ -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). diff --git a/src/emqx_sys.erl b/src/emqx_sys.erl index 667ef0f1a..57dc41703 100644 --- a/src/emqx_sys.erl +++ b/src/emqx_sys.erl @@ -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}, 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)))). diff --git a/src/emqx_sys_mon.erl b/src/emqx_sys_mon.erl index 435dfaaee..b42d96aa4 100644 --- a/src/emqx_sys_mon.erl +++ b/src/emqx_sys_mon.erl @@ -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}, Topic, Payload). + emqx_message:make(?SYSMON, #{sys => true}, Topic, Payload). diff --git a/src/emqx_topic.erl b/src/emqx_topic.erl index 43ab8e0df..3bf42f6ac 100644 --- a/src/emqx_topic.erl +++ b/src/emqx_topic.erl @@ -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}. diff --git a/src/emqx_tracer.erl b/src/emqx_tracer.erl index 0f16c858c..65e6f6378 100644 --- a/src/emqx_tracer.erl +++ b/src/emqx_tracer.erl @@ -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 %%------------------------------------------------------------------------------ diff --git a/src/emqx_vm.erl b/src/emqx_vm.erl index c367ddbc4..0f3a4eaa4 100644 --- a/src/emqx_vm.erl +++ b/src/emqx_vm.erl @@ -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, @@ -204,13 +198,13 @@ mem_info() -> [{total_memory, proplists:get_value(total_memory, Dataset)}, {used_memory, proplists:get_value(total_memory, Dataset) - proplists:get_value(free_memory, Dataset)}]. -ftos(F) -> +ftos(F) -> [S] = io_lib:format("~.2f", [F]), S. -%%%% erlang vm scheduler_usage fun copied from recon +%%%% erlang vm scheduler_usage fun copied from recon scheduler_usage(Interval) when is_integer(Interval) -> %% We start and stop the scheduler_wall_time system flag - %% if it wasn't in place already. Usually setting the flag + %% if it wasn't in place already. Usually setting the flag %% should have a CPU impact(make it higher) only when under low usage. FormerFlag = erlang:system_flag(scheduler_wall_time, true), First = erlang:statistics(scheduler_wall_time), @@ -300,7 +294,7 @@ get_process_group_leader_info(LeaderPid) when is_pid(LeaderPid) -> [{Key, Value}|| {Key, Value} <- process_info(LeaderPid), lists:member(Key, ?PROCESS_INFO)]. get_process_limit() -> - erlang:system_info(process_limit). + erlang:system_info(process_limit). get_ets_list() -> ets:all(). diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 83a55c21e..dadc2cc57 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -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_ws_connection). @@ -157,8 +155,8 @@ handle_cast({received, Packet}, State = #wsclient_state{proto_state = ProtoState end; handle_cast(Msg, State) -> - ?WSLOG(error, "Unexpected Msg: ~p", [Msg], State), - {noreply, State, hibernate}. + ?WSLOG(error, "unexpected msg: ~p", [Msg], State), + {noreply, State}. handle_info({subscribe, TopicTable}, State) -> with_proto( @@ -172,10 +170,17 @@ handle_info({unsubscribe, Topics}, State) -> emqx_protocol:unsubscribe(Topics, ProtoState) end, State); -handle_info({suback, PacketId, GrantedQos}, State) -> +handle_info({suback, PacketId, ReasonCodes}, State) -> with_proto( fun(ProtoState) -> - Packet = ?SUBACK_PACKET(PacketId, GrantedQos), + Packet = ?SUBACK_PACKET(PacketId, ReasonCodes), + emqx_protocol:send(Packet, ProtoState) + end, State); + +handle_info({unsuback, PacketId, ReasonCodes}, State) -> + with_proto( + fun(ProtoState) -> + Packet = ?UNSUBACK_PACKET(PacketId, ReasonCodes), emqx_protocol:send(Packet, ProtoState) end, State); diff --git a/src/emqx_zone.erl b/src/emqx_zone.erl new file mode 100644 index 000000000..0d874a38b --- /dev/null +++ b/src/emqx_zone.erl @@ -0,0 +1,78 @@ +%% 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([get_env/2, get_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). +-define(SERVER, ?MODULE). + +start_link() -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + +get_env(Zone, Par) -> + get_env(Zone, Par, undefined). + +get_env(Zone, Par, Def) -> + try ets:lookup_element(?TAB, {Zone, Par}, 2) catch error:badarg -> Def 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, Options}) -> + [ets:insert(?TAB, {{Zone, Par}, Val}) || {Par, Val} <- Options] + 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)}. +