Skip to main content

syncular_client/
progress.rs

1//! Ephemeral live sync work (SPEC ยง7.6), observable while the core is busy.
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::sync::{Arc, Mutex, Weak};
5
6#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "camelCase")]
8pub struct SyncProgress {
9    pub attempt: u64,
10    pub state: ProgressState,
11    pub phase: ProgressPhase,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub subscription_id: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub table: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub segment_id: Option<String>,
18    pub bytes_received: u64,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub bytes_total: Option<u64>,
21    pub rows_processed: u64,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub rows_total: Option<u64>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub error_code: Option<String>,
26    /// Present on `failed` when the core scheduled a background retry.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub retry_delay_ms: Option<u64>,
29}
30#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "camelCase")]
32pub enum ProgressState {
33    Running,
34    Complete,
35    Failed,
36}
37#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
38#[serde(rename_all = "camelCase")]
39pub enum ProgressPhase {
40    Request,
41    Download,
42    Import,
43}
44
45pub type SyncProgressListener = Arc<dyn Fn(&SyncProgress) + Send + Sync>;
46
47struct Delivery {
48    callback: SyncProgressListener,
49    revision: Mutex<u64>,
50}
51impl Delivery {
52    fn notify(&self, revision: u64, snapshot: &SyncProgress) {
53        let mut delivered = self.revision.lock().expect("progress delivery lock");
54        if revision <= *delivered {
55            return;
56        }
57        *delivered = revision;
58        // Serialize a late initial replay with updates from the owner thread.
59        // The observer registry is unlocked, so callbacks can read/unsubscribe.
60        let _ =
61            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.callback)(snapshot)));
62    }
63}
64#[derive(Default)]
65struct Inner {
66    snapshot: Option<SyncProgress>,
67    listeners: BTreeMap<u64, Arc<Delivery>>,
68    revision: u64,
69    next_listener: u64,
70}
71
72#[derive(Clone, Default)]
73pub struct ProgressObserver(Arc<Mutex<Inner>>);
74
75/// Keep this guard alive to receive updates; dropping it unsubscribes.
76pub struct ProgressSubscription {
77    inner: Weak<Mutex<Inner>>,
78    id: u64,
79}
80impl Drop for ProgressSubscription {
81    fn drop(&mut self) {
82        if let Some(inner) = self.inner.upgrade() {
83            inner
84                .lock()
85                .expect("progress lock")
86                .listeners
87                .remove(&self.id);
88        }
89    }
90}
91impl ProgressObserver {
92    pub fn snapshot(&self) -> Option<SyncProgress> {
93        self.0.lock().expect("progress lock").snapshot.clone()
94    }
95    pub fn subscribe(
96        &self,
97        listener: impl Fn(&SyncProgress) + Send + Sync + 'static,
98    ) -> ProgressSubscription {
99        let listener = Arc::new(Delivery {
100            callback: Arc::new(listener),
101            revision: Mutex::new(0),
102        });
103        let (id, revision, snapshot) = {
104            let mut inner = self.0.lock().expect("progress lock");
105            inner.next_listener += 1;
106            let id = inner.next_listener;
107            inner.listeners.insert(id, listener.clone());
108            (id, inner.revision, inner.snapshot.clone())
109        };
110        if let Some(snapshot) = snapshot {
111            listener.notify(revision, &snapshot);
112        }
113        ProgressSubscription {
114            inner: Arc::downgrade(&self.0),
115            id,
116        }
117    }
118    pub(crate) fn start(&self) {
119        let attempt = self.snapshot().map_or(1, |s| s.attempt + 1);
120        self.emit(SyncProgress {
121            attempt,
122            state: ProgressState::Running,
123            phase: ProgressPhase::Request,
124            subscription_id: None,
125            table: None,
126            segment_id: None,
127            bytes_received: 0,
128            bytes_total: None,
129            rows_processed: 0,
130            rows_total: None,
131            error_code: None,
132            retry_delay_ms: None,
133        });
134    }
135    pub(crate) fn update(&self, update: impl FnOnce(&mut SyncProgress)) {
136        if let Some(mut snapshot) = self.snapshot() {
137            if snapshot.state != ProgressState::Running {
138                return;
139            }
140            update(&mut snapshot);
141            self.emit(snapshot);
142        }
143    }
144    fn emit(&self, snapshot: SyncProgress) {
145        let (revision, listeners) = {
146            let mut inner = self.0.lock().expect("progress lock");
147            inner.snapshot = Some(snapshot.clone());
148            inner.revision += 1;
149            (
150                inner.revision,
151                inner.listeners.values().cloned().collect::<Vec<_>>(),
152            )
153        };
154        for listener in listeners {
155            listener.notify(revision, &snapshot);
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    #[test]
164    fn observes_running_work_and_unsubscribes_without_affecting_sync() {
165        let observer = ProgressObserver::default();
166        observer.start();
167        let events = Arc::new(Mutex::new(Vec::new()));
168        let captured = events.clone();
169        let reader = observer.clone();
170        let subscription = observer.subscribe(move |progress| {
171            assert!(reader.snapshot().is_some()); // Callback holds no observer lock.
172            captured.lock().unwrap().push(progress.clone());
173        });
174        let _broken = observer.subscribe(|_| panic!("observer failure"));
175        observer.update(|p| {
176            p.phase = ProgressPhase::Import;
177            p.rows_processed = 1024;
178        });
179        assert_eq!(events.lock().unwrap().len(), 2);
180        drop(subscription);
181        observer.update(|p| {
182            p.state = ProgressState::Failed;
183            p.error_code = Some("sync.transport_failed".into());
184        });
185        assert_eq!(events.lock().unwrap().len(), 2);
186        observer.start();
187        let snapshot = observer.snapshot().unwrap();
188        assert_eq!(snapshot.attempt, 2);
189        assert_eq!(snapshot.rows_processed, 0);
190        assert_eq!(snapshot.error_code, None);
191    }
192    #[test]
193    fn late_subscription_replay_cannot_move_progress_backwards() {
194        let observer = ProgressObserver::default();
195        observer.start();
196        let before = observer.snapshot().unwrap();
197        let events = Arc::new(Mutex::new(Vec::new()));
198        let captured = events.clone();
199        let delivery = Delivery {
200            callback: Arc::new(move |p| captured.lock().unwrap().push(p.rows_processed)),
201            revision: Mutex::new(0),
202        };
203        let mut after = before.clone();
204        after.rows_processed = 1024;
205        delivery.notify(2, &after);
206        delivery.notify(1, &before);
207        assert_eq!(*events.lock().unwrap(), vec![1024]);
208    }
209}