refactor(ds): Create a CRUD module for the persistent session
This commit is contained in:
parent
933c00c7ad
commit
2d08aa88d8
|
@ -0,0 +1,508 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% @doc CRUD interface for the persistent session
|
||||
%%
|
||||
%% This module encapsulates the data related to the state of the
|
||||
%% inflight messages for the persistent session based on DS.
|
||||
%%
|
||||
%% It is responsible for saving, caching, and restoring session state.
|
||||
%% It is completely devoid of business logic. Not even the default
|
||||
%% values should be set in this module.
|
||||
-module(emqx_persistent_session_ds_state).
|
||||
|
||||
-export([create_tables/0]).
|
||||
|
||||
-export([open/1, create_new/1, delete/1, commit/1, print_session/1]).
|
||||
-export([get_created_at/1, set_created_at/2]).
|
||||
-export([get_last_alive_at/1, set_last_alive_at/2]).
|
||||
-export([get_conninfo/1, set_conninfo/2]).
|
||||
-export([get_stream/2, put_stream/3, del_stream/2, fold_streams/3]).
|
||||
-export([get_seqno/2, put_seqno/3]).
|
||||
-export([get_rank/2, put_rank/3, del_rank/2, fold_ranks/3]).
|
||||
-export([get_subscriptions/1, put_subscription/4, del_subscription/3]).
|
||||
|
||||
%% internal exports:
|
||||
-export([]).
|
||||
|
||||
-export_type([t/0, seqno_type/0]).
|
||||
|
||||
-include("emqx_persistent_session_ds.hrl").
|
||||
|
||||
%%================================================================================
|
||||
%% Type declarations
|
||||
%%================================================================================
|
||||
|
||||
%% Generic key-value wrapper that is used for exporting arbitrary
|
||||
%% terms to mnesia:
|
||||
-record(kv, {
|
||||
k :: term(),
|
||||
v :: map()
|
||||
}).
|
||||
|
||||
%% Persistent map.
|
||||
%%
|
||||
%% Pmap accumulates the updates in a term stored in the heap of a
|
||||
%% process, so they can be committed all at once in a single
|
||||
%% transaction.
|
||||
%%
|
||||
%% It should be possible to make frequent changes to the pmap without
|
||||
%% stressing Mria.
|
||||
%%
|
||||
%% It's implemented as two maps: `clean' and `dirty'. Updates are made
|
||||
%% to the `dirty' area. `pmap_commit' function saves the updated
|
||||
%% entries to Mnesia and moves them to the `clean' area.
|
||||
-record(pmap, {table, clean, dirty, tombstones}).
|
||||
|
||||
-type pmap(K, V) ::
|
||||
#pmap{
|
||||
table :: atom(),
|
||||
clean :: #{K => V},
|
||||
dirty :: #{K => V},
|
||||
tombstones :: #{K => _}
|
||||
}.
|
||||
|
||||
%% Session metadata:
|
||||
-define(created_at, created_at).
|
||||
-define(last_alive_at, last_alive_at).
|
||||
-define(conninfo, conninfo).
|
||||
|
||||
-type metadata() ::
|
||||
#{
|
||||
?created_at => emqx_persistent_session_ds:timestamp(),
|
||||
?last_alive_at => emqx_persistent_session_ds:timestamp(),
|
||||
?conninfo => emqx_types:conninfo()
|
||||
}.
|
||||
|
||||
-type seqno_type() :: next | acked | pubrel.
|
||||
|
||||
-opaque t() :: #{
|
||||
id := emqx_persistent_session_ds:id(),
|
||||
dirty := boolean(),
|
||||
metadata := metadata(),
|
||||
subscriptions := emqx_persistent_session_ds:subscriptions(),
|
||||
seqnos := pmap(seqno_type(), emqx_persistent_session_ds:seqno()),
|
||||
streams := pmap(emqx_ds:stream(), emqx_persistent_message_ds_replayer:stream_state()),
|
||||
ranks := pmap(term(), integer())
|
||||
}.
|
||||
|
||||
-define(session_tab, emqx_ds_session_tab).
|
||||
-define(subscription_tab, emqx_ds_session_subscriptions).
|
||||
-define(stream_tab, emqx_ds_session_streams).
|
||||
-define(seqno_tab, emqx_ds_session_seqnos).
|
||||
-define(rank_tab, emqx_ds_session_ranks).
|
||||
-define(bag_tables, [?stream_tab, ?seqno_tab, ?rank_tab]).
|
||||
|
||||
%%================================================================================
|
||||
%% API funcions
|
||||
%%================================================================================
|
||||
|
||||
-spec create_tables() -> ok.
|
||||
create_tables() ->
|
||||
ok = mria:create_table(
|
||||
?session_tab,
|
||||
[
|
||||
{rlog_shard, ?DS_MRIA_SHARD},
|
||||
{type, set},
|
||||
{storage, rocksdb_copies},
|
||||
{record_name, kv},
|
||||
{attributes, record_info(fields, kv)}
|
||||
]
|
||||
),
|
||||
[create_kv_bag_table(Table) || Table <- ?bag_tables],
|
||||
mria:wait_for_tables([?session_tab | ?bag_tables]).
|
||||
|
||||
-spec open(emqx_persistent_session_ds:session_id()) -> {ok, t()} | undefined.
|
||||
open(SessionId) ->
|
||||
ro_transaction(fun() ->
|
||||
case kv_restore(?session_tab, SessionId) of
|
||||
[Metadata] ->
|
||||
Rec = #{
|
||||
id => SessionId,
|
||||
metadata => Metadata,
|
||||
subscriptions => read_subscriptions(SessionId),
|
||||
streams => pmap_open(?stream_tab, SessionId),
|
||||
seqnos => pmap_open(?seqno_tab, SessionId),
|
||||
ranks => pmap_open(?rank_tab, SessionId),
|
||||
dirty => false
|
||||
},
|
||||
{ok, Rec};
|
||||
[] ->
|
||||
undefined
|
||||
end
|
||||
end).
|
||||
|
||||
-spec print_session(emqx_persistent_session_ds:id()) -> map() | undefined.
|
||||
print_session(SessionId) ->
|
||||
case open(SessionId) of
|
||||
undefined ->
|
||||
undefined;
|
||||
#{
|
||||
metadata := Metadata,
|
||||
subscriptions := SubsGBT,
|
||||
streams := Streams,
|
||||
seqnos := Seqnos,
|
||||
ranks := Ranks
|
||||
} ->
|
||||
Subs = emqx_topic_gbt:fold(
|
||||
fun(Key, Sub, Acc) -> maps:put(Key, Sub, Acc) end,
|
||||
#{},
|
||||
SubsGBT
|
||||
),
|
||||
#{
|
||||
session => Metadata,
|
||||
subscriptions => Subs,
|
||||
streams => Streams#pmap.clean,
|
||||
seqnos => Seqnos#pmap.clean,
|
||||
ranks => Ranks#pmap.clean
|
||||
}
|
||||
end.
|
||||
|
||||
-spec delete(emqx_persistent_session_ds:id()) -> ok.
|
||||
delete(Id) ->
|
||||
transaction(
|
||||
fun() ->
|
||||
[kv_delete(Table, Id) || Table <- ?bag_tables],
|
||||
mnesia:delete(?session_tab, Id, write)
|
||||
end
|
||||
).
|
||||
|
||||
-spec commit(t()) -> t().
|
||||
commit(Rec = #{dirty := false}) ->
|
||||
Rec;
|
||||
commit(
|
||||
Rec = #{
|
||||
id := SessionId,
|
||||
metadata := Metadata,
|
||||
subscriptions := Subs,
|
||||
streams := Streams,
|
||||
seqnos := SeqNos,
|
||||
ranks := Ranks
|
||||
}
|
||||
) ->
|
||||
transaction(fun() ->
|
||||
kv_persist(?session_tab, SessionId, Metadata),
|
||||
Rec#{
|
||||
subscriptions => pmap_commit(SessionId, Subs),
|
||||
streams => pmap_commit(SessionId, Streams),
|
||||
seqnos => pmap_commit(SessionId, SeqNos),
|
||||
ranksz => pmap_commit(SessionId, Ranks),
|
||||
dirty => false
|
||||
}
|
||||
end).
|
||||
|
||||
-spec create_new(emqx_persistent_session_ds:id()) -> t().
|
||||
create_new(SessionId) ->
|
||||
transaction(fun() ->
|
||||
delete(SessionId),
|
||||
#{
|
||||
id => SessionId,
|
||||
metadata => #{},
|
||||
subscriptions => emqx_topic_gbt:new(),
|
||||
streams => pmap_open(?stream_tab, SessionId),
|
||||
seqnos => pmap_open(?seqno_tab, SessionId),
|
||||
ranks => pmap_open(?rank_tab, SessionId),
|
||||
dirty => true
|
||||
}
|
||||
end).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_created_at(t()) -> emqx_persistent_session_ds:timestamp() | undefined.
|
||||
get_created_at(Rec) ->
|
||||
get_meta(?created_at, Rec).
|
||||
|
||||
-spec set_created_at(emqx_persistent_session_ds:timestamp(), t()) -> t().
|
||||
set_created_at(Val, Rec) ->
|
||||
set_meta(?created_at, Val, Rec).
|
||||
|
||||
-spec get_last_alive_at(t()) -> emqx_persistent_session_ds:timestamp() | undefined.
|
||||
get_last_alive_at(Rec) ->
|
||||
get_meta(?last_alive_at, Rec).
|
||||
|
||||
-spec set_last_alive_at(emqx_persistent_session_ds:timestamp(), t()) -> t().
|
||||
set_last_alive_at(Val, Rec) ->
|
||||
set_meta(?last_alive_at, Val, Rec).
|
||||
|
||||
-spec get_conninfo(t()) -> emqx_types:conninfo() | undefined.
|
||||
get_conninfo(Rec) ->
|
||||
get_meta(?conninfo, Rec).
|
||||
|
||||
-spec set_conninfo(emqx_types:conninfo(), t()) -> t().
|
||||
set_conninfo(Val, Rec) ->
|
||||
set_meta(?conninfo, Val, Rec).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_stream(emqx_persistent_session_ds:stream(), t()) ->
|
||||
emqx_persistent_message_ds_replayer:stream_state() | undefined.
|
||||
get_stream(Key, Rec) ->
|
||||
gen_get(streams, Key, Rec).
|
||||
|
||||
-spec put_stream(
|
||||
emqx_persistent_session_ds:stream(), emqx_persistent_message_ds_replayer:stream_state(), t()
|
||||
) -> t().
|
||||
put_stream(Key, Val, Rec) ->
|
||||
gen_put(streams, Key, Val, Rec).
|
||||
|
||||
-spec del_stream(emqx_persistent_session_ds:stream(), t()) -> t().
|
||||
del_stream(Key, Rec) ->
|
||||
gen_del(stream, Key, Rec).
|
||||
|
||||
-spec fold_streams(fun(), Acc, t()) -> Acc.
|
||||
fold_streams(Fun, Acc, Rec) ->
|
||||
gen_fold(streams, Fun, Acc, Rec).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_seqno(seqno_type(), t()) -> emqx_persistent_session_ds:seqno() | undefined.
|
||||
get_seqno(Key, Rec) ->
|
||||
gen_get(seqnos, Key, Rec).
|
||||
|
||||
-spec put_seqno(seqno_type(), emqx_persistent_session_ds:seqno(), t()) -> t().
|
||||
put_seqno(Key, Val, Rec) ->
|
||||
gen_put(seqnos, Key, Val, Rec).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_rank(term(), t()) -> integer() | undefined.
|
||||
get_rank(Key, Rec) ->
|
||||
gen_get(ranks, Key, Rec).
|
||||
|
||||
-spec put_rank(term(), integer(), t()) -> t().
|
||||
put_rank(Key, Val, Rec) ->
|
||||
gen_put(ranks, Key, Val, Rec).
|
||||
|
||||
-spec del_rank(term(), t()) -> t().
|
||||
del_rank(Key, Rec) ->
|
||||
gen_del(ranks, Key, Rec).
|
||||
|
||||
-spec fold_ranks(fun(), Acc, t()) -> Acc.
|
||||
fold_ranks(Fun, Acc, Rec) ->
|
||||
gen_fold(ranks, Fun, Acc, Rec).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_subscriptions(t()) -> emqx_persistent_session_ds:subscriptions().
|
||||
get_subscriptions(#{subscriptions := Subs}) ->
|
||||
Subs.
|
||||
|
||||
-spec put_subscription(
|
||||
emqx_persistent_session_ds:subscription_id(),
|
||||
_SubId,
|
||||
emqx_persistent_session_ds:subscription(),
|
||||
t()
|
||||
) -> t().
|
||||
put_subscription(TopicFilter, SubId, Subscription, Rec = #{id := Id, subscriptions := Subs0}) ->
|
||||
%% Note: currently changes to the subscriptions are persisted immediately.
|
||||
Key = {TopicFilter, SubId},
|
||||
transaction(fun() -> kv_bag_persist(?subscription_tab, Id, Key, Subscription) end),
|
||||
Subs = emqx_topic_gbt:insert(TopicFilter, SubId, Subscription, Subs0),
|
||||
Rec#{subscriptions => Subs}.
|
||||
|
||||
-spec del_subscription(emqx_persistent_session_ds:topic_filter(), _SubId, t()) -> t().
|
||||
del_subscription(TopicFilter, SubId, Rec = #{id := Id, subscriptions := Subs0}) ->
|
||||
%% Note: currently the subscriptions are persisted immediately.
|
||||
Key = {TopicFilter, SubId},
|
||||
transaction(fun() -> kv_bag_delete(?subscription_tab, Id, Key) end),
|
||||
Subs = emqx_topic_gbt:delete(TopicFilter, SubId, Subs0),
|
||||
Rec#{subscriptions => Subs}.
|
||||
|
||||
%%================================================================================
|
||||
%% Internal functions
|
||||
%%================================================================================
|
||||
|
||||
%% All mnesia reads and writes are passed through this function.
|
||||
%% Backward compatiblity issues can be handled here.
|
||||
encoder(encode, _Table, Term) ->
|
||||
Term;
|
||||
encoder(decode, _Table, Term) ->
|
||||
Term.
|
||||
|
||||
%%
|
||||
|
||||
get_meta(K, #{metadata := Meta}) ->
|
||||
maps:get(K, Meta, undefined).
|
||||
|
||||
set_meta(K, V, Rec = #{metadata := Meta}) ->
|
||||
Rec#{metadata => maps:put(K, V, Meta), dirty => true}.
|
||||
|
||||
%%
|
||||
|
||||
gen_get(Field, Key, Rec) ->
|
||||
pmap_get(Key, maps:get(Field, Rec)).
|
||||
|
||||
gen_fold(Field, Fun, Acc, Rec) ->
|
||||
pmap_fold(Fun, Acc, maps:get(Field, Rec)).
|
||||
|
||||
gen_put(Field, Key, Val, Rec) ->
|
||||
maps:update_with(
|
||||
Field,
|
||||
fun(PMap) -> pmap_put(Key, Val, PMap) end,
|
||||
Rec#{dirty => true}
|
||||
).
|
||||
|
||||
gen_del(Field, Key, Rec) ->
|
||||
maps:update_with(
|
||||
Field,
|
||||
fun(PMap) -> pmap_del(Key, PMap) end,
|
||||
Rec#{dirty => true}
|
||||
).
|
||||
|
||||
%%
|
||||
|
||||
read_subscriptions(SessionId) ->
|
||||
Records = kv_bag_restore(?subscription_tab, SessionId),
|
||||
lists:foldl(
|
||||
fun({{TopicFilter, SubId}, Subscription}, Acc) ->
|
||||
emqx_topic_gbt:insert(TopicFilter, SubId, Subscription, Acc)
|
||||
end,
|
||||
emqx_topic_gbt:new(),
|
||||
Records
|
||||
).
|
||||
|
||||
%%
|
||||
|
||||
%% @doc Open a PMAP and fill the clean area with the data from DB.
|
||||
%% This functtion should be ran in a transaction.
|
||||
-spec pmap_open(atom(), emqx_persistent_session_ds:id()) -> pmap(_K, _V).
|
||||
pmap_open(Table, SessionId) ->
|
||||
Clean = maps:from_list(kv_bag_restore(Table, SessionId)),
|
||||
#pmap{
|
||||
table = Table,
|
||||
clean = Clean,
|
||||
dirty = #{},
|
||||
tombstones = #{}
|
||||
}.
|
||||
|
||||
-spec pmap_get(K, pmap(K, V)) -> V | undefined.
|
||||
pmap_get(K, #pmap{dirty = Dirty, clean = Clean}) ->
|
||||
case Dirty of
|
||||
#{K := V} ->
|
||||
V;
|
||||
_ ->
|
||||
case Clean of
|
||||
#{K := V} -> V;
|
||||
_ -> undefined
|
||||
end
|
||||
end.
|
||||
|
||||
-spec pmap_put(K, V, pmap(K, V)) -> pmap(K, V).
|
||||
pmap_put(K, V, Pmap = #pmap{dirty = Dirty, clean = Clean, tombstones = Tombstones}) ->
|
||||
Pmap#pmap{
|
||||
dirty = maps:put(K, V, Dirty),
|
||||
clean = maps:remove(K, Clean),
|
||||
tombstones = maps:remove(K, Tombstones)
|
||||
}.
|
||||
|
||||
-spec pmap_del(K, pmap(K, V)) -> pmap(K, V).
|
||||
pmap_del(
|
||||
Key,
|
||||
Pmap = #pmap{dirty = Dirty, clean = Clean, tombstones = Tombstones}
|
||||
) ->
|
||||
%% Update the caches:
|
||||
Pmap#pmap{
|
||||
dirty = maps:remove(Key, Dirty),
|
||||
clean = maps:remove(Key, Clean),
|
||||
tombstones = Tombstones#{Key => del}
|
||||
}.
|
||||
|
||||
-spec pmap_fold(fun((K, V, A) -> A), A, pmap(K, V)) -> A.
|
||||
pmap_fold(Fun, Acc0, #pmap{clean = Clean, dirty = Dirty}) ->
|
||||
Acc1 = maps:fold(Fun, Acc0, Dirty),
|
||||
maps:fold(Fun, Acc1, Clean).
|
||||
|
||||
-spec pmap_commit(emqx_persistent_session_ds:id(), pmap(K, V)) -> pmap(K, V).
|
||||
pmap_commit(
|
||||
SessionId, Pmap = #pmap{table = Tab, dirty = Dirty, clean = Clean, tombstones = Tombstones}
|
||||
) ->
|
||||
%% Commit deletions:
|
||||
maps:foreach(fun(K, _) -> kv_bag_delete(Tab, SessionId, K) end, Tombstones),
|
||||
%% Replace all records in the bag with the entries from the dirty area:
|
||||
maps:foreach(
|
||||
fun(K, V) ->
|
||||
kv_bag_persist(Tab, SessionId, K, V)
|
||||
end,
|
||||
Dirty
|
||||
),
|
||||
Pmap#pmap{
|
||||
dirty = #{},
|
||||
tombstones = #{},
|
||||
clean = maps:merge(Clean, Dirty)
|
||||
}.
|
||||
|
||||
%% Functions dealing with set tables:
|
||||
|
||||
kv_persist(Tab, SessionId, Val0) ->
|
||||
Val = encoder(encode, Tab, Val0),
|
||||
mnesia:write(Tab, #kv{k = SessionId, v = Val}, write).
|
||||
|
||||
kv_delete(Table, Namespace) ->
|
||||
mnesia:delete({Table, Namespace}).
|
||||
|
||||
kv_restore(Tab, SessionId) ->
|
||||
[encoder(decode, Tab, V) || #kv{v = V} <- mnesia:read(Tab, SessionId)].
|
||||
|
||||
%% Functions dealing with bags:
|
||||
|
||||
%% @doc Create a mnesia table for the PMAP:
|
||||
-spec create_kv_bag_table(atom()) -> ok.
|
||||
create_kv_bag_table(Table) ->
|
||||
mria:create_table(Table, [
|
||||
{type, bag},
|
||||
{rlog_shard, ?DS_MRIA_SHARD},
|
||||
{storage, rocksdb_copies},
|
||||
{record_name, kv},
|
||||
{attributes, record_info(fields, kv)}
|
||||
]).
|
||||
|
||||
kv_bag_persist(Tab, SessionId, Key, Val0) ->
|
||||
%% Remove the previous entry corresponding to the key:
|
||||
kv_bag_delete(Tab, SessionId, Key),
|
||||
%% Write data to mnesia:
|
||||
Val = encoder(encode, Tab, Val0),
|
||||
mnesia:write(Tab, #kv{k = SessionId, v = {Key, Val}}).
|
||||
|
||||
kv_bag_restore(Tab, SessionId) ->
|
||||
[{K, encoder(decode, Tab, V)} || #kv{v = {K, V}} <- mnesia:read(Tab, SessionId)].
|
||||
|
||||
kv_bag_delete(Table, SessionId, Key) ->
|
||||
%% Note: this match spec uses a fixed primary key, so it doesn't
|
||||
%% require a table scan, and the transaction doesn't grab the
|
||||
%% whole table lock:
|
||||
MS = [{#kv{k = SessionId, v = {Key, '_'}}, [], ['$_']}],
|
||||
Objs = mnesia:select(Table, MS, write),
|
||||
lists:foreach(
|
||||
fun(Obj) ->
|
||||
mnesia:delete_object(Table, Obj, write)
|
||||
end,
|
||||
Objs
|
||||
).
|
||||
|
||||
%%
|
||||
|
||||
transaction(Fun) ->
|
||||
case mnesia:is_transaction() of
|
||||
true ->
|
||||
Fun();
|
||||
false ->
|
||||
{atomic, Res} = mria:transaction(?DS_MRIA_SHARD, Fun),
|
||||
Res
|
||||
end.
|
||||
|
||||
ro_transaction(Fun) ->
|
||||
{atomic, Res} = mria:ro_transaction(?DS_MRIA_SHARD, Fun),
|
||||
Res.
|
Loading…
Reference in New Issue