use crate::infrastructure::RedisClient;
use crate::session::{Session, SessionStore};
use crate::utils::ChainError;
use redis::RedisError;
use serde_json;
use std::sync::Arc;
use tracing::{debug, error, info, instrument};
use uuid::Uuid;
pub struct InRedisSessionStore {
client: Arc<RedisClient>,
key_prefix: String,
session_ttl: u64,
}
impl InRedisSessionStore {
#[instrument(skip(client), level = "debug")]
pub fn new(
client: Arc<RedisClient>,
key_prefix: Option<String>,
session_ttl: Option<u64>,
) -> Self {
let prefix = key_prefix.unwrap_or_else(|| "session:".to_string());
let ttl = session_ttl.unwrap_or(1800);
info!(
key_prefix = %prefix,
session_ttl = ttl,
"Created new Redis session store"
);
Self {
client,
key_prefix: prefix,
session_ttl: ttl,
}
}
fn session_key(&self, id: Uuid) -> String {
format!("{}{}", self.key_prefix, id)
}
fn map_redis_error(err: RedisError) -> ChainError {
ChainError::Internal(format!("Redis error: {}", err))
}
}
impl SessionStore for InRedisSessionStore {
#[instrument(skip(self), level = "debug")]
fn get(&self, id: Uuid) -> Result<Session, ChainError> {
let key = self.session_key(id);
debug!(session_id = %id, key = %key, "Getting session from Redis");
match self.client.get::<String>(&key) {
Ok(Some(json_str)) => {
match serde_json::from_str::<Session>(&json_str) {
Ok(session) => {
debug!(session_id = %id, "Session retrieved successfully");
Ok(session)
}
Err(e) => {
error!(session_id = %id, error = %e, "Failed to deserialize session");
Err(ChainError::Internal(format!(
"Failed to deserialize session: {}",
e
)))
}
}
}
Ok(None) => {
debug!(session_id = %id, "Session not found in Redis");
Err(ChainError::NotFound(format!(
"Session with id {} not found",
id
)))
}
Err(e) => {
error!(session_id = %id, error = %e, "Redis error while getting session");
Err(Self::map_redis_error(e))
}
}
}
#[instrument(skip(self, session), level = "debug")]
fn save(&self, session: Session) -> Result<(), ChainError> {
let key = self.session_key(session.id);
debug!(session_id = %session.id, key = %key, "Saving session to Redis");
let json_str = match serde_json::to_string(&session) {
Ok(s) => s,
Err(e) => {
error!(session_id = %session.id, error = %e, "Failed to serialize session");
return Err(ChainError::Internal(format!(
"Failed to serialize session: {}",
e
)));
}
};
match self.client.set(&key, json_str, Some(self.session_ttl)) {
Ok(_) => {
debug!(session_id = %session.id, "Session saved successfully");
Ok(())
}
Err(e) => {
error!(session_id = %session.id, error = %e, "Redis error while saving session");
Err(Self::map_redis_error(e))
}
}
}
#[instrument(skip(self), level = "debug")]
fn delete(&self, id: Uuid) -> Result<bool, ChainError> {
let key = self.session_key(id);
debug!(session_id = %id, key = %key, "Deleting session from Redis");
match self.client.delete(&key) {
Ok(deleted) => {
debug!(session_id = %id, deleted = deleted, "Session delete result");
Ok(deleted)
}
Err(e) => {
error!(session_id = %id, error = %e, "Redis error while deleting session");
Err(Self::map_redis_error(e))
}
}
}
#[instrument(skip(self), level = "debug")]
fn cleanup(&self) -> Result<usize, ChainError> {
debug!("Cleaning up expired sessions from Redis");
info!("Redis handles automatic expiration of session keys");
Ok(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::{SessionState, SimulationMethod, SimulationParameters};
use optionstratlib::utils::TimeFrame;
use optionstratlib::{Positive, pos};
use rust_decimal::Decimal;
use std::sync::Mutex;
use std::time::SystemTime;
use uuid::Uuid;
struct TestInRedisSessionStore {
sessions: Mutex<std::collections::HashMap<String, String>>,
key_prefix: String,
session_ttl: u64,
}
impl TestInRedisSessionStore {
fn new(key_prefix: Option<String>, session_ttl: Option<u64>) -> Self {
let prefix = key_prefix.unwrap_or_else(|| "session:".to_string());
let ttl = session_ttl.unwrap_or(1800);
Self {
sessions: Mutex::new(std::collections::HashMap::new()),
key_prefix: prefix,
session_ttl: ttl,
}
}
fn session_key(&self, id: Uuid) -> String {
format!("{}{}", self.key_prefix, id)
}
fn get_store_size(&self) -> usize {
self.sessions.lock().unwrap().len()
}
}
impl SessionStore for TestInRedisSessionStore {
fn get(&self, id: Uuid) -> Result<Session, ChainError> {
let key = self.session_key(id);
let sessions = self.sessions.lock().unwrap();
match sessions.get(&key) {
Some(json_str) => {
match serde_json::from_str::<Session>(json_str) {
Ok(session) => Ok(session),
Err(e) => Err(ChainError::Internal(format!(
"Failed to deserialize session: {}",
e
))),
}
}
None => Err(ChainError::NotFound(format!(
"Session with id {} not found",
id
))),
}
}
fn save(&self, session: Session) -> Result<(), ChainError> {
let key = self.session_key(session.id);
let json_str = match serde_json::to_string(&session) {
Ok(s) => s,
Err(e) => {
return Err(ChainError::Internal(format!(
"Failed to serialize session: {}",
e
)));
}
};
let mut sessions = self.sessions.lock().unwrap();
sessions.insert(key, json_str);
Ok(())
}
fn delete(&self, id: Uuid) -> Result<bool, ChainError> {
let key = self.session_key(id);
let mut sessions = self.sessions.lock().unwrap();
Ok(sessions.remove(&key).is_some())
}
fn cleanup(&self) -> Result<usize, ChainError> {
Ok(0)
}
}
fn create_test_session() -> Session {
let params = SimulationParameters {
symbol: "TEST".to_string(),
steps: 10,
initial_price: pos!(100.0),
days_to_expiration: pos!(30.0),
volatility: pos!(0.2),
risk_free_rate: Decimal::new(0, 0),
dividend_yield: Positive::ZERO,
method: SimulationMethod::GeometricBrownian {
dt: pos!(1.0),
drift: Decimal::new(0, 0),
volatility: pos!(0.2),
},
time_frame: TimeFrame::Day,
chain_size: Some(5),
strike_interval: Some(pos!(5.0)),
skew_slope: None,
smile_curve: None,
spread: None,
seed: None,
};
Session {
id: Uuid::new_v4(),
created_at: SystemTime::now(),
updated_at: SystemTime::now(),
current_step: 0,
total_steps: 10,
parameters: params,
state: SessionState::Initialized,
}
}
#[test]
fn test_new_with_defaults() {
let store = TestInRedisSessionStore::new(None, None);
assert_eq!(store.key_prefix, "session:");
assert_eq!(store.session_ttl, 1800);
}
#[test]
fn test_new_with_custom_values() {
let store = TestInRedisSessionStore::new(Some("custom_prefix:".to_string()), Some(3600));
assert_eq!(store.key_prefix, "custom_prefix:");
assert_eq!(store.session_ttl, 3600);
}
#[test]
fn test_session_key_format() {
let store = TestInRedisSessionStore::new(Some("test:".to_string()), None);
let id = Uuid::parse_str("f47ac10b-58cc-4372-a567-0e02b2c3d479").unwrap();
let key = store.session_key(id);
assert_eq!(key, "test:f47ac10b-58cc-4372-a567-0e02b2c3d479");
}
#[test]
fn test_save_and_get_session() {
let store = TestInRedisSessionStore::new(None, None);
let session = create_test_session();
let session_id = session.id;
let save_result = store.save(session.clone());
assert!(save_result.is_ok());
assert_eq!(store.get_store_size(), 1);
let get_result = store.get(session_id);
assert!(get_result.is_ok());
let retrieved_session = get_result.unwrap();
assert_eq!(retrieved_session.id, session_id);
assert_eq!(retrieved_session.state, SessionState::Initialized);
assert_eq!(retrieved_session.current_step, 0);
assert_eq!(retrieved_session.total_steps, 10);
}
#[test]
fn test_get_non_existent_session() {
let store = TestInRedisSessionStore::new(None, None);
let non_existent_id = Uuid::new_v4();
let result = store.get(non_existent_id);
assert!(result.is_err());
match result {
Err(ChainError::NotFound(msg)) => {
assert!(msg.contains(&non_existent_id.to_string()));
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_delete_existing_session() {
let store = TestInRedisSessionStore::new(None, None);
let session = create_test_session();
let session_id = session.id;
store.save(session).unwrap();
assert_eq!(store.get_store_size(), 1);
let delete_result = store.delete(session_id);
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
assert_eq!(store.get_store_size(), 0);
let get_result = store.get(session_id);
assert!(get_result.is_err());
}
#[test]
fn test_delete_non_existent_session() {
let store = TestInRedisSessionStore::new(None, None);
let non_existent_id = Uuid::new_v4();
let result = store.delete(non_existent_id);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_cleanup() {
let store = TestInRedisSessionStore::new(None, None);
let result = store.cleanup();
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_update_existing_session() {
let store = TestInRedisSessionStore::new(None, None);
let mut session = create_test_session();
let session_id = session.id;
store.save(session.clone()).unwrap();
session.current_step = 5;
session.state = SessionState::InProgress;
let update_result = store.save(session.clone());
assert!(update_result.is_ok());
let get_result = store.get(session_id);
assert!(get_result.is_ok());
let updated_session = get_result.unwrap();
assert_eq!(updated_session.current_step, 5);
assert_eq!(updated_session.state, SessionState::InProgress);
}
struct TestErrorInRedisSessionStore {
}
impl SessionStore for TestErrorInRedisSessionStore {
fn get(&self, id: Uuid) -> Result<Session, ChainError> {
Err(ChainError::Internal(format!(
"Simulated error for session {}",
id
)))
}
fn save(&self, session: Session) -> Result<(), ChainError> {
Err(ChainError::Internal(format!(
"Simulated error saving session {}",
session.id
)))
}
fn delete(&self, id: Uuid) -> Result<bool, ChainError> {
Err(ChainError::Internal(format!(
"Simulated error deleting session {}",
id
)))
}
fn cleanup(&self) -> Result<usize, ChainError> {
Err(ChainError::Internal(
"Simulated error during cleanup".to_string(),
))
}
}
#[test]
fn test_error_handling() {
let error_store = TestErrorInRedisSessionStore {};
let session = create_test_session();
let session_id = session.id;
let save_result = error_store.save(session);
assert!(save_result.is_err());
let get_result = error_store.get(session_id);
assert!(get_result.is_err());
let delete_result = error_store.delete(session_id);
assert!(delete_result.is_err());
let cleanup_result = error_store.cleanup();
assert!(cleanup_result.is_err());
}
}