A mutable object is equivalent to a state transfer API

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.

Show Comments