Skip to main content

syncular_client/
progress.rs

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