Add mrdb:ms/2, switch to otp's edoc markdown

This commit is contained in:
Ulf Wiger 2025-10-10 13:07:23 +02:00
parent d6a4dca5f6
commit af341b34f4
18 changed files with 554 additions and 7 deletions

View File

@ -9,7 +9,7 @@
## Function Index ##
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#do-2">do/2</a></td><td></td></tr><tr><td valign="top"><a href="#ensure_tab-0">ensure_tab/0</a></td><td></td></tr></table>
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#do-2">do/2</a></td><td></td></tr></table>
<a name="functions"></a>
@ -22,9 +22,3 @@
`do(Rsrc, F) -> any()`
<a name="ensure_tab-0"></a>
### ensure_tab/0 ###
`ensure_tab() -> any()`

View File

@ -0,0 +1,72 @@
# Module mrdb_mutex_serializer #
* [Function Index](#index)
* [Function Details](#functions)
<a name="index"></a>
## Function Index ##
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#code_change-3">code_change/3</a></td><td></td></tr><tr><td valign="top"><a href="#done-2">done/2</a></td><td></td></tr><tr><td valign="top"><a href="#handle_call-3">handle_call/3</a></td><td></td></tr><tr><td valign="top"><a href="#handle_cast-2">handle_cast/2</a></td><td></td></tr><tr><td valign="top"><a href="#handle_info-2">handle_info/2</a></td><td></td></tr><tr><td valign="top"><a href="#init-1">init/1</a></td><td></td></tr><tr><td valign="top"><a href="#start_link-0">start_link/0</a></td><td></td></tr><tr><td valign="top"><a href="#terminate-2">terminate/2</a></td><td></td></tr><tr><td valign="top"><a href="#wait-1">wait/1</a></td><td></td></tr></table>
<a name="functions"></a>
## Function Details ##
<a name="code_change-3"></a>
### code_change/3 ###
`code_change(FromVsn, St, Extra) -> any()`
<a name="done-2"></a>
### done/2 ###
`done(Rsrc, Ref) -> any()`
<a name="handle_call-3"></a>
### handle_call/3 ###
`handle_call(X1, From, St) -> any()`
<a name="handle_cast-2"></a>
### handle_cast/2 ###
`handle_cast(X1, St) -> any()`
<a name="handle_info-2"></a>
### handle_info/2 ###
`handle_info(X1, St) -> any()`
<a name="init-1"></a>
### init/1 ###
`init(X1) -> any()`
<a name="start_link-0"></a>
### start_link/0 ###
`start_link() -> any()`
<a name="terminate-2"></a>
### terminate/2 ###
`terminate(X1, X2) -> any()`
<a name="wait-1"></a>
### wait/1 ###
`wait(Rsrc) -> any()`

128
doc/mrdb_stats.md Normal file
View File

@ -0,0 +1,128 @@
# Module mrdb_stats #
* [Description](#description)
* [Data Types](#types)
* [Function Index](#index)
* [Function Details](#functions)
Statistics API for the mnesia_rocksdb plugin.
<a name="description"></a>
## Description ##
Some counters are maintained for each active alias. Currently, the following
counters are supported:
* inner_retries
* outer_retries
<a name="types"></a>
## Data Types ##
### <a name="type-alias">alias()</a> ###
<pre><code>
alias() = <a href="http://www.erlang.org/doc/man/mnesia_rocksdb.html#type-alias">mnesia_rocksdb:alias()</a>
</code></pre>
### <a name="type-counter">counter()</a> ###
<pre><code>
counter() = atom()
</code></pre>
### <a name="type-counters">counters()</a> ###
<pre><code>
counters() = #{<a href="#type-counter">counter()</a> =&gt; integer()}
</code></pre>
### <a name="type-db_ref">db_ref()</a> ###
<pre><code>
db_ref() = <a href="http://www.erlang.org/doc/man/mrdb.html#type-db_ref">mrdb:db_ref()</a>
</code></pre>
### <a name="type-increment">increment()</a> ###
<pre><code>
increment() = integer()
</code></pre>
<a name="index"></a>
## Function Index ##
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#get-1">get/1</a></td><td>Fetches all known counters for <code>Alias</code>, in the form of a map,
<code>#{Counter => Value}</code>.</td></tr><tr><td valign="top"><a href="#get-2">get/2</a></td><td>Fetches the integer value of the known counter <code>Ctr</code> for <code>Alias</code>.</td></tr><tr><td valign="top"><a href="#incr-3">incr/3</a></td><td>Increment <code>Ctr</code> counter for <code>Alias` with increment `N</code>.</td></tr><tr><td valign="top"><a href="#new-0">new/0</a></td><td></td></tr></table>
<a name="functions"></a>
## Function Details ##
<a name="get-1"></a>
### get/1 ###
<pre><code>
get(Alias::<a href="#type-alias">alias()</a> | <a href="#type-db_ref">db_ref()</a>) -&gt; <a href="#type-counters">counters()</a>
</code></pre>
<br />
Fetches all known counters for `Alias`, in the form of a map,
`#{Counter => Value}`.
<a name="get-2"></a>
### get/2 ###
<pre><code>
get(Alias::<a href="#type-alias">alias()</a> | <a href="#type-db_ref">db_ref()</a>, Ctr::<a href="#type-counter">counter()</a>) -&gt; integer()
</code></pre>
<br />
Fetches the integer value of the known counter `Ctr` for `Alias`.
<a name="incr-3"></a>
### incr/3 ###
<pre><code>
incr(Alias::<a href="#type-alias">alias()</a> | <a href="#type-db_ref">db_ref()</a>, Ctr::<a href="#type-counter">counter()</a>, N::<a href="#type-increment">increment()</a>) -&gt; ok
</code></pre>
<br />
Increment `Ctr` counter for `Alias` with increment `N`.
Note that the first argument may also be a `db_ref()` map,
corresponding to `mrdb:get_ref({admin, Alias})`.
<a name="new-0"></a>
### new/0 ###
`new() -> any()`

180
doc/overview.md Normal file
View File

@ -0,0 +1,180 @@
# Mnesia Rocksdb - Rocksdb backend plugin for Mnesia
Copyright © 2013-21 Klarna AB
__Authors:__Ulf Wiger ([`ulf@wiger.net`](mailto:ulf@wiger.net)).
The Mnesia DBMS, part of Erlang/OTP, supports 'backend plugins', making it possible to utilize more capable key-value stores than the `dets` module (limited to 2 GB per table). Unfortunately, this support is undocumented. Below, some informal documentation for the plugin system is provided.
### Table of Contents
1. [Usage](#Usage)
1. [Prerequisites](#Prerequisites)
1. [Getting started](#Getting_started)
1. [Special features](#Special_features)
1. [Customization](#Customization)
1. [Handling of errors in write operations](#Handling_of_errors_in_write_operations)
1. [Caveats](#Caveats)
1. [Mnesia backend plugins](#Mnesia_backend_plugins)
1. [Background](#Background)
1. [Design](#Design)
1. [Mnesia index plugins](#Mnesia_index_plugins)
1. [Rocksdb](#Rocksdb)
### Usage
#### Prerequisites
* rocksdb (included as dependency)
* sext (included as dependency)
* Erlang/OTP 21.0 or newer (https://github.com/erlang/otp)
#### Getting started
Call `mnesia_rocksdb:register()` immediately after starting mnesia.
Put `{rocksdb_copies, [node()]}` into the table definitions of tables you want to be in RocksDB.
#### Special features
RocksDB tables support efficient selects on *prefix keys*.
The backend uses the `sext` module (see [`https://github.com/uwiger/sext`](https://github.com/uwiger/sext)) for mapping between Erlang terms and the binary data stored in the tables. This provides two useful properties:
* The records are stored in the Erlang term order of their keys.
* A prefix of a composite key is ordered just before any key for which it is a prefix. For example, `{x, '_'}` is a prefix for keys `{x, a}`, `{x, b}` and so on.
This means that a prefix key identifies the start of the sequence of entries whose keys match the prefix. The backend uses this to optimize selects on prefix keys.
\### Customization
RocksDB supports a number of customization options. These can be specified by providing a `{Key, Value}` list named `rocksdb_opts` under `user_properties`, for example:
```text
mnesia:create_table(foo, [{rocksdb_copies, [node()]},
...
{user_properties,
[{rocksdb_opts, [{max_open_files, 1024}]}]
}])
```
Consult the [RocksDB documentation](https://github.com/facebook/rocksdb/wiki/Setup-Options-and-Basic-Tuning) for information on configuration parameters. Also see the section below on handling write errors.
The default configuration for tables in `mnesia_rocksdb` is:
```text
default_open_opts() ->
[ {create_if_missing, true}
, {cache_size,
list_to_integer(get_env_default("ROCKSDB_CACHE_SIZE", "32212254"))}
, {block_size, 1024}
, {max_open_files, 100}
, {write_buffer_size,
list_to_integer(get_env_default(
"ROCKSDB_WRITE_BUFFER_SIZE", "4194304"))}
, {compression,
list_to_atom(get_env_default("ROCKSDB_COMPRESSION", "true"))}
, {use_bloomfilter, true}
].
```
It is also possible, for larger databases, to produce a tuning parameter file. This is experimental, and mostly copied from `mnesia_leveldb`. Consult the source code in `mnesia_rocksdb_tuning.erl` and `mnesia_rocksdb_params.erl`. Contributions are welcome.
#### Caveats
Avoid placing `bag` tables in RocksDB. Although they work, each write requires additional reads, causing substantial runtime overheads. There are better ways to represent and process bag data (see above about *prefix keys*).
The `mnesia:table_info(T, size)` call always returns zero for RocksDB tables. RocksDB itself does not track the number of elements in a table, and although it is possible to make the `mnesia_rocksdb` backend maintain a size counter, it incurs a high runtime overhead for writes and deletes since it forces them to first do a read to check the existence of the key. If you depend on having an up to date size count at all times, you need to maintain it yourself. If you only need the size occasionally, you may traverse the table to count the elements.
### Mnesia backend plugins
#### Background
Mnesia was initially designed to be a RAM-only DBMS, and Erlang's `ets` tables were developed for this purpose. In order to support persistence, e.g. for configuration data, a disk-based version of `ets` (called `dets`) was created. The `dets` API mimicks the `ets` API, and `dets` is quite convenient and fast for (nowadays) small datasets. However, using a 32-bit bucket system, it is limited to 2GB of data. It also doesn't support ordered sets. When used in Mnesia, dets-based tables are called `disc_only_copies`.
To circumvent these limitations, another table type, called `disc_copies` was added. This is a combination of `ets` and `disk_log`, where Mnesia periodically snapshots the `ets` data to a log file on disk, and meanwhile maintains a log of updates, which can be applied at startup. These tables are quite performant (especially on read access), but all data is kept in RAM, which can become a serious limitation.
A backend plugin system was proposed by Ulf Wiger in 2016, and further developed with Klarna's support, to finally become included in OTP 19. Klarna uses a LevelDb backend, but Aeternity, in 2017, instead chose to implement a Rocksdb backend plugin.
### Design
As backend plugins were added on a long-since legacy-stable Mnesia, they had to conform to the existing code structure. For this reason, the plugin callbacks hook into the already present low-level access API in the `mnesia_lib` module. As a consequence, backend plugins have the same access semantics and granularity as `ets` and `dets`. This isn't much of a disadvantage for key-value stores like LevelDb and RocksDB, but a more serious issue is that the update part of this API is called on *after* the point of no return. That is, Mnesia does not expect these updates to fail, and has no recourse if they do. As an aside, this could also happen if a `disc_only_copies` table exceeds the 2 GB limit (mnesia will not check it, and `dets` will not complain, but simply drop the update.)
### Mnesia index plugins
When adding support for backend plugins, index plugins were also added. Unfortunately, they remain undocumented.
An index plugin can be added in one of two ways:
1. When creating a schema, provide `{index_plugins, [{Name, Module, Function}]}` options.
1. Call the function `mnesia_schema:add_index_plugin(Name, Module, Function)`
`Name` must be an atom wrapped as a 1-tuple, e.g. `{words}`.
The plugin callback is called as `Module:Function(Table, Pos, Obj)`, where `Pos=={words}` in our example. It returns a list of index terms.
__Example__
Given the following index plugin implementation:
```text
-module(words).
-export([words_f/3]).
words_f(_,_,Obj) when is_tuple(Obj) ->
words_(tuple_to_list(Obj)).
words_(Str) when is_binary(Str) ->
string:lexemes(Str, [$\s, $\n, [$\r,$\n]]);
words_(L) when is_list(L) ->
lists:flatmap(fun words_/1, L);
words_(_) ->
[].
```
We can register the plugin and use it in table definitions:
```text
Eshell V12.1.3 (abort with ^G)
1> mnesia:start().
ok
2> mnesia_schema:add_index_plugin({words}, words, words_f).
{atomic,ok}
3> mnesia:create_table(i, [{index, [{words}]}]).
{atomic,ok}
```
Note that in this case, we had neither a backend plugin, nor even a persistent schema. Index plugins can be used with all table types. The registered indexing function (arity 3) must exist as an exported function along the node's code path.
To see what happens when we insert an object, we can turn on call trace.
```text
4> dbg:tracer().
{ok,<0.108.0>}
5> dbg:tp(words, x).
{ok,[{matched,nonode@nohost,3},{saved,x}]}
6> dbg:p(all,[c]).
{ok,[{matched,nonode@nohost,60}]}
7> mnesia:dirty_write({i,<<"one two">>, [<<"three">>, <<"four">>]}).
(<0.84.0>) call words:words_f(i,{words},{i,<<"one two">>,[<<"three">>,<<"four">>]})
(<0.84.0>) returned from words:words_f/3 -> [<<"one">>,<<"two">>,<<"three">>,
<<"four">>]
(<0.84.0>) call words:words_f(i,{words},{i,<<"one two">>,[<<"three">>,<<"four">>]})
(<0.84.0>) returned from words:words_f/3 -> [<<"one">>,<<"two">>,<<"three">>,
<<"four">>]
ok
8> dbg:ctp('_'), dbg:stop().
ok
9> mnesia:dirty_index_read(i, <<"one">>, {words}).
[{i,<<"one two">>,[<<"three">>,<<"four">>]}]
```
(The fact that the indexing function is called twice, seems like a performance bug.)
We can observe that the indexing callback is able to operate on the whole object. It needs to be side-effect free and efficient, since it will be called at least once for each update (if an old object exists in the table, the indexing function will be called on it too, before it is replaced by the new object.)
### Rocksdb
### Usage

View File

@ -18,6 +18,10 @@
{base_plt_apps, [erts, kernel, stdlib, mnesia ]}
]}.
{edoc_opts, [{preprocess, true},
{doclet, edoc_doclet_markdown},
{layout, edoc_layout_chunks}]}.
{profiles,
[
{test,

View File

@ -29,6 +29,11 @@
%% Usage: mnesia:create_table(Tab, [{rocksdb_copies, Nodes}, ...]).
-module(mnesia_rocksdb).
-moduledoc """
rocksdb storage backend for Mnesia.
This module implements a mnesia backend callback plugin. It's specifically documented to try to explain the workings of backend plugins.
""".
%% ----------------------------------------------------------------------------
%% BEHAVIOURS
@ -188,6 +193,7 @@
%% CONVENIENCE API
%% ----------------------------------------------------------------------------
-doc "Equivalent to [register(rocksdb_copies)](`register/1`).".
-spec register() -> {ok, alias()} | {error, _}.
%% @equiv register(rocksdb_copies)
register() ->
@ -199,6 +205,11 @@ register() ->
%% where `Module' implements a backend_type behavior. `Alias' is an atom, and is used
%% in the same way as `ram_copies' etc. The default alias is `rocksdb_copies'.
%% @end
-doc """
Convenience function for registering a mnesia_rocksdb backend plugin
The function used to register a plugin is `mnesia_schema:add_backend_type(Alias, Module)` where `Module` implements a backend_type behavior. `Alias` is an atom, and is used in the same way as `ram_copies` etc. The default alias is `rocksdb_copies`.
""".
-spec register(alias()) -> {ok, alias()} | error().
register(Alias) ->
Module = ?MODULE,
@ -248,6 +259,7 @@ decode_val(Val, Key, Metadata) when is_map(Metadata); is_reference(Metadata) ->
%% ----------------------------------------------------------------------------
%% @doc A debug function that shows the rocksdb table content
-doc "A debug function that shows the rocksdb table content".
show_table(Tab) ->
show_table(Tab, 100).
@ -297,6 +309,19 @@ i_show_table(I, Move, Limit, Ref) ->
%% * `type' (default: `worker')
%% * `modules' (default: `[Mod]')
%% @end
-doc """
Called by mnesia_schema in order to intialize the backend
This is called when the backend is registered with the first alias, or ...
See OTP issue #425 (16 Feb 2021). This callback is supposed to be called before first use of the backend, but unfortunately, it is only called at mnesia startup and when a backend module is registered MORE THAN ONCE. This means we need to handle this function being called multiple times.
The bug has been fixed as of OTP 24.0-rc3
If processes need to be started, this can be done using `mnesia_ext_sup:start_proc(Name, Mod, F, Args [, Opts])` where Opts are parameters for the supervised child:
\* `restart` (default: `transient`) * `shutdown` (default: `120000`) * `type` (default: `worker`) * `modules` (default: `[Mod]`)
""".
init_backend() ->
mnesia_rocksdb_admin:ensure_started().

View File

@ -1,5 +1,6 @@
%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*-
-module(mnesia_rocksdb_admin).
-moduledoc false.
-behaviour(gen_server).

View File

@ -18,6 +18,7 @@
%%----------------------------------------------------------------
-module(mnesia_rocksdb_app).
-moduledoc false.
-behaviour(application).

View File

@ -1,6 +1,7 @@
%%% @doc RocksDB update wrappers, in separate module for easy tracing and mocking.
%%%
-module(mnesia_rocksdb_lib).
-moduledoc "RocksDB update wrappers, in separate module for easy tracing and mocking.".
-export([ put/4
, write/3

View File

@ -18,6 +18,7 @@
%%----------------------------------------------------------------
-module(mnesia_rocksdb_params).
-moduledoc false.
-behaviour(gen_server).

View File

@ -18,6 +18,7 @@
%%----------------------------------------------------------------
-module(mnesia_rocksdb_sup).
-moduledoc false.
-behaviour(supervisor).

View File

@ -18,6 +18,7 @@
%%----------------------------------------------------------------
-module(mnesia_rocksdb_tuning).
-moduledoc false.
-export([describe_env/0,
get_maxfiles/0, get_maxfiles/1,

View File

@ -34,6 +34,29 @@
%% not be replicated.
-module(mrdb).
-moduledoc """
Mid-level access API for Mnesia-managed rocksdb tables
This module implements access functions for the mnesia_rocksdb backend plugin. The functions are designed to also support direct access to rocksdb with little overhead. Such direct access will maintain existing indexes, but not support replication.
Each table has a metadata structure stored as a persistent term for fast access. The structure of the metadata is as follows:
```text
#{ name := <Logical table name>
, db_ref := <Rocksdb database Ref>
, cf_handle := <Rocksdb column family handle>
, activity := Ongoing batch or transaction, if any (map())
, attr_pos := #{AttrName := Pos}
, mode := <Set to 'mnesia' for mnesia access flows>
, properties := <Mnesia table props in map format>
, type := column_family | standalone
}
```
Helper functions like `as_batch(Ref, fun(R) -> ... end)` and `with_iterator(Ref, fun(I) -> ... end)` add some extra convenience on top of the `rocksdb` API.
Note that no automatic provision exists to manage concurrent updates via mnesia AND direct access to this API. It's advisable to use ONE primary mode of access. If replication is used, the mnesia API will support this, but direct `mrdb` updates will not be replicated.
""".
-export([ get_ref/1
, ensure_ref/1 , ensure_ref/2
@ -68,6 +91,7 @@
, select/2 , select/3
, select_reverse/2, select_reverse/3
, select/1
, ms/2
, fold/3 , fold/4 , fold/5
, fold_reverse/3 , fold_reverse/4, fold_reverse/5
, rdb_fold/4 , rdb_fold/5
@ -125,6 +149,9 @@
-type outer() :: non_neg_integer().
-type retries() :: outer() | {inner(), outer()}.
-type matchpat_map() :: #{atom() => _}.
-type match_pattern() :: matchpat_map() | ets:match_pattern().
%% activity type 'ets' makes no sense in this context
-type mnesia_activity_type() :: transaction
| sync_transaction
@ -223,6 +250,7 @@
%% @private
%% Used by `trace_runner' to set up trace patterns.
%%
-doc false.
patterns() ->
[{?MODULE, F, A, []} || {F, A} <- ?MODULE:module_info(exports),
F =/= module_info andalso
@ -233,6 +261,11 @@ patterns() ->
%%
%% Snapshots provide consistent read-only views over the entire state of the key-value store.
%% @end
-doc """
Create a snapshot of the database instance associated with the table reference, table name or alias.
Snapshots provide consistent read-only views over the entire state of the key-value store.
""".
-spec snapshot(alias() | ref_or_tab()) -> {ok, snapshot_handle()} | error().
snapshot(Name) when is_atom(Name) ->
case mnesia_rocksdb_admin:get_ref(Name, error) of
@ -247,6 +280,7 @@ snapshot(_) ->
{error, unknown}.
%% @doc release a snapshot created by {@link snapshot/1}.
-doc "release a snapshot created by `snapshot/1`.".
-spec release_snapshot(snapshot_handle()) -> ok | error().
release_snapshot(SHandle) ->
rocksdb:release_snapshot(SHandle).
@ -280,6 +314,23 @@ release_snapshot(SHandle) ->
%% To simplify code adaptation, `tx | transaction | sync_transaction' are synonyms, and
%% `batch | async_dirty | sync_dirty' are synonyms.
%% @end
-doc """
Run an activity (similar to [`//mnesia/mnesia:activity/2`](`mnesia:activity/2`)).
Supported activity types are:
* `transaction` \- An optimistic `rocksdb` transaction
* `{tx, TxOpts}` \- A `rocksdb` transaction with sligth modifications
* `batch` \- A `rocksdb` batch operation
By default, transactions are combined with a snapshot with 1 retry. The snapshot ensures that writes from concurrent transactions don't leak into the transaction context. A transaction will be retried if it detects that the commit set conflicts with recent changes. A mutex is used to ensure that only one of potentially conflicting `mrdb` transactions is run at a time. The re-run transaction may still fail, if new transactions, or non-transaction writes interfere with the commit set. It will then be re-run again, until the retry count is exhausted.
For finer-grained retries, it's possible to set `retries => {Inner, Outer}`. Setting the retries to a single number, `Retries`, is analogous to `` {0, Retries}`. Each outer retry requests a ``mutex lock' by waiting in a FIFO queue. Once it receives the lock, it will try the activity once + as many retries as specified by `Inner`. If these fail, the activity again goes to the FIFO queue (ending up last in line) if there are outer retries remaining. When all retries are exhaused, the activity aborts with `retry_limit`. Note that transactions, being optimistic, do not request a lock on the first attempt, but only on outer retries (the first retry is always an outer retry).
Valid `TxOpts` are `#{no_snapshot => boolean(), retries => retries()}`.
To simplify code adaptation, `tx | transaction | sync_transaction` are synonyms, and `batch | async_dirty | sync_dirty` are synonyms.
""".
-spec activity(activity_type(), alias(), fun( () -> Res )) -> Res.
activity(Type, Alias, F) ->
#{db_ref := DbRef} = ensure_ref({admin, Alias}),
@ -592,6 +643,7 @@ rdb_release_batch(H) ->
rocksdb:release_batch(H).
%% @doc Aborts an ongoing {@link activity/2}
-doc "Aborts an ongoing `activity/2`".
-spec abort(_) -> no_return().
abort(Reason) ->
case mnesia_compatible_aborts() of
@ -674,6 +726,13 @@ inherit_ctxt(Ref, R) ->
%% closed once the fun terminates.
%% @equiv with_iterator(Tab, Fun, [])
%% @end
-doc """
Equivalent to [with_iterator(Tab, Fun, [])](`with_iterator/3`).
Create an iterator on table `Tab` for the duration of `Fun`
The iterator is passed to the provided fun as `Fun(Iterator)`, and is closed once the fun terminates.
""".
-spec with_iterator(ref_or_tab(), fun( (mrdb_iterator()) -> Res )) -> Res.
with_iterator(Tab, Fun) ->
with_iterator(Tab, Fun, []).
@ -687,6 +746,13 @@ with_iterator(Tab, Fun) ->
%% will return `{ok, Obj}' where `Obj' is the complete decoded object.
%% For rocksdb-level iterators, see {@link with_rdb_iterator/3}.
%% @end
-doc """
Create an iterator on table `Tab` with `ReadOptions` for the duration of `Fun`
The iterator is passed to the provided fun as `Fun(Iterator)`, and is closed once the fun terminates.
The iterator respects `mnesia_rocksdb` metadata, so accesses through the iterator will return `{ok, Obj}` where `Obj` is the complete decoded object. For rocksdb-level iterators, see `with_rdb_iterator/3`.
""".
-spec with_iterator(ref_or_tab(), fun( (mrdb_iterator()) -> Res ), read_options()) -> Res.
with_iterator(Tab, Fun, Opts) ->
R = ensure_ref(Tab),
@ -1063,6 +1129,7 @@ filter_objs([H|T], Val, ValsF) ->
end.
%% @doc Returns the alias of a given table or table reference.
-doc "Returns the alias of a given table or table reference.".
-spec alias_of(ref_or_tab()) -> alias().
alias_of(Tab) ->
#{alias := Alias} = ensure_ref(Tab),
@ -1084,11 +1151,17 @@ alias_of(Tab) ->
%% across db instances. At least, data should end up where you expect.
%%
%% @end
-doc """
Creates a `rocksdb` batch context and executes the fun `F` in it.
%% Rocksdb batches aren't tied to a specific DbRef until written. This can cause surprising problems if we're juggling multiple rocksdb instances (as we do if we have standalone tables). At the time of writing, all objects end up in the DbRef the batch is written to, albeit not necessarily in the intended column family. This will probably change, but no failure mode is really acceptable. The code below ensures that separate batches are created for each DbRef, under a unique reference stored in the pdict. When writing, all batches are written separately to the corresponding DbRef, and when releasing, all batches are released. This will not ensure atomicity, but there is no way in rocksdb to achieve atomicity across db instances. At least, data should end up where you expect.
""".
-spec as_batch(ref_or_tab(), fun( (db_ref()) -> Res )) -> Res.
as_batch(Tab, F) ->
as_batch(Tab, F, []).
%% @doc as {@link as_batch/2}, but with the ability to pass `Opts' to `rocksdb:write_batch/2'
-doc "as `as_batch/2`, but with the ability to pass `Opts` to `rocksdb:write_batch/2`".
as_batch(Tab, F, Opts) when is_function(F, 1), is_list(Opts) ->
as_batch_(ensure_ref(Tab), F, Opts).
@ -1332,6 +1405,55 @@ select_reverse(Tab, Pat, Limit) when Limit == infinity; is_integer(Limit), Limit
select(Cont) ->
mrdb_select:select(Cont).
%% @doc Produce a match specification for select(), supporting map-based match patterns
%%
%% Using record syntax in match patterns tends to conflict with type checking. This
%% function offers an alternative approach, drawing on the fact that mnesia_rocksdb
%% keeps the record name and attribute names readily available as persistent terms.
%%
%% When using the map-based representation, the match pattern is built by matching
%% attribute names to map elements; any attribute not found in the map gets set to '_'.
%% Thus, ```[{#balance{key = {Acct,'$1'},_='_'},[{'>=','$1',Height}],['$_']}]''' can be
%% created as ```ms(balance,[{#{key => {Acct,'$1'}},[{'>=','$1',Height}],['$_']}])'''.
%%
%% This has the advantage over `ms_transform' that it can handle bound variables
%% in the match pattern.
%% @end
-doc """
Produce a match specification for select(), supporting map-based match patterns
Using record syntax in match patterns tends to conflict with type checking. This function offers an alternative approach, drawing on the fact that mnesia_rocksdb keeps the record name and attribute names readily available as persistent terms.
When using the map-based representation, the match pattern is built by matching attribute names to map elements; any attribute not found in the map gets set to '_'. Thus,
```text
[{#balance{key = {Acct,'$1'},_='_'},[{'>=','$1',Height}],['$_']}]
```
can be created as
```text
ms(balance,[{#{key => {Acct,'$1'}},[{'>=','$1',Height}],['$_']}])
```
.
This has the advantage over `ms_transform` that it can handle bound variables in the match pattern.
""".
-spec ms(ref_or_tab(), [{match_pattern(), [_], [_]}]) -> ets:match_spec().
ms(Tab, Pat) ->
Ref = ensure_ref(Tab),
#{properties := #{ attributes := Attrs
, record_name := RecName }} = Ref,
[{headpat(RecName, Attrs, Hd), Gs, Body}
|| {Hd, Gs, Body} <- Pat].
headpat(RecName, Attrs, Hd) when is_map(Hd) ->
list_to_tuple([RecName | [maps:get(A, Hd, '_')
|| A <- Attrs]]);
headpat(_, _, Hd) when is_tuple(Hd); is_atom(Hd) ->
Hd.
clear_table(Tab) ->
match_delete(Tab, '_').

View File

@ -1,5 +1,6 @@
%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*-
-module(mrdb_index).
-moduledoc false.
-export([
with_iterator/3

View File

@ -1,5 +1,6 @@
%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*-
-module(mrdb_mutex).
-moduledoc false.
-export([ do/2 ]).

View File

@ -1,4 +1,5 @@
-module(mrdb_mutex_serializer).
-moduledoc false.
-behavior(gen_server).

View File

@ -1,5 +1,6 @@
%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*-
-module(mrdb_select).
-moduledoc false.
-export([ select/3 %% (Ref, MatchSpec, Limit)
, select/4 %% (Ref, MatchSpec, AccKeys, Limit)

View File

@ -7,6 +7,11 @@
%% * outer_retries
%%
-module(mrdb_stats).
-moduledoc """
Statistics API for the mnesia_rocksdb plugin
Some counters are maintained for each active alias. Currently, the following counters are supported: * inner_retries * outer_retries
""".
-export([new/0]).
@ -29,6 +34,11 @@ ctr_meta() ->
#{ inner_retries => 1
, outer_retries => 2 }.
-doc """
Increment `Ctr` counter for `` Alias` with increment `N ``.
Note that the first argument may also be a `db_ref()` map, corresponding to `mrdb:get_ref({admin, Alias})`.
""".
-spec incr(alias() | db_ref(), counter(), increment()) -> ok.
%% @doc Increment `Ctr' counter for `Alias` with increment `N'.
%%
@ -41,6 +51,7 @@ incr(Alias, Ctr, N) when is_atom(Alias) ->
incr(#{stats := #{ref := Ref, meta := Meta}}, Ctr, N) ->
incr_(Ref, Meta, Ctr, N).
-doc "Fetches the integer value of the known counter `Ctr` for `Alias`.".
-spec get(alias() | db_ref(), counter()) -> integer().
%% @doc Fetches the integer value of the known counter `Ctr' for `Alias'.
%% @end
@ -50,6 +61,7 @@ get(Alias, Ctr) when is_atom(Alias) ->
get(#{stats := #{ref := Ref, meta := Meta}}, Ctr) ->
get_(Ref, Meta, Ctr).
-doc "Fetches all known counters for `Alias`, in the form of a map, `#{Counter => Value}`.".
-spec get(alias() | db_ref()) -> counters().
%% @doc Fetches all known counters for `Alias', in the form of a map,
%% `#{Counter => Value}'.