use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use parking_lot::Mutex;
use theway_contract::dag::{PersistedRun, state_path_for_project};
use theway_core::multiagent::graph::engine::DagEngine;
use theway_core::multiagent::graph::persist::{DagPersistSink, to_persisted};
use theway_core::multiagent::graph::types::DagStatus;
use theway_storage::sqlite_dag::SqliteDagStore;
use tokio::sync::Notify;
use tokio::task::JoinHandle;
use crate::session_execution::SessionExecutionRegistry;
const DEBOUNCE: Duration = Duration::from_millis(500);
pub struct DagPersistHandle {
engine: Arc<DagEngine>,
cwd: PathBuf,
sessions: SessionExecutionRegistry,
stores: Mutex<HashMap<(PathBuf, Option<String>), SqliteDagStore>>,
dirty: Arc<Notify>,
task: Mutex<Option<JoinHandle<()>>>,
}
impl DagPersistHandle {
pub fn spawn(engine: Arc<DagEngine>, cwd: PathBuf) -> Arc<Self> {
Self::spawn_with_sessions(engine, cwd, SessionExecutionRegistry::new())
}
pub fn spawn_with_sessions(
engine: Arc<DagEngine>,
cwd: PathBuf,
sessions: SessionExecutionRegistry,
) -> Arc<Self> {
let dirty = Arc::new(Notify::new());
let handle = Arc::new(Self {
engine,
cwd,
sessions,
stores: Mutex::new(HashMap::new()),
dirty: dirty.clone(),
task: Mutex::new(None),
});
let task = tokio::spawn(handle.clone().run_loop());
*handle.task.lock() = Some(task);
handle.engine.set_persist_sink(Some(handle.clone()));
handle
}
async fn run_loop(self: Arc<Self>) {
loop {
self.dirty.notified().await;
loop {
tokio::select! {
_ = self.dirty.notified() => {
tokio::time::sleep(DEBOUNCE).await;
}
_ = tokio::time::sleep(DEBOUNCE) => break,
}
}
if let Err(e) = self.save_all().await {
tracing::warn!("dag persist: {e}");
}
}
}
async fn save_all(&self) -> Result<(), String> {
let runs = self.engine.list_runs();
let mut by_session: HashMap<Option<String>, Vec<PersistedRun>> = HashMap::new();
for run in runs {
let snapshots = by_session.entry(run.session_id.clone()).or_default();
if run.status == DagStatus::Running {
snapshots.push(to_persisted(&run));
}
}
for (session_id, session_runs) in by_session {
let store = self.store_for(session_id.as_deref()).await?;
store.save(&session_runs).await?;
}
Ok(())
}
async fn store_for(&self, session_id: Option<&str>) -> Result<SqliteDagStore, String> {
let owning_cwd = match session_id {
Some(session_id) => self
.sessions
.cwd_for(session_id)
.unwrap_or_else(|| self.cwd.clone()),
None => self.cwd.clone(),
};
let key = (owning_cwd.clone(), session_id.map(str::to_string));
if let Some(store) = self.stores.lock().get(&key) {
return Ok(store.clone());
}
let path = state_path_for_project(&owning_cwd.join(".pi"), session_id);
let store = SqliteDagStore::open(path).await?;
self.stores.lock().insert(key, store.clone());
Ok(store)
}
}
#[async_trait]
impl DagPersistSink for DagPersistHandle {
fn notify_dirty(&self) {
self.dirty.notify_one();
}
async fn flush(&self) {
if let Err(e) = self.save_all().await {
tracing::warn!("dag persist flush: {e}");
}
}
}
pub async fn load_session_runs(
cwd: &std::path::Path,
session_id: &str,
) -> Vec<theway_core::multiagent::graph::persist::PersistedRun> {
let path = state_path_for_project(&cwd.join(".pi"), Some(session_id));
match SqliteDagStore::open(path).await {
Ok(store) => store.load().await.unwrap_or_default(),
Err(e) => {
tracing::warn!("dag state open: {e}");
Vec::new()
}
}
}
#[cfg(test)]
tests_bridge_macro::tests_bridge!("dag_persist");