Skip to content

Commit 915a452

Browse files
committed
init
0 parents  commit 915a452

File tree

10 files changed

+726
-0
lines changed

10 files changed

+726
-0
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Graft
2+
Graft offers an Elixir implementation of the raft consensus algorithm, allowing the creation of a distributed cluster of servers, where each server manages a replicated state machine. The `Graft.Machine` behaviour allows users to define their own replicated state machines, that may handle user defined client requests.
3+
4+
In this project's documentation you will find terminology that has been defined in the [raft paper](https://raft.github.io/raft.pdf). The docs do not go into specifics of the raft algorithm, so if you wish to learn more about how raft achieves consensus, the [official raft webpage](https://raft.github.io/) is a great place to start.
5+
6+
## Installation
7+
To install the package, add it to your dependency list in `mix.exs`.
8+
9+
```elixir
10+
def deps do
11+
[{:graft, "~> 0.1.0"}]
12+
end
13+
```
14+
15+
## Documentation
16+
Find the full documentation as well as examples here.

lib/client.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
defmodule Graft.Client do
2+
@moduledoc false
3+
def request(server, entry) do
4+
case GenStateMachine.call(server, {:entry, entry}) do
5+
{:ok, response} -> response
6+
{:error, {:redirect, leader}} -> request(leader, entry)
7+
{:error, msg} -> msg
8+
end
9+
end
10+
end

lib/demo.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule MyMapMachine do
2+
@moduledoc false
3+
use Graft.Machine
4+
5+
@impl Graft.Machine
6+
def init([]) do
7+
{:ok, %{}}
8+
end
9+
10+
@impl Graft.Machine
11+
def handle_entry({:get, key}, state) do
12+
{state, Map.get(state, key)}
13+
end
14+
15+
def handle_entry({:put, key, value}, state) do
16+
{Map.put(state, key, value), :ok}
17+
end
18+
19+
def handle_entry(_default, state) do
20+
{state, :error}
21+
end
22+
end

lib/graft.ex

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
defmodule Graft do
2+
@moduledoc """
3+
An API of the raft consensus algorithm, allowing for custom client requests
4+
and custom replicated state machines.
5+
6+
## Example
7+
8+
Let's create a distributed stack. The first step is to set up the state machine.
9+
Here we will use the `Graft.Machine` behaviour.
10+
11+
```
12+
defmodule MyStackMachine do
13+
use Graft.Machine
14+
15+
@impl Graft.Machine
16+
def init([]) do
17+
{:ok, []}
18+
end
19+
20+
@impl Graft.Machine
21+
def handle_entry({:put, value}, state) do
22+
{:ok, [value | state]}
23+
end
24+
25+
def handle_entry(:pop, []) do
26+
{:noop, []}
27+
end
28+
29+
def handle_entry(:pop, [response | state]) do
30+
{response, state}
31+
end
32+
33+
def handle_entry(_, state) do
34+
{:invalid_request, state}
35+
end
36+
end
37+
```
38+
39+
Now that we have our state machine, we can define the servers that
40+
will make up the raft cluster. Each server must have a unique name.
41+
42+
```
43+
servers = [:server1, :server2, :server3]
44+
```
45+
46+
With both the servers and state machine, we can now run the graft funtion,
47+
which will start the servers and the consensus algorithm.
48+
49+
```
50+
{:ok, supervisor} = Graft.start servers, MyStackMachine
51+
```
52+
53+
`Graft.start` returns the supervisor pid from which we can terminate or restart
54+
the servers.
55+
56+
We can now use `Graft.request` to make requests to our consensus cluster.
57+
As long as we know at least one server, we can send requests, since the `Graft.Client`
58+
module will forward the request if the server we choose is not the current leader.
59+
60+
```
61+
Graft.request :server1, :pop
62+
#=> :noop
63+
64+
Graft.request :server1, {:put, :foo}
65+
#=> :ok
66+
67+
Graft.request :server1, :pop
68+
#=> :foo
69+
70+
Graft.request :server1, :bar
71+
#=> :invalid_request
72+
```
73+
74+
That completes the distributed stack.
75+
"""
76+
77+
@doc """
78+
Starts the raft cluster.
79+
80+
`servers` - list of server names.
81+
`machine_module` - module using the `Graft.Machine` behaviour to define the replicated state machine.
82+
`machine_args` - an optional list of arguments that are passed to the `init` function of the machine.
83+
84+
Returns `{:ok, pid}` where `pid` is the supervisor pid which can be used to supervise the cluster's servers.
85+
"""
86+
@spec start(list(atom), module(), list(any)) :: {:ok, pid()}
87+
def start(servers, machine_module, machine_args \\ []) do
88+
{:ok, supervisor_pid} = Graft.Supervisor.start_link servers, machine_module, machine_args
89+
for server <- servers, do: Supervisor.start_child supervisor_pid, [server, servers, machine_module, machine_args]
90+
for server <- servers, do: GenStateMachine.cast server, :start
91+
{:ok, supervisor_pid}
92+
end
93+
94+
@doc """
95+
Print out the internal state of the `server`.
96+
"""
97+
def data(server), do: GenStateMachine.call(server, :data)
98+
99+
@doc """
100+
Make a new client request to a server within the consensus cluster.
101+
102+
`server` - name of the server the request should be sent to.
103+
'entry' - processed and applied by the replicated state machine.
104+
"""
105+
@spec request(atom(), any()) :: response :: any()
106+
def request(server, entry), do: Graft.Client.request server, entry
107+
end

lib/machine.ex

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
defmodule Graft.Machine do
2+
@moduledoc """
3+
A behaviour module for implementing a replicated machine for the raft consensus
4+
algorithm. Look at the `Graft` module docs for examples on how to create such
5+
machines.
6+
"""
7+
8+
use GenServer
9+
10+
@typedoc """
11+
The state/data of the replicated machine (similar to the 'state' of GenServer).
12+
"""
13+
@type state :: any
14+
15+
@typedoc """
16+
The entry request sent by the client.
17+
"""
18+
@type entry :: any
19+
20+
@typedoc """
21+
The reply to be sent back to the client.
22+
"""
23+
@type response :: any
24+
25+
@doc """
26+
Invoked when the server starts and links to the machine.
27+
28+
`args` is a list accepted arguments. Look at `Graft.start` to see how to pass
29+
in these optional arguments.
30+
31+
Returning `{:ok, state}`, will initialise the state of the machine to `state`.
32+
"""
33+
@callback init(args :: list(any)) :: {:ok, state}
34+
35+
@doc """
36+
Invoked when a server in the raft cluster is commiting an entry to its log.
37+
Should apply the entry to the replicated machine.
38+
39+
Should return a tuple of the response for the server along with the new state of the
40+
replicated machine.
41+
"""
42+
@callback handle_entry(entry, state) :: {response, state}
43+
44+
defmacro __using__(_opts) do
45+
quote location: :keep do
46+
@behaviour Graft.Machine
47+
end
48+
end
49+
50+
@doc false
51+
@impl GenServer
52+
def init([module, args]) do
53+
{:ok, state} = module.init args
54+
{:ok, {module, state}}
55+
end
56+
57+
@doc false
58+
@impl GenServer
59+
def handle_call({:apply, entry}, _from, {module, state}) do
60+
{reply, state} = module.handle_entry entry, state
61+
{:reply, reply, {module, state}}
62+
end
63+
64+
@doc false
65+
def register(module, machine_args \\ []) do
66+
GenServer.start_link __MODULE__, [module, machine_args]
67+
end
68+
69+
@doc false
70+
def apply_entry(machine, entry) do
71+
GenServer.call machine, {:apply, entry}
72+
end
73+
end

lib/rpc.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule Graft.AppendEntriesRPC do
2+
@moduledoc false
3+
defstruct term: -1, # leader’s term
4+
leader_name: nil, # so follower can redirect clients
5+
prev_log_index: -1, # index of log entry immediately preceding new ones
6+
prev_log_term: -1, # term of prevLogIndex entry
7+
entries: [], # log entries to store (empty for heartbeat; may send more than one for efficiency)
8+
leader_commit: -1 # leader’s commit_index
9+
end
10+
11+
defmodule Graft.AppendEntriesRPCReply do
12+
@moduledoc false
13+
defstruct term: -1, # current_term, for leader to update itself
14+
success: false, # true if follower contained entry matching prev_log_index and prev_log_term
15+
last_log_index: -1, # index of last entry in follower's log
16+
last_log_term: -1 # term of last entry in follower's log
17+
end
18+
19+
defmodule Graft.RequestVoteRPC do
20+
@moduledoc false
21+
defstruct term: -1, # candidate’s term
22+
candidate_name: nil, # candidate requesting vote
23+
last_log_index: -1, # index of candidate’s last log entry
24+
last_log_term: -1 # term of candidate’s last log entry
25+
end
26+
27+
defmodule Graft.RequestVoteRPCReply do
28+
@moduledoc false
29+
defstruct term: -1, # current_term, for candidate to update itself
30+
vote_granted: false # true means candidate received vote
31+
end

0 commit comments

Comments
 (0)