Skip to content

Commit 7e8ed85

Browse files
committed
Revamp public API
Now, `khepri` is the main entry point for everything public. There is no longer a high-level API in `khepri` and a lower-level API in `khepri_machine`. `khepri` provides more user-friendly functions and we will add more when we see fit. Here are a few examples: * All functions accept both native paths (`[stock, wood, <<"oak">>]`) and Unix-like paths (`"/:stock/:wood/oak"`). Note that the syntax for Unix-like paths has changed: atoms are prefixed with `:` and binaries are left as-is. * `khepri:get_data(StoreId, PathPattern)` allows to quickly get the data attached to a specific node. It's easier than going through `khepri:get(StoreId, PathPattern)` and extracting the data from the returned map. Inside transactions, `khepri_tx` provides the same API as `khepri`, except when the function does not make sense in the context of a transaction. Unix-like paths are also accepted by `khepri_tx` functions. `khepri_cluster` is a new module to expose the clustering part of the API. This was in `khepri` before and was moved to this module. It is also part of the public interface. `khepri_path` and `khepri_condition` remain part of the public API for those needing to manipulate paths. Other modules are private however. They remain visible in the documentation because understaing the internals may help sometimes. But changes in those modules may not be documented in release notes and may not be reflected in the release versions.
1 parent 8ad858e commit 7e8ed85

27 files changed

+2629
-1372
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ khepri:insert([emails, <<"alice">>], "alice@example.org").
8989
khepri:insert("/:emails/alice", "alice@example.org").
9090
```
9191

92-
The `khepri` module provides the "simple API". It has several functions to
93-
cover the most common uses. For advanced uses, using the `khepri_machine`
94-
module directly is preferred.
95-
9692
### Read data back
9793

9894
To get Alice's email address back, **query** the same path:
@@ -178,7 +174,7 @@ the database itself and automatically execute it after some event occurs.
178174
on_action => Action} = Props
179175
end,
180176

181-
khepri_machine:put(
177+
khepri:put(
182178
StoreId,
183179
StoredProcPath,
184180
#kpayload_sproc{sproc = Fun}))}.
@@ -189,7 +185,7 @@ the database itself and automatically execute it after some event occurs.
189185
```erlang
190186
EventFilter = #kevf_tree{path = [stock, wood, <<"oak">>]},
191187

192-
ok = khepri_machine:register_trigger(
188+
ok = khepri:register_trigger(
193189
StoreId,
194190
TriggerId,
195191
EventFilter,

doc/overview.edoc

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ Because RabbitMQ already uses an implementation of the Raft consensus algorithm
4444
for its quorum queues, it was decided to leverage that library for all
4545
metadata. That's how Khepri was borned.
4646

47-
Thanks to Ra and Raft, it is <strong>clear how Khepri will behave during and
48-
recover from a network partition</strong>. This makes it more comfortable for
49-
the RabbitMQ team and users, thanks to the absence of unknowns.
47+
Thanks to Ra and Raft, it is <strong>clear how Khepri will behave during a
48+
network partition and recover from it</strong>. This makes it more comfortable
49+
for the RabbitMQ team and users, thanks to the absence of unknowns.
5050

5151
<blockquote>
5252
At the time of this writing, RabbitMQ does not use Khepri in a production
@@ -91,7 +91,7 @@ More payload types may be added in the future.
9191

9292
Payloads are represented using macros or helper functions:
9393
<ul>
94-
<li>`none' and {@link khepri:no_payload/0}</li>
94+
<li>`?NO_PAYLOAD' and {@link khepri:no_payload/0}</li>
9595
<li>`#kpayload_data{data = Term}' and {@link khepri:data_payload/1}</li>
9696
<li>`#kpayload_sproc{sproc = Fun}' and {@link khepri:sproc_payload/1}</li>
9797
</ul>
@@ -108,10 +108,10 @@ specific use cases and detect the type of payload.
108108
Properties are:
109109
<ul>
110110
<li>The version of the payload, tracking the number of times it was modified
111-
({@link khepri_machine:payload_version()}).</li>
111+
({@link khepri:payload_version()}).</li>
112112
<li>The version of the list of child nodes, tracking the number of times child
113-
nodes were added or removed ({@link khepri_machine:child_list_version()}).</li>
114-
<li>The number of child nodes ({@link khepri_machine:child_list_count()}).</li>
113+
nodes were added or removed ({@link khepri:child_list_version()}).</li>
114+
<li>The number of child nodes ({@link khepri:child_list_count()}).</li>
115115
</ul>
116116

117117
=== Addressing a tree node ===
@@ -189,68 +189,45 @@ KeepWhileCondition = #{[stock, wood] => #if_child_list_length{count = {gt, 0}}}.
189189
`keep_while' conditions on self (like the example above) are not evaluated on
190190
the first insert though.
191191

192-
== Khepri API ==
192+
== Stores ==
193+
194+
A Khepri store corresponds to one Ra cluster. In fact, the name of the Ra
195+
cluster is the name of the Khepri store. It is possible to have multiple
196+
database instances running on the same Erlang node or cluster by starting
197+
multiple Ra clusters. Note that it is called a "Ra cluster" but it can have a
198+
single member.
193199

194-
=== High-level API ===
200+
By default, {@link khepri:start/0} starts a default store called `khepri',
201+
based on Ra's default system. You can start a simple store using {@link
202+
khepri:start/1}. To configure a cluster, you need to use {@link
203+
khepri_clustering} to add or remove members.
204+
205+
== Khepri API ==
195206

196-
A high-level API is provided by the {@link khepri} module. It covers most
197-
common use cases and should be straightforward to use.
207+
The essential part of the public API is provided by the {@link khepri} module.
208+
It covers most common use cases and should be straightforward to use.
198209

199210
```
200-
khepri:insert([stock, wood, <<"lime tree">>], 150),
211+
{ok, _} = khepri:put([stock, wood, <<"lime tree">>], 150),
201212

202213
Ret = khepri:get([stock, wood, <<"lime tree">>]),
203214
{ok, #{[stock, wood, <<"lime tree">>] =>
204-
#{child_list_count => 0,
205-
child_list_version => 1,
206-
data => 150,
207-
payload_version => 1}}} = Ret,
215+
#{data => 150,
216+
payload_version => 1,
217+
child_list_count => 0,
218+
child_list_version => 1}}} = Ret,
208219

209220
true = khepri:exists([stock, wood, <<"lime tree">>]),
210221

211-
khepri:delete([stock, wood, <<"lime tree">>]).
222+
{ok, _} = khepri:delete([stock, wood, <<"lime tree">>]).
212223
'''
213224

214-
=== Low-level API ===
225+
Inside transaction funtions, {@link khepri_tx} must be used instead of {@link
226+
khepri}. The former provides the same API, except for functions which don't
227+
make sense in the context of a transaction function.
215228

216229
The high-level API is built on top of a low-level API. The low-level API is
217-
provided by the {@link khepri_machine} module.
218-
219-
The low-level API provides just a handful of primitives. More advanced or
220-
specific use cases may need to rely on that low-level API.
221-
222-
```
223-
%% Unlike the high-level API's `khepri:insert/2' function, this low-level
224-
%% insert returns whatever it replaced (if anything). In this case, there was
225-
%% nothing before, so the returned value is empty.
226-
Ret1 = khepri_machine:put(
227-
StoreId, [stock, wood, <<"lime tree">>],
228-
#kpayload_data{data = 150}),
229-
{ok, #{}} = Ret1,
230-
231-
Ret2 = khepri_machine:get(StoreId, [stock, wood, <<"lime tree">>]),
232-
{ok, #{[stock, wood, <<"lime tree">>] =>
233-
#{child_list_count => 0,
234-
child_list_version => 1,
235-
data => 150,
236-
payload_version => 1}}} = Ret2,
237-
238-
%% Unlike the high-level API's `khepri:delete/2' function, this low-level
239-
%% delete returns whatever it deleted.
240-
Ret3 = khepri_machine:delete(StoreId, [stock, wood, <<"lime tree">>]),
241-
{ok, #{[stock, wood, <<"lime tree">>] =>
242-
#{child_list_count => 0,
243-
child_list_version => 1,
244-
data => 150,
245-
payload_version => 1}}} = Ret3.
246-
'''
247-
248-
=== Stores ===
249-
250-
It is possible to have multiple database instances running on the same Erlang
251-
node or cluster.
252-
253-
By default, Khepri starts a default store, based on Ra's default system.
230+
provided by the private {@link khepri_machine} module.
254231

255232
== Transactions ==
256233

@@ -273,8 +250,7 @@ next section need to be taken into account.</li>
273250
</ul>
274251

275252
The nature of the anonymous function is passed as the `ReadWrite' argument to
276-
{@link khepri:transaction/3} or {@link khepri_machine:transaction/3}
277-
functions.
253+
{@link khepri:transaction/3}.
278254

279255
=== The constraints imposed by Raft ===
280256

@@ -344,9 +320,9 @@ outside of the changes to the tree nodes.
344320
If the transaction needs to have side effects, there are two options:
345321
<ul>
346322
<li>Perform any side effects after the transaction.</li>
347-
<li>Use {@link khepri_machine:put/3} with {@link
348-
khepri_condition:if_payload_version()} conditions in the path and retry if the
349-
put fails because the version changed in between.</li>
323+
<li>Use {@link khepri:put/3} with {@link khepri_condition:if_payload_version()}
324+
conditions in the path and retry if the put fails because the version changed
325+
in between.</li>
350326
</ul>
351327

352328
Here is an example of the second option:
@@ -355,7 +331,7 @@ Here is an example of the second option:
355331
Path = [stock, wood, <<"lime tree">>],
356332
{ok, #{Path := #{data = Term,
357333
payload_version = PayloadVersion}}} =
358-
khepri_machine:get(StoredId, Path),
334+
khepri:get(StoredId, Path),
359335

360336
%% Do anything with `Term` that depend on external factors and could have side
361337
%% effects.
@@ -420,40 +396,40 @@ A stored procedure can accept any numbers of arguments too.
420396

421397
It is possible to execute a stored procedure directly without configuring any
422398
triggers. To execute a stored procedure, you can call {@link
423-
khepri_machine:run_sproc/3}. Here is an example:
399+
khepri:run_sproc/3}. Here is an example:
424400

425401
```
426-
Ret = khepri_machine:run_sproc(
402+
Ret = khepri:run_sproc(
427403
StoreId,
428404
StoredProcPath,
429405
[] = _Args).
430406
'''
431407

432408
This works exactly like {@link erlang:apply/2}. The list of arguments passed
433-
to {@link khepri_machine:run_sproc/3} must correspond to the stored procedure
409+
to {@link khepri:run_sproc/3} must correspond to the stored procedure
434410
arity.
435411

436412
=== Configuring a trigger ===
437413

438414
Khepri uses <em>event filters</em> to associate a type of events with a stored
439415
procedure. Khepri supports tree changes events and thus only supports a single
440-
event filter called {@link khepri_machine:event_filter_tree()}.
416+
event filter called {@link khepri:event_filter_tree()}.
441417

442-
An event filter is registered using {@link khepri_machine:register_trigger/4}:
418+
An event filter is registered using {@link khepri:register_trigger/4}:
443419

444420
```
445421
EventFilter = #kevf_tree{path = [stock, wood, <<"oak">>], %% Required
446422
props = #{on_actions => [delete], %% Optional
447423
priority => 10}}, %% Optional
448424

449-
ok = khepri_machine:register_trigger(
425+
ok = khepri:register_trigger(
450426
StoreId,
451427
TriggerId,
452428
EventFilter,
453429
StoredProcPath))}.
454430
'''
455431

456-
In this example, the {@link khepri_machine:event_filter_tree()} record only
432+
In this example, the {@link khepri:event_filter_tree()} record only
457433
requires the path to monitor. The path can be any path pattern and thus can
458434
have conditions to monitor several nodes at once.
459435

include/khepri.hrl

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@
3737
%% Payload types.
3838
%% -------------------------------------------------------------------
3939

40-
-record(kpayload_data, {data :: khepri_machine:data()}).
40+
-define(NO_PAYLOAD, '$__NO_PAYLOAD__').
41+
-record(kpayload_data, {data :: khepri:data()}).
4142
-record(kpayload_sproc, {sproc :: khepri_fun:standalone_fun()}).
4243

43-
-define(IS_KHEPRI_PAYLOAD(Payload), (Payload =:= none orelse
44+
-define(IS_KHEPRI_PAYLOAD(Payload), (Payload =:= ?NO_PAYLOAD orelse
4445
is_record(Payload, kpayload_data) orelse
4546
is_record(Payload, kpayload_sproc))).
4647

@@ -73,14 +74,14 @@
7374
{exists = true :: boolean()}).
7475

7576
-record(if_payload_version,
76-
{version = 0 :: khepri_machine:payload_version() |
77+
{version = 0 :: khepri:payload_version() |
7778
khepri_condition:comparison_op(
78-
khepri_machine:payload_version())}).
79+
khepri:payload_version())}).
7980

8081
-record(if_child_list_version,
81-
{version = 0 :: khepri_machine:child_list_version() |
82+
{version = 0 :: khepri:child_list_version() |
8283
khepri_condition:comparison_op(
83-
khepri_machine:child_list_version())}).
84+
khepri:child_list_version())}).
8485

8586
-record(if_child_list_length,
8687
{count = 0 :: non_neg_integer() |
@@ -105,3 +106,6 @@
105106
%-record(kevf_process, {pid :: pid(),
106107
% props = #{} :: #{on_reason => ets:match_pattern(),
107108
% priority => integer()}}).
109+
110+
-define(IS_KHEPRI_EVENT_FILTER(EventFilter),
111+
(is_record(EventFilter, kevf_tree))).

src/internal.hrl

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
%% Copyright (c) 2021-2022 VMware, Inc. or its affiliates. All rights reserved.
66
%%
77

8-
-define(DEFAULT_RA_CLUSTER_NAME, ?MODULE).
8+
-define(DEFAULT_RA_CLUSTER_NAME, khepri).
99
-define(DEFAULT_RA_FRIENDLY_NAME, "Khepri datastore").
1010

1111
-define(INIT_DATA_VERSION, 1).
@@ -19,36 +19,29 @@
1919
%% Structure representing each node in the tree, including the root node.
2020
%% TODO: Rename stat to something more correct?
2121
-record(node, {stat = ?INIT_NODE_STAT :: khepri_machine:stat(),
22-
payload = none :: khepri_machine:payload(),
22+
payload = ?NO_PAYLOAD :: khepri:payload(),
2323
child_nodes = #{} :: #{khepri_path:component() := #node{}}}).
2424

2525
%% State machine commands.
2626

2727
-record(put, {path :: khepri_path:pattern(),
28-
payload = none :: khepri_machine:payload(),
28+
payload = ?NO_PAYLOAD :: khepri:payload(),
2929
extra = #{} :: #{keep_while =>
30-
khepri_machine:keep_while_conds_map()}}).
30+
khepri:keep_while_conds_map()}}).
3131

3232
-record(delete, {path :: khepri_path:pattern()}).
3333

3434
-record(tx, {'fun' :: khepri_fun:standalone_fun()}).
3535

36-
-record(register_trigger, {id :: khepri_machine:trigger_id(),
37-
event_filter :: khepri_machine:event_filter(),
36+
-record(register_trigger, {id :: khepri:trigger_id(),
37+
event_filter :: khepri:event_filter(),
3838
sproc :: khepri_path:path()}).
3939

4040
-record(ack_triggered, {triggered :: [khepri_machine:triggered()]}).
4141

42-
-record(triggered, {id :: khepri_machine:trigger_id(),
42+
-record(triggered, {id :: khepri:trigger_id(),
4343
%% TODO: Do we need a ref to distinguish multiple
4444
%% instances of the same trigger?
45-
event_filter :: khepri_machine:event_filter(),
45+
event_filter :: khepri:event_filter(),
4646
sproc :: khepri_fun:standalone_fun(),
4747
props = #{} :: map()}).
48-
49-
%% Structure representing an anonymous function "extracted" as a compiled
50-
%% module for storage.
51-
-record(standalone_fun, {module :: module(),
52-
beam :: binary(),
53-
arity :: arity(),
54-
env :: list()}).

0 commit comments

Comments
 (0)