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:
accusys
2026-03-25 00:40:31 +08:00
parent 13e208b569
commit 732ef9296b
16 changed files with 8381 additions and 366 deletions

162
src/core/cache/redis_cache.rs vendored Normal file
View 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");
}
}