recipes: Leader election#60
Conversation
Signed-off-by: Rodion Borovyk <rodion.borovyk@gmail.com>
Signed-off-by: Rodion Borovyk <rodion.borovyk@gmail.com>
Signed-off-by: Rodion Borovyk <rodion.borovyk@gmail.com>
|
I've marked it as ready for review because apparently no one receives notifications about drafts? |
|
Hi @rodio , thank you for the contribution. I had a look, i think the general approach looks good. I see two problems (not sure if Draft related):
use tokio::sync::watch;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LeaderState {
// volunteered, watching predecessor
Pending,
// at index 0
Leader,
// session/candidacy ended; terminal
Resigned,
}
/// Configuration for participating in a leader election.
#[derive(Debug, Clone)]
pub struct LeaderElection {
election_node: String,
zk: ZooKeeper,
}
/// An active candidacy. Drop to stop observing (the ephemeral znode
/// is still only removed when the underlying session ends).
#[derive(Debug)]
pub struct Candidate {
my_path: String,
state: watch::<LeaderState>,
...
}
impl Candidate {
pub fn path(&self) -> &str { &self.my_path }
pub fn state(&self) -> LeaderState { self.state.borrow().clone() }
/// Resolves once we become leader, or returns `Err` if we lose
/// the session before that happens.
pub async fn wait_for_leadership(&mut self) -> Result<(), ElectionError> {
loop {
match *self.state.borrow_and_update() {
LeaderState::Leader => return Ok(()),
// Should probably even be split into something like `SessionLost` and `Withdrawn`
LeaderState::Resigned => return Err(ElectionError::SessionLost),
LeaderState::Pending => {}
}
self.state.changed().await.map_err(|_| ElectionError::SessionLost)?;
}
}
}With a volunteer impl like: pub async fn volunteer(&self) -> Result<Candidate, ElectionError> {...}So the candidate only exists after you volunteered and This will help with a couple of your TODOs:
Some other things:
Malte |
|
Hi, @maltesander! Thank you for the thorough review! I'd like to argue a bit here and defend the oneshot approach. The "Resigned" state that you mentioned in your code snippet is not a valid state at all and should not be exposed to users. You have the reason in your comment there: In fact, if users really want to stop participating they should drop their connection like I did in the test. The connection and the participation should be always be coupled and it should not be possible to drop one without dropping the other. This way there'll be no "orphan" ephemeral nodes. And the way to do it is the one-shot approach: you either are the leader, or wait for the leadership or drop the whole connection. |
|
Hi @rodio, yeah, i agree that "Resigned" is wrong and we shouldnt expose "Withdrawal". Id still argue about involuntary withdrawal? E.g. Session loss (which the candidate did not choose)? Consider: A candidate becomes leader, fires Leader on the oneshot. Application starts doing leader work. A little later, the ZK session expires (network partition, GC pause, ZK server failover, whatever). The ephemeral znode is gone. Some other node is now the leader. The original application is still doing leader work, because nobody told it otherwise. The oneshot already fired and can't fire again. Without withdrawal, if we go with the oneshot we should document explicitly that we have to watch connect() for session expiry. Then it should treat itself as no longer Leader, regardless of the oneshot? Im fine with both approaches. Malte |
|
I think you're right that we need to monitor session loss after and I think it is better to hide it inside the implementation of the recipe because it probably is something that almost everyone needs. I'll think more about it later and we'll see what I can come up with. Thanks! |
I am implementing the leader election recipe from #10 and would appreciate early feedback to see if it makes sense.
I thought that a nice API for leader would be similar to watches: the leader election function would return a receiving end of a oneshot channel so that users could use it to check if a node is a leader. There would no way to "withdraw" yourself from leader election process once you've volunteered to be a leader because I think that no one needs it in practice.
So now that I have a somewhat working draft, I'd appreciate if someone could give feedback on this API and function signatures or really anything. Eventually I would like to resolve all these TODOs hide this recipe behind a feture flag.
To test it:
./bin/zkCli.sh -server localhost:2181 create /election ""cargo test election_works -- --nocapture