Skip to main content

whatsapp_rust/
sync_task.rs

1use std::sync::{Arc, Mutex, MutexGuard};
2
3use wacore::appstate::patch_decode::WAPatchName;
4use wacore::messages::DetachedHistorySyncNotification;
5
6/// Shared accounting for queued/running history-sync work. Payload bytes are
7/// logical compressed lengths: this keeps owned downloads comparable with
8/// sliced [`bytes::Bytes`], whose larger shared allocation is not observable.
9pub(crate) struct HistorySyncActivity {
10    state: Mutex<HistorySyncActivityState>,
11    idle_notifier: event_listener::Event,
12}
13
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15struct HistorySyncGeneration(usize);
16
17#[derive(Debug, Default)]
18struct HistorySyncActivityState {
19    generation: HistorySyncGeneration,
20    tasks: usize,
21    tasks_peak: usize,
22    payload_bytes: usize,
23    payload_bytes_peak: usize,
24}
25
26#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27pub(crate) struct HistorySyncActivitySnapshot {
28    pub(crate) tasks: usize,
29    pub(crate) tasks_peak: usize,
30    pub(crate) payload_bytes: usize,
31    pub(crate) payload_bytes_peak: usize,
32}
33
34impl HistorySyncActivity {
35    pub(crate) fn new() -> Self {
36        Self {
37            state: Mutex::new(HistorySyncActivityState::default()),
38            idle_notifier: event_listener::Event::new(),
39        }
40    }
41
42    pub(crate) fn begin(self: &Arc<Self>, payload_bytes: usize) -> HistorySyncTaskTracker {
43        let mut state = self.state_guard();
44        let generation = state.generation;
45        state.tasks = state.tasks.saturating_add(1);
46        state.tasks_peak = state.tasks_peak.max(state.tasks);
47        state.payload_bytes = state.payload_bytes.saturating_add(payload_bytes);
48        state.payload_bytes_peak = state.payload_bytes_peak.max(state.payload_bytes);
49        drop(state);
50        HistorySyncTaskTracker {
51            activity: Arc::clone(self),
52            generation,
53            payload_bytes,
54        }
55    }
56
57    pub(crate) fn reset(&self) {
58        let mut state = self.state_guard();
59        state.generation.0 = state.generation.0.wrapping_add(1);
60        state.tasks = 0;
61        state.payload_bytes = 0;
62        drop(state);
63        self.idle_notifier.notify(usize::MAX);
64    }
65
66    pub(crate) fn listen(&self) -> event_listener::EventListener {
67        self.idle_notifier.listen()
68    }
69
70    pub(crate) fn tasks(&self) -> usize {
71        self.state_guard().tasks
72    }
73
74    pub(crate) fn snapshot(&self) -> HistorySyncActivitySnapshot {
75        let state = self.state_guard();
76        HistorySyncActivitySnapshot {
77            tasks: state.tasks,
78            tasks_peak: state.tasks_peak,
79            payload_bytes: state.payload_bytes,
80            payload_bytes_peak: state.payload_bytes_peak,
81        }
82    }
83
84    fn release(&self, generation: HistorySyncGeneration, payload_bytes: usize) {
85        let mut state = self.state_guard();
86        if state.generation != generation {
87            return;
88        }
89        state.payload_bytes = state.payload_bytes.saturating_sub(payload_bytes);
90        state.tasks = state.tasks.saturating_sub(1);
91        let is_idle = state.tasks == 0;
92        drop(state);
93        if is_idle {
94            self.idle_notifier.notify(usize::MAX);
95        }
96    }
97
98    fn state_guard(&self) -> MutexGuard<'_, HistorySyncActivityState> {
99        self.state
100            .lock()
101            .unwrap_or_else(std::sync::PoisonError::into_inner)
102    }
103}
104
105#[doc(hidden)]
106pub struct HistorySyncTaskTracker {
107    activity: Arc<HistorySyncActivity>,
108    generation: HistorySyncGeneration,
109    payload_bytes: usize,
110}
111
112impl HistorySyncTaskTracker {
113    pub(crate) fn set_payload_bytes(&mut self, payload_bytes: usize) {
114        let mut state = self.activity.state_guard();
115        if state.generation != self.generation {
116            self.payload_bytes = payload_bytes;
117            return;
118        }
119        match payload_bytes.cmp(&self.payload_bytes) {
120            std::cmp::Ordering::Greater => {
121                let additional = payload_bytes - self.payload_bytes;
122                state.payload_bytes = state.payload_bytes.saturating_add(additional);
123                state.payload_bytes_peak = state.payload_bytes_peak.max(state.payload_bytes);
124            }
125            std::cmp::Ordering::Less => {
126                let released = self.payload_bytes - payload_bytes;
127                state.payload_bytes = state.payload_bytes.saturating_sub(released);
128            }
129            std::cmp::Ordering::Equal => {}
130        }
131        self.payload_bytes = payload_bytes;
132    }
133}
134
135impl std::fmt::Debug for HistorySyncTaskTracker {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.debug_struct("HistorySyncTaskTracker")
138            .field("generation", &self.generation)
139            .field("payload_bytes", &self.payload_bytes)
140            .finish_non_exhaustive()
141    }
142}
143
144impl Drop for HistorySyncTaskTracker {
145    fn drop(&mut self) {
146        self.activity.release(self.generation, self.payload_bytes);
147    }
148}
149
150#[derive(Debug)]
151pub enum MajorSyncTask {
152    HistorySync {
153        message_id: String,
154        notification: Box<DetachedHistorySyncNotification>,
155        tracker: HistorySyncTaskTracker,
156    },
157    AppStateSync {
158        name: WAPatchName,
159        full_sync: bool,
160    },
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn history_sync_tracker_updates_payload_peaks_and_releases_exactly_once() {
169        let activity = Arc::new(HistorySyncActivity::new());
170        let mut first = activity.begin(100);
171        let second = activity.begin(200);
172
173        first.set_payload_bytes(150);
174        assert_eq!(
175            activity.snapshot(),
176            HistorySyncActivitySnapshot {
177                tasks: 2,
178                tasks_peak: 2,
179                payload_bytes: 350,
180                payload_bytes_peak: 350,
181            }
182        );
183
184        drop(first);
185        assert_eq!(activity.tasks(), 1);
186        assert_eq!(activity.snapshot().payload_bytes, 200);
187
188        drop(second);
189        assert_eq!(
190            activity.snapshot(),
191            HistorySyncActivitySnapshot {
192                tasks: 0,
193                tasks_peak: 2,
194                payload_bytes: 0,
195                payload_bytes_peak: 350,
196            }
197        );
198    }
199
200    #[test]
201    fn queued_history_sync_tracker_cannot_release_current_generation() {
202        let activity = Arc::new(HistorySyncActivity::new());
203        let mut stale_tracker = activity.begin(1024);
204        activity.reset();
205        let current_tracker = activity.begin(2048);
206
207        stale_tracker.set_payload_bytes(4096);
208        drop(stale_tracker);
209
210        assert_eq!(activity.tasks(), 1);
211        assert_eq!(activity.snapshot().payload_bytes, 2048);
212
213        drop(current_tracker);
214        assert_eq!(activity.tasks(), 0);
215        assert_eq!(activity.snapshot().payload_bytes, 0);
216    }
217}