use async_trait::async_trait;
use do_memory_core::memory::attribution::{
RecommendationFeedback, RecommendationSession, RecommendationStats,
};
use do_memory_core::storage::circuit_breaker::{
CircuitBreaker, CircuitBreakerConfig, CircuitState,
};
use do_memory_core::{Episode, Heuristic, Pattern, Result, StorageBackend};
use std::sync::Arc;
use tracing::{info, warn};
use uuid::Uuid;
#[cfg(test)]
use do_memory_core::Error;
use crate::TursoStorage;
pub struct ResilientStorage {
storage: Arc<TursoStorage>,
circuit_breaker: Arc<CircuitBreaker>,
}
impl ResilientStorage {
pub fn new(storage: TursoStorage, config: CircuitBreakerConfig) -> Self {
info!("Creating resilient storage with circuit breaker protection");
Self {
storage: Arc::new(storage),
circuit_breaker: Arc::new(CircuitBreaker::new(config)),
}
}
pub async fn circuit_state(&self) -> CircuitState {
self.circuit_breaker.state().await
}
pub async fn circuit_stats(
&self,
) -> do_memory_core::storage::circuit_breaker::CircuitBreakerStats {
self.circuit_breaker.stats().await
}
pub async fn reset_circuit(&self) {
self.circuit_breaker.reset().await;
}
pub async fn health_check(&self) -> Result<bool> {
let circuit_state = self.circuit_state().await;
if circuit_state != CircuitState::Closed {
warn!("Health check: circuit breaker is {:?}", circuit_state);
return Ok(false);
}
self.circuit_call(|s| async move { s.health_check().await })
.await
}
async fn circuit_call<F, Fut, T>(&self, op: F) -> Result<T>
where
F: FnOnce(Arc<TursoStorage>) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<T>> + Send,
T: Send + 'static,
{
let storage = Arc::clone(&self.storage);
self.circuit_breaker
.call(move || {
let storage = Arc::clone(&storage);
op(storage)
})
.await
}
}
#[async_trait]
impl StorageBackend for ResilientStorage {
async fn store_episode(&self, episode: &Episode) -> Result<()> {
let episode = episode.clone();
self.circuit_call(move |s| async move { s.store_episode(&episode).await })
.await
}
async fn get_episode(&self, id: Uuid) -> Result<Option<Episode>> {
self.circuit_call(move |s| async move { s.get_episode(id).await })
.await
}
async fn delete_episode(&self, id: Uuid) -> Result<()> {
self.circuit_call(move |s| async move { s.delete_episode(id).await })
.await
}
async fn store_pattern(&self, pattern: &Pattern) -> Result<()> {
let pattern = pattern.clone();
self.circuit_call(move |s| async move { s.store_pattern(&pattern).await })
.await
}
async fn get_pattern(&self, id: do_memory_core::episode::PatternId) -> Result<Option<Pattern>> {
self.circuit_call(move |s| async move { s.get_pattern(id).await })
.await
}
async fn store_heuristic(&self, heuristic: &Heuristic) -> Result<()> {
let heuristic = heuristic.clone();
self.circuit_call(move |s| async move { s.store_heuristic(&heuristic).await })
.await
}
async fn get_heuristic(&self, id: Uuid) -> Result<Option<Heuristic>> {
self.circuit_call(move |s| async move { s.get_heuristic(id).await })
.await
}
async fn query_episodes_since(
&self,
since: chrono::DateTime<chrono::Utc>,
limit: Option<usize>,
) -> Result<Vec<Episode>> {
self.circuit_call(move |s| async move { s.query_episodes_since(since, limit).await })
.await
}
async fn query_episodes_by_metadata(
&self,
key: &str,
value: &str,
limit: Option<usize>,
) -> Result<Vec<Episode>> {
let key = key.to_string();
let value = value.to_string();
self.circuit_call(move |s| {
let key = key;
let value = value;
async move { s.query_episodes_by_metadata(&key, &value, limit).await }
})
.await
}
async fn store_embedding(&self, id: &str, embedding: Vec<f32>) -> Result<()> {
let id = id.to_string();
self.circuit_call(move |s| async move { s.store_embedding(&id, embedding).await })
.await
}
async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>> {
let id = id.to_string();
self.circuit_call(move |s| async move { s.get_embedding(&id).await })
.await
}
async fn delete_embedding(&self, id: &str) -> Result<bool> {
let id = id.to_string();
self.circuit_call(move |s| async move { s.delete_embedding(&id).await })
.await
}
async fn store_embeddings_batch(&self, embeddings: Vec<(String, Vec<f32>)>) -> Result<()> {
self.circuit_call(move |s| async move { s.store_embeddings_batch(embeddings).await })
.await
}
async fn get_embeddings_batch(&self, ids: &[String]) -> Result<Vec<Option<Vec<f32>>>> {
let ids = ids.to_vec();
self.circuit_call(move |s| async move { s.get_embeddings_batch(&ids).await })
.await
}
async fn store_recommendation_session(&self, session: &RecommendationSession) -> Result<()> {
let session = session.clone();
self.circuit_call(move |s| async move { s.store_recommendation_session(&session).await })
.await
}
async fn get_recommendation_session(
&self,
session_id: Uuid,
) -> Result<Option<RecommendationSession>> {
self.circuit_call(move |s| async move { s.get_recommendation_session(session_id).await })
.await
}
async fn get_recommendation_session_for_episode(
&self,
episode_id: Uuid,
) -> Result<Option<RecommendationSession>> {
self.circuit_call(move |s| async move {
s.get_recommendation_session_for_episode(episode_id).await
})
.await
}
async fn store_recommendation_feedback(&self, feedback: &RecommendationFeedback) -> Result<()> {
let feedback = feedback.clone();
self.circuit_call(move |s| async move { s.store_recommendation_feedback(&feedback).await })
.await
}
async fn get_recommendation_feedback(
&self,
session_id: Uuid,
) -> Result<Option<RecommendationFeedback>> {
self.circuit_call(move |s| async move { s.get_recommendation_feedback(session_id).await })
.await
}
async fn get_recommendation_stats(&self) -> Result<RecommendationStats> {
self.circuit_call(move |s| async move { s.get_recommendation_stats().await })
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use do_memory_core::storage::circuit_breaker::CircuitBreakerConfig;
use std::time::Duration;
use tempfile::TempDir;
async fn create_test_storage() -> Result<(ResilientStorage, TempDir)> {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("test.db");
let db = libsql::Builder::new_local(&db_path)
.build()
.await
.map_err(|e| Error::Storage(format!("Failed to create test database: {}", e)))?;
let turso = TursoStorage::from_database(db)?;
turso.initialize_schema().await?;
let config = CircuitBreakerConfig {
failure_threshold: 3,
timeout: Duration::from_secs(1),
..Default::default()
};
let resilient = ResilientStorage::new(turso, config);
Ok((resilient, dir))
}
#[tokio::test]
async fn test_resilient_storage_creation() {
let result = create_test_storage().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_health_check_with_closed_circuit() {
let (storage, _dir) = create_test_storage().await.unwrap();
let healthy = storage.health_check().await.unwrap();
assert!(healthy);
assert_eq!(storage.circuit_state().await, CircuitState::Closed);
}
#[tokio::test]
async fn test_circuit_stats_tracking() {
let (storage, _dir) = create_test_storage().await.unwrap();
let episode = Episode::new(
"test".to_string(),
Default::default(),
do_memory_core::TaskType::CodeGeneration,
);
let result = storage.store_episode(&episode).await;
assert!(result.is_ok());
let stats = storage.circuit_stats().await;
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.successful_calls, 1);
assert_eq!(stats.failed_calls, 0);
}
#[tokio::test]
async fn test_circuit_reset() {
let (storage, _dir) = create_test_storage().await.unwrap();
storage.reset_circuit().await;
assert_eq!(storage.circuit_state().await, CircuitState::Closed);
let stats = storage.circuit_stats().await;
assert_eq!(stats.consecutive_failures, 0);
}
#[tokio::test]
async fn test_health_check_returns_false_when_circuit_open() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("test.db");
let db = libsql::Builder::new_local(&db_path)
.build()
.await
.map_err(|e| Error::Storage(format!("Failed to create test database: {}", e)))
.unwrap();
let turso = TursoStorage::from_database(db).unwrap();
turso.initialize_schema().await.unwrap();
let config = CircuitBreakerConfig {
failure_threshold: 3,
timeout: Duration::from_secs(1),
..Default::default()
};
let storage = ResilientStorage::new(turso, config);
for _ in 0..3 {
let _ = storage.get_episode(Uuid::nil()).await;
}
assert_eq!(
storage.circuit_state().await,
CircuitState::Closed,
"Circuit should remain closed when DB is healthy"
);
let healthy = storage.health_check().await.unwrap();
assert!(
healthy,
"Health check should return true when circuit is closed and DB is healthy"
);
drop(storage);
}
#[tokio::test]
async fn test_circuit_stats_tracking_failures() {
let (storage, _dir) = create_test_storage().await.unwrap();
let episode = Episode::new(
"test".to_string(),
Default::default(),
do_memory_core::TaskType::CodeGeneration,
);
let _ = storage.store_episode(&episode).await;
let _ = storage.delete_episode(Uuid::nil()).await;
let stats = storage.circuit_stats().await;
assert!(
stats.total_calls >= 1,
"Should have at least 1 tracked call"
);
assert!(
stats.successful_calls >= 1,
"Should have at least 1 success"
);
}
#[tokio::test]
async fn test_concurrent_operations_through_circuit() {
let (storage, _dir) = create_test_storage().await.unwrap();
let episode = Episode::new(
"concurrent_test".to_string(),
Default::default(),
do_memory_core::TaskType::CodeGeneration,
);
let episode2 = Episode::new(
"concurrent_test_2".to_string(),
Default::default(),
do_memory_core::TaskType::Analysis,
);
let (r1, r2) = tokio::join!(
storage.store_episode(&episode),
storage.store_episode(&episode2)
);
assert!(r1.is_ok(), "First concurrent operation should succeed");
assert!(r2.is_ok(), "Second concurrent operation should succeed");
let stats = storage.circuit_stats().await;
assert!(
stats.total_calls >= 2,
"Should have at least 2 tracked calls after concurrent ops"
);
}
}