use std::env; #[derive(Debug, Clone)] pub struct Config { pub valkey_url: String, pub frame_stream: String, /// Where entries go that must not be retried: a malformed task, or one /// that has been redelivered too many times. A separate stream so a /// poison message can neither block the queue nor vanish. pub dead_letter_stream: String, pub consumer_group: String, pub consumer_name: String, /// How long XREADGROUP blocks waiting for new entries. pub block_ms: usize, /// How long an unacknowledged entry must sit before another consumer may /// claim it. Must comfortably exceed the time it takes to process one /// frame, or two consumers will work the same entry. pub claim_idle_ms: usize, /// How many times an entry may be delivered before it is dead-lettered. /// Without a cap, an entry that always fails is redelivered forever. pub max_deliveries: usize, } impl Config { pub fn from_env() -> anyhow::Result { Ok(Self { valkey_url: env_or("CONTINUUM_VALKEY_URL", "redis://localhost:6379"), frame_stream: env_or("CONTINUUM_FRAME_STREAM", "stream:camera_frames"), dead_letter_stream: env_or( "CONTINUUM_DEAD_LETTER_STREAM", "stream:camera_frames:dead", ), consumer_group: env_or("CONTINUUM_CONSUMER_GROUP", "ai-worker"), consumer_name: env_or("CONTINUUM_CONSUMER_NAME", "ai-worker-1"), block_ms: env_num("CONTINUUM_BLOCK_MS", 5_000)?, claim_idle_ms: env_num("CONTINUUM_CLAIM_IDLE_MS", 60_000)?, max_deliveries: env_num("CONTINUUM_MAX_DELIVERIES", 5)?, }) } } fn env_or(key: &str, default: &str) -> String { env::var(key).unwrap_or_else(|_| default.to_string()) } fn env_num(key: &str, default: usize) -> anyhow::Result { match env::var(key) { Err(_) => Ok(default), // Refuse rather than fall back to the default: a typo in a tuning // value should not quietly run with different tuning. Ok(raw) => raw .parse() .map_err(|_| anyhow::anyhow!("{key} must be a non-negative integer, got '{raw}'")), } }