feat: add momentry_playground binary for development
- Add separate momentry_playground binary with distinct configuration - Production (momentry): Port 3002, Redis prefix 'momentry:' - Development (momentry_playground): Port 3003, Redis prefix 'momentry_dev:' - Add SERVER_PORT and REDIS_KEY_PREFIX config via environment variables - Replace all hardcoded Redis key prefixes with configurable values - Create .env.development for playground environment settings - Update .env with production defaults - Add dotenv dependency for environment file loading Configuration isolation allows running both binaries simultaneously without port conflicts or Redis key collisions.
This commit is contained in:
162
src/core/cache/redis_cache.rs
vendored
Normal file
162
src/core/cache/redis_cache.rs
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
use anyhow::Result;
|
||||
use redis::AsyncCommands;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::core::config::{cache as cache_config, REDIS_KEY_PREFIX};
|
||||
use crate::core::db::RedisClient;
|
||||
|
||||
pub struct RedisCache {
|
||||
client: Arc<RwLock<RedisClient>>,
|
||||
}
|
||||
|
||||
impl RedisCache {
|
||||
pub fn new() -> Result<Self> {
|
||||
let client = RedisClient::new()?;
|
||||
Ok(Self {
|
||||
client: Arc::new(RwLock::new(client)),
|
||||
})
|
||||
}
|
||||
|
||||
fn prefixed_key(&self, key: &str) -> String {
|
||||
format!("{}cache:{}", REDIS_KEY_PREFIX.as_str(), key)
|
||||
}
|
||||
|
||||
pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let prefixed = self.prefixed_key(key);
|
||||
let value: Option<String> = conn.get(&prefixed).await?;
|
||||
|
||||
match value {
|
||||
Some(json) => {
|
||||
let result = serde_json::from_str(&json)?;
|
||||
Ok(Some(result))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set<T: Serialize>(&self, key: &str, value: &T, ttl_secs: u64) -> Result<()> {
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let prefixed = self.prefixed_key(key);
|
||||
let json = serde_json::to_string(value)?;
|
||||
let _: String = conn.set_ex(&prefixed, json, ttl_secs).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, key: &str) -> Result<bool> {
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let prefixed = self.prefixed_key(key);
|
||||
let _: () = conn.del(&prefixed).await?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn exists(&self, key: &str) -> Result<bool> {
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let prefixed = self.prefixed_key(key);
|
||||
let exists: bool = conn.exists(&prefixed).await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub async fn invalidate_pattern(&self, pattern: &str) -> Result<u64> {
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let prefixed_pattern = self.prefixed_key(pattern);
|
||||
let keys: Vec<String> = redis::cmd("KEYS")
|
||||
.arg(&prefixed_pattern)
|
||||
.query_async(&mut conn)
|
||||
.await?;
|
||||
let count = keys.len() as u64;
|
||||
|
||||
if !keys.is_empty() {
|
||||
let _: () = conn.del(&keys).await?;
|
||||
}
|
||||
|
||||
tracing::debug!("Invalidated {} keys matching pattern: {}", count, pattern);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn get_or_fetch<F, Fut, T>(&self, key: &str, ttl_secs: u64, fetcher: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = Result<T>>,
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
if let Some(cached) = self.get::<T>(key).await? {
|
||||
tracing::debug!("Redis cache hit for key: {}", key);
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
tracing::debug!("Redis cache miss for key: {}", key);
|
||||
let value = fetcher().await?;
|
||||
if let Err(e) = self.set(key, &value, ttl_secs).await {
|
||||
tracing::warn!("Failed to cache value in Redis: {}", e);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn get_health(&self) -> Result<Option<String>> {
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let key = self.prefixed_key("health");
|
||||
let value: Option<String> = conn.get(&key).await?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub async fn set_health(&self, status: &str) -> Result<()> {
|
||||
let ttl = *cache_config::REDIS_CACHE_TTL_HEALTH;
|
||||
let client = self.client.read().await;
|
||||
let mut conn = client.get_conn_internal().await?;
|
||||
let key = self.prefixed_key("health");
|
||||
let _: String = conn.set_ex(&key, status, ttl).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_video_meta(&self, uuid: &str) -> Result<Option<serde_json::Value>> {
|
||||
self.get(uuid).await
|
||||
}
|
||||
|
||||
pub async fn set_video_meta(&self, uuid: &str, value: &serde_json::Value) -> Result<()> {
|
||||
let ttl = *cache_config::REDIS_CACHE_TTL_VIDEO_META;
|
||||
self.set(uuid, value, ttl).await
|
||||
}
|
||||
|
||||
pub async fn invalidate_video_meta(&self, uuid: &str) -> Result<bool> {
|
||||
self.delete(uuid).await
|
||||
}
|
||||
|
||||
pub async fn invalidate_videos_list(&self) -> Result<u64> {
|
||||
self.invalidate_pattern("videos:*").await
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for RedisCache {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: Arc::clone(&self.client),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RedisCache {
|
||||
fn default() -> Self {
|
||||
Self::new().expect("Failed to create Redis cache")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_prefixed_key() {
|
||||
let cache = RedisCache::new().unwrap();
|
||||
assert_eq!(cache.prefixed_key("test"), "momentry:cache:test");
|
||||
assert_eq!(cache.prefixed_key("video:abc"), "momentry:cache:video:abc");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user