Redis Integration Guide

Armature provides a unified Redis integration through armature-redis with connection pooling, pub/sub, and dependency injection.

Table of Contents

Overview

The armature-redis crate provides a centralized Redis client that other Armature crates depend on. This ensures:

  • Single Connection Pool: All crates share one pool
  • DI Integration: Redis is injected from the DI container
  • Consistent Configuration: One place to configure Redis
  • Connection Reuse: Efficient resource utilization

Features

  • โœ… Connection pooling with bb8
  • โœ… Pub/Sub messaging
  • โœ… Cluster support
  • โœ… TLS encryption
  • โœ… Sentinel support
  • โœ… DI container integration
  • โœ… Convenience methods for common operations

Installation

[dependencies]
armature-redis = "0.1"

With optional features:

[dependencies]
armature-redis = { version = "0.1", features = ["cluster", "tls"] }

Basic Usage

use armature_redis::{RedisService, RedisConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Configure Redis
    let config = RedisConfig::builder()
        .url("redis://localhost:6379")
        .pool_size(10)
        .build();

    // Create service
    let redis = RedisService::new(config).await?;

    // Use convenience methods
    redis.set_value("key", "value").await?;
    let value: Option<String> = redis.get_value("key").await?;

    // Or get a connection for raw commands
    let mut conn = redis.get().await?;
    redis::cmd("HSET")
        .arg("hash")
        .arg("field")
        .arg("value")
        .query_async(&mut *conn)
        .await?;

    Ok(())
}

Connection Pooling

The service manages a connection pool automatically:

use armature_redis::{RedisService, RedisConfig};

let config = RedisConfig::builder()
    .url("redis://localhost:6379")
    .pool_size(20)        // Max connections
    .min_idle(Some(5))    // Keep at least 5 idle
    .connection_timeout(Duration::from_secs(5))
    .build();

let redis = RedisService::new(config).await?;

// Get pool statistics
let stats = redis.pool_stats();
println!("Connections: {}, Idle: {}", stats.connections, stats.idle_connections);

Pub/Sub

Redis pub/sub for real-time messaging:

use armature_redis::{RedisService, RedisConfig};

let redis = RedisService::new(config).await?;
let pubsub = redis.pubsub()?;

// Subscribe to a channel
let mut subscription = pubsub.subscribe("events").await?;

// Spawn task to receive messages
tokio::spawn(async move {
    while let Some(message) = subscription.recv().await {
        println!("Received: {} on {}", message.payload, message.channel);
    }
});

// Publish messages
pubsub.publish("events", "Hello, World!").await?;

Pattern subscriptions:

// Subscribe to all channels matching "user:*"
let mut subscription = pubsub.psubscribe("user:*").await?;

Dependency Injection

Register Redis as a singleton in your application:

use armature_framework::prelude::*;
use armature_redis::{RedisService, RedisConfig};
use std::sync::Arc;

#[module]
struct RedisModule;

#[module_impl]
impl RedisModule {
    #[provider(singleton)]
    async fn redis_service() -> Arc<RedisService> {
        let config = RedisConfig::from_env().build();
        Arc::new(RedisService::new(config).await.unwrap())
    }
}

// Use in controllers
#[controller("/data")]
struct DataController;

#[controller_impl]
impl DataController {
    #[get("/:key")]
    async fn get_data(
        &self,
        #[inject] redis: Arc<RedisService>,
        key: Path<String>,
    ) -> Result<Json<Value>, HttpError> {
        let value: Option<String> = redis.get_value(&key).await?;
        Ok(Json(json!({ "value": value })))
    }
}

Using with Other Crates

Other Armature crates automatically use armature-redis when the redis feature is enabled:

armature-cache

use armature_cache::RedisCache;
use armature_redis::RedisService;
use std::sync::Arc;

#[module_impl]
impl CacheModule {
    #[provider(singleton)]
    fn redis_cache(redis: Arc<RedisService>) -> RedisCache {
        RedisCache::from_service(redis)
    }
}

armature-queue

use armature_queue::Queue;
use armature_redis::RedisService;

#[module_impl]
impl QueueModule {
    #[provider(singleton)]
    fn job_queue(redis: Arc<RedisService>) -> Queue {
        Queue::from_service(redis, "jobs")
    }
}

armature-ratelimit

use armature_ratelimit::RedisRateLimiter;
use armature_redis::RedisService;

#[module_impl]
impl RateLimitModule {
    #[provider(singleton)]
    fn rate_limiter(redis: Arc<RedisService>) -> RedisRateLimiter {
        RedisRateLimiter::from_service(redis)
    }
}

armature-distributed

use armature_distributed::{DistributedLock, LeaderElection};
use armature_redis::RedisService;

#[module_impl]
impl DistributedModule {
    #[provider(singleton)]
    fn distributed_lock(redis: Arc<RedisService>) -> DistributedLock {
        DistributedLock::from_service(redis)
    }

    #[provider(singleton)]
    fn leader_election(redis: Arc<RedisService>) -> LeaderElection {
        LeaderElection::from_service(redis, "my-service")
    }
}

armature-session

use armature_session::RedisSessionStore;
use armature_redis::RedisService;

#[module_impl]
impl SessionModule {
    #[provider(singleton)]
    fn session_store(redis: Arc<RedisService>) -> RedisSessionStore {
        RedisSessionStore::from_service(redis)
    }
}

Configuration

Environment Variables

# Required
REDIS_URL=redis://localhost:6379

# Optional
REDIS_POOL_SIZE=10
REDIS_DATABASE=0
REDIS_USERNAME=default
REDIS_PASSWORD=secret
REDIS_TLS=true
REDIS_CLUSTER=true
REDIS_CLUSTER_NODES=node1:6379,node2:6379,node3:6379

Programmatic Configuration

let config = RedisConfig::builder()
    .url("redis://localhost:6379")
    .pool_size(20)
    .min_idle(5)
    .database(1)
    .username("user")
    .password("secret")
    .connection_timeout(Duration::from_secs(5))
    .command_timeout(Duration::from_secs(30))
    .tls(true)
    .connection_name("my-app")
    .build();

Cluster Mode

let config = RedisConfig::builder()
    .cluster_nodes(vec![
        "redis://node1:6379".to_string(),
        "redis://node2:6379".to_string(),
        "redis://node3:6379".to_string(),
    ])
    .build();

Best Practices

1. Register as Singleton

Always register RedisService as a singleton to share the connection pool:

#[provider(singleton)]  // Important!
async fn redis_service() -> Arc<RedisService> {
    // ...
}

2. Use Environment Configuration

// Prefer environment-based config for flexibility
let config = RedisConfig::from_env().build();

3. Use Convenience Methods

The service provides convenience methods for common operations:

// Instead of raw commands
redis.set_value("key", "value").await?;
redis.get_value::<String>("key").await?;
redis.delete("key").await?;
redis.incr("counter", 1).await?;
redis.hset("hash", "field", "value").await?;

4. Handle Connection Errors

match redis.get_value::<String>("key").await {
    Ok(Some(value)) => println!("Found: {}", value),
    Ok(None) => println!("Key not found"),
    Err(e) if e.is_retryable() => {
        // Retry logic
    }
    Err(e) => return Err(e.into()),
}

5. Use Pub/Sub for Real-Time Events

// Publisher
redis.pubsub()?.publish("events", &event_json).await?;

// Subscriber
let mut sub = redis.pubsub()?.subscribe("events").await?;
while let Some(msg) = sub.recv().await {
    handle_event(&msg.payload);
}

Summary

Key Concepts:

  1. Centralized Redis: armature-redis is the single source for Redis connections
  2. Connection Pooling: Efficient connection management with bb8
  3. DI Integration: Register once, inject everywhere
  4. Crate Integration: All Redis-dependent crates use armature-redis
  5. Pub/Sub: Built-in messaging support

Crates Using armature-redis:

Crate Purpose
armature-cache Redis caching backend
armature-queue Job queue storage
armature-distributed Locks, leader election
armature-ratelimit Rate limiting storage
armature-session Session storage

Configuration Quick Reference:

REDIS_URL=redis://localhost:6379
REDIS_POOL_SIZE=10
REDIS_PASSWORD=secret