use crate::session::{Session, SessionStore};
use crate::utils::ChainError;
use async_trait::async_trait;
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())),
}
}
}
#[async_trait]
impl SessionStore for InMemorySessionStore {
async 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)))
}
async fn create(&self, session: Session) -> Result<(), ChainError> {
let mut sessions = self.sessions.lock().map_err(|_| {
ChainError::Internal("Failed to acquire lock on session store".to_string())
})?;
if sessions.contains_key(&session.id) {
return Err(ChainError::AlreadyExists(format!(
"Session with id {} already exists",
session.id
)));
}
sessions.insert(session.id, session);
Ok(())
}
async 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(())
}
async fn save_cas(&self, session: Session, expected_version: u64) -> Result<(), ChainError> {
let mut sessions = self.sessions.lock().map_err(|_| {
ChainError::Internal("Failed to acquire lock on session store".to_string())
})?;
match sessions.get(&session.id) {
None => Err(ChainError::NotFound(format!(
"Session with id {} not found",
session.id
))),
Some(existing) if existing.version != expected_version => {
Err(ChainError::Conflict(format!(
"Session {} was modified concurrently (expected version {}, found {})",
session.id, expected_version, existing.version
)))
}
Some(_) => {
sessions.insert(session.id, session);
Ok(())
}
}
}
async 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())
}
async 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 positive::{Positive, pos_or_panic};
use rust_decimal::Decimal;
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_or_panic!(100.0),
days_to_expiration: pos_or_panic!(30.0),
volatility: pos_or_panic!(0.2),
risk_free_rate: Decimal::ZERO,
dividend_yield: Positive::ZERO,
method: SimulationMethod::GeometricBrownian {
dt: pos_or_panic!(1.0),
drift: Decimal::ZERO,
volatility: pos_or_panic!(0.2),
},
time_frame: TimeFrame::Day,
chain_size: Some(5),
strike_interval: Some(pos_or_panic!(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,
version: 0,
}
} 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);
}
#[tokio::test]
async fn test_get_nonexistent_session() {
let store = InMemorySessionStore::new();
let id = Uuid::new_v4();
let result = store.get(id).await;
assert!(result.is_err());
match result {
Err(ChainError::NotFound(msg)) => {
assert!(msg.contains(&id.to_string()));
}
_ => panic!("Expected NotFound error"),
}
}
#[tokio::test]
async 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()).await;
assert!(save_result.is_ok());
let get_result = store.get(id).await;
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);
}
#[tokio::test]
async fn test_create_new_session() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
let id = session.id;
assert!(store.create(session).await.is_ok());
assert!(store.get(id).await.is_ok());
}
#[tokio::test]
async fn test_create_duplicate_returns_already_exists() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
assert!(store.create(session.clone()).await.is_ok());
match store.create(session).await {
Err(ChainError::AlreadyExists(msg)) => {
assert!(msg.contains("already exists"));
}
other => panic!("Expected AlreadyExists error, got {:?}", other),
}
}
#[tokio::test]
async fn test_save_still_updates_after_create() {
let store = InMemorySessionStore::new();
let mut session = create_test_session(None);
let id = session.id;
assert!(store.create(session.clone()).await.is_ok());
session.current_step = 7;
session.state = SessionState::InProgress;
assert!(store.save(session).await.is_ok());
let retrieved = store.get(id).await.unwrap();
assert_eq!(retrieved.current_step, 7);
assert_eq!(retrieved.state, SessionState::InProgress);
}
#[tokio::test]
async 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).await.is_ok());
assert!(store.save(session2).await.is_ok());
let retrieved1 = store.get(id1).await.unwrap();
let retrieved2 = store.get(id2).await.unwrap();
assert_eq!(retrieved1.id, id1);
assert_eq!(retrieved2.id, id2);
let sessions = store.sessions.lock().unwrap();
assert_eq!(sessions.len(), 2);
}
#[tokio::test]
async 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()).await.is_ok());
session.state = SessionState::InProgress;
session.current_step = 1;
assert!(store.save(session).await.is_ok());
let retrieved = store.get(id).await.unwrap();
assert_eq!(retrieved.state, SessionState::InProgress);
assert_eq!(retrieved.current_step, 1);
}
#[tokio::test]
async fn test_save_cas_matching_version_commits() {
let store = InMemorySessionStore::new();
let mut session = create_test_session(None);
let id = session.id;
assert_eq!(session.version, 0);
assert!(store.create(session.clone()).await.is_ok());
session.current_step = 3;
session.state = SessionState::InProgress;
assert!(session.bump_version().is_ok());
assert!(store.save_cas(session, 0).await.is_ok());
let stored = store.get(id).await.unwrap();
assert_eq!(stored.current_step, 3);
assert_eq!(stored.state, SessionState::InProgress);
assert_eq!(stored.version, 1);
}
#[tokio::test]
async fn test_save_cas_stale_version_conflicts_and_leaves_store_unchanged() {
let store = InMemorySessionStore::new();
let mut session = create_test_session(None);
let id = session.id;
assert!(store.create(session.clone()).await.is_ok());
session.current_step = 1;
assert!(session.bump_version().is_ok());
assert!(store.save_cas(session.clone(), 0).await.is_ok());
let mut stale = session.clone();
stale.current_step = 99;
assert!(stale.bump_version().is_ok());
match store.save_cas(stale, 0).await {
Err(ChainError::Conflict(_)) => {}
other => panic!("expected Conflict, got {other:?}"),
}
let stored = store.get(id).await.unwrap();
assert_eq!(stored.current_step, 1);
assert_eq!(stored.version, 1);
}
#[tokio::test]
async fn test_save_cas_missing_id_returns_not_found() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
match store.save_cas(session, 0).await {
Err(ChainError::NotFound(_)) => {}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[tokio::test]
async fn test_delete_session() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
let id = session.id;
assert!(store.save(session).await.is_ok());
let delete_result = store.delete(id).await;
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
assert!(store.get(id).await.is_err());
}
#[tokio::test]
async fn test_delete_nonexistent_session() {
let store = InMemorySessionStore::new();
let id = Uuid::new_v4();
let delete_result = store.delete(id).await;
assert!(delete_result.is_ok());
assert!(!delete_result.unwrap()); }
#[tokio::test]
async 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,
version: 0,
};
assert!(store.save(current_session).await.is_ok());
assert!(store.save(expired_session).await.is_ok());
let cleanup_result = store.cleanup().await;
assert!(cleanup_result.is_ok());
assert_eq!(cleanup_result.unwrap(), 1);
assert!(store.get(current_id).await.is_ok());
assert!(store.get(expired_id).await.is_err());
}
#[tokio::test]
async fn test_concurrent_access() {
let store = Arc::new(InMemorySessionStore::new());
let session = create_test_session(None);
let id = session.id;
assert!(store.save(session).await.is_ok());
let store_clone = Arc::clone(&store);
let handle = tokio::spawn(async move {
let result = store_clone.get(id).await;
assert!(result.is_ok());
let mut session = result.unwrap();
session.state = SessionState::InProgress;
assert!(store_clone.save(session).await.is_ok());
});
handle.await.unwrap();
let retrieved = store.get(id).await.unwrap();
assert_eq!(retrieved.state, SessionState::InProgress);
}
#[tokio::test]
async fn test_concurrent_ops_on_different_sessions_do_not_serialize() {
let store = Arc::new(InMemorySessionStore::new());
let session_a = create_test_session(None);
let session_b = create_test_session(None);
let (id_a, id_b) = (session_a.id, session_b.id);
let store_a = Arc::clone(&store);
let store_b = Arc::clone(&store);
let (res_a, res_b) =
tokio::join!(async move { store_a.create(session_a).await }, async move {
store_b.create(session_b).await
},);
assert!(res_a.is_ok());
assert!(res_b.is_ok());
assert!(store.get(id_a).await.is_ok());
assert!(store.get(id_b).await.is_ok());
}
#[tokio::test]
async fn test_lock_poisoning_recovery() {
let store = InMemorySessionStore::new();
let session = create_test_session(None);
let id = session.id;
assert!(store.save(session).await.is_ok());
{
let mutex_guard = store.sessions.lock().unwrap();
drop(mutex_guard);
}
let _ = store.get(id).await;
let _ = store.delete(id).await;
let _ = store.cleanup().await;
}
}