use crate::session::{Session, SessionStore};
use crate::utils::ChainError;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
pub struct InMemorySessionStore {
sessions: Arc<Mutex<HashMap<Uuid, Session>>>,
}
impl Default for InMemorySessionStore {
fn default() -> Self {
Self::new()
}
}
impl InMemorySessionStore {
pub fn new() -> Self {
Self {
sessions: Arc::new(Mutex::new(HashMap::new())),
}
}
}
impl SessionStore for InMemorySessionStore {
fn get(&self, id: Uuid) -> Result<Session, ChainError> {
let sessions = self.sessions.lock().map_err(|_| {
ChainError::Internal("Failed to acquire lock on session store".to_string())
})?;
sessions
.get(&id)
.cloned()
.ok_or_else(|| ChainError::NotFound(format!("Session with id {} not found", id)))
}
fn save(&self, session: Session) -> Result<(), ChainError> {
let mut sessions = self.sessions.lock().map_err(|_| {
ChainError::Internal("Failed to acquire lock on session store".to_string())
})?;
sessions.insert(session.id, session);
Ok(())
}
fn delete(&self, id: Uuid) -> Result<bool, ChainError> {
let mut sessions = self.sessions.lock().map_err(|_| {
ChainError::Internal("Failed to acquire lock on session store".to_string())
})?;
Ok(sessions.remove(&id).is_some())
}
fn cleanup(&self) -> Result<usize, ChainError> {
let mut sessions = self.sessions.lock().map_err(|_| {
ChainError::Internal("Failed to acquire lock on session store".to_string())
})?;
let now = std::time::SystemTime::now();
let expired_ids: Vec<Uuid> = sessions
.iter()
.filter_map(
|(id, session)| match now.duration_since(session.updated_at) {
Ok(duration) if duration.as_secs() > 1800 => Some(*id),
_ => None,
},
)
.collect();
let count = expired_ids.len();
for id in expired_ids {
sessions.remove(&id);
}
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::SimulationMethod;
use crate::session::model::{Session, SessionState, SimulationParameters};
use crate::utils::UuidGenerator;
use optionstratlib::utils::TimeFrame;
use optionstratlib::{Positive, pos};
use rust_decimal::Decimal;
use std::thread;
use std::time::{Duration, SystemTime};
use uuid::Uuid;
fn create_test_session(id_option: Option<Uuid>) -> 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::ZERO,
dividend_yield: Positive::ZERO,
method: SimulationMethod::GeometricBrownian {
dt: pos!(1.0),
drift: Decimal::ZERO,
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,
};
let now = SystemTime::now();
let namespace_uuid = Uuid::new_v4().to_string();
let namespace =
Uuid::parse_str(&namespace_uuid).expect("Failed to parse default UUID namespace");
let uuid_generator = UuidGenerator::new(namespace);
if let Some(id) = id_option {
Session {
id,
created_at: now,
updated_at: now,
current_step: 0,
total_steps: params.steps,
parameters: params,
state: SessionState::Initialized,
}
} else {
Session::new(params, &uuid_generator)
}
}
#[test]
fn test_in_memory_session_store_new() {
let store = InMemorySessionStore::new();
let sessions = store.sessions.lock().unwrap();
assert_eq!(sessions.len(), 0);
}
#[test]
fn test_get_nonexistent_session() {
let store = InMemorySessionStore::new();
let id = Uuid::new_v4();
let result = store.get(id);
assert!(result.is_err());
match result {
Err(ChainError::NotFound(msg)) => {
assert!(msg.contains(&id.to_string()));
}
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_save_and_get_session() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
let id = session.id;
let save_result = store.save(session.clone());
assert!(save_result.is_ok());
let get_result = store.get(id);
assert!(get_result.is_ok());
let retrieved_session = get_result.unwrap();
assert_eq!(retrieved_session.id, id);
assert_eq!(retrieved_session.parameters.symbol, "TEST");
assert_eq!(retrieved_session.state, SessionState::Initialized);
}
#[test]
fn test_save_multiple_sessions() {
let store = InMemorySessionStore::new();
let id1 = Uuid::new_v4();
let id2 = Uuid::new_v4();
let session1 = create_test_session(Some(id1));
let session2 = create_test_session(Some(id2));
assert!(store.save(session1).is_ok());
assert!(store.save(session2).is_ok());
let retrieved1 = store.get(id1).unwrap();
let retrieved2 = store.get(id2).unwrap();
assert_eq!(retrieved1.id, id1);
assert_eq!(retrieved2.id, id2);
let sessions = store.sessions.lock().unwrap();
assert_eq!(sessions.len(), 2);
}
#[test]
fn test_update_existing_session() {
let store = InMemorySessionStore::new();
let mut session = create_test_session(None);
let id = session.id;
assert!(store.save(session.clone()).is_ok());
session.state = SessionState::InProgress;
session.current_step = 1;
assert!(store.save(session).is_ok());
let retrieved = store.get(id).unwrap();
assert_eq!(retrieved.state, SessionState::InProgress);
assert_eq!(retrieved.current_step, 1);
}
#[test]
fn test_delete_session() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
let id = session.id;
assert!(store.save(session).is_ok());
let delete_result = store.delete(id);
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
assert!(store.get(id).is_err());
}
#[test]
fn test_delete_nonexistent_session() {
let store = InMemorySessionStore::new();
let id = Uuid::new_v4();
let delete_result = store.delete(id);
assert!(delete_result.is_ok());
assert!(!delete_result.unwrap()); }
#[test]
fn test_cleanup_expired_sessions() {
let store = InMemorySessionStore::new();
let current_session = create_test_session(None);
let current_id = current_session.id;
let expired_time = SystemTime::now()
.checked_sub(Duration::from_secs(3600))
.unwrap();
let expired_id = Uuid::new_v4();
let expired_session = Session {
id: expired_id,
created_at: expired_time,
updated_at: expired_time,
current_step: 0,
total_steps: 10,
parameters: current_session.parameters.clone(),
state: SessionState::Initialized,
};
assert!(store.save(current_session).is_ok());
assert!(store.save(expired_session).is_ok());
let cleanup_result = store.cleanup();
assert!(cleanup_result.is_ok());
assert_eq!(cleanup_result.unwrap(), 1);
assert!(store.get(current_id).is_ok());
assert!(store.get(expired_id).is_err());
}
#[test]
fn test_concurrent_access() {
let store = Arc::new(InMemorySessionStore::new());
let session = create_test_session(None);
let id = session.id;
assert!(store.save(session).is_ok());
let store_clone = Arc::clone(&store);
let handle = thread::spawn(move || {
let result = store_clone.get(id);
assert!(result.is_ok());
let mut session = result.unwrap();
session.state = SessionState::InProgress;
assert!(store_clone.save(session).is_ok());
});
handle.join().unwrap();
let retrieved = store.get(id).unwrap();
assert_eq!(retrieved.state, SessionState::InProgress);
}
#[test]
fn test_lock_poisoning_recovery() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
let id = session.id;
assert!(store.save(session).is_ok());
{
let mutex_guard = store.sessions.lock().unwrap();
drop(mutex_guard);
}
let _ = store.get(id);
let _ = store.delete(id);
let _ = store.cleanup();
}
}