%% ========================================================================================================== %% Ram - An in-memory distributed KV store for Erlang and Elixir. %% %% The MIT License (MIT) %% %% Copyright (c) 2021 Roberto Ostinelli . %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %% THE SOFTWARE. %% ========================================================================================================== %% =================================================================== %% @doc Exposes all of the Key Value store APIs. %% %% Ram doesn't have to run on every node of the Erlang cluster. You may start Ram only on those nodes where you %% need it. When started, Ram creates a logical overlay network running on top of the Erlang distribution cluster. %% %% Nodes where Ram runs form a subcluster: they will synchronize data between themselves, and themselves only. %% All of the data is replicated on every node of the subcluster. %% %%

Quickstart

%%

Elixir

%% ``` %% iex(1)> :ram.get("key") %% :undefined %% iex(2)> :ram.put("key", "value") %% :ok %% iex(3)> :ram.get("key") %% "value" %% ''' %%

Erlang

%% ``` %% 1> ram:get("key"). %% undefined %% 2> ram:put("key", "value"). %% ok %% 3> ram:get("key"). %% "value" %% ''' %% %%

Internals

%% Ram operations are Atomic (take effect on all nodes involved, or on none of the nodes), %% Consistent (the data is the same across all nodes) %% and Isolated (operations on different nodes in a network do not interfere with each other). %% They are not Durable since Ram is an in-memory only database. %% %% To do so, every operation creates a global lock with {@link global:trans/4} in the caller process, which then %% runs a transaction with a 2-phase commit protocol. If transactions fail, they raise %% `error({commit_timeout, {bad_nodes, BadNodes}})' where `BadNodes' is the list of subcluster nodes where the %% transaction could not complete. %% %%

Conflict Resolution

%% In case of net splits or bad network conditions, a specific Key might get put simultaneously on two different nodes. %% When this happens, the cluster experiences a Key conflict. %% %% Ram will resolve this conflict by choosing a single Value. By default, Ram keeps track of the time %% when a registration takes place with {@link erlang:system_time/0}, compares values between conflicting processes and %% keeps the one with the higher value (the Value that was put more recently). This is a very simple mechanism %% that can be imprecise, as system clocks are not perfectly aligned in a cluster. %% @end %% =================================================================== -module(ram). %% API -export([start/0, stop/0]). -export([subcluster_nodes/0]). -export([get/1, get/2, fetch/1]). -export([put/2]). -export([update/3]). -export([delete/1]). %% =================================================================== %% API %% =================================================================== %% @doc Starts Ram manually. %% %% In most cases Ram will be started as one of your application's dependencies, %% however you may use this helper method to start it manually. -spec start() -> ok. start() -> {ok, _} = application:ensure_all_started(ram), ok. %% @doc Stops Ram manually. -spec stop() -> ok | {error, Reason :: term()}. stop() -> application:stop(ram). %% @doc Returns the nodes of Ram's subcluster. -spec subcluster_nodes() -> [node()] | not_running. subcluster_nodes() -> ram_kv:subcluster_nodes(). %% @equiv get(Key, undefined) %% @end -spec get(Key :: term()) -> Value :: term(). get(Key) -> get(Key, undefined). %% @doc Returns the Key's Value or Default if the Key is not found. %% %%

Examples

%%

Elixir

%% ``` %% iex(1)> :ram.get("key") %% :undefined %% iex(2)> :ram.get("key", "default") %% "default" %% iex(3)> :ram.put("key", "value") %% :ok %% iex(4)> :ram.get("key") %% "value" %% ''' %%

Erlang

%% ``` %% 1> ram:get("key"). %% undefined %% 2> ram:get("key", "default"). %% "default" %% 3> ram:put("key", "value"). %% ok %% 4> ram:get("key"). %% "value" %% ''' -spec get(Key :: term(), Default :: term()) -> Value :: term(). get(Key, Default) -> ram_kv:get(Key, Default). %% @doc Looks up a Key. %% %% Returns `error' if the Key is not found. -spec fetch(Key :: term()) -> {ok, Value :: term()} | error. fetch(Key) -> ram_kv:fetch(Key). %% @doc Puts a Value for a Key. -spec put(Key :: term(), Value :: term()) -> ok. put(Key, Value) -> ram_kv:put(Key, Value). %% @doc Atomically updates a Key with the given function. %% %% If Key is found then the existing Value is passed to the fun and its result is used as the updated Value of Key. %% If Key is not found, Default is put as the Value of Key. The Default value will not be passed through the update function. %% %%

Examples

%%

Elixir

%% ``` %% iex(1)> update_fun = fn existing_value -> existing_value * 2 end %% #Function<44.65746770/1 in :erl_eval.expr/5> %% iex(2)> :ram.update("key", 10, update_fun) %% ok %% iex(3)> :ram.get("key") %% 10 %% iex(4)> :ram.update("key", 10, update_fun) %% ok %% iex(5)> :ram.get("key") %% 20 %% ''' %%

Erlang

%% ``` %% 1> UpdateFun = fun(ExistingValue) -> ExistingValue * 2 end. %% #Fun %% 2> ram:update("key", 10, UpdateFun). %% ok %% 3> ram:get("key"). %% 10 %% 4> ram:update("key", 10, UpdateFun). %% ok %% 5> ram:get("key"). %% 20 %% ''' -spec update(Key :: term(), Default :: term(), function()) -> ok. update(Key, Default, Fun) -> ram_kv:update(Key, Default, Fun). %% @doc Deletes a Key. -spec delete(Key :: term()) -> ok. delete(Key) -> ram_kv:delete(Key).