Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Redis — Stress

Throughput benchmark for the Redis backend. Publishes a configurable burst of messages on a non-sequenced topic and consumes them through a coordinated consumer group with concurrent processing. Useful for sanity-checking client tuning (prefetch_count, max_consumers, response_timeout) against your Redis or Valkey deployment before rolling production traffic.

Prerequisites

  • Redis 6.2+ or Valkey running locally (see command below)
  • Cargo feature: redis-streams

Run

docker run --rm -p 6379:6379 redis:7-alpine
cargo run --example redis_stress --features redis-streams --release

Pass --release — the JSON codec and XADD batching show their real cost only with optimisations on.

Source

//! Stress benchmarks for the Redis Streams backend.
//!
//! Spins up a Redis testcontainer for the lifetime of the process. Requires a
//! running Docker daemon.
//!
//!     cargo run -q --example redis_stress --features redis-streams
//!     cargo run -q --example redis_stress --features redis-streams -- --tier moderate
 
#[path = "../common/stress_test.rs"]
mod harness;
 
use redis::AsyncCommands;
use shove::redis::{RedisConfig, RedisConsumerGroupConfig, RedisMode};
use shove::{Broker, Redis};
use testcontainers::ImageExt;
use testcontainers::runners::AsyncRunner;
use testcontainers_modules::redis::{REDIS_PORT, Redis as RedisImage};
 
use harness::{HarnessConfig, run_all_scenarios};
 
const STREAM_KEY: &str = "shove-stress-bench";
 
#[tokio::main]
async fn main() {
    let container = RedisImage::default()
        .with_tag("7.0")
        .start()
        .await
        .expect("failed to start Redis container");
    let port = container
        .get_host_port_ipv4(REDIS_PORT)
        .await
        .expect("failed to read Redis port");
    let url = format!("redis://127.0.0.1:{port}/");
 
    wait_until_ready(&url).await;
 
    let purge_url = url.clone();
    let purge: harness::PurgeFn = Box::new(move || {
        let url = purge_url.clone();
        Box::pin(async move {
            // Drop the stream itself — the next scenario's topology declare
            // recreates it together with the consumer group. XGROUP CREATE
            // uses MKSTREAM so this is safe.
            let Ok(client) = redis::Client::open(url) else {
                return;
            };
            let Ok(mut conn) = client.get_multiplexed_async_connection().await else {
                return;
            };
            let _: redis::RedisResult<i64> = conn.del(STREAM_KEY).await;
        })
    });
 
    let hcfg = HarnessConfig::<Redis>::new("redis").with_purge(purge);
    run_all_scenarios(
        hcfg,
        || {
            let url = url.clone();
            async move {
                Broker::<Redis>::new(RedisConfig::new(RedisMode::Standalone { url }))
                    .await
                    .expect("connect Redis")
            }
        },
        |consumers, prefetch, concurrent| {
            RedisConsumerGroupConfig::new(consumers..=consumers)
                .with_prefetch_count(prefetch)
                .with_concurrent_processing(concurrent)
        },
    )
    .await;
 
    // Explicit async cleanup. `ContainerAsync::Drop` spawns a background
    // task that may be aborted when the tokio runtime tears down, leaking
    // the Redis container. `rm()` runs synchronously here, so the container
    // is gone before `main` returns.
    container.rm().await.expect("remove Redis container");
}
 
/// Block until `PING` returns `PONG`. Testcontainers exits `.start()` once
/// Redis logs "Ready to accept connections", but the multiplexed-connection
/// handshake can still race; this confirms the server actually serves a
/// command before the first scenario starts measuring.
async fn wait_until_ready(url: &str) {
    let client = redis::Client::open(url).expect("build Redis probe client");
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
    loop {
        if let Ok(mut conn) = client.get_multiplexed_async_connection().await {
            let pong: redis::RedisResult<String> = redis::cmd("PING").query_async(&mut conn).await;
            if matches!(pong, Ok(ref s) if s == "PONG") {
                return;
            }
        }
        if std::time::Instant::now() >= deadline {
            panic!("Redis did not become ready within 30s");
        }
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    }
}