cognee_http_server/sync/
registry.rs1use 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
16pub 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#[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#[derive(Debug, Clone)]
44pub struct AlreadyRunning(pub RunningSyncSnapshot);
45
46#[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 pub fn new() -> Self {
61 Self {
62 inner: Arc::new(DashMap::new()),
63 }
64 }
65
66 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 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 pub fn complete(&self, user_id: Uuid) {
88 self.inner.remove(&user_id);
89 }
90
91 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 pub fn snapshot_all(&self) -> Vec<RunningSyncSnapshot> {
101 self.inner
102 .iter()
103 .map(|entry| snapshot(entry.value()))
104 .collect()
105 }
106
107 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(®);
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}