refactor(emqx_retainer): use hierarchical limiter for the flow control
This commit is contained in:
parent
c5d0c8da94
commit
5bf1d77993
|
@ -4,10 +4,7 @@
|
||||||
|
|
||||||
limiter {
|
limiter {
|
||||||
bytes_in {
|
bytes_in {
|
||||||
global.rate = infinity # token generation rate
|
|
||||||
zone.default.rate = infinity
|
|
||||||
bucket.default {
|
bucket.default {
|
||||||
zone = default
|
|
||||||
aggregated.rate = infinity
|
aggregated.rate = infinity
|
||||||
aggregated.capacity = infinity
|
aggregated.capacity = infinity
|
||||||
per_client.rate = infinity
|
per_client.rate = infinity
|
||||||
|
@ -16,10 +13,7 @@ limiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
message_in {
|
message_in {
|
||||||
global.rate = infinity
|
|
||||||
zone.default.rate = infinity
|
|
||||||
bucket.default {
|
bucket.default {
|
||||||
zone = default
|
|
||||||
aggregated.rate = infinity
|
aggregated.rate = infinity
|
||||||
aggregated.capacity = infinity
|
aggregated.capacity = infinity
|
||||||
per_client.rate = infinity
|
per_client.rate = infinity
|
||||||
|
@ -28,10 +22,7 @@ limiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
connection {
|
connection {
|
||||||
global.rate = infinity
|
|
||||||
zone.default.rate = infinity
|
|
||||||
bucket.default {
|
bucket.default {
|
||||||
zone = default
|
|
||||||
aggregated.rate = infinity
|
aggregated.rate = infinity
|
||||||
aggregated.capacity = infinity
|
aggregated.capacity = infinity
|
||||||
per_client.rate = infinity
|
per_client.rate = infinity
|
||||||
|
@ -40,10 +31,16 @@ limiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
message_routing {
|
message_routing {
|
||||||
global.rate = infinity
|
|
||||||
zone.default.rate = infinity
|
|
||||||
bucket.default {
|
bucket.default {
|
||||||
zone = default
|
aggregated.rate = infinity
|
||||||
|
aggregated.capacity = infinity
|
||||||
|
per_client.rate = infinity
|
||||||
|
per_client.capacity = infinity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared {
|
||||||
|
bucket.retainer {
|
||||||
aggregated.rate = infinity
|
aggregated.rate = infinity
|
||||||
aggregated.capacity = infinity
|
aggregated.capacity = infinity
|
||||||
per_client.rate = infinity
|
per_client.rate = infinity
|
||||||
|
|
|
@ -27,7 +27,8 @@
|
||||||
-type limiter_type() :: bytes_in
|
-type limiter_type() :: bytes_in
|
||||||
| message_in
|
| message_in
|
||||||
| connection
|
| connection
|
||||||
| message_routing.
|
| message_routing
|
||||||
|
| shared.
|
||||||
|
|
||||||
-type bucket_name() :: atom().
|
-type bucket_name() :: atom().
|
||||||
-type zone_name() :: atom().
|
-type zone_name() :: atom().
|
||||||
|
@ -66,22 +67,29 @@ fields(limiter) ->
|
||||||
, {message_in, sc(ref(limiter_opts), #{})}
|
, {message_in, sc(ref(limiter_opts), #{})}
|
||||||
, {connection, sc(ref(limiter_opts), #{})}
|
, {connection, sc(ref(limiter_opts), #{})}
|
||||||
, {message_routing, sc(ref(limiter_opts), #{})}
|
, {message_routing, sc(ref(limiter_opts), #{})}
|
||||||
|
, {shared, sc(ref(shared_limiter_opts),
|
||||||
|
#{description => <<"some functions that do not need to use global and zone scope, them can shared use this type">>})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(limiter_opts) ->
|
fields(limiter_opts) ->
|
||||||
[ {global, sc(ref(rate_burst), #{})}
|
[ {global, sc(ref(rate_burst), #{nuallabe => true})}
|
||||||
, {zone, sc(map("zone name", ref(rate_burst)), #{})}
|
, {zone, sc(map("zone name", ref(rate_burst)), #{nullable => true})}
|
||||||
, {bucket, sc(map("bucket_id", ref(bucket)),
|
, {bucket, sc(map("bucket_id", ref(bucket)),
|
||||||
#{desc => "Token bucket"})}
|
#{desc => "Token bucket"})}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
fields(shared_limiter_opts) ->
|
||||||
|
[{bucket, sc(map("bucket_id", ref(bucket)),
|
||||||
|
#{desc => "Token bucket"})}
|
||||||
|
];
|
||||||
|
|
||||||
fields(rate_burst) ->
|
fields(rate_burst) ->
|
||||||
[ {rate, sc(rate(), #{})}
|
[ {rate, sc(rate(), #{})}
|
||||||
, {burst, sc(burst_rate(), #{default => "0/0s"})}
|
, {burst, sc(burst_rate(), #{default => "0/0s"})}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(bucket) ->
|
fields(bucket) ->
|
||||||
[ {zone, sc(atom(), #{desc => "The bucket's zone"})}
|
[ {zone, sc(atom(), #{desc => "The bucket's zone", default => default})}
|
||||||
, {aggregated, sc(ref(bucket_aggregated), #{})}
|
, {aggregated, sc(ref(bucket_aggregated), #{})}
|
||||||
, {per_client, sc(ref(client_bucket), #{})}
|
, {per_client, sc(ref(client_bucket), #{})}
|
||||||
];
|
];
|
||||||
|
|
|
@ -451,9 +451,15 @@ dispatch_burst_to_buckets([], _, Alloced, Nodes) ->
|
||||||
|
|
||||||
-spec init_tree(emqx_limiter_schema:limiter_type(), state()) -> state().
|
-spec init_tree(emqx_limiter_schema:limiter_type(), state()) -> state().
|
||||||
init_tree(Type, State) ->
|
init_tree(Type, State) ->
|
||||||
|
case emqx:get_config([limiter, Type]) of
|
||||||
#{global := Global,
|
#{global := Global,
|
||||||
zone := Zone,
|
zone := Zone,
|
||||||
bucket := Bucket} = emqx:get_config([limiter, Type]),
|
bucket := Bucket} -> ok;
|
||||||
|
#{bucket := Bucket} ->
|
||||||
|
Global = default_rate_burst_cfg(),
|
||||||
|
Zone = #{default => default_rate_burst_cfg()},
|
||||||
|
ok
|
||||||
|
end,
|
||||||
{Factor, Root} = make_root(Global, Zone),
|
{Factor, Root} = make_root(Global, Zone),
|
||||||
State2 = State#{root := Root},
|
State2 = State#{root := Root},
|
||||||
{NodeId, State3} = make_zone(maps:to_list(Zone), Factor, 1, State2),
|
{NodeId, State3} = make_zone(maps:to_list(Zone), Factor, 1, State2),
|
||||||
|
@ -592,3 +598,6 @@ get_initial_val(#{initial := Initial,
|
||||||
true ->
|
true ->
|
||||||
0
|
0
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
default_rate_burst_cfg() ->
|
||||||
|
#{rate => infinity, burst => 0}.
|
||||||
|
|
|
@ -73,6 +73,15 @@ limiter {
|
||||||
per_client.capacity = infinity
|
per_client.capacity = infinity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared {
|
||||||
|
bucket.retainer {
|
||||||
|
aggregated.rate = infinity
|
||||||
|
aggregated.capacity = infinity
|
||||||
|
per_client.rate = infinity
|
||||||
|
per_client.capacity = infinity
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
""">>).
|
""">>).
|
||||||
|
|
|
@ -45,20 +45,20 @@ retainer {
|
||||||
## repeat this, until all retianed messages are delivered
|
## repeat this, until all retianed messages are delivered
|
||||||
##
|
##
|
||||||
flow_control {
|
flow_control {
|
||||||
## The max messages number per read from storage. 0 means no limit
|
## The messages number per read from storage. 0 means no limit
|
||||||
##
|
##
|
||||||
## Default: 0
|
## Default: 0
|
||||||
max_read_number = 0
|
batch_read_number = 0
|
||||||
|
|
||||||
## The max number of retained message can be delivered in emqx per quota_release_interval.0 means no limit
|
## The number of retained message can be delivered per batch
|
||||||
##
|
##
|
||||||
## Default: 0
|
## Default: 0
|
||||||
msg_deliver_quota = 0
|
batch_deliver_number = 0
|
||||||
|
|
||||||
## deliver quota reset interval
|
## deliver limiter bucket
|
||||||
##
|
##
|
||||||
## Default: 0s
|
## Default: 0s
|
||||||
quota_release_interval = 0s
|
limiter_bucket_name = retainer
|
||||||
}
|
}
|
||||||
|
|
||||||
## Maximum retained message size.
|
## Maximum retained message size.
|
||||||
|
@ -66,11 +66,11 @@ retainer {
|
||||||
## Value: Bytes
|
## Value: Bytes
|
||||||
max_payload_size = 1MB
|
max_payload_size = 1MB
|
||||||
|
|
||||||
## Storage connect parameters
|
## Storage backend parameters
|
||||||
##
|
##
|
||||||
## Value: built_in_database
|
## Value: built_in_database
|
||||||
##
|
##
|
||||||
config {
|
backend {
|
||||||
|
|
||||||
type = built_in_database
|
type = built_in_database
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,10 @@
|
||||||
, on_message_publish/2
|
, on_message_publish/2
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([ dispatch/4
|
-export([ delete_message/2
|
||||||
, delete_message/2
|
|
||||||
, store_retained/2
|
, store_retained/2
|
||||||
, deliver/5]).
|
, get_backend_module/0
|
||||||
|
]).
|
||||||
|
|
||||||
-export([ get_expiry_time/1
|
-export([ get_expiry_time/1
|
||||||
, update_config/1
|
, update_config/1
|
||||||
|
@ -54,8 +54,6 @@
|
||||||
, context_id := non_neg_integer()
|
, context_id := non_neg_integer()
|
||||||
, context := undefined | context()
|
, context := undefined | context()
|
||||||
, clear_timer := undefined | reference()
|
, clear_timer := undefined | reference()
|
||||||
, release_quota_timer := undefined | reference()
|
|
||||||
, wait_quotas := list()
|
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-define(DEF_MAX_PAYLOAD_SIZE, (1024 * 1024)).
|
-define(DEF_MAX_PAYLOAD_SIZE, (1024 * 1024)).
|
||||||
|
@ -116,45 +114,6 @@ on_message_publish(Msg, _) ->
|
||||||
start_link() ->
|
start_link() ->
|
||||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||||
|
|
||||||
-spec dispatch(context(), pid(), topic(), cursor()) -> ok.
|
|
||||||
dispatch(Context, Pid, Topic, Cursor) ->
|
|
||||||
Mod = get_backend_module(),
|
|
||||||
case Cursor =/= undefined orelse emqx_topic:wildcard(Topic) of
|
|
||||||
false ->
|
|
||||||
{ok, Result} = Mod:read_message(Context, Topic),
|
|
||||||
deliver(Result, Context, Pid, Topic, undefined);
|
|
||||||
true ->
|
|
||||||
{ok, Result, NewCursor} = Mod:match_messages(Context, Topic, Cursor),
|
|
||||||
deliver(Result, Context, Pid, Topic, NewCursor)
|
|
||||||
end.
|
|
||||||
|
|
||||||
deliver([], Context, Pid, Topic, Cursor) ->
|
|
||||||
case Cursor of
|
|
||||||
undefined ->
|
|
||||||
ok;
|
|
||||||
_ ->
|
|
||||||
dispatch(Context, Pid, Topic, Cursor)
|
|
||||||
end;
|
|
||||||
deliver(Result, #{context_id := Id} = Context, Pid, Topic, Cursor) ->
|
|
||||||
case erlang:is_process_alive(Pid) of
|
|
||||||
false ->
|
|
||||||
ok;
|
|
||||||
_ ->
|
|
||||||
#{msg_deliver_quota := MaxDeliverNum} = emqx:get_config([retainer, flow_control]),
|
|
||||||
case MaxDeliverNum of
|
|
||||||
0 ->
|
|
||||||
_ = [Pid ! {deliver, Topic, Msg} || Msg <- Result],
|
|
||||||
ok;
|
|
||||||
_ ->
|
|
||||||
case do_deliver(Result, Id, Pid, Topic) of
|
|
||||||
ok ->
|
|
||||||
deliver([], Context, Pid, Topic, Cursor);
|
|
||||||
abort ->
|
|
||||||
ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end.
|
|
||||||
|
|
||||||
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}) ->
|
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := 0}}}) ->
|
||||||
0;
|
0;
|
||||||
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
|
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
|
||||||
|
@ -198,7 +157,6 @@ stats_fun() ->
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
emqx_conf:add_handler([retainer], ?MODULE),
|
emqx_conf:add_handler([retainer], ?MODULE),
|
||||||
init_shared_context(),
|
|
||||||
State = new_state(),
|
State = new_state(),
|
||||||
#{enable := Enable} = Cfg = emqx:get_config([retainer]),
|
#{enable := Enable} = Cfg = emqx:get_config([retainer]),
|
||||||
{ok,
|
{ok,
|
||||||
|
@ -213,9 +171,6 @@ handle_call({update_config, NewConf, OldConf}, _, State) ->
|
||||||
State2 = update_config(State, NewConf, OldConf),
|
State2 = update_config(State, NewConf, OldConf),
|
||||||
{reply, ok, State2};
|
{reply, ok, State2};
|
||||||
|
|
||||||
handle_call({wait_semaphore, Id}, From, #{wait_quotas := Waits} = State) ->
|
|
||||||
{noreply, State#{wait_quotas := [{Id, From} | Waits]}};
|
|
||||||
|
|
||||||
handle_call(clean, _, #{context := Context} = State) ->
|
handle_call(clean, _, #{context := Context} = State) ->
|
||||||
clean(Context),
|
clean(Context),
|
||||||
{reply, ok, State};
|
{reply, ok, State};
|
||||||
|
@ -249,30 +204,12 @@ handle_info(clear_expired, #{context := Context} = State) ->
|
||||||
Interval = emqx_conf:get([retainer, msg_clear_interval], ?DEF_EXPIRY_INTERVAL),
|
Interval = emqx_conf:get([retainer, msg_clear_interval], ?DEF_EXPIRY_INTERVAL),
|
||||||
{noreply, State#{clear_timer := add_timer(Interval, clear_expired)}, hibernate};
|
{noreply, State#{clear_timer := add_timer(Interval, clear_expired)}, hibernate};
|
||||||
|
|
||||||
handle_info(release_deliver_quota, #{context := Context, wait_quotas := Waits} = State) ->
|
|
||||||
insert_shared_context(?DELIVER_SEMAPHORE, get_msg_deliver_quota()),
|
|
||||||
case Waits of
|
|
||||||
[] ->
|
|
||||||
ok;
|
|
||||||
_ ->
|
|
||||||
#{context_id := NowId} = Context,
|
|
||||||
Waits2 = lists:reverse(Waits),
|
|
||||||
lists:foreach(fun({Id, From}) ->
|
|
||||||
gen_server:reply(From, Id =:= NowId)
|
|
||||||
end,
|
|
||||||
Waits2)
|
|
||||||
end,
|
|
||||||
Interval = emqx:get_config([retainer, flow_control, quota_release_interval]),
|
|
||||||
{noreply, State#{release_quota_timer := add_timer(Interval, release_deliver_quota),
|
|
||||||
wait_quotas := []}};
|
|
||||||
|
|
||||||
handle_info(Info, State) ->
|
handle_info(Info, State) ->
|
||||||
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
?SLOG(error, #{msg => "unexpected_info", info => Info}),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
terminate(_Reason, #{clear_timer := TRef1, release_quota_timer := TRef2}) ->
|
terminate(_Reason, #{clear_timer := ClearTimer}) ->
|
||||||
_ = stop_timer(TRef1),
|
_ = stop_timer(ClearTimer),
|
||||||
_ = stop_timer(TRef2),
|
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
code_change(_OldVsn, State, _Extra) ->
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
@ -286,22 +223,19 @@ new_state() ->
|
||||||
#{enable => false,
|
#{enable => false,
|
||||||
context_id => 0,
|
context_id => 0,
|
||||||
context => undefined,
|
context => undefined,
|
||||||
clear_timer => undefined,
|
clear_timer => undefined
|
||||||
release_quota_timer => undefined,
|
}.
|
||||||
wait_quotas => []}.
|
|
||||||
|
|
||||||
-spec new_context(pos_integer()) -> context().
|
-spec new_context(pos_integer()) -> context().
|
||||||
new_context(Id) ->
|
new_context(Id) ->
|
||||||
#{context_id => Id}.
|
#{context_id => Id}.
|
||||||
|
|
||||||
|
|
||||||
payload_size_limit() ->
|
payload_size_limit() ->
|
||||||
emqx_conf:get(?MAX_PAYLOAD_SIZE_CONFIG_PATH, ?DEF_MAX_PAYLOAD_SIZE).
|
emqx_conf:get(?MAX_PAYLOAD_SIZE_CONFIG_PATH, ?DEF_MAX_PAYLOAD_SIZE).
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
dispatch(Context, Topic) ->
|
dispatch(Context, Topic) ->
|
||||||
emqx_retainer_pool:async_submit(fun ?MODULE:dispatch/4,
|
emqx_retainer_dispatcher:dispatch(Context, Topic).
|
||||||
[Context, self(), Topic, undefined]).
|
|
||||||
|
|
||||||
-spec delete_message(context(), topic()) -> ok.
|
-spec delete_message(context(), topic()) -> ok.
|
||||||
delete_message(Context, Topic) ->
|
delete_message(Context, Topic) ->
|
||||||
|
@ -328,53 +262,6 @@ clean(Context) ->
|
||||||
Mod = get_backend_module(),
|
Mod = get_backend_module(),
|
||||||
Mod:clean(Context).
|
Mod:clean(Context).
|
||||||
|
|
||||||
-spec do_deliver(list(term()), pos_integer(), pid(), topic()) -> ok | abort.
|
|
||||||
do_deliver([Msg | T], Id, Pid, Topic) ->
|
|
||||||
case require_semaphore(?DELIVER_SEMAPHORE, Id) of
|
|
||||||
true ->
|
|
||||||
Pid ! {deliver, Topic, Msg},
|
|
||||||
do_deliver(T, Id, Pid, Topic);
|
|
||||||
_ ->
|
|
||||||
abort
|
|
||||||
end;
|
|
||||||
do_deliver([], _, _, _) ->
|
|
||||||
ok.
|
|
||||||
|
|
||||||
-spec require_semaphore(semaphore(), pos_integer()) -> boolean().
|
|
||||||
require_semaphore(Semaphore, Id) ->
|
|
||||||
Remained = ets:update_counter(?SHARED_CONTEXT_TAB,
|
|
||||||
Semaphore,
|
|
||||||
{#shared_context.value, -1, -1, -1}),
|
|
||||||
wait_semaphore(Remained, Id).
|
|
||||||
|
|
||||||
-spec wait_semaphore(non_neg_integer(), pos_integer()) -> boolean().
|
|
||||||
wait_semaphore(X, Id) when X < 0 ->
|
|
||||||
call({?FUNCTION_NAME, Id});
|
|
||||||
wait_semaphore(_, _) ->
|
|
||||||
true.
|
|
||||||
|
|
||||||
-spec init_shared_context() -> ok.
|
|
||||||
init_shared_context() ->
|
|
||||||
?SHARED_CONTEXT_TAB = ets:new(?SHARED_CONTEXT_TAB,
|
|
||||||
[ set, named_table, public
|
|
||||||
, {keypos, #shared_context.key}
|
|
||||||
, {write_concurrency, true}
|
|
||||||
, {read_concurrency, true}]),
|
|
||||||
lists:foreach(fun({K, V}) ->
|
|
||||||
insert_shared_context(K, V)
|
|
||||||
end,
|
|
||||||
[{?DELIVER_SEMAPHORE, get_msg_deliver_quota()}]).
|
|
||||||
|
|
||||||
|
|
||||||
-spec insert_shared_context(shared_context_key(), term()) -> ok.
|
|
||||||
insert_shared_context(Key, Term) ->
|
|
||||||
ets:insert(?SHARED_CONTEXT_TAB, #shared_context{key = Key, value = Term}),
|
|
||||||
ok.
|
|
||||||
|
|
||||||
-spec get_msg_deliver_quota() -> non_neg_integer().
|
|
||||||
get_msg_deliver_quota() ->
|
|
||||||
emqx:get_config([retainer, flow_control, msg_deliver_quota]).
|
|
||||||
|
|
||||||
-spec update_config(state(), hocons:config(), hocons:config()) -> state().
|
-spec update_config(state(), hocons:config(), hocons:config()) -> state().
|
||||||
update_config(State, Conf, OldConf) ->
|
update_config(State, Conf, OldConf) ->
|
||||||
update_config(maps:get(enable, Conf),
|
update_config(maps:get(enable, Conf),
|
||||||
|
@ -391,24 +278,19 @@ update_config(true, false, State, NewConf, _) ->
|
||||||
enable_retainer(State, NewConf);
|
enable_retainer(State, NewConf);
|
||||||
|
|
||||||
update_config(true, true,
|
update_config(true, true,
|
||||||
#{clear_timer := ClearTimer,
|
#{clear_timer := ClearTimer} = State, NewConf, OldConf) ->
|
||||||
release_quota_timer := QuotaTimer} = State, NewConf, OldConf) ->
|
#{backend := BackendCfg,
|
||||||
#{config := Cfg,
|
|
||||||
flow_control := #{quota_release_interval := QuotaInterval},
|
|
||||||
msg_clear_interval := ClearInterval} = NewConf,
|
msg_clear_interval := ClearInterval} = NewConf,
|
||||||
|
|
||||||
#{config := OldCfg} = OldConf,
|
#{backend := OldBackendCfg} = OldConf,
|
||||||
|
|
||||||
StorageType = maps:get(type, Cfg),
|
StorageType = maps:get(type, BackendCfg),
|
||||||
OldStrorageType = maps:get(type, OldCfg),
|
OldStrorageType = maps:get(type, OldBackendCfg),
|
||||||
case OldStrorageType of
|
case OldStrorageType of
|
||||||
StorageType ->
|
StorageType ->
|
||||||
State#{clear_timer := check_timer(ClearTimer,
|
State#{clear_timer := check_timer(ClearTimer,
|
||||||
ClearInterval,
|
ClearInterval,
|
||||||
clear_expired),
|
clear_expired)};
|
||||||
release_quota_timer := check_timer(QuotaTimer,
|
|
||||||
QuotaInterval,
|
|
||||||
release_deliver_quota)};
|
|
||||||
_ ->
|
_ ->
|
||||||
State2 = disable_retainer(State),
|
State2 = disable_retainer(State),
|
||||||
enable_retainer(State2, NewConf)
|
enable_retainer(State2, NewConf)
|
||||||
|
@ -417,29 +299,23 @@ update_config(true, true,
|
||||||
-spec enable_retainer(state(), hocon:config()) -> state().
|
-spec enable_retainer(state(), hocon:config()) -> state().
|
||||||
enable_retainer(#{context_id := ContextId} = State,
|
enable_retainer(#{context_id := ContextId} = State,
|
||||||
#{msg_clear_interval := ClearInterval,
|
#{msg_clear_interval := ClearInterval,
|
||||||
flow_control := #{quota_release_interval := ReleaseInterval},
|
backend := BackendCfg}) ->
|
||||||
config := Config}) ->
|
|
||||||
NewContextId = ContextId + 1,
|
NewContextId = ContextId + 1,
|
||||||
Context = create_resource(new_context(NewContextId), Config),
|
Context = create_resource(new_context(NewContextId), BackendCfg),
|
||||||
load(Context),
|
load(Context),
|
||||||
State#{enable := true,
|
State#{enable := true,
|
||||||
context_id := NewContextId,
|
context_id := NewContextId,
|
||||||
context := Context,
|
context := Context,
|
||||||
clear_timer := add_timer(ClearInterval, clear_expired),
|
clear_timer := add_timer(ClearInterval, clear_expired)}.
|
||||||
release_quota_timer := add_timer(ReleaseInterval, release_deliver_quota)}.
|
|
||||||
|
|
||||||
-spec disable_retainer(state()) -> state().
|
-spec disable_retainer(state()) -> state().
|
||||||
disable_retainer(#{clear_timer := TRef1,
|
disable_retainer(#{clear_timer := ClearTimer,
|
||||||
release_quota_timer := TRef2,
|
context := Context} = State) ->
|
||||||
context := Context,
|
|
||||||
wait_quotas := Waits} = State) ->
|
|
||||||
unload(),
|
unload(),
|
||||||
ok = lists:foreach(fun(E) -> gen_server:reply(E, false) end, Waits),
|
|
||||||
ok = close_resource(Context),
|
ok = close_resource(Context),
|
||||||
State#{enable := false,
|
State#{enable := false,
|
||||||
clear_timer := stop_timer(TRef1),
|
clear_timer := stop_timer(ClearTimer)
|
||||||
release_quota_timer := stop_timer(TRef2),
|
}.
|
||||||
wait_quotas := []}.
|
|
||||||
|
|
||||||
-spec stop_timer(undefined | reference()) -> undefined.
|
-spec stop_timer(undefined | reference()) -> undefined.
|
||||||
stop_timer(undefined) ->
|
stop_timer(undefined) ->
|
||||||
|
@ -466,7 +342,7 @@ check_timer(Timer, _, _) ->
|
||||||
|
|
||||||
-spec get_backend_module() -> backend().
|
-spec get_backend_module() -> backend().
|
||||||
get_backend_module() ->
|
get_backend_module() ->
|
||||||
ModName = case emqx:get_config([retainer, config]) of
|
ModName = case emqx:get_config([retainer, backend]) of
|
||||||
#{type := built_in_database} -> mnesia;
|
#{type := built_in_database} -> mnesia;
|
||||||
#{type := Backend} -> Backend
|
#{type := Backend} -> Backend
|
||||||
end,
|
end,
|
||||||
|
|
|
@ -14,27 +14,41 @@
|
||||||
%% limitations under the License.
|
%% limitations under the License.
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
-module(emqx_retainer_pool).
|
-module(emqx_retainer_dispatcher).
|
||||||
|
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include("emqx_retainer.hrl").
|
||||||
-include_lib("emqx/include/logger.hrl").
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([start_link/2,
|
-export([start_link/2
|
||||||
async_submit/2]).
|
, dispatch/2
|
||||||
|
, refresh_limiter/0
|
||||||
|
]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||||
terminate/2, code_change/3, format_status/2]).
|
terminate/2, code_change/3, format_status/2]).
|
||||||
|
|
||||||
|
-type limiter() :: emqx_htb_limiter:limiter().
|
||||||
|
|
||||||
-define(POOL, ?MODULE).
|
-define(POOL, ?MODULE).
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% API
|
%%% API
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
async_submit(Fun, Args) ->
|
dispatch(Context, Topic) ->
|
||||||
cast({async_submit, {Fun, Args}}).
|
cast({?FUNCTION_NAME, Context, self(), Topic}).
|
||||||
|
|
||||||
|
%% sometimes it is necessary to reset the client's limiter after updated the limiter's config
|
||||||
|
%% an limiter update handler maybe added later, now this is a workaround
|
||||||
|
refresh_limiter() ->
|
||||||
|
Workers = gproc_pool:active_workers(?POOL),
|
||||||
|
lists:foreach(fun({_, Pid}) ->
|
||||||
|
gen_server:cast(Pid, ?FUNCTION_NAME)
|
||||||
|
end,
|
||||||
|
Workers).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% @doc
|
%% @doc
|
||||||
|
@ -66,7 +80,9 @@ start_link(Pool, Id) ->
|
||||||
ignore.
|
ignore.
|
||||||
init([Pool, Id]) ->
|
init([Pool, Id]) ->
|
||||||
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
|
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
|
||||||
{ok, #{pool => Pool, id => Id}}.
|
Bucket = emqx:get_config([retainer, flow_control, limiter_bucket_name]),
|
||||||
|
Limiter = emqx_limiter_server:connect(shared, Bucket),
|
||||||
|
{ok, #{pool => Pool, id => Id, limiter => Limiter}}.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% @private
|
%% @private
|
||||||
|
@ -98,12 +114,14 @@ handle_call(Req, _From, State) ->
|
||||||
{noreply, NewState :: term(), Timeout :: timeout()} |
|
{noreply, NewState :: term(), Timeout :: timeout()} |
|
||||||
{noreply, NewState :: term(), hibernate} |
|
{noreply, NewState :: term(), hibernate} |
|
||||||
{stop, Reason :: term(), NewState :: term()}.
|
{stop, Reason :: term(), NewState :: term()}.
|
||||||
handle_cast({async_submit, Task}, State) ->
|
handle_cast({dispatch, Context, Pid, Topic}, #{limiter := Limiter} = State) ->
|
||||||
try run(Task)
|
{ok, Limiter2} = dispatch(Context, Pid, Topic, undefined, Limiter),
|
||||||
catch _:Error:Stacktrace ->
|
{noreply, State#{limiter := Limiter2}};
|
||||||
?SLOG(error, #{msg => "crashed_handling_async_task", exception => Error, stacktrace => Stacktrace})
|
|
||||||
end,
|
handle_cast(refresh_limiter, State) ->
|
||||||
{noreply, State};
|
Bucket = emqx:get_config([retainer, flow_control, limiter_bucket_name]),
|
||||||
|
Limiter = emqx_limiter_server:connect(shared, Bucket),
|
||||||
|
{noreply, State#{limiter := Limiter}};
|
||||||
|
|
||||||
handle_cast(Msg, State) ->
|
handle_cast(Msg, State) ->
|
||||||
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
?SLOG(error, #{msg => "unexpected_cast", cast => Msg}),
|
||||||
|
@ -174,9 +192,74 @@ cast(Msg) ->
|
||||||
worker() ->
|
worker() ->
|
||||||
gproc_pool:pick_worker(?POOL, self()).
|
gproc_pool:pick_worker(?POOL, self()).
|
||||||
|
|
||||||
run({M, F, A}) ->
|
-spec dispatch(context(), pid(), topic(), cursor(), limiter()) -> {ok, limiter()}.
|
||||||
erlang:apply(M, F, A);
|
dispatch(Context, Pid, Topic, Cursor, Limiter) ->
|
||||||
run({F, A}) when is_function(F), is_list(A) ->
|
Mod = emqx_retainer:get_backend_module(),
|
||||||
erlang:apply(F, A);
|
case Cursor =/= undefined orelse emqx_topic:wildcard(Topic) of
|
||||||
run(Fun) when is_function(Fun) ->
|
false ->
|
||||||
Fun().
|
{ok, Result} = Mod:read_message(Context, Topic),
|
||||||
|
deliver(Result, Context, Pid, Topic, undefined, Limiter);
|
||||||
|
true ->
|
||||||
|
{ok, Result, NewCursor} = Mod:match_messages(Context, Topic, Cursor),
|
||||||
|
deliver(Result, Context, Pid, Topic, NewCursor, Limiter)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec deliver(list(emqx_types:message()), context(), pid(), topic(), cursor(), limiter()) -> {ok, limiter()}.
|
||||||
|
deliver([], _Context, _Pid, _Topic, undefined, Limiter) ->
|
||||||
|
{ok, Limiter};
|
||||||
|
|
||||||
|
deliver([], Context, Pid, Topic, Cursor, Limiter) ->
|
||||||
|
dispatch(Context, Pid, Topic, Cursor, Limiter);
|
||||||
|
|
||||||
|
deliver(Result, Context, Pid, Topic, Cursor, Limiter) ->
|
||||||
|
case erlang:is_process_alive(Pid) of
|
||||||
|
false ->
|
||||||
|
{ok, Limiter};
|
||||||
|
_ ->
|
||||||
|
DeliverNum = emqx:get_config([retainer, flow_control, batch_deliver_number]),
|
||||||
|
case DeliverNum of
|
||||||
|
0 ->
|
||||||
|
do_deliver(Result, Pid, Topic),
|
||||||
|
{ok, Limiter};
|
||||||
|
_ ->
|
||||||
|
case do_deliver(Result, DeliverNum, Pid, Topic, Limiter) of
|
||||||
|
{ok, Limiter2} ->
|
||||||
|
deliver([], Context, Pid, Topic, Cursor, Limiter2);
|
||||||
|
{drop, Limiter2} ->
|
||||||
|
{ok, Limiter2}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_deliver([], _DeliverNum, _Pid, _Topic, Limiter) ->
|
||||||
|
{ok, Limiter};
|
||||||
|
|
||||||
|
do_deliver(Msgs, DeliverNum, Pid, Topic, Limiter) ->
|
||||||
|
{Num, ToDelivers, Msgs2} = safe_split(DeliverNum, Msgs),
|
||||||
|
case emqx_htb_limiter:consume(Num, Limiter) of
|
||||||
|
{ok, Limiter2} ->
|
||||||
|
do_deliver(ToDelivers, Pid, Topic),
|
||||||
|
do_deliver(Msgs2, DeliverNum, Pid, Topic, Limiter2);
|
||||||
|
{drop, _} = Drop ->
|
||||||
|
?SLOG(error, #{msg => "the retainer deliver failed because the required quota could not be obtained"}),
|
||||||
|
Drop
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_deliver([Msg | T], Pid, Topic) ->
|
||||||
|
Pid ! {deliver, Topic, Msg},
|
||||||
|
do_deliver(T, Pid, Topic);
|
||||||
|
|
||||||
|
do_deliver([], _, _) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
safe_split(N, List) ->
|
||||||
|
safe_split(N, List, 0, []).
|
||||||
|
|
||||||
|
safe_split(0, List, Count, Acc) ->
|
||||||
|
{Count, lists:reverse(Acc), List};
|
||||||
|
|
||||||
|
safe_split(_N, [], Count, Acc) ->
|
||||||
|
{Count, lists:reverse(Acc), []};
|
||||||
|
|
||||||
|
safe_split(N, [H | T], Count, Acc) ->
|
||||||
|
safe_split(N - 1, T, Count + 1, [H | Acc]).
|
|
@ -144,17 +144,17 @@ page_read(_, Topic, Page, Limit) ->
|
||||||
{ok, Rows}.
|
{ok, Rows}.
|
||||||
|
|
||||||
match_messages(_, Topic, Cursor) ->
|
match_messages(_, Topic, Cursor) ->
|
||||||
MaxReadNum = emqx:get_config([retainer, flow_control, max_read_number]),
|
BatchReadNum = emqx:get_config([retainer, flow_control, batch_read_number]),
|
||||||
case Cursor of
|
case Cursor of
|
||||||
undefined ->
|
undefined ->
|
||||||
case MaxReadNum of
|
case BatchReadNum of
|
||||||
0 ->
|
0 ->
|
||||||
{ok, sort_retained(match_messages(Topic)), undefined};
|
{ok, sort_retained(match_messages(Topic)), undefined};
|
||||||
_ ->
|
_ ->
|
||||||
start_batch_read(Topic, MaxReadNum)
|
start_batch_read(Topic, BatchReadNum)
|
||||||
end;
|
end;
|
||||||
_ ->
|
_ ->
|
||||||
batch_read_messages(Cursor, MaxReadNum)
|
batch_read_messages(Cursor, BatchReadNum)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
clean(_) ->
|
clean(_) ->
|
||||||
|
@ -253,7 +253,7 @@ make_cursor(Topic) ->
|
||||||
|
|
||||||
-spec is_table_full() -> boolean().
|
-spec is_table_full() -> boolean().
|
||||||
is_table_full() ->
|
is_table_full() ->
|
||||||
#{max_retained_messages := Limit} = emqx:get_config([retainer, config]),
|
Limit = emqx:get_config([retainer, backend, max_retained_messages]),
|
||||||
Limit > 0 andalso (table_size() >= Limit).
|
Limit > 0 andalso (table_size() >= Limit).
|
||||||
|
|
||||||
-spec table_size() -> non_neg_integer().
|
-spec table_size() -> non_neg_integer().
|
||||||
|
|
|
@ -17,7 +17,7 @@ fields("retainer") ->
|
||||||
, {flow_control, ?TYPE(hoconsc:ref(?MODULE, flow_control))}
|
, {flow_control, ?TYPE(hoconsc:ref(?MODULE, flow_control))}
|
||||||
, {max_payload_size, sc(emqx_schema:bytesize(), "1MB")}
|
, {max_payload_size, sc(emqx_schema:bytesize(), "1MB")}
|
||||||
, {stop_publish_clear_msg, sc(boolean(), false)}
|
, {stop_publish_clear_msg, sc(boolean(), false)}
|
||||||
, {config, config()}
|
, {backend, backend_config()}
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(mnesia_config) ->
|
fields(mnesia_config) ->
|
||||||
|
@ -27,9 +27,9 @@ fields(mnesia_config) ->
|
||||||
];
|
];
|
||||||
|
|
||||||
fields(flow_control) ->
|
fields(flow_control) ->
|
||||||
[ {max_read_number, sc(integer(), 0, fun is_pos_integer/1)}
|
[ {batch_read_number, sc(integer(), 0, fun is_pos_integer/1)}
|
||||||
, {msg_deliver_quota, sc(integer(), 0, fun is_pos_integer/1)}
|
, {batch_deliver_number, sc(range(0, 50), 0)}
|
||||||
, {quota_release_interval, sc(emqx_schema:duration_ms(), "0ms")}
|
, {limiter_bucket_name, sc(atom(), retainer)}
|
||||||
].
|
].
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -45,5 +45,5 @@ sc(Type, Default, Validator) ->
|
||||||
is_pos_integer(V) ->
|
is_pos_integer(V) ->
|
||||||
V >= 0.
|
V >= 0.
|
||||||
|
|
||||||
config() ->
|
backend_config() ->
|
||||||
#{type => hoconsc:union([hoconsc:ref(?MODULE, mnesia_config)])}.
|
#{type => hoconsc:union([hoconsc:ref(?MODULE, mnesia_config)])}.
|
||||||
|
|
|
@ -26,8 +26,8 @@ start_link() ->
|
||||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
init([]) ->
|
init([]) ->
|
||||||
PoolSpec = emqx_pool_sup:spec([emqx_retainer_pool, hash, emqx_vm:schedulers(),
|
PoolSpec = emqx_pool_sup:spec([emqx_retainer_dispatcher, hash, emqx_vm:schedulers(),
|
||||||
{emqx_retainer_pool, start_link, []}]),
|
{emqx_retainer_dispatcher, start_link, []}]),
|
||||||
{ok, {{one_for_one, 10, 3600},
|
{ok, {{one_for_one, 10, 3600},
|
||||||
[#{id => retainer,
|
[#{id => retainer,
|
||||||
start => {emqx_retainer, start_link, []},
|
start => {emqx_retainer, start_link, []},
|
||||||
|
|
|
@ -34,15 +34,15 @@ retainer {
|
||||||
msg_expiry_interval = 0s
|
msg_expiry_interval = 0s
|
||||||
max_payload_size = 1MB
|
max_payload_size = 1MB
|
||||||
flow_control {
|
flow_control {
|
||||||
max_read_number = 0
|
batch_read_number = 0
|
||||||
msg_deliver_quota = 0
|
batch_deliver_number = 0
|
||||||
quota_release_interval = 0s
|
limiter_bucket_name = retainer
|
||||||
}
|
}
|
||||||
config {
|
backend {
|
||||||
type = built_in_database
|
type = built_in_database
|
||||||
storage_type = ram
|
storage_type = ram
|
||||||
max_retained_messages = 0
|
max_retained_messages = 0
|
||||||
}
|
|
||||||
}""">>).
|
}""">>).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -57,7 +57,8 @@ init_per_suite(Config) ->
|
||||||
meck:expect(emqx_alarm, activate, 3, ok),
|
meck:expect(emqx_alarm, activate, 3, ok),
|
||||||
meck:expect(emqx_alarm, deactivate, 3, ok),
|
meck:expect(emqx_alarm, deactivate, 3, ok),
|
||||||
|
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_retainer_schema, ?BASE_CONF),
|
base_conf(),
|
||||||
|
emqx_ratelimiter_SUITE:base_conf(),
|
||||||
emqx_common_test_helpers:start_apps([emqx_retainer]),
|
emqx_common_test_helpers:start_apps([emqx_retainer]),
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
@ -83,6 +84,9 @@ end_per_testcase(_, Config) ->
|
||||||
end,
|
end,
|
||||||
Config.
|
Config.
|
||||||
|
|
||||||
|
base_conf() ->
|
||||||
|
ok = emqx_common_test_helpers:load_config(emqx_retainer_schema, ?BASE_CONF).
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Test Cases
|
%% Test Cases
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -282,10 +286,20 @@ t_stop_publish_clear_msg(_) ->
|
||||||
ok = emqtt:disconnect(C1).
|
ok = emqtt:disconnect(C1).
|
||||||
|
|
||||||
t_flow_control(_) ->
|
t_flow_control(_) ->
|
||||||
|
#{per_client := PerClient} = RetainerCfg = emqx_config:get([limiter, shared, bucket, retainer]),
|
||||||
|
RetainerCfg2 = RetainerCfg#{per_client := PerClient#{rate := emqx_ratelimiter_SUITE:to_rate("1/1s"),
|
||||||
|
capacity := 1}},
|
||||||
|
emqx_config:put([limiter, shared, bucket, retainer], RetainerCfg2),
|
||||||
|
emqx_limiter_manager:restart_server(shared),
|
||||||
|
timer:sleep(500),
|
||||||
|
|
||||||
|
emqx_retainer_dispatcher:refresh_limiter(),
|
||||||
|
timer:sleep(500),
|
||||||
|
|
||||||
emqx_retainer:update_config(#{<<"flow_control">> =>
|
emqx_retainer:update_config(#{<<"flow_control">> =>
|
||||||
#{<<"max_read_number">> => 1,
|
#{<<"batch_read_number">> => 1,
|
||||||
<<"msg_deliver_quota">> => 1,
|
<<"batch_deliver_number">> => 1,
|
||||||
<<"quota_release_interval">> => <<"1s">>}}),
|
<<"limiter_bucket_name">> => retainer}}),
|
||||||
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
|
{ok, C1} = emqtt:start_link([{clean_start, true}, {proto_ver, v5}]),
|
||||||
{ok, _} = emqtt:connect(C1),
|
{ok, _} = emqtt:connect(C1),
|
||||||
emqtt:publish(
|
emqtt:publish(
|
||||||
|
@ -309,11 +323,19 @@ t_flow_control(_) ->
|
||||||
End = erlang:system_time(millisecond),
|
End = erlang:system_time(millisecond),
|
||||||
Diff = End - Begin,
|
Diff = End - Begin,
|
||||||
|
|
||||||
%% msg_deliver_quota = 1 and quota_release_interval = 1, and there has three message
|
?assert(Diff > timer:seconds(2.5) andalso Diff < timer:seconds(3.9),
|
||||||
%% so total wait time is between in 1 ~ 2s(may be timer will delay, so plus 0.5s to maximum)
|
lists:flatten(io_lib:format("Diff is :~p~n", [Diff]))),
|
||||||
?assert(Diff > timer:seconds(1) andalso Diff < timer:seconds(2.5)),
|
|
||||||
|
|
||||||
ok = emqtt:disconnect(C1).
|
ok = emqtt:disconnect(C1),
|
||||||
|
|
||||||
|
%% recover the limiter
|
||||||
|
emqx_config:put([limiter, shared, bucket, retainer], RetainerCfg),
|
||||||
|
emqx_limiter_manager:restart_server(shared),
|
||||||
|
timer:sleep(500),
|
||||||
|
|
||||||
|
emqx_retainer_dispatcher:refresh_limiter(),
|
||||||
|
timer:sleep(500),
|
||||||
|
ok.
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Helper functions
|
%% Helper functions
|
||||||
|
|
|
@ -21,28 +21,10 @@
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
-define(BASE_CONF, <<"""
|
|
||||||
retainer {
|
|
||||||
enable = true
|
|
||||||
msg_clear_interval = 0s
|
|
||||||
msg_expiry_interval = 0s
|
|
||||||
max_payload_size = 1MB
|
|
||||||
flow_control {
|
|
||||||
max_read_number = 0
|
|
||||||
msg_deliver_quota = 0
|
|
||||||
quota_release_interval = 0s
|
|
||||||
}
|
|
||||||
config {
|
|
||||||
type = built_in_database
|
|
||||||
storage_type = ram
|
|
||||||
max_retained_messages = 0
|
|
||||||
}
|
|
||||||
}""">>).
|
|
||||||
|
|
||||||
all() -> emqx_common_test_helpers:all(?MODULE).
|
all() -> emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
init_per_suite(Config) ->
|
init_per_suite(Config) ->
|
||||||
ok = emqx_common_test_helpers:load_config(emqx_retainer_schema, ?BASE_CONF),
|
emqx_retainer_SUITE:base_conf(),
|
||||||
%% Meck emqtt
|
%% Meck emqtt
|
||||||
ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]),
|
ok = meck:new(emqtt, [non_strict, passthrough, no_history, no_link]),
|
||||||
%% Start Apps
|
%% Start Apps
|
||||||
|
|
Loading…
Reference in New Issue