3 ms·
I prefer giving the transitions explicit names over relying on the From implemenations defined on the machine (defining them on the states still prevents bad tr
by joshka 2y ago
I prefer giving the transitions explicit names over relying on the From implemenations defined on the machine (defining them on the states still prevents bad transitions). The raft example drops a bunch of syntactic noise and repetition this way:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=69f5db2ae38230123a96253e95de5cbd https://play.rust-lang.org/?version=stable&mode=debug&editio...
fn main() {
let is_follower = Raft::new(/* ... */);
// Raft typically comes in groups of 3, 5, or 7. Just 1 for us. :)
// Simulate this node timing out first.
let is_candidate = is_follower.on_timeout();
// It wins! How unexpected.
let is_leader = is_candidate.on_wins_vote();
// Then it fails and rejoins later, becoming a Follower again.
let is_follower_again = is_leader.on_disconnected();
// And goes up for election...
let is_candidate_again = is_follower_again.on_timeout();
// But this time it fails!
let is_follower_another_time = is_candidate_again.on_lose_vote();
}
// This is our state machine.
struct Raft<S> {
// ... Shared Values
state: S
}
// The three cluster states a Raft node can be in
// If the node is the Leader of the cluster services requests and replicates its state.
struct Leader {
// ... Specific State Values
}
// If it is a Candidate it is attempting to become a leader due to timeout or initialization.
struct Candidate {
// ... Specific State Values
}
// Otherwise the node is a follower and is replicating state it receives.
struct Follower {
// ... Specific State Values
}
impl<S> Raft<S> {
fn transition<T: From<S>>(self) -> Raft<T> {
let state = self.state.into();
// ... Logic prior to transition
Raft {
// ... attr: val.attr
state,
}
}
}
// Raft starts in the Follower state
impl Raft<Follower> {
fn new(/* ... */) -> Self {
// ...
Raft {
// ...
state: Follower { /* ... */ }
}
}
// When a follower timeout triggers it begins to campaign
fn on_timeout(self) -> Raft<Candidate> {
self.transition()
}
}
impl Raft<Candidate> {
// If it doesn't receive a majority of votes it loses and becomes a follower again.
fn on_lose_vote(self) -> Raft<Follower> {
self.transition()
}
// If it wins it becomes the leader.
fn on_wins_vote(self) -> Raft<Leader> {
self.transition()
}
}
impl Raft<Leader> {
// If the leader becomes disconnected it may rejoin to discover it is no longer leader
fn on_disconnected(self) -> Raft<Follower> {
self.transition()
}
}
// The following are the defined transitions between states.
// When a follower timeout triggers it begins to campaign
impl From<Follower> for Candidate {
fn from(state: Follower) -> Self {
Candidate { /* ... */ }
}
}
// If it doesn't receive a majority of votes it loses and becomes a follower again.
impl From<Candidate> for Follower {
fn from(state: Candidate) -> Self {
Follower { /* ... */ }
}
}
// If it wins it becomes the leader.
impl From<Candidate> for Leader {
fn from(val: Candidate) -> Self {
Leader { /* ... */ }
}
}
// If the leader becomes disconnected it may rejoin to discover it is no longer leader
impl From<Leader> for Follower {
fn from(val: Leader) -> Self {
Follower { /* ... */ }
}
}
- gnatolf 2y agoI did the same. In short examples like the ones used in the article, it's easy to reason about the states and transitions. But in a much larger codebase, it gets so much harder to even discover available transitions if one is leaning too much on the from/into implementations. Nice descriptive function names go a long way in terms of ergonomic coding.