Expand description
Defines types related to the clients structs and their dependencies:
Client, ExclusiveClient, PooledClientManager, Pipeline, Transaction
and how to configure them
§Clients
The central object in rustis is the Client.
It will allow you to connect to the Redis server, to send command requests and to receive command responses and push messages.
There are 3 ways to use a connection
- As a single client
- As a multiplexer
- In a pool of clients
Two of them share the connection, and a shared connection cannot run the
commands that hold it: those live on ExclusiveClient, the client that owns
its connection — see Exclusive commands.
§The single client
The single Client maintains a unique connection to a Redis Server or cluster.
This use case of the client is not meant to be used directly in a Web application, where multiple HTTP connections access the Redis server at the same time in a multi-threaded architecture (like Actix or Rocket).
It could be used in tools where the load is minimal.
use rustis::{
client::Client,
commands::{FlushingMode, ServerCommands, StringCommands},
Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::connect("127.0.0.1:6379").await?;
client.flushdb(FlushingMode::Sync).await?;
client.set("key", "value").await?;
let value: String = client.get("key").await?;
println!("value: {value:?}");
Ok(())
}§The multiplexer
A Client instance can be cloned, allowing multiple requests
to be sent concurrently on the same underlying connection.
Multiplexer mode is highly efficient in multi-threaded architectures because it uses only a single underlying connection. It is the preferred mode for most Web applications.
§Managing Multiplexed Subscriptions
Because rustis implements the RESP3 protocol, there is no limitation when using subscriptions on a multiplexed connection. Pub/Sub messages and regular command responses are cleanly distinguished at the protocol level, allowing both to coexist safely on the same shared connection.
§Limitations
Blocking commands and watch hold or
attach state to the connection, so they are not available on a multiplexed Client —
see Exclusive commands.
§The pooled client manager
The pooled client manager holds a pool of Clients, based on bb8.
Each time a new command must be sent to the Redis Server, a client will be borrowed temporarily to the manager and automatically given back to it at the end of the operation.
It is an alternative to multiplexing, for managing rustis within a Web application.
The manager can be configured via bb8 with a various of options like maximum size, maximum lifetime, etc.
For you convenience, bb8 is reexported from the rustis crate.
That re-export, and the bb8::ManageConnection implementation behind it, put bb8 in rustis’
public API: a bb8 major release breaks rustis’ semver on its own, and a dependency graph holds
exactly one bb8 version.
#[cfg(feature = "pool")]
use rustis::{
client::PooledClientManager, commands::StringCommands,
};
use rustis::Result;
#[tokio::main]
async fn main() -> Result<()> {
#[cfg(feature = "pool")] {
let manager = PooledClientManager::new("127.0.0.1:6379")?;
let pool = rustis::bb8::Pool::builder()
.max_size(10)
.build(manager).await?;
let client1 = pool.get().await.unwrap();
client1.set("key1", "value1").await?;
let value: String = client1.get("key1").await?;
println!("value: {value:?}");
let client2 = pool.get().await.unwrap();
client2.set("key2", "value2").await?;
let value: String = client2.get("key2").await?;
println!("value: {value:?}");
}
Ok(())
}§Exclusive commands
Two families of commands are incompatible with a shared connection:
- blocking commands, which monopolize the
connection until they return — every other caller queued behind a
BLPOPwith a 30-second timeout waits 30 seconds; watch, whose watched state applies to the connection itself, not to the handle that issued it, so any clone can invalidate or discard it.
They are therefore not implemented for Client, which is clonable, but for
ExclusiveClient, which is not. Reaching for one of them on a multiplexed
client is a compile error rather than a stalled connection at run time.
use rustis::{
client::{Client, ExclusiveClient},
commands::{BlockingCommands, TransactionCommands},
Result,
};
async fn example() -> Result<()> {
// A connection of its own …
let client = ExclusiveClient::connect("127.0.0.1:6379").await?;
// … or an existing client, which the conversion refuses while another
// handle on the connection is alive.
let client = Client::connect("127.0.0.1:6379").await?.into_exclusive()?;
let result: Option<(String, String)> = client.blpop("key", 30.).await?;
client.watch("key").await?;
Ok(())
}A PooledClientManager hands out an ExclusiveClient for the same reason:
a borrowed connection returns to the pool only once the command completes, so
the block stays confined to it.
No other command is refused. Pub/sub is fine on a multiplexed connection —
RESP3 keeps messages and command replies apart — and so are MULTI/EXEC
transactions, which Client::create_transaction still opens: of the two
families above, only WATCH attaches state to the connection. A handful of
commands do attach state without being incompatible with sharing; they are the
subject of the next section.
§Connection-scoped commands
Some commands configure the connection instead of acting on data: select,
auth, client_setname, client_setinfo, client_no_evict, client_no_touch,
client_tracking, client_reply and script_debug.
A Client is clonable, and one connection carries the commands of every clone.
These commands therefore apply to every clone: client.select(5) moves the
commands of all clones to database 5.
The client records each of them and replays it after a reconnection, so the state survives a broken socket. It cannot scope the state to one clone, because the connection is the scope.
Set the database with Config::database.
Set the credentials with Config::username,
Config::password or
Config::credentials_provider. The
handshake sends them on every connection. An ExclusiveClient owns its
connection, which scopes these commands to the caller.
§Configuration
A Client instance can be configured with the Config struct:
- Authentication
TlsConfigServerConfig(Standalone, Sentinel or Cluster)
IntoConfig is a convenient trait to convert more known types to a Config instance:
- &
str: host and port separated by a colon (impl Into<String>, u16): a pair of host and portString: host and port separated by a colonUrl: see Url syntax below.
§Url Syntax
The rustis Config can also be built from an URL
§Standalone
redis|rediss://[[<username>]:<password>@]<host>[:<port>][/<database>]§Cluster
redis|rediss[+cluster]://[[<username>]:<password>@]<host1>[:<port1>][,<host2>:[<port2>][,<hostN>:[<portN>]]]§Sentinel
redis|rediss[+sentinel]://[[<username>]:<password>@]<host>[:<port>]/<service>[/<database>]
[?wait_between_failures=<250>[&sentinel_username=<username>][&sentinel_password=<password>]]service is the required name of the sentinel service
§Unix socket
unix://<path>[?db=<database>]path is the absolute path of the socket the server listens on. It is the whole path of the
URI, so the database is the db query parameter here rather than the last path segment, and
there is no authority to carry credentials: set Config::username / Config::password on
the config. keep_alive and no_delay describe a TCP
socket and are not applied.
§Schemes
The URL scheme is used to detect the server type:
redis://- Non secure TCP connection to a standalone Redis serverrediss://- Secure (TSL) TCP connection to a standalone Redis serverredis+sentinel://orredis-sentinel://- Non secure TCP connection to a Redis sentinel networkrediss+sentinel://orrediss-sentinel://- Secure (TSL) TCP connection to a Redis sentinel networkredis+cluster://orredis-cluster://- Non secure TCP connection to a Redis clusterrediss+cluster://orrediss-cluster://- Secure (TSL) TCP connection to a Redis clusterunix://,redis+unix://orredis-unix://- Connection to a standalone Redis server listening on a Unix domain socket
§QueryParameters
Query parameters set optional configuration fields of the struct Config or its
dependencies. The list below is exhaustive: an unknown parameter, or a value that
does not parse, is rejected with an error rather than ignored. A parameter marked
for one server type below is read only on that type’s scheme, so writing it on any
other is an error naming the URI it belongs to.
connect_timeout- The time to attempt a connection before timing out (default10,000ms).command_timeout- If a command does not return a reply within a set number of milliseconds, a timeout error will be thrown. If set to 0, no timeout is apply (default0).auto_resubscribe- When the client reconnects, channels subscribed in the previous connection will be resubscribed automatically ifauto_resubscribeistrue(defaulttrue).auto_remonitor- When the client reconnects, if inmonitormode, themonitorcommand will be resent automaticallyconnection_name- Set the name of the connection to make it easier to identify the connection in client list.keep_alive- Idle time before the TCP keep-alive probes start, orNone(keep_alive=0in a URL) to disable keep-alive (default30s).no_delay- Enable/disable the use of Nagle’s algorithm (defaulttrue)retry_on_error- Defines the default strategy for retries on network error (defaultfalse).max_command_attempts- Maximum number of times a command is sent before giving up (default5).buffers.read_capacity,buffers.tape_capacity,buffers.shrink_factor,buffers.shrink_hysteresis- Sizing and recycling policy of the connection’s buffers, one parameter per field ofBufferConfig.backpressure.max_queued_bytes,backpressure.max_pubsub_bytes,backpressure.max_push_bytes- Memory budgets, in bytes,0to disable one.limits.max_nesting_depth,limits.max_bulk_length,limits.max_collection_length- What the RESP parser accepts from the server.reconnection- The reconnection policy:constant,linearorexponential. Its fields follow, each one spelledreconnection.<field>:max_attempts(default0, retry forever) andjitter(default100ms) on all three,delay(default1000ms) onconstant,delayandmax_delayonlinear,min_delay,max_delayandmultiplicative_factoronexponential. A field the policy does not carry is rejected, and the ones with no default are required:max_delayclamps the delay, so a policy given none would reconnect with no backoff at all.ReconnectionConfig::Customis Rust code and has no URL spelling.read_preference- (Cluster only) Which node of a shard reads are routed to (defaultmaster).topology_refresh_interval- (Cluster only) How often the topology is reloaded on its own,0to reload it only on a redirection (default60000ms).wait_between_failures- (Sentinel only) Waiting time after failing before connecting to the next Sentinel instance (default250ms).sentinel_username- (Sentinel only) Sentinel usernamesentinel_password- (Sentinel only) Sentinel passworddb- (Unix socket only) The default database, which the TCP schemes spell as the last path segment instead (default0).
§Rotating credentials
A password embedded in an URL is fixed for the life of the client. When the password is a
short-lived token (AWS ElastiCache IAM, GCP Memorystore IAM, Azure Entra ID, Vault), set
Config::credentials_provider instead: it is consulted at every handshake, so each
reconnection authenticates with the current token. SentinelConfig::credentials_provider does
the same for the Sentinel instances themselves. Neither has a URL representation; both are set on
the config itself. See CredentialsProvider.
§Supplying the transport
ServerConfig::Custom takes a TransportFactory, which hands the client a byte stream to
speak RESP over instead of one it opens itself: an in-memory pipe, a tunnel, an
SSH-forwarded channel, a TLS stack configured elsewhere. It is asked for a stream at every dial,
so a reconnection gets a fresh one. Like a credentials provider, it has no URL representation.
use rustis::client::{Config, CustomTransport, ServerConfig, TransportReader, TransportWriter};
let mut config = Config::default();
config.server = ServerConfig::Custom(CustomTransport::new(|| async {
let (client_side, server_side) = tokio::io::duplex(4096);
// drive `server_side` with a server of your own here
drop(server_side);
let (reader, writer) = tokio::io::split(client_side);
Ok((Box::new(reader) as TransportReader, Box::new(writer) as TransportWriter))
}));§Example
use rustis::{client::Client, resp::cmd, Result};
#[tokio::main]
async fn main() -> Result<()> {
// standalone, host=localhost, port=6379 (default), database=1
let client = Client::connect("redis://localhost/1").await?;
Ok(())
}§Cancellation and timeouts
Dropping the future of a command does not cancel the command. By the time the future is awaited, the message is already in the send queue: it is written to the connection and executed by the server. Dropping the future only discards the reply.
This is what happens on every tokio::time::timeout, every select! branch that loses, every
JoinHandle::abort, every HTTP request cancelled by the client — and on
Config::command_timeout, which is itself a timeout
around the wait for the reply.
So a timeout error tells you nothing about whether the command ran. For an idempotent command
(GET, SET of a constant, DEL) that is harmless; for INCR, LPUSH, XADD or a Lua script
with side effects, the effect may have happened, and retrying applies it twice.
The reason is the protocol: commands are pipelined on a shared connection, and replies come back
in the order the commands were sent. A message already handed to the connection cannot be pulled
back, and nothing in RESP cancels a command in flight.
Config::retry_on_error and
Config::max_command_attempts add the same
duplication on a network error: a command that reached the server before the connection broke is
sent again.
Design the commands you put a deadline on to be idempotent, or check the state after a timeout instead of assuming the command did not run.
use rustis::{client::Client, commands::StringCommands, Result};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::connect("127.0.0.1:6379").await?;
match tokio::time::timeout(Duration::from_millis(10), client.incr("counter")).await {
Ok(result) => {
let value: i64 = result?;
println!("counter: {value}");
}
// The reply never came back in time. The counter may still have been
// incremented: incrementing again here would count twice.
Err(_elapsed) => {
let value: i64 = client.get("counter").await?;
println!("counter after timeout: {value}");
}
}
Ok(())
}§Pipelining
One of the most performant Redis feature is pipelining. This allow to optimize round-trip times by batching Redis commands.
§API description
You can create a pipeline on a Client instance by calling the associated function create_pipeline.
Be sure to store the pipeline instance in a mutable variable because a pipeline requires an exclusive access.
Once the pipeline is created, you can use exactly the same commands that you would directly use on a client instance.
This is possible because the Pipeline implements all the built-in command traits.
The main difference, is that you have to choose for each command:
- to
queueit, meaning that thePipelineinstance will queue the command in an internal queue to be able to send later the batch of commands to the Redis server. - to
forgetit, meaning that the command will be queued as well BUT its response won’t be awaited by thePipelineinstance
Finally, call the execute associated function.
It is the caller’s responsibility to use the right type to cast the server response to the right tuple or collection depending on which command has been queued or forgotten.
The most generic type that can be requested as a result is Vec<resp::Value>
§Example
use rustis::{
client::{Client, Pipeline, BatchPreparedCommand},
commands::StringCommands,
resp::{cmd, Value}, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::connect("127.0.0.1:6379").await?;
let mut pipeline = client.create_pipeline();
pipeline.set("key1", "value1").forget();
pipeline.set("key2", "value2").forget();
pipeline.get::<()>("key1").queue();
pipeline.get::<()>("key2").queue();
let (value1, value2): (String, String) = pipeline.execute().await?;
assert_eq!("value1", value1);
assert_eq!("value2", value2);
Ok(())
}§Transactions
Redis Transactions allow the execution of a group of commands in a single step.
All the commands in a transaction are serialized and executed sequentially. A request sent by another client will never be served in the middle of the execution of a Redis Transaction. This guarantees that the commands are executed as a single isolated operation.
§API description
You can create a transaction on a client instance by calling the associated function create_transaction.
Be sure to store the transaction instance in a mutable variable because a transaction requires an exclusive access.
Once the transaction is created, you can use exactly the same commands that you would directly use on a client instance.
This is possible because the Transaction implements all the built-in command traits.
The main difference, is that you have to choose for each command:
- to
queueit, meaning that theTransactioninstance will queue the command in an internal queue to be able to send later the batch of commands to the Redis server. - to
forgetit, meaning that the command will be queued as well BUT its response won’t be awaited by theTransactioninstance.
Finally, call the execute associated function.
It is the caller’s responsibility to use the right type to cast the server response to the right tuple or collection depending on which command has been queued or forgotten.
The most generic type that can be requested as a result is Vec<resp::Value>
§Example
use rustis::{
client::{Client, Transaction, BatchPreparedCommand},
commands::StringCommands,
resp::{cmd, Value}, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = Client::connect("127.0.0.1:6379").await?;
let mut transaction = client.create_transaction();
transaction.set("key1", "value1").forget();
transaction.set("key2", "value2").forget();
transaction.get::<()>("key1").queue();
let value: String = transaction.execute().await?;
assert_eq!("value1", value);
Ok(())
}§Pub/Sub
Pub/Sub is a Redis architecture were senders can publish messages into channels
and subscribers can subscribe by channel names or patterns to receive messages.
§Publishing
To publish a message, you can call the publish
associated function on its dedicated trait.
It also possible to use the sharded flavor of the publish function: spublish.
§Subscribing
rustis implements subsribing through an async Stream.
You can create a PubSubStream by calling subscribe,
psubscribe, or ssubscribe.
Then by calling next on the pub/sub stream, you can
wait for an incoming message in the form of the struct PubSubMessage.
You can also create a PubSubStream without an upfront subscription by calling create_pub_sub.
§Managing Multiplexed Subscriptions
Because rustis implements the RESP3 protocol, there is no limitation when using subscriptions on a multiplexed connection. Pub/Sub messages and regular command responses are cleanly distinguished at the protocol level, allowing both to coexist safely on the same shared connection.
§Simple Example
use rustis::{
client::{Client, ClientPreparedCommand},
commands::{FlushingMode, PubSubCommands, ServerCommands},
resp::{cmd, Value}, Result,
};
use futures_util::StreamExt;
#[tokio::main]
async fn main() -> Result<()> {
let subscribing_client = Client::connect("127.0.0.1:6379").await?;
let publishing_client = Client::connect("127.0.0.1:6379").await?;
subscribing_client.flushdb(FlushingMode::Sync).await?;
// Create a subscription from the subscribing client:
let mut pub_sub_stream = subscribing_client.subscribe("mychannel").await?;
// The publishing client publishes a message on the channel:
publishing_client.publish("mychannel", "mymessage").await?;
// Let's now iterate over messages received:
while let Some(Ok(message)) = pub_sub_stream.next().await {
assert_eq!("mychannel", message.channel_str()?);
assert_eq!("mymessage", message.payload_as::<&str>()?);
break;
}
Ok(())
}Once the stream has been created, it is still possible to add additional subscriptions
by calling subscribe, psubscribe
or ssubscribe on the PubSubStream instance.
§Split Stream Example
To make it easy to modify subscriptions while iterating over messages, you can use the split method to
split the stream into sink and stream parts. Once this is done, you call subscribe
or unsubscribe (and related methods) on the sink while the split stream is used only for iteration. This can be useful
when you want to split ownership between async tasks.
use rustis::{
client::{Client, ClientPreparedCommand},
commands::{FlushingMode, PubSubCommands, ServerCommands},
resp::{cmd, Value}, Result,
};
use futures_util::StreamExt;
#[tokio::main]
async fn main() -> Result<()> {
let subscribing_client = Client::connect("127.0.0.1:6379").await?;
let regular_client = Client::connect("127.0.0.1:6379").await?;
regular_client.flushdb(FlushingMode::Sync).await?;
// This time we will split the stream into sink and stream parts:
let (mut sink, mut stream) = subscribing_client.subscribe("mychannel").await?.split();
// You can then subscribe or unsubscribe using the sink.
// Typically you would pass ownership of the sink to another async task.
sink.subscribe("otherchannel").await?;
sink.psubscribe("o*").await?;
regular_client.publish("mychannel", "mymessage").await?;
// Iterate over messages using the split stream:
while let Some(Ok(message)) = stream.next().await {
assert_eq!("mychannel", message.channel_str()?);
assert_eq!("mymessage", message.payload_as::<&str>()?);
break;
}
Ok(())
}Structs§
- Backpressure
Config - Memory budgets bounding what the client holds on behalf of a consumer that has stopped keeping up.
- Buffer
Config - Sizing and recycling policy for the buffers a connection keeps alive between commands: the read/write framing buffers and the RESP parse tape.
- Client
- Client
Stats - What a connection is doing, read from a live
Clientwithstats. - Client
Tracking Invalidation Stream - Cluster
Config - Configuration for connecting to a Redis
Cluster - Command
Future - The future
client.get("key").awaitdrives. - Config
- Configuration options for a
clientor apooled client - Credentials
- The credentials a connection authenticates with, as handed to the
HELLOhandshake. - Custom
Interceptor - A
CommandInterceptoras held byConfig::interceptor. - Custom
Reconnection Policy - A
ReconnectionPolicyas held byReconnectionConfig::Custom. - Custom
Transport - A
TransportFactoryas held byServerConfig::Custom. - Exclusive
Client - Client that owns its connection, and may therefore block it.
- Monitor
Stream - Stream to get
MONITORcommand events when the stream is dropped or closed, a reset command is sent to the Redis server - Monitored
Command Info - Result for the
monitorcommand. - Pipeline
- Represents a Redis command pipeline.
- Pooled
Client Manager pool - An object which manages a pool of clients, based on bb8
- Prepared
Command - Wrapper around a command about to be send with a marker for the response type and a few options to decide how the response send back by Redis should be processed.
- PubSub
Message - Pub/Sub Message that can be streamed from
PubSubStream - PubSub
Split Sink - A pub sub
Sinkpart of thesplitpair. It allows to subscribe/unsubscribe to/from channels or patterns - PubSub
Split Stream - A pub sub
Streampart of thesplitpair. It allows to get messages from the channels or patterns subscribed to - PubSub
Stream - Stream to get messages from the channels or patterns
subscribedto It allows also to subscribe/unsubscribe to/from channels or patterns - Resp
Limits - Limits the RESP parser enforces against hostile or corrupt server input.
- Sentinel
Config - Configuration for connecting to a Redis server via
Sentinel - TlsConfig
rustls - Config for TLS.
- Transaction
- Represents an on-going
transactionon a specific client instance.
Enums§
- Close
Outcome - What
Client::closedid. - Monitored
Command Source - Origin of a command reported by
MONITOR. - Read
Preference - Which node of a shard a Cluster client reads from.
- Reconnection
Config - The type of reconnection policy to use. This will apply to every connection used by the client. This code has been mostly inspired by fred ReconnectPolicy
- Server
Config - Configuration for connecting to a Redis server
Traits§
- Batch
Prepared Command - Extension trait dedicated to
PreparedCommandto add specific methods for thePipeline& theTransactionexecutors - Client
Prepared Command - Extension trait dedicated to
PreparedCommandto add specific methods for theClientexecutor - Command
Interceptor - Sits between a caller and the wire, on every command the client sends.
- Credentials
Provider - A source of credentials consulted at every handshake: the initial connection and each reconnection.
- Into
Config - A value-to-
Configconversion that consumes the input value. - Reconnection
Policy - How long to wait before the next reconnection attempt, and when to stop trying.
- Transport
Factory - A source of byte streams the client speaks RESP over, in place of the TCP connection it would otherwise open itself.
Functions§
- prepare_
command - Shortcut function to creating a
PreparedCommand.
Type Aliases§
- Transport
Reader - The reading half of a transport handed to the client.
- Transport
Writer - The writing half of a transport handed to the client.