Raft
C implementation of the Raft Consensus protocol, BSD licensed
Install / Use
/learn @willemt/RaftREADME
.. image:: https://travis-ci.org/willemt/raft.png :target: https://travis-ci.org/willemt/raft
.. image:: https://coveralls.io/repos/willemt/raft/badge.png :target: https://coveralls.io/r/willemt/raft
C implementation of the Raft consensus protocol, BSD licensed.
See raft.h <https://github.com/willemt/raft/blob/master/include/raft.h>_ for full documentation.
See ticketd <https://github.com/willemt/ticketd>_ for real life use of this library.
Networking is out of scope for this project. The implementor will need to do all the plumbing. The library doesn't assume a network layer with ordering or duplicate detection. This means you could use UDP for transmission.
There are no dependencies, however https://github.com/willemt/linked-List-queue is required for testing.
Building
.. code-block:: bash :class: ignore
make tests
Quality Assurance
We use the following methods to ensure that the library is safe:
- A simulator (virtraft2) is used to test the Raft invariants on unreliable networks
Fuzzing/property-based-testing <https://github.com/willemt/virtraft/blob/master/tests/test_fuzzer.py>_ viaHypothesis <https://github.com/DRMacIver/hypothesis/>_- All bugs have regression tests
- Many unit tests
Usage <https://github.com/willemt/ticketd>_
virtraft2
This cluster simulator checks the following:
- Log Matching (servers must have matching logs)
- State Machine Safety (applied entries have the same ID)
- Election Safety (only one valid leader per term)
- Current Index Validity (does the current index have an existing entry?)
- Entry ID Monotonicity (entries aren't appended out of order)
- Committed entry popping (committed entries are not popped from the log)
- Log Accuracy (does the server's log match mirror an independent log?)
- Deadlock detection (does the cluster continuously make progress?)
Chaos generated by virtraft2:
- Random bi-directional partitions between nodes
- Message dropping
- Message duplication
- Membership change injection
- Random compactions
Run the simulator using:
.. code-block:: bash :class: ignore
make test_virtraft
virtraft2 succeeds virtraft <https://github.com/willemt/virtraft>_
Single file amalgamation
The source has been amalgamated into a single raft.h header file.
Use clib <https://github.com/clibs/clib>_ to download the source into your project's deps folder, ie:
.. code-block:: bash
brew install clib
clib install willemt/raft_amalgamation
The file is stored in the deps folder like below:
.. code-block:: bash
deps/raft/raft.h
How to integrate with this library
See ticketd <https://github.com/willemt/ticketd>_ for an example of how to integrate with this library.
If you don't have access to coroutines it's easiest to use two separate threads - one for handling Raft peer traffic, and another for handling client traffic.
Be aware that this library is not thread safe. You will need to ensure that the library's functions are called exclusively.
Initializing the Raft server
Instantiate a new Raft server using raft_new.
.. code-block:: c
void* raft = raft_new();
We tell the Raft server what the cluster configuration is by using the raft_add_node function. For example, if we have 5 servers [#]_ in our cluster, we call raft_add_node 5 [#]_ times.
.. code-block:: c
raft_add_node(raft, connection_user_data, node_id, peer_is_self);
Where:
connection_user_datais a pointer to user data.peer_is_selfis boolean indicating that this is the current server's server index.node_idis the unique integer ID of the node. Peers use this to identify themselves. This SHOULD be a random integer.
.. [#] AKA "Raft peer" .. [#] We have to also include the Raft server itself in the raft_add_node calls. When we call raft_add_node for the Raft server, we set peer_is_self to 1.
Calling raft_periodic() periodically
We need to call raft_periodic at periodic intervals.
.. code-block:: c
raft_periodic(raft, 1000);
Example using a libuv timer:
.. code-block:: c
static void __periodic(uv_timer_t* handle)
{
raft_periodic(sv->raft, PERIOD_MSEC);
}
uv_timer_t *periodic_req;
periodic_req = malloc(sizeof(uv_timer_t));
periodic_req->data = sv;
uv_timer_init(&peer_loop, periodic_req);
uv_timer_start(periodic_req, __periodic, 0, 1000);
Receiving the entry (ie. client sends entry to Raft cluster)
Our Raft application receives log entries from the client.
When this happens we need to:
- Redirect the client to the Raft cluster leader (if necessary)
- Append the entry to our log
- Block until the log entry has been committed [#]_
.. [#] When the log entry has been replicated across a majority of servers in the Raft cluster
Append the entry to our log
We call raft_recv_entry when we want to append the entry to the log.
.. code-block:: c
msg_entry_response_t response;
e = raft_recv_entry(raft, &entry, &response);
You should populate the entry struct with the log entry the client has sent. After the call completes the response parameter is populated and can be used by the raft_msg_entry_response_committed function to check if the log entry has been committed or not.
Blocking until the log entry has been committed
When the server receives a log entry from the client, it has to block until the entry is committed. This is necessary as our Raft server has to replicate the log entry with the other peers of the Raft cluster.
The raft_recv_entry function does not block! This means you will need to implement the blocking functionality yourself.
Example below is from the ticketd client thread. This shows that we need to block on client requests. ticketd does the blocking by waiting on a conditional, which is signalled by the peer thread. The separate thread is responsible for handling traffic between Raft peers.
.. code-block:: c
msg_entry_response_t response;
e = raft_recv_entry(sv->raft, &entry, &response);
if (0 != e)
return h2oh_respond_with_error(req, 500, "BAD");
/* block until the entry is committed */
int done = 0;
do {
uv_cond_wait(&sv->appendentries_received, &sv->raft_lock);
e = raft_msg_entry_response_committed(sv->raft, &r);
switch (e)
{
case 0:
/* not committed yet */
break;
case 1:
done = 1;
uv_mutex_unlock(&sv->raft_lock);
break;
case -1:
uv_mutex_unlock(&sv->raft_lock);
return h2oh_respond_with_error(req, 400, "TRY AGAIN");
}
} while (!done);
Example from ticketd of the peer thread. When an appendentries response is received from a Raft peer, we signal to the client thread that an entry might be committed.
.. code-block:: c
e = raft_recv_appendentries_response(sv->raft, conn->node, &m.aer);
uv_cond_signal(&sv->appendentries_received);
Redirecting the client to the leader
When we receive an entry log from the client it's possible we might not be a leader.
If we aren't currently the leader of the raft cluster, we MUST send a redirect error message to the client. This is so that the client can connect directly to the leader in future connections. This enables future requests to be faster (ie. no redirects are required after the first redirect until the leader changes).
We use the raft_get_current_leader function to check who is the current leader.
Example of ticketd sending a 301 HTTP redirect response:
.. code-block:: c
/* redirect to leader if needed */
raft_node_t* leader = raft_get_current_leader_node(sv->raft);
if (!leader)
{
return h2oh_respond_with_error(req, 503, "Leader unavailable");
}
else if (raft_node_get_id(leader) != sv->node_id)
{
/* send redirect */
peer_connection_t* conn = raft_node_get_udata(leader);
char leader_url[LEADER_URL_LEN];
static h2o_generator_t generator = { NULL, NULL };
static h2o_iovec_t body = { .base = "", .len = 0 };
req->res.status = 301;
req->res.reason = "Moved Permanently";
h2o_start_response(req, &generator);
snprintf(leader_url, LEADER_URL_LEN, "http://%s:%d/",
inet_ntoa(conn->addr.sin_addr), conn->http_port);
h2o_add_header(&req->pool,
&req->res.headers,
H2O_TOKEN_LOCATION,
leader_url,
strlen(leader_url));
h2o_send(req, &body, 1, 1);
return 0;
}
Function callbacks
You provide your callbacks to the Raft server using raft_set_callbacks.
The following callbacks MUST be implemented: send_requestvote, send_appendentries, applylog, persist_vote, persist_term, log_offer, and log_pop.
Example of function callbacks being set:
.. code-block:: c
raft_cbs_t raft_callbacks = {
.send_requestvote = __send_requestvote,
.send_appendentries = __send_appendentries,
.applylog = __applylog,
.persist_vote = __persist_vote,
.persist_term = __persist_term,
.log_offer = __raft_logentry_offer,
.log_poll = __raft_logentry_poll,
.log_pop = __raft_logentry_pop,
.log = __raft_log,
};
char* user_data = "test";
raft_set_callbacks(raft, &raft_callbacks, user_data);
send_requestvote()
For this callback we have to serialize a msg_requestvote_t struct, and
