feat(emqx_ds): Add API draft for logic layer
This commit is contained in:
parent
a343cdb1d5
commit
b29c5ad23c
|
@ -0,0 +1,75 @@
|
||||||
|
# General concepts
|
||||||
|
|
||||||
|
In the logic layer we don't speak about replication.
|
||||||
|
This is because we could use an external DB with its own replication logic.
|
||||||
|
|
||||||
|
On the other hand, we introduce notion of shard right here at the logic.
|
||||||
|
This is because shared subscription logic needs to be aware of it to some extend, as it has to split work between subscribers somehow.
|
||||||
|
|
||||||
|
# Tables
|
||||||
|
|
||||||
|
## Message storage
|
||||||
|
|
||||||
|
Data is written every time a message matching certain pattern is published.
|
||||||
|
This pattern is not part of the logic layer spec.
|
||||||
|
|
||||||
|
Write throughput: very high
|
||||||
|
Data size: very high
|
||||||
|
Write pattern: append only
|
||||||
|
Read pattern: pseudoserial
|
||||||
|
|
||||||
|
Number of records: O(total write throughput * retention time)
|
||||||
|
|
||||||
|
## Session storage
|
||||||
|
|
||||||
|
Data there is updated when:
|
||||||
|
|
||||||
|
- A new client connects with clean session = false
|
||||||
|
- Client subscribes to a topic
|
||||||
|
- Client unsubscribes to a topic
|
||||||
|
- Garbage collection is performed
|
||||||
|
|
||||||
|
Write throughput: low
|
||||||
|
|
||||||
|
Data is read when a client connects and replay agents are started
|
||||||
|
|
||||||
|
Read throughput: low
|
||||||
|
|
||||||
|
Data format:
|
||||||
|
|
||||||
|
`#session{clientId = "foobar", iterators = [ItKey1, ItKey2, ItKey3, ...]}`
|
||||||
|
|
||||||
|
Number of records: O(N clients)
|
||||||
|
Size of record: O(N subscriptions per clients)
|
||||||
|
|
||||||
|
## Iterator storage
|
||||||
|
|
||||||
|
Data is written every time a client acks a message.
|
||||||
|
Data is read when a client reconnects and we restart replay agents.
|
||||||
|
|
||||||
|
`#iterator{key = IterKey, data = Blob}`
|
||||||
|
|
||||||
|
Number of records: O(N clients * N subscriptions per client)
|
||||||
|
Size of record: O(1)
|
||||||
|
Write throughput: high, lots of small updates
|
||||||
|
Write pattern: mostly key overwrite
|
||||||
|
Read throughput: low
|
||||||
|
Read pattern: random
|
||||||
|
|
||||||
|
# Push vs. Pull model
|
||||||
|
|
||||||
|
In push model we have replay agents iterating over the dataset in the shards.
|
||||||
|
|
||||||
|
In pull model the the client processes work with iterators.
|
||||||
|
|
||||||
|
## Push pros:
|
||||||
|
- Lower latency: message can be dispatched to the client as soon as it's persisted
|
||||||
|
- Less worry about buffering
|
||||||
|
|
||||||
|
## Push cons:
|
||||||
|
- Need pushback logic
|
||||||
|
- It's not entirely justified when working with external DB that may not provide streaming API
|
||||||
|
|
||||||
|
## Pull pros:
|
||||||
|
- No need for pushback: client advances iterators at its own tempo
|
||||||
|
-
|
|
@ -0,0 +1,177 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-module(emqx_ds).
|
||||||
|
|
||||||
|
%% API:
|
||||||
|
%% Messages:
|
||||||
|
-export([message_store/2, message_store/1, message_stats/0]).
|
||||||
|
%% Iterator:
|
||||||
|
-export([iterator_update/2, iterator_next/1, iterator_stats/0]).
|
||||||
|
%% Session:
|
||||||
|
-export([
|
||||||
|
session_open/1,
|
||||||
|
session_drop/1,
|
||||||
|
session_suspend/1,
|
||||||
|
session_add_iterator/2,
|
||||||
|
session_del_iterator/2,
|
||||||
|
session_stats/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% internal exports:
|
||||||
|
-export([]).
|
||||||
|
|
||||||
|
-export_type([
|
||||||
|
message_id/0,
|
||||||
|
message_stats/0,
|
||||||
|
message_store_opts/0,
|
||||||
|
session_id/0,
|
||||||
|
iterator_id/0,
|
||||||
|
iterator/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include("emqx_ds_int.hrl").
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Type declarations
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
-type session_id() :: emqx_types:clientid().
|
||||||
|
|
||||||
|
-type iterator() :: term().
|
||||||
|
|
||||||
|
-opaque iterator_id() :: binary().
|
||||||
|
|
||||||
|
%%-type session() :: #session{}.
|
||||||
|
|
||||||
|
-type message_store_opts() :: #{}.
|
||||||
|
|
||||||
|
-type message_stats() :: #{}.
|
||||||
|
|
||||||
|
-type message_id() :: binary().
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% API funcions
|
||||||
|
%%================================================================================
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------------------
|
||||||
|
%% Message
|
||||||
|
%%--------------------------------------------------------------------------------
|
||||||
|
-spec message_store([emqx_types:message()], message_store_opts()) ->
|
||||||
|
{ok, [message_id()]} | {error, _}.
|
||||||
|
message_store(_Msg, _Opts) ->
|
||||||
|
%% TODO
|
||||||
|
ok.
|
||||||
|
|
||||||
|
-spec message_store([emqx_types:message()]) -> {ok, [message_id()]} | {error, _}.
|
||||||
|
message_store(Msg) ->
|
||||||
|
%% TODO
|
||||||
|
message_store(Msg, #{}).
|
||||||
|
|
||||||
|
-spec message_stats() -> message_stats().
|
||||||
|
message_stats() ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------------------
|
||||||
|
%% Session
|
||||||
|
%%--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Called when a client connects. This function looks up a
|
||||||
|
%% session or creates a new one if previous one couldn't be found.
|
||||||
|
%%
|
||||||
|
%% This function also spawns replay agents for each iterator.
|
||||||
|
%%
|
||||||
|
%% Note: session API doesn't handle session takeovers, it's the job of
|
||||||
|
%% the broker.
|
||||||
|
-spec session_open(emqx_types:clientid()) -> {_New :: boolean(), session_id(), [iterator_id()]}.
|
||||||
|
session_open(ClientID) ->
|
||||||
|
{atomic, Ret} =
|
||||||
|
mria:transaction(
|
||||||
|
?DS_SHARD,
|
||||||
|
fun() ->
|
||||||
|
case mnesia:read(?SESSION_TAB, ClientID) of
|
||||||
|
[#session{iterators = Iterators}] ->
|
||||||
|
{false, ClientID, Iterators};
|
||||||
|
[] ->
|
||||||
|
Session = #session{id = ClientID, iterators = []},
|
||||||
|
mnesia:write(?SESSION_TAB, Session),
|
||||||
|
{true, ClientID, []}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
),
|
||||||
|
Ret.
|
||||||
|
|
||||||
|
%% @doc Called when a client reconnects with `clean session=true' or
|
||||||
|
%% during session GC
|
||||||
|
-spec session_drop(emqx_types:clientid()) -> ok.
|
||||||
|
session_drop(ClientID) ->
|
||||||
|
{atomic, ok} = mnesia:transaction(
|
||||||
|
?DS_SHARD,
|
||||||
|
fun() ->
|
||||||
|
mnesia:delete(?SESSION_TAB, ClientID)
|
||||||
|
end
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Called when a client disconnects. This function terminates all
|
||||||
|
%% active processes related to the session.
|
||||||
|
-spec session_suspend(session_id()) -> ok | {error, session_not_found}.
|
||||||
|
session_suspend(_SessionId) ->
|
||||||
|
%% TODO
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Called when a client subscribes to a topic. Idempotent.
|
||||||
|
-spec session_add_iterator(session_id(), emqx_topic:words()) ->
|
||||||
|
{ok, iterator_id()} | {error, session_not_found}.
|
||||||
|
session_add_iterator(_SessionId, _TopicFilter) ->
|
||||||
|
%% TODO
|
||||||
|
{ok, <<"">>}.
|
||||||
|
|
||||||
|
%% @doc Called when a client unsubscribes from a topic. Returns `true'
|
||||||
|
%% if the session contained the subscription or `false' if it wasn't
|
||||||
|
%% subscribed.
|
||||||
|
-spec session_del_iterator(session_id(), emqx_topic:words()) ->
|
||||||
|
{ok, boolean()} | {error, session_not_found}.
|
||||||
|
session_del_iterator(_SessionId, _TopicFilter) ->
|
||||||
|
%% TODO
|
||||||
|
false.
|
||||||
|
|
||||||
|
-spec session_stats() -> #{}.
|
||||||
|
session_stats() ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------------------
|
||||||
|
%% Iterator (pull API)
|
||||||
|
%%--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Called when a client acks a message
|
||||||
|
-spec iterator_update(iterator_id(), iterator()) -> ok.
|
||||||
|
iterator_update(_IterId, _Iter) ->
|
||||||
|
%% TODO
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @doc Called when a client acks a message
|
||||||
|
-spec iterator_next(iterator()) -> {value, emqx_types:message(), iterator()} | none | {error, _}.
|
||||||
|
iterator_next(_Iter) ->
|
||||||
|
%% TODO
|
||||||
|
ok.
|
||||||
|
|
||||||
|
-spec iterator_stats() -> #{}.
|
||||||
|
iterator_stats() ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
%%================================================================================
|
||||||
|
%% Internal functions
|
||||||
|
%%================================================================================
|
|
@ -6,5 +6,20 @@
|
||||||
|
|
||||||
-export([start/2]).
|
-export([start/2]).
|
||||||
|
|
||||||
|
-include("emqx_ds_int.hrl").
|
||||||
|
|
||||||
start(_Type, _Args) ->
|
start(_Type, _Args) ->
|
||||||
|
init_mnesia(),
|
||||||
emqx_ds_sup:start_link().
|
emqx_ds_sup:start_link().
|
||||||
|
|
||||||
|
init_mnesia() ->
|
||||||
|
ok = mria:create_table(
|
||||||
|
?SESSION_TAB,
|
||||||
|
[
|
||||||
|
{rlog_shard, ?DS_SHARD},
|
||||||
|
{type, set},
|
||||||
|
{storage, rocksdb_copies},
|
||||||
|
{record_name, session},
|
||||||
|
{attributes, record_info(fields, session)}
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2022-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.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-ifndef(EMQX_DS_HRL).
|
||||||
|
-define(EMQX_DS_HRL, true).
|
||||||
|
|
||||||
|
-define(SESSION_TAB, emqx_ds_session).
|
||||||
|
-define(DS_SHARD, emqx_ds_shard).
|
||||||
|
|
||||||
|
-record(session, {
|
||||||
|
id :: emqx_ds:session_id(),
|
||||||
|
iterators :: [{emqx_topic:words(), emqx_ds:iterator_id()}]
|
||||||
|
}).
|
||||||
|
|
||||||
|
-endif.
|
Loading…
Reference in New Issue