raft_kv v0.2.3 RaftKV View Source

An Elixir library to store key-value pairs in a distributed, fault-tolerant, self-adjusting data structure.

Hex.pm Build Status Coverage Status

Feature & Design

  • Each value can be arbitrary data structure.

    • Operation on a stored value must be given as an implementation of RaftKV.ValuePerKey behaviour.
  • Built on top of raft_fleet.

    • Key-value pairs are sharded into multiple Raft consensus groups by hash partitioning.
  • Based on number of keys, data size and current load, shards are automatically split/merged in a transparent manner.
  • Designed for many key-value pairs and throughput.

Usage

Suppose we have the following callback module for simple key-value store.

defmodule KV do
  alias RaftKV.ValuePerKey
  @behaviour ValuePerKey

  @impl true
  def command(_previous_value, _size, _key, {:set, value}) do
    {:ok, 5, value, 0}
  end
  def command(_previous_value, _size, _key, :unset) do
    {:ok, 5, nil, 0}
  end

  @impl true
  def query(value, _size, _key, :get) do
    {value, 1}
  end

  #
  # API
  #
  def get(k) do
    case RaftKV.query(:kv, k, :get) do
      {:ok, v}                 -> v
      {:error, :key_not_found} -> nil
    end
  end

  def set(k, v) do
    {:ok, :ok} = RaftKV.command(:kv, k, {:set, v})
    :ok
  end

  def unset(k) do
    {:ok, :ok} = RaftKV.command(:kv, k, :unset)
    :ok
  end
end

Let’s initialize :raft_kv and then register a keyspace.

RaftFleet.activate("some_zone")
RaftKV.init()
RaftKV.register_keyspace(:kv, [], KV, nil, %RaftKV.SplitMergePolicy{max_shards: 16, max_keys_per_shard: 100})

Now we can get/set values with arbitrary keys.

KV.get("foo")        # => nil
KV.set("foo", "bar") # => :ok
KV.get("foo")        # => "bar"

Initially there’s only one shard.

RaftKV.reduce_keyspace_shard_names(:kv, [], &[&1 | &2]) # => [:kv_0]

When the number of key-value pairs exceeds the limit per shard (100) as follows…

Enum.each(1..200, fn i -> KV.set("#{i}", i) end)

then shards are automatically split. (Depending on the configurations it may take several minutes. See RaftKV.Config for more detail.)

RaftKV.reduce_keyspace_shard_names(:kv, [], &[&1 | &2]) # => [:kv_100663296, :kv_67108864, :kv_0]

Similarly, when you remove key-value pairs…

Enum.each(1..200, fn i -> KV.unset("#{i}") end)

the shards are automatically merged.

RaftKV.reduce_keyspace_shard_names(:kv, [], &[&1 | &2]) # => [:kv_0]

Shard splits and merges are transparent from client processes interacting with key-value pairs.

Link to this section Summary

Functions

Executes a command on the replicated value identified by keyspace_name and key

(shard-aware API) Executes a command on all existing keys in the specified shard

Removes an existing keyspace

Retrieves the current value of RaftKV.SplitMergePolicy.t/0 used for the specified keyspace

Initializes :raft_kv so that processes in Node.self() can interact with appropriate shard(s) for command/query

(shard-aware API) Fetches all keys in the specified shard

List all registered keyspace names

Executes a read-only query on the replicated value identified by keyspace_name and key

(shard-aware API) Traverses all shards in the specified keyspace

Replaces the current RaftKV.SplitMergePolicy.t/0 of a keyspace with the specified one

Link to this section Types

Link to this type option() View Source
option() ::
  {:timeout, pos_integer()}
  | {:retry, non_neg_integer()}
  | {:retry_interval, pos_integer()}
  | {:call_module, module()}
  | {:range_shift_retry, non_neg_integer()}
  | {:range_shift_retry_interval, pos_integer()}

Options for command/4, query/4 and command_on_all_keys_in_shard/3.

  • :timeout, :retry, :retry_interval, :call_module are directly passed to RaftFleet.command/5 or RaftFleet.query/5.
  • :shard_lock_retry and :shard_lock_retry_interval are intended to mask temporary unavailability of a shard due to an ongoing splitting/merging.

Default values:

  • :timeout : 500
  • :retry : 3
  • :retry_interval : 1000
  • :call_module : :gen_statem
  • :shard_lock_retry : 3
  • :shard_lock_retry_interval : 200

Link to this section Functions

Link to this function command(keyspace_name, key, command_arg, options \\ []) View Source
command(atom(), RaftKV.ValuePerKey.key(), RaftKV.ValuePerKey.command_arg(), [
  option()
]) :: {:ok, RaftKV.ValuePerKey.command_ret()} | {:error, :no_leader}

Executes a command on the replicated value identified by keyspace_name and key.

See also RaftKV.ValuePerKey, RaftFleet.command/6 and RaftedValue.command/5.

Link to this function command_on_all_keys_in_shard(shard_name, command_arg, options \\ []) View Source
command_on_all_keys_in_shard(atom(), RaftKV.ValuePerKey.command_arg(), [
  option()
]) :: :ok | {:error, :no_leader}

(shard-aware API) Executes a command on all existing keys in the specified shard.

Note that values of RaftKV.ValuePerKey.command_ret that were returned by existing keys’ command are not returned to the caller of this function.

Link to this function deregister_keyspace(keyspace_name) View Source
deregister_keyspace(atom()) :: :ok | {:error, :no_such_keyspace}

Removes an existing keyspace.

Associated resources (processes, ETS records, etc.) for the keyspace are not removed immediately; they are removed by a background worker process.

Link to this function get_keyspace_policy(keyspace_name) View Source
get_keyspace_policy(atom()) :: nil | RaftKV.SplitMergePolicy.t()

Retrieves the current value of RaftKV.SplitMergePolicy.t/0 used for the specified keyspace.

Initializes :raft_kv so that processes in Node.self() can interact with appropriate shard(s) for command/query.

:raft_kv heavily depends on :raft_fleet and thus it’s necessary to call RaftFleet.activate/1 before calling this function.

Link to this function list_keys_in_shard(shard_name) View Source
list_keys_in_shard(atom()) :: [RaftKV.ValuePerKey.key()]

(shard-aware API) Fetches all keys in the specified shard.

Link to this function list_keyspaces() View Source
list_keyspaces() :: [atom()]

List all registered keyspace names.

Link to this function query(keyspace_name, key, query_arg, options \\ []) View Source
query(atom(), RaftKV.ValuePerKey.key(), RaftKV.ValuePerKey.query_arg(), [
  option()
]) ::
  {:ok, RaftKV.ValuePerKey.query_ret()} | {:error, :key_not_found | :no_leader}

Executes a read-only query on the replicated value identified by keyspace_name and key.

See also RaftKV.ValuePerKey, RaftFleet.query/6 and RaftedValue.query/4.

Link to this function reduce_keyspace_shard_names(keyspace_name, acc, f) View Source
reduce_keyspace_shard_names(atom(), a, (atom(), a -> a)) :: a when a: any()

(shard-aware API) Traverses all shards in the specified keyspace.

Link to this function register_keyspace(keyspace_name, rv_config_options, data_module, hook_module, policy) View Source
register_keyspace(
  atom(),
  Keyword.t(),
  module(),
  nil | module(),
  RaftKV.SplitMergePolicy.t()
) :: :ok | {:error, :invalid_policy | :already_registered}

Registers a keyspace with the given arguments.

Parameters:

Link to this function set_keyspace_policy(keyspace_name, policy) View Source
set_keyspace_policy(atom(), RaftKV.SplitMergePolicy.t()) ::
  :ok | {:error, :invalid_policy | :no_such_keyspace}

Replaces the current RaftKV.SplitMergePolicy.t/0 of a keyspace with the specified one.