When coding on ruaft I adopted this "state transfer" style API to allow a graceful shutdown. To make the shutdown "safe" I insisted on taking the Raft instance away when the API is called, like the following.
impl Raft {
pub fn kill(self) {
// do things.
}
}
After all, the instance is shutdown, there is no use with the Raft object itself anymore.
Later I used my Raft implementation and this API in a key-value store. My server must stay around when the underlying Raft instance is being killed. Thus we must support a state where "kill()
has been called on the Raft instance, but shutdown is not done yet". Essentially I had to wrap the Raft instance in an Option
.
struct KvServer {
raft: Mutex<Option<Raft>>
}
This has caused all kinds of pain for me. For example I had to use Option::unwrap()
everywhere during normal operations. And where is there an extra Mutex
in there? Well KvServer
has to be thread safe so the mutability must be kept internal.
Then I realized I did something wrong with the original Raft
API. I pushed the burden of managing a "shutdown" state to the client. What I really should have done is support that directly in Raft itself.
enum RaftInner {
Running(RaftRunningData),
Shutdown(RaftJoinHandler),
}
struct Raft {
inner: RaftInner,
}
impl Raft {
pub fn kill(&mut self) {
// do things.
self.inner = RaftInner::Shutdown(...);
}
}
Here is my conclusion: from an API design point of view, a mutable object plus internal state transfer is much better than the "state transfer API" I originally had. We could even throw in a Mutex
there to remove the mut
requirement of kill()
.
Of course there are even better ways of designing this API. I'll post a more comprehensive analysis in a followup post.