ExRaft (ExRaft v0.2.1) View Source

ExRaft provides an Elixir implementation and an easy to use API for the raft consensus protocol.

Internals

Servers

When starting an ExRaft server, there are 2 processes spawned:

  1. a Supervisor process that ensures that the logs are not lost when the server crashes
  2. the actual server process

Note that the Supervisor will generate an additional atom to register itself (for more information, check out the ExRaft.start_server/3 function).

Timeouts

When calling ExRaft.write/3, ExRaft.add_server/3 or ExRaft.remove_server/3, you may experience timeouts.

This is most likely caused by the majority of the cluster not being available to the leader.

Note that even though the call may time out, once the majority comes back online your command might still be applied, provided the rest of the cluster did not advance their log in the meantime.

Cluster membership changes

Dynamic cluster membership is supported with one-at-a-time changes to the configuration, but as of now new servers are added without any sort of catch-up mechanism or validation (these features will be present in a future release).

Servers can be added to- or removed from the cluster by making ExRaft.add_server/3 or ExRaft.remove_server/3 calls to the leader.

Log compaction

As of now log compaction is not supported but is planned to be implemented and released in the future.

Example

init_arg = [very_cool: true]
initial_config = [raft1: node(), raft2: node()]

# after starting these servers, they will time out and eventually elect a leader
# amongst themselves
{:ok, _} = ExRaft.start_server(YourStateMachine, init_arg, name: :raft1, initial_config: initial_config)
{:ok, _} = ExRaft.start_server(YourStateMachine, init_arg, name: :raft2, initial_config: initial_config)

# this server can never become leader unless its added to an existing cluster
# since it doesn't know of any other server but is required to achieve
# a minimum majority of 2 in elections and log replication
{:ok, _} = ExRaft.start_server(YourStateMachine, init_arg, name: :raft3, min_majority: 2)

# we could pick any server to await the leader
leader = ExRaft.await_leader(:raft1)
:ok = ExRaft.add_server(leader, :raft3)

# the success result of write/3 depends on the state machine
:ok = ExRaft.write(leader, :hello)

# making a write/3 call to a follower results in an error
follower = List.delete(initial_config, leader) |> List.first()
{:error, {:redirect, ^leader}} = ExRaft.write(follower, :hello)

# if we stop the active leader (or it crashes)
# the rest of the cluster will be able to recover
:ok = ExRaft.stop_server(leader)
:ok = ExRaft.trigger_election(:raft3)
new_leader = ExRaft.await_leader(:raft3)
true = Enum.member?([raft2: node(), raft3: node()], new_leader)

Link to this section Summary

Types

Option values used by the start_server functions.

Options used by the start_server functions.

Reference to a peer.

Reference to a server.

Functions

Makes a synchronous call to server to add a new server to the configuration.

Makes a synchronous call to server and waits until server learns of; or becomes the cluster leader.

Sends an asynchronous request to server to inspect and write its context or log to the device.

Makes a synchronous call to server to retrieve its known leader and waits for its reply.

Makes a synchronous call to server to read query from the state and waits for its reply.

Same as read/3, but if :dirty_read is set to true (see start_server/3), followers are also allowed to execute the query.

Makes a synchronous call to server to remove a server from the configuration.

Same as start_server/3 but sets [] as init_arg.

Starts a server process linked to the current process.

Makes a synchronous call to server to retrieve its state and waits for its reply.

Synchronously stops the server with the given reason.

Makes a synchronous call to server to start a new election.

Makes a synchronous call to server to apply command to the state and waits for its reply.

Link to this section Types

Link to this type

option()

View Source (since 0.1.0)

Specs

option() ::
  {:name, atom()}
  | {:debug, boolean()}
  | {:dirty_read, boolean()}
  | {:initial_config, [peer()]}
  | {:min_majority, nil | non_neg_integer()}
  | {:min_election_timeout, pos_integer()}
  | {:max_election_timeout, pos_integer()}
  | {:heartbeat_timeout, pos_integer()}
  | {:batch_size, pos_integer()}

Option values used by the start_server functions.

See start_server/3 for more information.

Link to this type

options()

View Source (since 0.1.0)

Specs

options() :: [option()]

Options used by the start_server functions.

See start_server/3 for more information.

Specs

peer() :: {atom(), node()}

Reference to a peer.

Used internally by ExRaft servers to communicate with each other.

Link to this type

server()

View Source (since 0.1.0)

Specs

server() :: atom() | peer()

Reference to a server.

Used to make calls to ExRaft servers.

Link to this section Functions

Link to this function

add_server(server, new_server, timeout \\ 60000)

View Source (since 0.1.0)

Specs

add_server(server :: server(), new_server :: peer(), timeout :: timeout()) ::
  :ok | {:error, reason :: atom() | {:redirect, peer()}}

Makes a synchronous call to server to add a new server to the configuration.

Return values

If all checks pass, returns :ok once the majority of the new configuration (including new_server) knows of new_server being a member of the cluster. (Returns :ok immediately if new_server is already a member of the cluster.)

Otherwise returns {:error, reason}, where reason is one of the following:

  • :no_commit - server has yet to commit an entry in it's term as leader.
  • :unstable_config - another configuration change is pending.
  • can also return any of the error reasons specified at write/3
Link to this function

await_leader(server, timeout \\ 60000)

View Source (since 0.1.0)

Specs

await_leader(server :: server(), timeout :: timeout()) :: peer()

Makes a synchronous call to server and waits until server learns of; or becomes the cluster leader.

Returns the elected cluster leader.

Link to this function

inspect(server, flag)

View Source (since 0.2.0)

Specs

inspect(server :: server(), flag :: :context | :log) :: :ok

Sends an asynchronous request to server to inspect and write its context or log to the device.

Link to this function

leader(server, timeout \\ 1000)

View Source (since 0.1.0)

Specs

leader(server :: server(), timeout :: timeout()) :: peer() | nil

Makes a synchronous call to server to retrieve its known leader and waits for its reply.

Link to this function

read(server, query, timeout \\ 60000)

View Source (since 0.1.0)

Specs

read(server :: server(), query :: any(), timeout :: timeout()) ::
  StateMachine.reply() | {:error, reason :: atom() | {:redirect, peer()}}

Makes a synchronous call to server to read query from the state and waits for its reply.

Return values

If server is leader, it returns the result of calling the handle_read/2 function of the underlying state machine (see ExRaft.StateMachine).

Otherwise if server is not leader, it returns the same error as write/3.

Link to this function

read_dirty(server, query, timeout \\ 60000)

View Source (since 0.1.0)

Specs

read_dirty(server :: server(), query :: any(), timeout :: timeout()) ::
  StateMachine.reply()

Same as read/3, but if :dirty_read is set to true (see start_server/3), followers are also allowed to execute the query.

If :dirty_read was set to false, server never replies.

Link to this function

remove_server(server, server_to_remove, timeout \\ 60000)

View Source (since 0.1.0)

Specs

remove_server(
  server :: server(),
  server_to_remove :: peer(),
  timeout :: timeout()
) :: :ok | {:error, reason :: atom() | {:redirect, peer()}}

Makes a synchronous call to server to remove a server from the configuration.

Return values

If all checks pass, returns :ok once the majority of the new configuration (without server_to_remove) knows of server_to_remove not being a member of the cluster. (Returns :ok immediately if server_to_remove is not a member of the cluster.)

Otherwise returns {:error, reason}, where reason is one of the following:

  • :last_server - server_to_remove is the last server in the configuration.
  • :no_commit - server has yet to commit an entry in it's term as leader.
  • :unstable_config - another configuration change is pending.
  • can also return any of the error reasons specified at write/3
Link to this function

start_server(module, options)

View Source (since 0.1.0)

Specs

start_server(module :: module(), options :: options()) :: Supervisor.on_start()

Same as start_server/3 but sets [] as init_arg.

Link to this function

start_server(module, init_arg, options)

View Source (since 0.1.0)

Specs

start_server(module :: module(), init_arg :: any(), options :: options()) ::
  Supervisor.on_start()

Starts a server process linked to the current process.

Once the server is started, the init/1 function (see ExRaft.StateMachine) of the given module is called with init_arg as its argument to initialize the server.

Options

  • :name - used for name registration as described in the "Name Registration" section of the GenServer documentation. Note that only local names (atoms) are accepted. By design ExRaft will create an additional atom for the server supervisor under :"Elixir.ExRaft.Supervisor.#{name}".

    This is the only option required to ensure internal server communication.

  • :debug - can be set to true to enable verbose server logs. Defaults to false.

  • :dirty_read - can be set to false to disable reading "dirty" (maybe out of date) state from followers. Defaults to true.

  • :initial_config - used to start the server with a known set of peers. Defaults to [].

  • :min_majority - used to set a minimum expected majority. When set, servers will decide on achieving majority in elections and log replication based on the active configuration and this value (the greater value will be used). As an example, with a value of 2 in a 2 member cluster, the required majority will be 2, with the same value in a 5 member cluster, the required majority will be 3. Defaults to nil.

  • :min_election_timeout - used to tune the minimum amount of time in milliseconds, that must pass before a follower times out. Note that the actual timeout will be picked randomly between :min_election_timeout and :max_election_timeout. Defaults to 1_000.

  • :max_election_timeout - used to tune the maximum amount of time in milliseconds, that must pass before a follower times out. Note that the actual timeout will be picked randomly between :min_election_timeout and :max_election_timeout. Defaults to 10_000.

  • :heartbeat_timeout - used to set the heartbeat timeout (aka.: the time that will pass between leader heartbeats in log replication) in milliseconds. Defaults to 100.

  • :batch_size - used to set the number of entries sent by leaders to followers in heartbeat RPCs. Defaults to 100.

Link to this function

state(server, timeout \\ 1000)

View Source (since 0.2.0)

Specs

state(server :: server(), timeout :: timeout()) ::
  :follower | :candidate | :leader

Makes a synchronous call to server to retrieve its state and waits for its reply.

Link to this function

stop_server(server, reason \\ :normal, timeout \\ :infinity)

View Source (since 0.1.0)

Specs

stop_server(server :: server(), reason :: atom(), timeout :: timeout()) :: :ok

Synchronously stops the server with the given reason.

server will call the terminate/2 function of the underlying state machine before exiting (see ExRaft.StateMachine).

This function keeps OTP semantics regarding error reporting. If the reason is any other than :normal, :shutdown or {:shutdown, _}, an error report is logged.

Link to this function

trigger_election(server, timeout \\ 1000)

View Source (since 0.1.0)

Specs

trigger_election(server :: server(), timeout :: timeout()) :: :ok

Makes a synchronous call to server to start a new election.

server replies with :ok immediately and becomes candidate to start a new election unless server is already a candidate in which case the reply will still be :ok but nothing happens.

Link to this function

write(server, command, timeout \\ 60000)

View Source (since 0.1.0)

Specs

write(server :: server(), command :: any(), timeout :: timeout()) ::
  StateMachine.reply() | {:error, reason :: atom() | {:redirect, peer()}}

Makes a synchronous call to server to apply command to the state and waits for its reply.

Return values

If server is leader, it appends command to its log, and once the majority of the cluster replicates command, it applies command to the state by calling the handle_write/2 function of the underlying state machine (see ExRaft.StateMachine). It then returns reply from the handle_write/2 call.

Otherwise if server is not leader, it returns {:error, reason} where reason is one of the following:

  • :no_leader - server does not know the leader of the cluster.
  • {:redirect, leader} - where leader is the leader of the cluster.