Skip to content

Commit fb52579

Browse files
committed
khepri_projection: Document projection function types
[Why] The overview document just gave an example of a simple projection function. We needed a way to document all types and when each one is useful.
1 parent 12d848f commit fb52579

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

doc/overview.edoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,3 +587,7 @@ Projections have some costs though. Expect some increased memory consumption
587587
since information in the projection tables is duplicated between the Khepri
588588
store and ETS. Khepri may also take longer to accomplish writes since
589589
projections are updated by Khepri synchronously when writing to the store.
590+
591+
There are several types of projection functions, ranging from the most
592+
efficient to the more flexible one capable of managing several ETS tables. See
593+
{@link khepri_projection} for more details.

src/khepri_projection.erl

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,164 @@
2727
%% Updates to projection tables are immediately consistent for the member of
2828
%% the cluster on which the change to the store is made and the leader member
2929
%% but are eventually consistent for all other followers.
30+
%%
31+
%% == Projection functions ==
32+
%%
33+
%% There are three supported types of projection functions:
34+
%% <ol>
35+
%% <li>`copy' (the literal atom)</li>
36+
%% <li>the simple projection function (identified by its arity of 2)</li>
37+
%% <li>the extended projection function (identified by its arity of 4)</li>
38+
%% </ol>
39+
%%
40+
%% === copy ===
41+
%%
42+
%% This projection "function" inserts the value of a tree node matching the
43+
%% projection path pattern to the ETS table. The value is expected to be a
44+
%% tuple or a record that can be inserted in the table.
45+
%%
46+
%% If the tree node was deleted, the old value is deleted from the ETS table.
47+
%%
48+
%% This is the most efficient projection function as it doesn't rely on an
49+
%% anonymous function.
50+
%%
51+
%% === Simple projection function ===
52+
%%
53+
%% This projection function is called to convert the value of a tree node
54+
%% matching the projection path pattern to some arbitrary term. If the tree
55+
%% node is created or updated, the value passed to the projection function is
56+
%% the new value after the creation/update. If the tree node is deleted, the
57+
%% value is the one of the tree node before the deletion. The result of the
58+
%% projection function is then inserted in or deleted from the ETS table.
59+
%%
60+
%% The projection function is expected to map one path/value to one ETS entry.
61+
%% There is no way for the projection function to indicate that a path/value
62+
%% should not be intserted in ETS for instance.
63+
%%
64+
%% The simple projection function takes 2 arguments:
65+
%% <ol>
66+
%% <li>the tree node path</li>
67+
%% <li>the tree node value</li>
68+
%% </ol>
69+
%%
70+
%% Example:
71+
%%
72+
%% ```
73+
%% ProjectionName = wood_stocks,
74+
%% ProjectionFun = fun([stock, wood, Kind] = _Path, Stock) ->
75+
%% {Kind, Stock}
76+
%% end,
77+
%% Options = #{type => set,
78+
%% read_concurrency => true},
79+
%% Projection = khepri_projection:new(ProjectionName, ProjectionFun, Options).
80+
%% '''
81+
%%
82+
%% The resulting ETS will look like this:
83+
%%
84+
%% ```
85+
%% [
86+
%% {<<"oak">>, 100},
87+
%% {<<"maple">>, 180}
88+
%% ] = ets:tab2list(wood_stoks).
89+
%% '''
90+
%%
91+
%% === Extended projection function ===
92+
%%
93+
%% This projection function is responsible for managing the content of the ETS
94+
%% table(s), even though the ETS tables are still created by Khepri.
95+
%%
96+
%% This is useful when a tree node value can be mapped to several entries in an
97+
%% ETS table or it can be mapped to entries in several ETS tables.
98+
%%
99+
%% By default, if the projection is created like a simple projection function,
100+
%% a single ETS table is created, named after the projection. To use several
101+
%% ETS tables, the caller has to specify the list of ETS tables and their
102+
%% per-table ETS options.
103+
%%
104+
%% The simple projection function takes 4 arguments:
105+
%% <ol>
106+
%% <li>the table ID, or the map of table names/IDs if multiple tables where configured</li>
107+
%% <li>the tree node path</li>
108+
%% <li>the tree node properies before the update/deletion</li>
109+
%% <li>the tree node properies after the update/deletion</li>
110+
%% </ol>
111+
%%
112+
%% Example:
113+
%%
114+
%% This extended projection function reproduces the behaviour of the simple
115+
%% projection function from the example above basically.
116+
%%
117+
%% ```
118+
%% ProjectionName = wood_stocks,
119+
%% ProjectionFun = fun
120+
%% %% Stock update.
121+
%% (Tid, [stock, wood, Kind], _OldProps, #{data := Stock}) ->
122+
%% ets:insert(Tid, {Kind, Stock});
123+
%%
124+
%% %% Stock deletion.
125+
%% (Tid, [stock, wood, Kind], #{data := Stock}, _NewProps) ->
126+
%% ets:delete(Tid, {Kind, Stock})
127+
%% end,
128+
%% Options = #{type => set,
129+
%% read_concurrency => true},
130+
%% Projection = khepri_projection:new(ProjectionName, ProjectionFun, Options).
131+
%% '''
132+
%%
133+
%% The resulting ETS will look like this:
134+
%%
135+
%% ```
136+
%% [
137+
%% {<<"oak">>, 100},
138+
%% {<<"maple">>, 180}
139+
%% ] = ets:tab2list(wood_stoks).
140+
%% '''
141+
%%
142+
%% A projection function becomes useful when there are more involved mapping of
143+
%% ETS entries and possibly multiple ETS tables to manage:
144+
%%
145+
%% ```
146+
%% ProjectionName = wood_stocks,
147+
%% ProjectionFun = fun
148+
%% %% Stock update.
149+
%% (#{wood_stocks := WoodStocksTid,
150+
%% wood_needs := WoodNeedsTid},
151+
%% [stock, wood, Kind],
152+
%% _OldProps,
153+
%% #{data := Stock}) ->
154+
%% ets:insert(WoodStocksTid, {Kind, Stock}),
155+
%%
156+
%% %% Depending on the stock, we check if we need to order
157+
%% %% new wood.
158+
%% if
159+
%% Stock < 50 ->
160+
%% ets:insert(WoodNeedsTid, {Kind, true});
161+
%% Stock > 1000 ->
162+
%% ets:delete(WoodNeedsTid, {Kind, true});
163+
%% true ->
164+
%% ok
165+
%% end;
166+
%%
167+
%% %% Stock deletion.
168+
%% (#{wood_stocks := WoodStocksTid,
169+
%% wood_needs := WoodNeedsTid},
170+
%% [stock, wood, Kind],
171+
%% #{data := Stock},
172+
%% _NewProps) ->
173+
%% ets:delete(WoodStocksTid, {Kind, Stock}),
174+
%% %% We definitely need to order wood of this kind.
175+
%% ets:insert(WoodNeedsTid, {Kind, true});
176+
%% end,
177+
%% Options = #{%% Map of ETS tables and their specific ETS options; here, we don't
178+
%% %% need specific ETS options: they will use the globablly defined
179+
%% %% options as a fallback.
180+
%% tables => #{wood_stocks => #{},
181+
%% wood_needs => #{}},
182+
%%
183+
%% %% Global ETS options used for tables that do not override them.
184+
%% type => set,
185+
%% read_concurrency => true},
186+
%% Projection = khepri_projection:new(ProjectionName, ProjectionFun, Options).
187+
%% '''
30188

31189
-module(khepri_projection).
32190

0 commit comments

Comments
 (0)