|
|
@@ -21,12 +21,13 @@
|
|
|
-include_lib("proper/include/proper.hrl").
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
|
|
--define(tab, ?MODULE).
|
|
|
-
|
|
|
%%================================================================================
|
|
|
%% Type declarations
|
|
|
%%================================================================================
|
|
|
|
|
|
+-define(tab, ?MODULE).
|
|
|
+-define(DB, emqx_persistent_session).
|
|
|
+
|
|
|
%% Note: here `committed' != `dirty'. It means "has been committed at
|
|
|
%% least once since the creation", and it's used by the iteration
|
|
|
%% test.
|
|
|
@@ -34,6 +35,15 @@
|
|
|
|
|
|
-type state() :: #{emqx_persistent_session_ds:id() => #s{}}.
|
|
|
|
|
|
+-define(metadata_domain, metadata).
|
|
|
+-define(metadata_domain_bin, <<"metadata">>).
|
|
|
+-define(subscription_domain, subscription).
|
|
|
+-define(subscription_state_domain, subscription_state).
|
|
|
+-define(stream_domain, stream).
|
|
|
+-define(rank_domain, rank).
|
|
|
+-define(seqno_domain, seqno).
|
|
|
+-define(awaiting_rel_domain, awaiting_rel).
|
|
|
+
|
|
|
%%================================================================================
|
|
|
%% Properties
|
|
|
%%================================================================================
|
|
|
@@ -62,6 +72,51 @@ prop_consistency() ->
|
|
|
end
|
|
|
).
|
|
|
|
|
|
+-ifdef(STORE_STATE_IN_DS).
|
|
|
+%% Verifies that our internal keys generated for stream keys preserve the order relation
|
|
|
+%% between them.
|
|
|
+stream_order_internal_keys_proper_test_() ->
|
|
|
+ Props = [prop_stream_order_internal_keys()],
|
|
|
+ Opts = [{numtests, 100}, {to_file, user}, {max_size, 100}],
|
|
|
+ {timeout, 300, [?_assert(proper:quickcheck(Prop, Opts)) || Prop <- Props]}.
|
|
|
+
|
|
|
+prop_stream_order_internal_keys() ->
|
|
|
+ ?FORALL(
|
|
|
+ {Id, Streams0},
|
|
|
+ {session_id(), list({non_neg_integer(), value_gen(), stream_state()})},
|
|
|
+ try
|
|
|
+ init(),
|
|
|
+ Streams = lists:uniq(Streams0),
|
|
|
+ StreamKeys = [{R, S} || {R, S, _SS} <- Streams],
|
|
|
+ ExpectedRanks = lists:sort([R || {R, _S, _SS} <- Streams]),
|
|
|
+ S = lists:foldl(
|
|
|
+ fun({R, S, SS}, Acc) ->
|
|
|
+ emqx_persistent_session_ds_state:put_stream({R, S}, SS, Acc)
|
|
|
+ end,
|
|
|
+ emqx_persistent_session_ds_state:create_new(Id),
|
|
|
+ Streams
|
|
|
+ ),
|
|
|
+ RevRanks = emqx_persistent_session_ds_state:fold_streams(
|
|
|
+ fun({R, _S}, _SS, Acc) -> [R | Acc] end,
|
|
|
+ [],
|
|
|
+ S
|
|
|
+ ),
|
|
|
+ Ranks = lists:reverse(RevRanks),
|
|
|
+ ?WHENFAIL(
|
|
|
+ io:format(
|
|
|
+ user,
|
|
|
+ "Expected ranks:\n ~p\nRanks:\n ~p\nStream keys:\n ~p\n",
|
|
|
+ [ExpectedRanks, Ranks, StreamKeys]
|
|
|
+ ),
|
|
|
+ ExpectedRanks =:= Ranks
|
|
|
+ )
|
|
|
+ after
|
|
|
+ clean()
|
|
|
+ end
|
|
|
+ ).
|
|
|
+%% -ifdef(STORE_STATE_IN_DS).
|
|
|
+-endif.
|
|
|
+
|
|
|
%%================================================================================
|
|
|
%% Generators
|
|
|
%%================================================================================
|
|
|
@@ -109,17 +164,26 @@ seqno_track() ->
|
|
|
seqno() ->
|
|
|
range(1, 100).
|
|
|
|
|
|
+-ifdef(STORE_STATE_IN_DS).
|
|
|
+stream_id() ->
|
|
|
+ {range(1, 3), oneof([#{}, {}])}.
|
|
|
+%% ELSE ifdef(STORE_STATE_IN_DS).
|
|
|
+-else.
|
|
|
stream_id() ->
|
|
|
+ %% Note: this does not match the stream id type used in practice, which is a
|
|
|
+ %% `{emqx_persistent_session_ds:subscription_id(), emqx_ds:stream()}'
|
|
|
range(1, 1).
|
|
|
+%% END ifdef(STORE_STATE_IN_DS).
|
|
|
+-endif.
|
|
|
|
|
|
-stream() ->
|
|
|
+stream_state() ->
|
|
|
oneof([#{}]).
|
|
|
|
|
|
put_req() ->
|
|
|
oneof([
|
|
|
?LET(
|
|
|
{Id, Stream},
|
|
|
- {stream_id(), stream()},
|
|
|
+ {stream_id(), stream_state()},
|
|
|
{#s.streams, put_stream, Id, Stream}
|
|
|
),
|
|
|
?LET(
|
|
|
@@ -147,6 +211,47 @@ del_req() ->
|
|
|
{#s.subs, del_subscription, topic()}
|
|
|
]).
|
|
|
|
|
|
+value_gen() ->
|
|
|
+ oneof([#{}, loose_tuple(oneof([range(1, 3), binary()]))]).
|
|
|
+
|
|
|
+session_id_gen() ->
|
|
|
+ frequency([
|
|
|
+ {5, clientid()},
|
|
|
+ {1, <<"a/">>},
|
|
|
+ {1, <<"a/b">>},
|
|
|
+ {1, <<"a/+">>},
|
|
|
+ {1, <<"a/+/#">>},
|
|
|
+ {1, <<"#">>},
|
|
|
+ {1, <<"+">>},
|
|
|
+ {1, <<"/">>}
|
|
|
+ ]).
|
|
|
+
|
|
|
+clientid() ->
|
|
|
+ %% empty string is not valid...
|
|
|
+ ?SUCHTHAT(ClientId, emqx_proper_types:clientid(), ClientId =/= <<>>).
|
|
|
+
|
|
|
+domain_gen() ->
|
|
|
+ oneof([
|
|
|
+ ?metadata_domain,
|
|
|
+ ?subscription_domain,
|
|
|
+ ?subscription_state_domain,
|
|
|
+ ?stream_domain,
|
|
|
+ ?rank_domain,
|
|
|
+ ?seqno_domain,
|
|
|
+ ?awaiting_rel_domain
|
|
|
+ ]).
|
|
|
+
|
|
|
+key_gen(?metadata_domain) ->
|
|
|
+ <<"metadata">>;
|
|
|
+key_gen(?stream_domain) ->
|
|
|
+ ?LET(
|
|
|
+ {Rank, X},
|
|
|
+ {integer(), integer()},
|
|
|
+ <<Rank:64, X:64>>
|
|
|
+ );
|
|
|
+key_gen(_) ->
|
|
|
+ integer().
|
|
|
+
|
|
|
command(S) ->
|
|
|
case maps:size(S) > 0 of
|
|
|
true ->
|
|
|
@@ -316,12 +421,57 @@ get_state(SessionId) ->
|
|
|
put_state(SessionId, S) ->
|
|
|
ets:insert(?tab, {SessionId, S}).
|
|
|
|
|
|
+-ifdef(STORE_STATE_IN_DS).
|
|
|
+init() ->
|
|
|
+ _ = ets:new(?tab, [named_table, public, {keypos, 1}]),
|
|
|
+ mria:start(),
|
|
|
+ {ok, _} = application:ensure_all_started(emqx_ds_backends),
|
|
|
+ Dir = binary_to_list(filename:join(["/tmp", emqx_guid:to_hexstr(emqx_guid:gen())])),
|
|
|
+ persistent_term:put({?MODULE, data_dir}, Dir),
|
|
|
+ application:set_env(emqx_durable_storage, db_data_dir, Dir),
|
|
|
+ Defaults = #{
|
|
|
+ backend => builtin_local,
|
|
|
+ force_monotonic_timestamps => false,
|
|
|
+ atomic_batches => true,
|
|
|
+ storage =>
|
|
|
+ {emqx_ds_storage_bitfield_lts, #{
|
|
|
+ topic_index_bytes => 4,
|
|
|
+ epoch_bits => 10,
|
|
|
+ bits_per_topic_level => 64
|
|
|
+ }},
|
|
|
+ n_shards => 16,
|
|
|
+ n_sites => 1,
|
|
|
+ replication_factor => 3,
|
|
|
+ replication_options => #{}
|
|
|
+ },
|
|
|
+ ok = emqx_persistent_session_ds_state:open_db(Defaults),
|
|
|
+ ok.
|
|
|
+%% ELSE ifdef(STORE_STATE_IN_DS).
|
|
|
+-else.
|
|
|
init() ->
|
|
|
_ = ets:new(?tab, [named_table, public, {keypos, 1}]),
|
|
|
mria:start(),
|
|
|
emqx_persistent_session_ds_state:create_tables().
|
|
|
+%% END ifdef(STORE_STATE_IN_DS).
|
|
|
+-endif.
|
|
|
|
|
|
+-ifdef(STORE_STATE_IN_DS).
|
|
|
+clean() ->
|
|
|
+ ets:delete(?tab),
|
|
|
+ emqx_ds:drop_db(?DB),
|
|
|
+ application:stop(emqx_ds_backends),
|
|
|
+ application:stop(emqx_ds_builtin_local),
|
|
|
+ mria:stop(),
|
|
|
+ mria_mnesia:delete_schema(),
|
|
|
+ Dir = persistent_term:get({?MODULE, data_dir}),
|
|
|
+ persistent_term:erase({?MODULE, data_dir}),
|
|
|
+ ok = file:del_dir_r(Dir),
|
|
|
+ ok.
|
|
|
+%% ELSE ifdef(STORE_STATE_IN_DS).
|
|
|
+-else.
|
|
|
clean() ->
|
|
|
ets:delete(?tab),
|
|
|
mria:stop(),
|
|
|
mria_mnesia:delete_schema().
|
|
|
+%% END ifdef(STORE_STATE_IN_DS).
|
|
|
+-endif.
|