Redis, Local Cache & Queue Drivers
Rullst v12 keeps cache and queue backends explicit. Enabling a Cargo feature
only compiles the adapter; it does not inspect REDIS_URL, switch drivers, or
silently fall back when Redis is unavailable.
Cache choices
Cache::memory() uses a process-local DashMap. Values disappear on restart
and are not shared between replicas:
#![allow(unused)]
fn main() {
use rullst_core::cache::{Cache, CacheError};
use std::sync::Arc;
async fn load_profile(cache: &Cache) -> Result<Arc<String>, CacheError> {
cache
.remember("profile:42", 300, || async {
Ok("serialized profile".to_string())
})
.await
}
async fn read_profile() -> Result<(), CacheError> {
let cache = Cache::memory();
let profile = load_profile(&cache).await?;
let _ = profile;
Ok(())
}
}
For a shared Redis cache, enable cache-redis (or the umbrella redis
feature) and construct the adapter explicitly:
[dependencies]
rullst-core = { version = "12.0.0-rc.1", features = ["cache-redis"] }
#![allow(unused)]
fn main() {
use rullst_core::cache::Cache;
async fn cache_featured_catalog() -> Result<(), Box<dyn std::error::Error>> {
let redis_url = std::env::var("REDIS_URL")?;
let cache = Cache::redis(redis_url)?;
cache.put("catalog:featured", "[...]", Some(600)).await?;
Ok(())
}
}
Constructing the driver validates the Redis URL but does not establish a
connection. Operations open a multiplexed async connection and return a typed
CacheError if Redis is unavailable. Choose an application-specific policy:
fail startup, retry with bounds, or explicitly select Cache::memory() for a
documented single-instance development mode.
The built-in Redis cache prefixes keys with rullst:cache:. flush() scans and
unlinks keys under that prefix; use dedicated credentials/database boundaries
when multiple applications share a Redis service.
ORM .remember(...) queries
The ORM has a separate opt-in query-cache contract behind its redis feature:
[dependencies]
rullst-orm = { version = "12.0.0-rc.1", features = ["redis"] }
#![allow(unused)]
fn main() {
use rullst_orm::{FromRow, Orm};
#[derive(Debug, Clone, FromRow, Orm)]
#[orm(table = "users")]
struct User {
id: i32,
active: bool,
}
async fn load_recent_users() -> Result<(), Box<dyn std::error::Error>> {
let redis_url = std::env::var("REDIS_URL")?;
Orm::init_redis_with_namespace(&redis_url, "academy-production").await?;
let recent = User::query()
.where_eq("active", true)
.remember(30)
.get()
.await?;
let _ = recent;
Ok(())
}
}
Use a stable, unique namespace for every application that shares a Redis
database. Query keys bind that namespace, an opaque digest of the active tenant
scope, table, generated SQL and typed bindings. They do not expose raw tenant
identifiers. The older Orm::init_redis(url) API remains available and uses
default; only use it with a dedicated Redis database.
The failure and consistency rules are explicit:
remember(0)is rejected.- Missing Redis initialization is a configuration error for a remembered query outside a transaction.
- Redis command failures or corrupt JSON fall back to the authoritative database; a successful read is returned even if cache population fails.
- Explicit and task-scoped ORM transactions always bypass query cache.
- Generated model saves/deletes invalidate the active tenant/table’s remembered results only after a managed commit; rollback keeps existing cache entries. Raw SQL, bulk builders and writes from another process cannot be inferred. Keep defensive TTLs and do not cache authorization or reads that require a stronger distributed consistency contract.
The Core Cache facade and ORM query cache use different keyspaces and APIs;
initializing one does not initialize the other.
Queue choices
Rullst provides explicit SQLite and Redis queue constructors:
[dependencies]
rullst-core = { version = "12.0.0-rc.1", features = ["queue-sqlite"] }
serde_json = "1"
#![allow(unused)]
fn main() {
use rullst_core::queue::Queue;
use serde_json::json;
async fn enqueue_receipt() -> Result<(), Box<dyn std::error::Error>> {
let queue = Queue::sqlite("sqlite://jobs.sqlite?mode=rwc").await?;
let job_id = queue
.dispatch("send_receipt", json!({ "invoice_id": 42 }))
.await?;
println!("queued {job_id}");
Ok(())
}
}
With queue-redis, construct Queue::redis(redis_url) instead. The Redis
driver uses atomic Lua transitions for pending, processing, failed, and
dead-letter state. Production validation must still cover Redis persistence,
eviction policy, credentials/TLS, failover, monitoring, and worker recovery in
the target topology.
There is no automatic interchange between the SQLite and Redis queues: they store independent state. Switching a live deployment requires an explicit drain/migration plan.
Real-time boundary
Core’s current WebSocket broadcast/presence helpers are process-local. Redis Streams, Redis Pub/Sub, Kafka, and RabbitMQ transports remain roadmap work; do not describe the cache or queue adapter as cross-instance real-time sync.
Deployment checklist
- Choose the backend in application configuration and make fallback policy explicit.
- Never commit Redis credentials; prefer TLS and least-privilege network access.
- Namespace application/tenant keys above the built-in driver prefix where
isolation is required.
TenantCachesupplies validated tenant namespaces. - Test disconnects, timeouts, retries, eviction, restart, and worker recovery.
- Benchmark the deployed service. Rullst does not claim universal cache latency, memory usage, or infrastructure cost.