I would say that one of the core properties of a system is the agreement on which data
are right at a given moment.
At that moment, one of the more important aspects of a distributed system shows up:
the consensus. Where every participant agrees on a single state, even if some actor
fails.
Some use cases where consensus brings a strong advantage are network agreement in a
server cluster, database replicati ons, and even in blockchain networks.
The Simple Paxos algorithm is the most simple implementation of the Paxos Protocol family.
As presented by Leslie Lamport on the The Part-Time Parliament
and Paxos Made Simple,
the idea of the Simple Paxos is simple:
Your node can assume 3 different roles: proposers, acceptors and learners.
Your node can be all these roles at the same time.
An acceptor will accept the first value that is received as a proposal.
A proposer will receive a promise from every acceptor, that promise let
the proposer knows that the node will accept the proposed value.
A proposer reach the quorum, that is, the majority, when receive a promise
of N/2+1 where N is the number of nodes in the system.
After reach the quorum, the learners will receive the accepted value from
the acceptors. The learners will put the accepted value into their state
machines.
(This is a simple sequence diagram coming from Wikipedia, but it’s a good representation)
A proposer is the node that will propose a new value to reach the consensus
around their system. In this case, each proposal follows this pattern: (id, v),
where the id is an incrementally natural number, and v is the proposed value,
for our implementation, we will assume v as a string.
First things first. We need to start our server. In this case, using Rust, I will
use Axum to do all the work for me:
Now, we have a server running on the port that we want. You can test it by running:
We will implement the proposer logic on the /prepare endpoint. The proposer
will send a prepare message to all acceptor messages and wait
for the promises.
In this first phase, we will only send the prepare message to all acceptors. The
prepare message is a simple message with the id of the proposal. The acceptor will
receive this message and will send a promise to the proposer.
We implement the proposer.prepare method to send the prepare message to all acceptors. What
this is doing is:
Calling the /handle-prepare endpoint of all nodes in the system.
Waiting for the promises of the acceptors.
Checking if the proposer received the quorum of promises.
If the proposer received the quorum, it will check if there is a value that was accepted
by the acceptors. If there is, the proposer will use this value as the proposed value. If
not, the proposer will use the value that it wants to propose.
Cool. We’re coming to what we can call the phase 1b. The acceptor will receive
the prepare message and need to send a promise if everything is OK. In this case, which
things do we need to check?
If the proposal number is greater than the last proposal number that the acceptor
received.
If the acceptor already accepted a value, if yes, it needs to send the accepted
value to the proposer.
We improved the handle-prepare function to handle all the logic of the acceptor
receiving the prepare message, instead of just a todo!(). If everything is OK,
the acceptor will return a promise to the proposer. If not, it will return an error
message.
Now that the proposer received the promise, we need to start with the phase 2a.
The proposer will send the accept message to all acceptors. The accept message needs
to contain the proposal and their identifier.
Then, the first part of our implementation will be improving the implementation of our
Proposer struct. In this case, we will need to add a new function called propose.
This propose function will send the accept message to all acceptors and wait for the
acceptance of the proposal. If the proposer receives the quorum of acceptances, it will
return Ok(()). If not, it will return an error message.
Now, we need to implement the /handle-accept endpoint to handle the accept message.
Now, we have the acceptor receiving the accept message and handling it. If everything
is OK, the acceptor will accept the proposal and return an acceptance message to the
proposer.
We will need to add the proposer.propose call on the /prepare endpoint.
Removing that old todo!() and putting the proposer.propose call on the /prepare,
we have the proposer sending the accept message to all acceptors and waiting for
the acceptance of the proposal. If the proposer receives the quorum of acceptances,
it will send the accepted value to all learners.
The last part of our implementation is the /handle-learn endpoint. The learner
will receive the accepted value and put it into their state machines.
After learning the accepted value, the learner will put it into their state machines. And
then we have the consensus around the system. The acceptor will
send the accepted value to the learner, but I think it’s easier to implement
just sending the accepted value to the learner via proposer.
This is the most simple implementation of Paxos Protocol consensus algorithm.
You have some variants that brings some improvements to the Paxos, like the
Multi-Paxos and EPaxos. All of these have some properties that are best for
specific use cases.
If you want to see the repo with the implementation of this algorithm, you can
access it here.
Thanks to hnrbs, one of my inspirations to write this
code and learn more about Paxos and consensus.
You can see his implementation here.