Skip to main content

cognee_http_server/sync/
registry.rs

1//! In-memory `SyncRegistry` keyed by `user_id`.
2//!
3//! The registry is the *optimistic* layer of the "one running sync per user"
4//! rule (the *authoritative* layer is the DB query). Insertion is atomic —
5//! two threads racing through `try_register` will see exactly one win.
6
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU32, Ordering};
9
10use chrono::{DateTime, Utc};
11use dashmap::DashMap;
12use dashmap::mapref::entry::Entry;
13use tokio::task::AbortHandle;
14use uuid::Uuid;
15
16/// One in-flight sync. Stored behind an `Arc` so `snapshot_for` does not need
17/// the entire entry to live across the call.
18pub struct RunningSync {
19    pub run_id: String,
20    pub user_id: Uuid,
21    pub dataset_ids: Vec<Uuid>,
22    pub dataset_names: Vec<String>,
23    pub created_at: DateTime<Utc>,
24    pub progress_percentage: AtomicU32,
25    pub abort: Option<AbortHandle>,
26}
27
28/// Read-only snapshot of a running sync, used by handlers and conflict
29/// responses.
30#[derive(Debug, Clone)]
31pub struct RunningSyncSnapshot {
32    pub run_id: String,
33    pub user_id: Uuid,
34    pub dataset_ids: Vec<Uuid>,
35    pub dataset_names: Vec<String>,
36    pub created_at: DateTime<Utc>,
37    pub progress_percentage: u32,
38}
39
40/// Returned by [`SyncRegistry::try_register`] when the user already has a
41/// running sync. Carries a snapshot of the existing run so the caller can
42/// build the 409 conflict response.
43#[derive(Debug, Clone)]
44pub struct AlreadyRunning(pub RunningSyncSnapshot);
45
46/// In-memory registry: `user_id → RunningSync`.
47#[derive(Clone)]
48pub struct SyncRegistry {
49    inner: Arc<DashMap<Uuid, Arc<RunningSync>>>,
50}
51
52impl Default for SyncRegistry {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl SyncRegistry {
59    /// Build an empty registry.
60    pub fn new() -> Self {
61        Self {
62            inner: Arc::new(DashMap::new()),
63        }
64    }
65
66    /// Atomically insert a running sync for `user_id` if no sync is currently
67    /// in flight for that user. On collision, returns a snapshot of the
68    /// existing run.
69    pub fn try_register(&self, user_id: Uuid, run: RunningSync) -> Result<(), AlreadyRunning> {
70        match self.inner.entry(user_id) {
71            Entry::Occupied(occ) => Err(AlreadyRunning(snapshot(occ.get()))),
72            Entry::Vacant(vac) => {
73                vac.insert(Arc::new(run));
74                Ok(())
75            }
76        }
77    }
78
79    /// Return a snapshot of the running sync for `user_id`, if any.
80    pub fn snapshot_for(&self, user_id: Uuid) -> Option<RunningSyncSnapshot> {
81        self.inner
82            .get(&user_id)
83            .map(|entry| snapshot(entry.value()))
84    }
85
86    /// Drop the entry for `user_id`. Idempotent.
87    pub fn complete(&self, user_id: Uuid) {
88        self.inner.remove(&user_id);
89    }
90
91    /// Update the progress percentage on the slot for `user_id` (no-op when
92    /// the slot is gone).
93    pub fn update_progress(&self, user_id: Uuid, pct: u32) {
94        if let Some(entry) = self.inner.get(&user_id) {
95            entry.progress_percentage.store(pct, Ordering::Relaxed);
96        }
97    }
98
99    /// Iterate every running sync (used by graceful shutdown).
100    pub fn snapshot_all(&self) -> Vec<RunningSyncSnapshot> {
101        self.inner
102            .iter()
103            .map(|entry| snapshot(entry.value()))
104            .collect()
105    }
106
107    /// Abort every in-flight task and clear the registry. Returns the run
108    /// ids that were aborted so callers can mark them `failed` in the DB.
109    pub fn abort_all(&self) -> Vec<String> {
110        let mut aborted = Vec::new();
111        let keys: Vec<Uuid> = self.inner.iter().map(|e| *e.key()).collect();
112        for key in keys {
113            if let Some((_uid, entry)) = self.inner.remove(&key) {
114                if let Some(handle) = entry.abort.as_ref() {
115                    handle.abort();
116                }
117                aborted.push(entry.run_id.clone());
118            }
119        }
120        aborted
121    }
122}
123
124fn snapshot(run: &RunningSync) -> RunningSyncSnapshot {
125    RunningSyncSnapshot {
126        run_id: run.run_id.clone(),
127        user_id: run.user_id,
128        dataset_ids: run.dataset_ids.clone(),
129        dataset_names: run.dataset_names.clone(),
130        created_at: run.created_at,
131        progress_percentage: run.progress_percentage.load(Ordering::Relaxed),
132    }
133}
134
135#[cfg(test)]
136#[allow(
137    clippy::unwrap_used,
138    clippy::expect_used,
139    reason = "test code — panics are acceptable failures"
140)]
141mod tests {
142    use super::*;
143    use std::sync::Arc;
144
145    fn build_run(user: Uuid) -> RunningSync {
146        RunningSync {
147            run_id: format!("run-{user}"),
148            user_id: user,
149            dataset_ids: vec![],
150            dataset_names: vec![],
151            created_at: Utc::now(),
152            progress_percentage: AtomicU32::new(0),
153            abort: None,
154        }
155    }
156
157    #[test]
158    fn try_register_atomic_on_collision() {
159        let reg = SyncRegistry::new();
160        let user = Uuid::new_v4();
161        assert!(reg.try_register(user, build_run(user)).is_ok());
162        let conflict = reg
163            .try_register(user, build_run(user))
164            .expect_err("second insert must fail");
165        assert_eq!(conflict.0.user_id, user);
166    }
167
168    #[tokio::test(flavor = "multi_thread")]
169    async fn try_register_only_one_winner_under_concurrency() {
170        let reg = Arc::new(SyncRegistry::new());
171        let user = Uuid::new_v4();
172
173        let mut handles = Vec::new();
174        for _ in 0..32 {
175            let reg2 = Arc::clone(&reg);
176            handles.push(tokio::spawn(async move {
177                reg2.try_register(user, build_run(user)).is_ok()
178            }));
179        }
180        let mut wins = 0_u32;
181        for h in handles {
182            if h.await.expect("join task") {
183                wins += 1;
184            }
185        }
186        assert_eq!(wins, 1, "exactly one concurrent inserter wins");
187    }
188
189    #[test]
190    fn snapshot_for_returns_none_after_complete() {
191        let reg = SyncRegistry::new();
192        let user = Uuid::new_v4();
193        reg.try_register(user, build_run(user)).expect("insert");
194        assert!(reg.snapshot_for(user).is_some());
195        reg.complete(user);
196        assert!(reg.snapshot_for(user).is_none());
197    }
198
199    #[test]
200    fn update_progress_persists_until_complete() {
201        let reg = SyncRegistry::new();
202        let user = Uuid::new_v4();
203        reg.try_register(user, build_run(user)).expect("insert");
204        reg.update_progress(user, 42);
205        let snap = reg.snapshot_for(user).expect("snap");
206        assert_eq!(snap.progress_percentage, 42);
207    }
208}