Fred uses a decoupled architecture to separate request-response logic from connection management. This is achieved through a routing task (the connection manager) and a Client (the request interface).
The Routing Task
When you call client.connect(), it spawns a Tokio task that manages all connections to the servers. This task is responsible for:
- Managing private state (connections, retry buffers, replicas) via a
Router struct. - Listening for commands on a channel (
command_rx). - Handling connection-related events (errors, closures, reconnections) independently of individual requests.
The Client Interface
The Client is a thin, clonable wrapper around ClientInner. It provides the public API (e.g., get, set) by sending Command objects to the routing task via an unbounded channel (command_tx).
Request-Response Lifecycle
- The caller invokes a method (e.g.,
client.get(key)). - A
Command is created containing the command type, arguments, and a oneshot channel sender (tx). - The
Command is sent to the routing task. - The routing task processes the command and sends the response back to the caller via the
oneshot receiver (rx).
This model allows the client to handle connection failures or reconnections without requiring the user to manually manage connection state for every request.
// High-level pattern for request-response functions
impl Client {
pub async fn get<K: Into<Key>>(&self, key: K) -> Result<Value, Error> {
let (tx, rx) = oneshot_channel();
let command = Command {
kind: CommandKind::Get,
args: vec![key.into().into()],
tx
};
self.inner.command_tx.load().send(command);
rx.await.and_then(|f| f.into())
}
}