test: add scenario for node stopping midway during subscribe
This commit is contained in:
parent
3239f5ac5b
commit
e4e88ebf36
|
@ -93,13 +93,27 @@ add_subscription(TopicFilterBin, DSSessionID) ->
|
||||||
{ok, IteratorID, StartMS, IsNew} = emqx_ds:session_add_iterator(
|
{ok, IteratorID, StartMS, IsNew} = emqx_ds:session_add_iterator(
|
||||||
DSSessionID, TopicFilter
|
DSSessionID, TopicFilter
|
||||||
),
|
),
|
||||||
ok = open_iterator_on_all_nodes(TopicFilter, StartMS, IteratorID),
|
Ctx = #{
|
||||||
|
iterator_id => IteratorID,
|
||||||
|
start_time => StartMS,
|
||||||
|
is_new => IsNew
|
||||||
|
},
|
||||||
|
?tp(persistent_session_ds_iterator_added, Ctx),
|
||||||
|
?tp_span(
|
||||||
|
persistent_session_ds_open_iterators,
|
||||||
|
Ctx,
|
||||||
|
ok = open_iterator_on_all_nodes(TopicFilter, StartMS, IteratorID)
|
||||||
|
),
|
||||||
{ok, IteratorID, IsNew}
|
{ok, IteratorID, IsNew}
|
||||||
end
|
end
|
||||||
).
|
).
|
||||||
|
|
||||||
-spec open_iterator_on_all_nodes(emqx_topic:words(), emqx_ds:time(), emqx_ds:iterator_id()) -> ok.
|
-spec open_iterator_on_all_nodes(emqx_topic:words(), emqx_ds:time(), emqx_ds:iterator_id()) -> ok.
|
||||||
open_iterator_on_all_nodes(TopicFilter, StartMS, IteratorID) ->
|
open_iterator_on_all_nodes(TopicFilter, StartMS, IteratorID) ->
|
||||||
|
?tp(persistent_session_ds_will_open_iterators, #{
|
||||||
|
iterator_id => IteratorID,
|
||||||
|
start_time => StartMS
|
||||||
|
}),
|
||||||
Nodes = emqx:running_nodes(),
|
Nodes = emqx:running_nodes(),
|
||||||
Results = emqx_persistent_session_ds_proto_v1:open_iterator(
|
Results = emqx_persistent_session_ds_proto_v1:open_iterator(
|
||||||
Nodes, TopicFilter, StartMS, IteratorID
|
Nodes, TopicFilter, StartMS, IteratorID
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
-module(emqx_cth_cluster).
|
-module(emqx_cth_cluster).
|
||||||
|
|
||||||
-export([start/2]).
|
-export([start/2]).
|
||||||
-export([stop/1]).
|
-export([stop/1, stop_node/1]).
|
||||||
|
|
||||||
-export([share_load_module/2]).
|
-export([share_load_module/2]).
|
||||||
-export([node_name/1]).
|
-export([node_name/1]).
|
||||||
|
@ -80,7 +80,12 @@ when
|
||||||
%% Working directory
|
%% Working directory
|
||||||
%% Everything a test produces should go here. Each node's stuff should go in its
|
%% Everything a test produces should go here. Each node's stuff should go in its
|
||||||
%% own directory.
|
%% own directory.
|
||||||
work_dir := file:name()
|
work_dir := file:name(),
|
||||||
|
%% Usually, we want to ensure the node / test suite starts from a clean slate.
|
||||||
|
%% However, sometimes, we may want to test restarting a node. For such
|
||||||
|
%% situations, we need to disable this check to allow resuming from an existing
|
||||||
|
%% state.
|
||||||
|
skip_clean_suite_state_check => boolean()
|
||||||
}.
|
}.
|
||||||
start(Nodes, ClusterOpts) ->
|
start(Nodes, ClusterOpts) ->
|
||||||
NodeSpecs = mk_nodespecs(Nodes, ClusterOpts),
|
NodeSpecs = mk_nodespecs(Nodes, ClusterOpts),
|
||||||
|
@ -124,12 +129,14 @@ mk_init_nodespec(N, Name, NodeOpts, ClusterOpts) ->
|
||||||
Node = node_name(Name),
|
Node = node_name(Name),
|
||||||
BasePort = base_port(N),
|
BasePort = base_port(N),
|
||||||
WorkDir = maps:get(work_dir, ClusterOpts),
|
WorkDir = maps:get(work_dir, ClusterOpts),
|
||||||
|
SkipCleanSuiteStateCheck = maps:get(skip_clean_suite_state_check, ClusterOpts, false),
|
||||||
Defaults = #{
|
Defaults = #{
|
||||||
name => Node,
|
name => Node,
|
||||||
role => core,
|
role => core,
|
||||||
apps => [],
|
apps => [],
|
||||||
base_port => BasePort,
|
base_port => BasePort,
|
||||||
work_dir => filename:join([WorkDir, Node]),
|
work_dir => filename:join([WorkDir, Node]),
|
||||||
|
skip_clean_suite_state_check => SkipCleanSuiteStateCheck,
|
||||||
driver => ct_slave
|
driver => ct_slave
|
||||||
},
|
},
|
||||||
maps:merge(Defaults, NodeOpts).
|
maps:merge(Defaults, NodeOpts).
|
||||||
|
@ -288,17 +295,20 @@ load_apps(Node, #{apps := Apps}) ->
|
||||||
erpc:call(Node, emqx_cth_suite, load_apps, [Apps]).
|
erpc:call(Node, emqx_cth_suite, load_apps, [Apps]).
|
||||||
|
|
||||||
start_apps_clustering(Node, #{apps := Apps} = Spec) ->
|
start_apps_clustering(Node, #{apps := Apps} = Spec) ->
|
||||||
SuiteOpts = maps:with([work_dir], Spec),
|
SuiteOpts = suite_opts(Spec),
|
||||||
AppsClustering = [lists:keyfind(App, 1, Apps) || App <- ?APPS_CLUSTERING],
|
AppsClustering = [lists:keyfind(App, 1, Apps) || App <- ?APPS_CLUSTERING],
|
||||||
_Started = erpc:call(Node, emqx_cth_suite, start, [AppsClustering, SuiteOpts]),
|
_Started = erpc:call(Node, emqx_cth_suite, start, [AppsClustering, SuiteOpts]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
start_apps(Node, #{apps := Apps} = Spec) ->
|
start_apps(Node, #{apps := Apps} = Spec) ->
|
||||||
SuiteOpts = maps:with([work_dir], Spec),
|
SuiteOpts = suite_opts(Spec),
|
||||||
AppsRest = [AppSpec || AppSpec = {App, _} <- Apps, not lists:member(App, ?APPS_CLUSTERING)],
|
AppsRest = [AppSpec || AppSpec = {App, _} <- Apps, not lists:member(App, ?APPS_CLUSTERING)],
|
||||||
_Started = erpc:call(Node, emqx_cth_suite, start_apps, [AppsRest, SuiteOpts]),
|
_Started = erpc:call(Node, emqx_cth_suite, start_apps, [AppsRest, SuiteOpts]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
suite_opts(Spec) ->
|
||||||
|
maps:with([work_dir, skip_clean_suite_state_check], Spec).
|
||||||
|
|
||||||
maybe_join_cluster(_Node, #{role := replicant}) ->
|
maybe_join_cluster(_Node, #{role := replicant}) ->
|
||||||
ok;
|
ok;
|
||||||
maybe_join_cluster(Node, Spec) ->
|
maybe_join_cluster(Node, Spec) ->
|
||||||
|
|
|
@ -358,6 +358,8 @@ stop_apps(Apps) ->
|
||||||
|
|
||||||
%%
|
%%
|
||||||
|
|
||||||
|
verify_clean_suite_state(#{skip_clean_suite_state_check := true}) ->
|
||||||
|
ok;
|
||||||
verify_clean_suite_state(#{work_dir := WorkDir}) ->
|
verify_clean_suite_state(#{work_dir := WorkDir}) ->
|
||||||
{ok, []} = file:list_dir(WorkDir),
|
{ok, []} = file:list_dir(WorkDir),
|
||||||
none = persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, none),
|
none = persistent_term:get(?EMQX_AUTHENTICATION_SCHEMA_MODULE_PT_KEY, none),
|
||||||
|
|
|
@ -183,6 +183,8 @@ t_session_subscription_iterators(Config) ->
|
||||||
ok
|
ok
|
||||||
end,
|
end,
|
||||||
?assertMatch([_], IteratorIds),
|
?assertMatch([_], IteratorIds),
|
||||||
|
?assertMatch({ok, [_]}, get_all_iterator_ids(Node1)),
|
||||||
|
?assertMatch({ok, [_]}, get_all_iterator_ids(Node2)),
|
||||||
[IteratorId] = IteratorIds,
|
[IteratorId] = IteratorIds,
|
||||||
ReplayMessages1 = erpc:call(Node1, fun() -> consume(?DS_SHARD, IteratorId) end),
|
ReplayMessages1 = erpc:call(Node1, fun() -> consume(?DS_SHARD, IteratorId) end),
|
||||||
ExpectedMessages = [Message2, Message3],
|
ExpectedMessages = [Message2, Message3],
|
||||||
|
@ -280,3 +282,9 @@ cluster() ->
|
||||||
get_mqtt_port(Node, Type) ->
|
get_mqtt_port(Node, Type) ->
|
||||||
{_IP, Port} = erpc:call(Node, emqx_config, get, [[listeners, Type, default, bind]]),
|
{_IP, Port} = erpc:call(Node, emqx_config, get, [[listeners, Type, default, bind]]),
|
||||||
Port.
|
Port.
|
||||||
|
|
||||||
|
get_all_iterator_ids(Node) ->
|
||||||
|
Fn = fun(K, _V, Acc) -> [K | Acc] end,
|
||||||
|
erpc:call(Node, fun() ->
|
||||||
|
emqx_ds_storage_layer:foldl_iterator_prefix(?DS_SHARD, <<>>, Fn, [])
|
||||||
|
end).
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
restore_iterator/2,
|
restore_iterator/2,
|
||||||
discard_iterator/2,
|
discard_iterator/2,
|
||||||
is_iterator_present/2,
|
is_iterator_present/2,
|
||||||
discard_iterator_prefix/2
|
discard_iterator_prefix/2,
|
||||||
|
foldl_iterator_prefix/4
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% behaviour callbacks:
|
%% behaviour callbacks:
|
||||||
|
@ -204,6 +205,16 @@ discard_iterator(Shard, ReplayID) ->
|
||||||
discard_iterator_prefix(Shard, KeyPrefix) ->
|
discard_iterator_prefix(Shard, KeyPrefix) ->
|
||||||
do_discard_iterator_prefix(Shard, KeyPrefix).
|
do_discard_iterator_prefix(Shard, KeyPrefix).
|
||||||
|
|
||||||
|
-spec foldl_iterator_prefix(
|
||||||
|
emqx_ds:shard(),
|
||||||
|
binary(),
|
||||||
|
fun((_Key :: binary(), _Value :: binary(), Acc) -> Acc),
|
||||||
|
Acc
|
||||||
|
) -> {ok, Acc} | {error, _TODO} when
|
||||||
|
Acc :: term().
|
||||||
|
foldl_iterator_prefix(Shard, KeyPrefix, Fn, Acc) ->
|
||||||
|
do_foldl_iterator_prefix(Shard, KeyPrefix, Fn, Acc).
|
||||||
|
|
||||||
%%================================================================================
|
%%================================================================================
|
||||||
%% behaviour callbacks
|
%% behaviour callbacks
|
||||||
%%================================================================================
|
%%================================================================================
|
||||||
|
@ -414,26 +425,31 @@ restore_iterator_state(
|
||||||
open_restore_iterator(meta_get_gen(Shard, Gen), It, State).
|
open_restore_iterator(meta_get_gen(Shard, Gen), It, State).
|
||||||
|
|
||||||
do_discard_iterator_prefix(Shard, KeyPrefix) ->
|
do_discard_iterator_prefix(Shard, KeyPrefix) ->
|
||||||
|
#db{handle = DBHandle, cf_iterator = CF} = meta_lookup(Shard, db),
|
||||||
|
Fn = fun(K, _V, _Acc) -> ok = rocksdb:delete(DBHandle, CF, K, ?ITERATION_WRITE_OPTS) end,
|
||||||
|
do_foldl_iterator_prefix(Shard, KeyPrefix, Fn, ok).
|
||||||
|
|
||||||
|
do_foldl_iterator_prefix(Shard, KeyPrefix, Fn, Acc) ->
|
||||||
#db{handle = Handle, cf_iterator = CF} = meta_lookup(Shard, db),
|
#db{handle = Handle, cf_iterator = CF} = meta_lookup(Shard, db),
|
||||||
case rocksdb:iterator(Handle, CF, ?ITERATION_READ_OPTS) of
|
case rocksdb:iterator(Handle, CF, ?ITERATION_READ_OPTS) of
|
||||||
{ok, It} ->
|
{ok, It} ->
|
||||||
NextAction = {seek, KeyPrefix},
|
NextAction = {seek, KeyPrefix},
|
||||||
do_discard_iterator_prefix(Handle, CF, It, KeyPrefix, NextAction);
|
do_foldl_iterator_prefix(Handle, CF, It, KeyPrefix, NextAction, Fn, Acc);
|
||||||
Error ->
|
Error ->
|
||||||
Error
|
Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
do_discard_iterator_prefix(DBHandle, CF, It, KeyPrefix, NextAction) ->
|
do_foldl_iterator_prefix(DBHandle, CF, It, KeyPrefix, NextAction, Fn, Acc) ->
|
||||||
case rocksdb:iterator_move(It, NextAction) of
|
case rocksdb:iterator_move(It, NextAction) of
|
||||||
{ok, K = <<KeyPrefix:(size(KeyPrefix))/binary, _/binary>>, _V} ->
|
{ok, K = <<KeyPrefix:(size(KeyPrefix))/binary, _/binary>>, V} ->
|
||||||
ok = rocksdb:delete(DBHandle, CF, K, ?ITERATION_WRITE_OPTS),
|
NewAcc = Fn(K, V, Acc),
|
||||||
do_discard_iterator_prefix(DBHandle, CF, It, KeyPrefix, next);
|
do_foldl_iterator_prefix(DBHandle, CF, It, KeyPrefix, next, Fn, NewAcc);
|
||||||
{ok, _K, _V} ->
|
{ok, _K, _V} ->
|
||||||
ok = rocksdb:iterator_close(It),
|
ok = rocksdb:iterator_close(It),
|
||||||
ok;
|
{ok, Acc};
|
||||||
{error, invalid_iterator} ->
|
{error, invalid_iterator} ->
|
||||||
ok = rocksdb:iterator_close(It),
|
ok = rocksdb:iterator_close(It),
|
||||||
ok;
|
{ok, Acc};
|
||||||
Error ->
|
Error ->
|
||||||
ok = rocksdb:iterator_close(It),
|
ok = rocksdb:iterator_close(It),
|
||||||
Error
|
Error
|
||||||
|
|
|
@ -0,0 +1,178 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_ds_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include_lib("stdlib/include/assert.hrl").
|
||||||
|
-include_lib("common_test/include/ct.hrl").
|
||||||
|
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
|
||||||
|
|
||||||
|
-define(DS_SHARD, <<"local">>).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% CT boilerplate
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
emqx_common_test_helpers:all(?MODULE).
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
%% avoid inter-suite flakiness...
|
||||||
|
application:stop(emqx),
|
||||||
|
application:stop(emqx_durable_storage),
|
||||||
|
TCApps = emqx_cth_suite:start(
|
||||||
|
app_specs(),
|
||||||
|
#{work_dir => ?config(priv_dir, Config)}
|
||||||
|
),
|
||||||
|
[{tc_apps, TCApps} | Config].
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
TCApps = ?config(tc_apps, Config),
|
||||||
|
emqx_cth_suite:stop(TCApps),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
init_per_testcase(t_session_subscription_idempotency, Config) ->
|
||||||
|
Cluster = cluster(#{n => 1}),
|
||||||
|
Nodes = emqx_cth_cluster:start(Cluster, #{work_dir => ?config(priv_dir, Config)}),
|
||||||
|
[{cluster, Cluster}, {nodes, Nodes} | Config];
|
||||||
|
init_per_testcase(_TestCase, Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_testcase(t_session_subscription_idempotency, Config) ->
|
||||||
|
Nodes = ?config(nodes, Config),
|
||||||
|
ok = emqx_cth_cluster:stop(Nodes),
|
||||||
|
ok;
|
||||||
|
end_per_testcase(_TestCase, _Config) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Helper fns
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
cluster(#{n := N}) ->
|
||||||
|
Node1 = ds_SUITE1,
|
||||||
|
Spec = #{
|
||||||
|
role => core,
|
||||||
|
join_to => emqx_cth_cluster:node_name(Node1),
|
||||||
|
listeners => true,
|
||||||
|
apps => app_specs()
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{Node1, Spec}
|
||||||
|
| lists:map(
|
||||||
|
fun(M) ->
|
||||||
|
Name = binary_to_atom(<<"ds_SUITE", (integer_to_binary(M))/binary>>),
|
||||||
|
{Name, Spec}
|
||||||
|
end,
|
||||||
|
lists:seq(2, N)
|
||||||
|
)
|
||||||
|
].
|
||||||
|
|
||||||
|
app_specs() ->
|
||||||
|
[
|
||||||
|
emqx_durable_storage,
|
||||||
|
{emqx, #{
|
||||||
|
before_start => fun() ->
|
||||||
|
emqx_app:set_config_loader(?MODULE)
|
||||||
|
end,
|
||||||
|
config => #{persistent_session_store => #{ds => true}},
|
||||||
|
override_env => [{boot_modules, [broker, listeners]}]
|
||||||
|
}}
|
||||||
|
].
|
||||||
|
|
||||||
|
get_mqtt_port(Node, Type) ->
|
||||||
|
{_IP, Port} = erpc:call(Node, emqx_config, get, [[listeners, Type, default, bind]]),
|
||||||
|
Port.
|
||||||
|
|
||||||
|
get_all_iterator_ids(Node) ->
|
||||||
|
Fn = fun(K, _V, Acc) -> [K | Acc] end,
|
||||||
|
erpc:call(Node, fun() ->
|
||||||
|
emqx_ds_storage_layer:foldl_iterator_prefix(?DS_SHARD, <<>>, Fn, [])
|
||||||
|
end).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Testcases
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
t_session_subscription_idempotency(Config) ->
|
||||||
|
Cluster = ?config(cluster, Config),
|
||||||
|
[Node1] = ?config(nodes, Config),
|
||||||
|
Port = get_mqtt_port(Node1, tcp),
|
||||||
|
SubTopicFilter = <<"t/+">>,
|
||||||
|
ClientId = <<"myclientid">>,
|
||||||
|
?check_trace(
|
||||||
|
begin
|
||||||
|
?force_ordering(
|
||||||
|
#{?snk_kind := persistent_session_ds_iterator_added},
|
||||||
|
_NEvents0 = 1,
|
||||||
|
#{?snk_kind := will_restart_node},
|
||||||
|
_Guard0 = true
|
||||||
|
),
|
||||||
|
?force_ordering(
|
||||||
|
#{?snk_kind := restarted_node},
|
||||||
|
_NEvents1 = 1,
|
||||||
|
#{?snk_kind := persistent_session_ds_open_iterators, ?snk_span := start},
|
||||||
|
_Guard1 = true
|
||||||
|
),
|
||||||
|
|
||||||
|
spawn_link(fun() ->
|
||||||
|
?tp(will_restart_node, #{}),
|
||||||
|
ct:pal("stopping node ~p", [Node1]),
|
||||||
|
ok = emqx_cth_cluster:stop_node(Node1),
|
||||||
|
ct:pal("stopped node ~p; restarting...", [Node1]),
|
||||||
|
[Node1] = emqx_cth_cluster:start(Cluster, #{
|
||||||
|
work_dir => ?config(priv_dir, Config),
|
||||||
|
skip_clean_suite_state_check => true
|
||||||
|
}),
|
||||||
|
ct:pal("node ~p restarted", [Node1]),
|
||||||
|
?tp(restarted_node, #{}),
|
||||||
|
ok
|
||||||
|
end),
|
||||||
|
|
||||||
|
ct:pal("starting 1"),
|
||||||
|
{ok, Client0} = emqtt:start_link([
|
||||||
|
{port, Port},
|
||||||
|
{clientid, ClientId},
|
||||||
|
{proto_ver, v5}
|
||||||
|
]),
|
||||||
|
{ok, _} = emqtt:connect(Client0),
|
||||||
|
ct:pal("subscribing 1"),
|
||||||
|
process_flag(trap_exit, true),
|
||||||
|
catch emqtt:subscribe(Client0, SubTopicFilter, qos2),
|
||||||
|
receive
|
||||||
|
{'EXIT', {shutdown, _}} ->
|
||||||
|
ok
|
||||||
|
after 0 -> ok
|
||||||
|
end,
|
||||||
|
process_flag(trap_exit, false),
|
||||||
|
|
||||||
|
{ok, _} = ?block_until(#{?snk_kind := restarted_node}, 15_000),
|
||||||
|
ct:pal("starting 2"),
|
||||||
|
{ok, Client1} = emqtt:start_link([
|
||||||
|
{port, Port},
|
||||||
|
{clientid, ClientId},
|
||||||
|
{proto_ver, v5}
|
||||||
|
]),
|
||||||
|
{ok, _} = emqtt:connect(Client1),
|
||||||
|
ct:pal("subscribing 2"),
|
||||||
|
{ok, _, [2]} = emqtt:subscribe(Client1, SubTopicFilter, qos2),
|
||||||
|
|
||||||
|
ok = emqtt:stop(Client1),
|
||||||
|
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
fun(Trace) ->
|
||||||
|
ct:pal("trace:\n ~p", [Trace]),
|
||||||
|
%% Exactly one iterator should have been opened.
|
||||||
|
?assertMatch({ok, [_]}, get_all_iterator_ids(Node1)),
|
||||||
|
?assertMatch(
|
||||||
|
{_IsNew = false, ClientId, _},
|
||||||
|
erpc:call(Node1, emqx_ds, session_open, [ClientId])
|
||||||
|
),
|
||||||
|
ok
|
||||||
|
end
|
||||||
|
),
|
||||||
|
ok.
|
Loading…
Reference in New Issue