HashRing.Managed (libring v1.8.0)

Copy Markdown View Source

This module defines the API for working with hash rings where the ring state is managed in a GenServer process.

There is a performance penalty with working with the ring this way, but it is the best approach if you need to share the ring across multiple processes, or need to maintain multiple rings.

If your rings map 1:1 with Erlang node membership, you can configure rings to automatically monitor node up/down events and update the hash ring accordingly, with a default weight, and either whitelist or blacklist nodes from the ring. You configure this at the ring level in your config.exs.

Static rings can be configured in config.exs with a list of initial nodes, and you can then add/remove nodes using the API here. Each node on the ring can be configured with a weight, which affects the amount of the total keyspace it owns. The default weight is 128. It's best to base the weight of nodes on some concrete relative value, such as the amount of memory a node has.

Summary

Functions

Adds a node to the given hash ring.

Same as add_node/2, but takes a weight value.

Adds a list of nodes to the ring.

Maps a key to a node on the hash ring.

Maps a key to a set of nodes on the hash ring.

Creates a new stateful hash ring with the given name.

Same as HashRing.nodes/1, returns a list of nodes on the ring.

Removes a node from the given hash ring.

Starts a managed ring linked to the calling process.

Types

child_spec_option()

@type child_spec_option() ::
  {:name, atom()}
  | {:id, term()}
  | {:start, {module(), function_name :: atom(), args :: [term()]}}
  | {:restart, :permanent | :transient | :temporary}
  | {:shutdown, timeout() | :brutal_kill}
  | {:type, :worker | :supervisor}
  | {:modules, [module()] | :dynamic}
  | {:significant, boolean()}
  | {:nodes, node_list()}
  | {:monitor_nodes, boolean()}
  | {:node_blacklist, pattern_list()}
  | {:node_whitelist, pattern_list()}
  | {:node_type, :all | :hidden | :visible}

child_spec_options()

@type child_spec_options() :: [child_spec_option()]

key()

@type key() :: any()

node_list()

@type node_list() :: [term() | {term(), weight()}]

pattern_list()

@type pattern_list() :: [String.t() | Regex.t()]

ring()

@type ring() :: pid() | atom()

ring_options()

@type ring_options() :: [
  name: atom(),
  nodes: node_list(),
  monitor_nodes: boolean(),
  node_blacklist: pattern_list(),
  node_whitelist: pattern_list(),
  node_type: :all | :hidden | :visible
]

weight()

@type weight() :: pos_integer()

Functions

add_node(ring, node)

@spec add_node(ring(), key()) :: :ok | {:error, :no_such_ring}

Adds a node to the given hash ring.

An error is returned if the ring does not exist. Adding an existing node returns :ok without changing its weight or the ring.

Examples

iex> {:ok, _pid} = HashRing.Managed.new(:test4)
...> HashRing.Managed.add_node(:test4, "a")
...> HashRing.Managed.key_to_node(:test4, :foo)
"a"

iex> HashRing.Managed.add_node(:no_exist, "a")
{:error, :no_such_ring}

add_node(ring, node, weight)

@spec add_node(ring(), key(), weight()) ::
  :ok | {:error, :no_such_ring} | {:error, {:invalid_weight, key(), term()}}

Same as add_node/2, but takes a weight value.

The weight controls the relative presence this node will have on the ring, the default is 128, but it's best to give each node a weight value which maps to a concrete resource such as memory or priority. It's not ideal to have a number which is too high, as it will make the ring data structure larger, but a good value is probably in the range of 64-256.

Examples

iex> {:ok, _pid} = HashRing.Managed.new(:test5)
...> HashRing.Managed.add_node(:test5, "a", 64)
...> HashRing.Managed.key_to_node(:test5, :foo)
"a"

iex> HashRing.Managed.add_node(:no_exist, "a")
{:error, :no_such_ring}

add_nodes(ring, nodes)

@spec add_nodes(ring(), node_list()) ::
  :ok | {:error, :no_such_ring} | {:error, [{:invalid_weight, key(), term()}]}

Adds a list of nodes to the ring.

The list of nodes can contain either node names or {node_name, weight} tuples. If there is an error with any of the node weights, an error will be returned, and the ring will remain unchanged.

Two-tuples are always interpreted as weighted entries here, including when their weights are invalid. To use a two-tuple as a node key, wrap it in an explicitly weighted entry, such as {{:region, :node}, 128}. Other bare terms are accepted as node keys. Empty binary keys raise ArgumentError before mutation.

Examples

iex> {:ok, _pid} = HashRing.Managed.new(:test6)
...> :ok = HashRing.Managed.add_nodes(:test6, ["a", {"b", 64}])
...> HashRing.Managed.key_to_node(:test6, :foo)
"a"

iex> {:ok, _pid} = HashRing.Managed.new(:test7)
...> HashRing.Managed.add_nodes(:test7, ["a", {"b", :wrong}])
{:error, [{:invalid_weight, "b", :wrong}]}

child_spec(opts)

@spec child_spec(child_spec_options()) :: Supervisor.child_spec()

key_to_node(ring, key)

@spec key_to_node(ring(), any()) ::
  key() | {:error, :no_such_ring} | {:error, {:invalid_ring, :no_nodes}}

Maps a key to a node on the hash ring.

An error is returned if the ring does not exist.

key_to_nodes(ring, key, count)

@spec key_to_nodes(ring(), any(), pos_integer()) ::
  [key()] | {:error, :no_such_ring} | {:error, {:invalid_ring, :no_nodes}}

Maps a key to a set of nodes on the hash ring.

An error is returned if the ring does not exist.

new(name, ring_options \\ [])

@spec new(atom(), ring_options()) :: {:ok, pid()} | {:error, term()}

Creates a new stateful hash ring with the given name.

This name is how you will refer to the hash ring via other API calls.

It takes an optional set of options which control how the ring behaves. Valid options are as follows:

  • nodes: list - a list of initial nodes for a static ring (monitor_nodes: false).
  • monitor_nodes: boolean - will automatically monitor Erlang node membership, if new nodes are connected or nodes are disconnected, the ring will be updated automatically. In this configuration, valid manual add/remove requests return :ok without changing membership. The local node is included unless filtered out, regardless of node_type; that option selects which remote node types are included at startup and monitored afterward.
  • node_blacklist: [String.t | Regex.t] - Used in conjunction with monitor_nodes: true, this is a list of patterns, either as literal strings, or as regex patterns (in either string or literal form), and will be used to ignore nodeup/down events for nodes which are blacklisted. If a node whitelist is provided, the blacklist has no effect.

  • node_whitelist: [String.t | Regex.t] - The same as node_blacklist, except the opposite; only nodes which match a pattern in the whitelist will result in the ring being updated.

  • node_type: :all | :hidden | :visible: refers what kind of nodes will be monitored when monitor_nodes is true. For more information, see :net_kernel.monitor_nodes/2.

An error is returned if the ring already exists. Invalid options raise ArgumentError.

Examples

iex> {:ok, _pid} = HashRing.Managed.new(:test1, [nodes: ["a", {"b", 64}]])
...> HashRing.Managed.key_to_node(:test1, :foo)
"a"

iex> {:ok, pid} = HashRing.Managed.new(:test2)
...> {:error, {:already_started, existing_pid}} = HashRing.Managed.new(:test2)
...> pid == existing_pid
true
iex> HashRing.Managed.new(:test3, [nodes: "a"])
** (ArgumentError) {:nodes, "a"} is an invalid option for `HashRing.Managed.new/2`

nodes(ring)

@spec nodes(ring()) :: [term()]

Same as HashRing.nodes/1, returns a list of nodes on the ring.

Examples

iex> {:ok, _pid} = HashRing.Managed.new(:nodes_test)
...> HashRing.Managed.add_nodes(:nodes_test, [:a, :b])
...> HashRing.Managed.nodes(:nodes_test)
[:b, :a]

remove_node(ring, node)

@spec remove_node(ring(), key()) :: :ok | {:error, :no_such_ring}

Removes a node from the given hash ring.

An error is returned if the ring does not exist.

Examples

iex> {:ok, _pid} = HashRing.Managed.new(:test8)
...> :ok = HashRing.Managed.add_nodes(:test8, ["a", {"b", 64}])
...> :ok = HashRing.Managed.remove_node(:test8, "b")
...> HashRing.Managed.key_to_node(:test8, :foo)
"a"

start_link(opts)

@spec start_link(ring_options()) :: GenServer.on_start()

Starts a managed ring linked to the calling process.

This is the startup entry point used by child_spec/1. The :name option is required; other ring options are the same as for new/2. The calling supervisor owns the worker's restart and shutdown lifecycle.