ExRaft.StateMachine behaviour (ExRaft v0.1.1) View Source
A behaviour module for implementing consistently replicated state machines.
Example
To start things off, let's see a code example implementing a simple
key value store in ExRaft style:
defmodule KeyValueStore do
@behaviour ExRaft.StateMachine
@impl true
def init(init_state) do
{:ok, init_state}
end
@impl true
def command?({:put, _, _}, _), do: true
def command?(_, _), do: false
@impl true
def handle_write({:put, key, value}, state) do
new_state = Map.put(state, key, value)
reply = {:ok, "I GOT IT BOSS"}
{reply, new_state}
end
@impl true
def handle_read(key, state) do
Map.get(state, key)
end
end
initial_config = [foo: node(), bar: node(), baz: node()]
init_state = %{}
{:ok, _} = ExRaft.start_server(KeyValueStore, init_state, name: :foo, initial_config: initial_config)
{:ok, _} = ExRaft.start_server(KeyValueStore, init_state, name: :bar, initial_config: initial_config)
{:ok, _} = ExRaft.start_server(KeyValueStore, init_state, name: :baz, initial_config: initial_config)
leader = ExRaft.await_leader(:foo)
# leader is now one of [{:foo, node()}, {:bar, node()}, {:baz, node()}]
ExRaft.read(leader, :some_key)
# nil
ExRaft.write(KeyValueStore, {:put, :some_key, :some_value})
# {:ok, "I GOT IT BOSS"}
ExRaft.read(leader, :some_key)
# :some_valueWe start a member of our KeyValueStore cluster by calling ExRaft.start_server/3,
passing the module with the state machine implementation and its initial state
(in this example this is just an empty map).
In the example above we start a 3 server cluster with the names :foo, :bar and :baz.
Any of these servers may become the cluster leader at startup or when the old leader fails,
that is why we first wait for a leader to be elected by calling ExRaft.await_leader/2.
After a leader was elected we can write to- and read from our replicated key value store
by calling ExRaft.write/3 and ExRaft.read/3 respectively.
Once the ExRaft.write/3 call returns with our state machine reply
({:ok, "I GOT IT BOSS"}) we know that the majority of our cluster (2 servers in
this example) knows that :some_key is in fact :some_value.
This means that if the leader crashes or a network split happens between
the leader and the rest of the cluster, the remaining 2 servers will still
be able to elect a new leader and this new leader will still know of
:some_key being :some_value.
In fact if we were to issue an ExRaft.read_dirty/3 call on the followers
after writing :some_key to the state at least one, if not both of them
would reply with :some_value.
Improvements
In the above example we interacted with the key value store using the ExRaft
module. This is not ideal since we don't want our users to necessarily know
how to use ExRaft.
Also we started the servers by calling ExRaft.start_server/3 directly.
It would be better if we started them as part of a supervision tree.
So let's fix these issues:
defmodule KeyValueStore do
use ExRaft.StateMachine
@init_state %{}
def start_link(opts),
do: ExRaft.start_server(__MODULE__, @init_state, opts)
def put(server, key, value, timeout \\ nil),
do: ExRaft.write(server, {:put, key, value}, timeout)
def get(server, key, timeout \\ nil),
do: ExRaft.read(server, key, timeout)
@impl true
def init(init_state) do
{:ok, init_state}
end
@impl true
def command?({:put, _, _}, _), do: true
def command?(_, _), do: false
@impl true
def handle_write({:put, key, value}, state) do
new_state = Map.put(state, key, value)
reply = {:ok, "I GOT IT BOSS"}
{reply, new_state}
end
@impl true
def handle_read(key, state) do
Map.get(state, key)
end
endNow we can simply call KeyValueStore.put/3 and KeyValueStore.get/3
to write to- and read from our replicated key value store.
Supervision
When we invoke use ExRaft.StateMachine, two things happen:
It defines that the current module implements the ExRaft.StateMachine
behaviour (@behaviour ExRaft.StateMachine).
It also defines an overridable child_spec/1 function, that allows
us to start the KeyValueStore directly under a supervisor.
children = [
{KeyValueStore, name: :foo, initial_config: [...]}
]
Supervisor.start_link(children, strategy: :one_for_one)This will invoke KeyValueStore.start_link/1, lucky for us
we already defined this function in the improved implementation.
Note
Be aware that all callbacks (except for init/1 and terminate/2)
block the server until they return, so it is recommended to try and keep
any heavy lifting outside of them or adjusting the :min_election_timeout
and :max_election_timeout options of the server (see ExRaft.start_server/3).
Also note that setting a high election timeout range may cause the cluster to stay without leader and thus become unresponsive for a longer period of time.
Link to this section Summary
Types
The state machine reply.
Side effect values.
Side effects executed only by the leader.
The state machine state.
Callbacks
Invoked to check commands upon ExRaft.write/3 calls.
Invoked to handle queries from ExRaft.read/3 and when enabled ExRaft.read_dirty/3 calls.
Invoked to handle configuration changes. Other internal command types may be added in future releases.
Invoked to handle commands from ExRaft.write/3 calls.
Invoked when the server is started.
Invoked when the server is about to exit. It should do any cleanup required.
Invoked when the server transitions to a new raft state.
Link to this section Types
Specs
reply() :: any()
The state machine reply.
Specs
side_effect() :: {:mfa, mfa()}
Side effect values.
Specs
side_effects() :: [side_effect()]
Side effects executed only by the leader.
Specs
state() :: any()
The state machine state.
Link to this section Callbacks
Specs
Invoked to check commands upon ExRaft.write/3 calls.
Returning true will cause the server to continue with log replication
and eventually apply command to the state.
Returning false will make the server ignore the command,
causing the client to eventually time out.
This callback is optional. If one is not implemented, all commands
will be replicated and eventually applied to the state.
Specs
Invoked to handle queries from ExRaft.read/3 and when enabled ExRaft.read_dirty/3 calls.
The returned reply will be sent to the caller and the server will continue its loop.
Specs
handle_system_write(type :: :config, state :: state()) :: {:ok, state()} | {:ok, state(), side_effects()}
Invoked to handle configuration changes. Other internal command types may be added in future releases.
Returning {:ok, new_state} sends the response :ok to the caller
and continues the loop with new state new_state.
Returning {:ok, new_state, side_effects} is similar to {:ok, new_state}
except it causes the leader to execute side_effects (see side_effects/0).
Specs
handle_write(command :: any(), state :: state()) :: {reply(), state()} | {reply(), state(), side_effects()}
Invoked to handle commands from ExRaft.write/3 calls.
Called when command is replicated to the majority of servers
and can be safely applied to the state.
Returning {reply, new_state} sends the response reply to the caller
and continues the loop with new state new_state.
Returning {reply, new_state, side_effects} is similar to {reply, new_state}
except it causes the leader to execute side_effects (see side_effects/0).
Specs
Invoked when the server is started.
ExRaft.start_server/3 and ExRaft.start_server/2 will block until it returns.
init_arg is the second argument passed to ExRaft.start_server/3
or [] when calling ExRaft.start_server/2.
Returning {:ok, state} will cause start_link/3 to return
{:ok, pid} and the process to enter its loop.
Returning {:stop, reason} will cause start_link/3 to return
{:error, reason} and the process to exit with reason reason without
entering the loop or calling terminate/2.
This callback is optional. If one is not implemented, init_arg will
be passed along as state.
Specs
Invoked when the server is about to exit. It should do any cleanup required.
reason is exit reason and state is the current state of the state machine.
The return value is ignored.
terminate/2 is called if the state machine traps exits (using Process.flag/2)
and the parent process sends an exit signal.
If reason is neither :normal, :shutdown, nor {:shutdown, term} an error is
logged.
For a more in-depth explanation, please read the "Shutdown values (:shutdown)"
section in the Supervisor module and the GenServer.terminate/2
callback documentation.
This callback is optional.
Specs
Invoked when the server transitions to a new raft state.
The return value is ignored.
This callback is optional.