This document gives some background on how the library is structured and how to contribute.
- Run rustfmt and clippy before submitting any changes.
- Formatting should adhere to rustfmt.toml, i.e. by running
cargo +nightly fmt --all
- VS Code users should use the checked-in settings in the .vscode directory
- Formatting should adhere to rustfmt.toml, i.e. by running
- Please use conventional commits.
The code has the following structure:
- The commands folder contains the public interface and private implementation for each of the Redis commands, organized by category. This is roughly the same categorization used by the public docs. Each of these public command category interfaces are exposed as a trait with default implementations for each command.
- The clients folder contains public client structs that implement and/or override the traits
from the command category traits folder. The interfaces file contains the
shared traits required by most of the command category traits, such as
ClientLike
. - The monitor folder contains the implementation of the
MONITOR
command and the parser for the response stream. - The protocol folder contains the implementation of the base
Connection
struct and the logic for splitting a connection to interact with reader and writer halves in separate tasks. The TLS interface is also implemented here. - The router folder contains the logic that implements the sentinel and cluster interfaces. Clients
interact with this struct via a message passing interface. The interface exposed by the
Router
attempts to hide all the complexity associated with sentinel or clustered deployments. - The trace folder contains the tracing implementation. Span IDs are manually tracked on each command as they move across tasks.
- The types folder contains the type definitions used by the public interface. The Redis interface is relatively loosely typed but Rust can support more strongly typed interfaces. The types in this module aim to support an optional but more strongly typed interface for callers.
- The modules folder contains smaller helper interfaces such as a lazy Backchannel connection interface and the response type conversion logic.
This example shows how to add MGET
to the commands.
-
Add the new variant to the
RedisCommandKind
enum, if needed.pub enum RedisCommandKind { // ... Mget, // ... } impl RedisCommandKind { // .. pub fn to_str_debug(&self) -> &str { match *self { // .. RedisCommandKind::Mget => "MGET", // .. } } // .. pub fn cmd_str(&self) -> Str { match *self { // .. RedisCommandKind::Mget => "MGET" // .. } } // .. }
-
Create the private function implementing the command in src/commands/impls/keys.rs.
pub async fn mget<C: ClientLike>(client: &C, keys: MultipleKeys) -> Result<RedisValue, RedisError> { // maybe do some kind of validation utils::check_empty_keys(&keys)?; let frame = utils::request_response(client, move || { // time spent here will show up in traces in the `prepare_command` span Ok((RedisCommandKind::Mget, keys.into_values())) }) .await?; protocol_utils::frame_to_results(frame) }
Or use one of the shorthand helper functions or macros.
pub async fn mget<C: ClientLike>(client: &C, keys: MultipleKeys) -> Result<RedisValue, RedisError> { utils::check_empty_keys(&keys)?; args_values_cmd(client, keys.into_values()).await }
-
Create the public function in the src/commands/interfaces/keys.rs file.
// ... pub trait KeysInterface: ClientLike { // ... /// Returns the values of all specified keys. For every key that does not hold a string value or does not exist, the special value nil is returned. /// /// <https://redis.io/commands/mget> fn mget<R, K>(&self, keys: K) -> impl Future<Output=RedisResult<R>> + Send where R: FromRedis, K: Into<MultipleKeys> + Send, { async move { into!(keys); commands::keys::mget(self, keys).await?.convert() } } // ... }
-
Implement the interface on the client structs, if needed.
In the RedisClient file.
impl KeysInterface for RedisClient {}
In the transaction file.
impl KeysInterface for Transaction {}
Integration tests are in the tests/integration folder organized by category. See the tests README for more information.
Using MGET
as an example again:
-
Write tests in the keys file.
pub async fn should_mget_values(client: RedisClient, _: RedisConfig) -> Result<(), RedisError> { let expected: Vec<(&str, RedisValue)> = vec![("a{1}", 1.into()), ("b{1}", 2.into()), ("c{1}", 3.into())]; for (key, value) in expected.iter() { let _: () = client.set(key, value.clone(), None, None, false).await?; } let values: Vec<i64> = client.mget(vec!["a{1}", "b{1}", "c{1}"]).await?; assert_eq!(values, vec![1, 2, 3]); Ok(()) }
-
Call the tests from the centralized server tests.
mod keys { // .. centralized_test!(keys, should_mget_values); }
-
Call the tests from the cluster server tests.
mod keys { // .. cluster_test!(keys, should_mget_values); }
These macros will generate test wrapper functions to call your test 8 times based on the following options:
- Clustered vs centralized deployments
- Pipelined vs non-pipelined clients
- RESP2 vs RESP3 protocol modes