Packages

A distributed, cluster-wide key-value store implemented on the BEAM.

Current section

Files

Jump to
paxos_kv src paxos_kv.erl
Raw

src/paxos_kv.erl

-module(paxos_kv).
-if(?OTP_RELEASE >= 28).
-moduledoc """
Erlang interface for the PaxosKV distributed key-value store.
This module provides an Erlang-friendly API for interacting with the PaxosKV
system, which is a distributed, fault-tolerant key-value store built on the
Paxos consensus algorithm. All operations are forwarded to the underlying
Elixir implementation.
## Features
- Distributed consensus using Multi-Paxos
- Automatic key-based sharding across cluster nodes
- Fault tolerance and high availability
- Simple get/put interface with optional parameters
## Example Usage
% Store a value
{ok, _} = paxos_kv:put(my_key, my_value).
% Retrieve a value
{ok, Value} = paxos_kv:get(my_key).
% Store with expiration time
ExpiryTime = paxos_kv:now() + 60000,
{ok, _} = paxos_kv:put(session, data, [{until, ExpiryTime}]).
""".
-endif.
-export([put/2, put/3, get/1, get/2, keys/0, keys/1, pid/1, node/1, now/0]).
-if(?OTP_RELEASE >= 28).
-doc """
Stores a key-value pair in the distributed store.
This operation uses the Paxos consensus algorithm to ensure the value is
safely replicated across the cluster. The key is automatically routed to
the appropriate node based on consistent hashing.
Note: The returned value may not be the same as the value argument if another
process has already set a different value for the key. Once a key has a consensus
value, it cannot be changed.
## Parameters
- `Key`: The key under which to store the value (any Erlang term)
- `Value`: The value to store (any Erlang term)
## Returns
- `{ok, Value}` when consensus is reached for the key
- `{error, invalid_value}` when the value is invalid
- `{error, no_quorum}` when the cluster doesn't have enough nodes
## Examples
% Store a simple value
{ok, <<"John Doe">>} = paxos_kv:put(user_123, <<"John Doe">>).
% Store complex terms
{ok, _} = paxos_kv:put({session, 456}, #{user => john}).
% Store any Erlang term
{ok, Config} = paxos_kv:put(config, [{port, 8080}, {host, "localhost"}]).
""".
-endif.
put(Key, Value) ->
'Elixir.PaxosKV':put(Key, Value).
-if(?OTP_RELEASE >= 28).
-doc """
Stores a key-value pair in the distributed store with additional options.
This is the extended version of `put/2` that allows you to specify lifecycle
management and behavior options for the key-value pair.
Note: The returned value may not be the same as the value argument if another
process has already set a different value for the key. Once a key has a consensus
value, it cannot be changed.
## Parameters
- `Key`: The key under which to store the value (any Erlang term)
- `Value`: The value to store (any Erlang term)
- `Options`: A list of options (keyword list)
## Options
- `{bucket, Bucket}`: Use bucket Bucket to store the key-value pair
- `{pid, Pid}`: Keep the key-value pair as long as pid Pid is alive
- `{node, Node}`: Keep the key-value pair as long as node Node is connected
- `{key, Key}`: Keep the key-value pair as long as key Key is present
- `{until, Timestamp}`: Keep the key-value pair until system time Timestamp
(in milliseconds). This defines the expiration time of the key-value pair.
- `{no_quorum, Behavior}`: Controls behavior when there's no quorum:
- `return` (default) - returns `{error, no_quorum}`
- `retry` - retries until quorum is reached (blocks indefinitely)
- `fail` - throws an exception
## Returns
- `{ok, Value}` when consensus is reached for the key
- `{error, invalid_value}` when the value is invalid
- `{error, no_quorum}` when the cluster doesn't have enough nodes (default behavior)
## Examples
% Store with default options
{ok, _} = paxos_kv:put(my_key, my_value, []).
% Store with expiration time (TTL)
ExpiryTime = paxos_kv:now() + 60000,
{ok, <<"session_data">>} = paxos_kv:put(session_key, <<"session_data">>,
[{until, ExpiryTime}]).
% Store bound to a process lifetime
Pid = spawn(fun() -> timer:sleep(5000) end),
{ok, _} = paxos_kv:put(temp_key, temp_value, [{pid, Pid}]).
% Store bound to a node connection
{ok, _} = paxos_kv:put(node_config, config_data,
[{node, 'node1@localhost'}]).
% Store with retry on no quorum
{ok, _} = paxos_kv:put(critical_key, data, [{no_quorum, retry}]).
""".
-endif.
put(Key, Value, Options) ->
'Elixir.PaxosKV':put(Key, Value, Options).
-if(?OTP_RELEASE >= 28).
-doc """
Retrieves a value from the distributed store by its key.
This operation queries the Paxos cluster to retrieve the latest committed
value for the given key. The request is automatically routed to the
appropriate node.
## Parameters
- `Key`: The key to look up (any Erlang term)
## Returns
- `{ok, Value}` when the key is registered
- `{error, not_found}` when the key is not registered
- `{error, no_quorum}` when the cluster doesn't have enough nodes to reach consensus
## Examples
% Retrieve a value
case paxos_kv:get(user_123) of
{ok, User} ->
io:format("Found user: ~p~n", [User]);
{error, not_found} ->
io:format("User not found~n");
{error, no_quorum} ->
io:format("No quorum available~n")
end.
% Simple pattern match
{ok, Value} = paxos_kv:get(my_key).
""".
-endif.
get(Key) ->
'Elixir.PaxosKV':get(Key).
-if(?OTP_RELEASE >= 28).
-doc """
Returns a list of all keys stored in the cluster.
This function queries all nodes in the cluster and returns a deduplicated
list of all keys that have been stored in the default bucket ('Elixir.PaxosKV').
## Returns
- A list of keys (any Erlang terms)
## Examples
% Store some values
{ok, _} = paxos_kv:put(user_name, <<"Alice">>).
{ok, _} = paxos_kv:put(counter, 42).
% Get all keys
Keys = paxos_kv:keys().
% Keys = [user_name, counter]
% Use the keys to iterate over all values
lists:foreach(fun(Key) ->
{ok, Value} = paxos_kv:get(Key),
io:format("~p: ~p~n", [Key, Value])
end, Keys).
""".
-endif.
keys() ->
'Elixir.PaxosKV':keys().
-if(?OTP_RELEASE >= 28).
-doc """
Returns a list of all keys stored in the specified bucket.
This function queries all nodes in the cluster and returns a deduplicated
list of all keys that have been stored in the specified bucket namespace.
Buckets allow you to partition your key-value data into separate logical
namespaces.
## Parameters
- `Bucket`: The bucket (namespace) to query for keys (any Erlang term)
## Returns
- A list of keys (any Erlang terms)
## Examples
% Store values in a custom bucket
{ok, _} = paxos_kv:put(config_key, config_value, [{bucket, 'MyBucket'}]).
{ok, _} = paxos_kv:put(setting_key, setting_value, [{bucket, 'MyBucket'}]).
% Get all keys from the custom bucket
BucketKeys = paxos_kv:keys('MyBucket').
% BucketKeys = [config_key, setting_key]
% Get all keys from the default bucket
DefaultKeys = paxos_kv:keys('Elixir.PaxosKV').
% Compare keys across different buckets
AllBuckets = ['Elixir.PaxosKV', 'MyBucket', 'AnotherBucket'],
AllKeys = lists:flatten([paxos_kv:keys(B) || B <- AllBuckets]).
""".
-endif.
keys(Bucket) ->
'Elixir.PaxosKV':keys(Bucket).
-if(?OTP_RELEASE >= 28).
-doc """
Retrieves a value from the distributed store by its key with additional options.
This is the extended version of `get/1` that allows you to specify a default
value to return when the key is not found.
## Parameters
- `Key`: The key to look up (any Erlang term)
- `Options`: A list of options (keyword list)
## Options
- `{default, Value}`: Defines the value that should be returned (as `{ok, Value}`)
when the key is not yet set or has been deleted.
## Returns
- `{ok, Value}` when the key is registered
- `{ok, Default}` when the key is not found and the `default` option is provided
- `{error, not_found}` when the key is not registered and no default is provided
- `{error, no_quorum}` when the cluster doesn't have enough nodes to reach consensus
## Examples
% Retrieve with default options
{ok, Value} = paxos_kv:get(my_key, []).
% Retrieve with a default value
{ok, Value} = paxos_kv:get(non_existent_key, [{default, <<"default">>}]).
% Handle missing keys gracefully
case paxos_kv:get(user_key, [{default, #{name => <<"Guest">>}}]) of
{ok, User} ->
io:format("User: ~p~n", [User])
end.
""".
-endif.
get(Key, Options) ->
'Elixir.PaxosKV':get(Key, Options).
-if(?OTP_RELEASE >= 28).
-doc """
Returns the pid the key-value pair is bound to.
This function returns the pid that was set via the `{pid, Pid}` option in
`put/3`. The key-value pair will be automatically deleted when this process
terminates.
## Parameters
- `Key`: The key to look up (any Erlang term)
## Returns
- `{ok, Pid}` when a pid is associated with the key
- `{error, not_found}` when the key is not registered
- `{error, no_quorum}` when the cluster doesn't have enough nodes to reach consensus
## Examples
% Store a value bound to a process
Pid = spawn(fun() -> timer:sleep(5000) end),
{ok, _} = paxos_kv:put(session, <<"session_data">>, [{pid, Pid}]).
% Get the associated pid
{ok, Pid} = paxos_kv:pid(session).
io:format("Session is bound to process: ~p~n", [Pid]).
% Check if the bound process is still alive
case paxos_kv:pid(session) of
{ok, Pid} ->
case erlang:is_process_alive(Pid) of
true -> io:format("Bound process is alive~n");
false -> io:format("Bound process is dead~n")
end;
{error, not_found} ->
io:format("Session not found~n")
end.
""".
-endif.
pid(Key) ->
'Elixir.PaxosKV':pid(Key).
-if(?OTP_RELEASE >= 28).
-doc """
Returns the node name the key-value pair is bound to.
This function returns the node name that was set via the `{node, Node}` option
in `put/3`. The key-value pair will be automatically deleted when this node
disconnects from the cluster.
## Parameters
- `Key`: The key to look up (any Erlang term)
## Returns
- `{ok, Node}` when a node is associated with the key
- `{error, not_found}` when the key is not registered
- `{error, no_quorum}` when the cluster doesn't have enough nodes to reach consensus
## Examples
% Store a value bound to a node
{ok, _} = paxos_kv:put(node_config, config_data,
[{node, 'node1@localhost'}]).
% Get the associated node
{ok, Node} = paxos_kv:node(node_config).
io:format("Config is bound to node: ~p~n", [Node]).
% Check if the bound node is still connected
case paxos_kv:node(node_config) of
{ok, Node} ->
case lists:member(Node, erlang:nodes()) of
true -> io:format("Bound node is connected~n");
false -> io:format("Bound node is disconnected~n")
end;
{error, not_found} ->
io:format("Config not found~n")
end.
""".
-endif.
node(Key) ->
'Elixir.PaxosKV':node(Key).
-if(?OTP_RELEASE >= 28).
-doc """
Returns the current time that can be used in the `until` option.
The time is measured in milliseconds and is actually the system time. This is
a convenience function for creating timestamps for the `{until, Timestamp}`
option in `put/3`.
## Returns
- Current system time in milliseconds
## Examples
% Get current time
Now = paxos_kv:now().
% Store with 60 second TTL
ExpiryTime = paxos_kv:now() + 60000,
{ok, _} = paxos_kv:put(session, data, [{until, ExpiryTime}]).
% Store with 5 minute TTL
{ok, _} = paxos_kv:put(cache_key, value, [{until, paxos_kv:now() + 300000}]).
""".
-endif.
now() ->
'Elixir.PaxosKV':now().