Skip to main content

whatsapp_rust/store/
persistence_manager.rs

1use super::error::StoreError;
2use crate::store::Device;
3use crate::store::traits::Backend;
4use async_lock::RwLock;
5use event_listener::Event;
6use futures::FutureExt;
7use log::{debug, error};
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::Duration;
11use wacore::runtime::{AbortHandle, Runtime, ShutdownSignal, wait_for_shutdown};
12
13pub struct PersistenceManager {
14    device: Arc<RwLock<Device>>,
15    /// Read-mostly snapshot, rebuilt under the device write guard in
16    /// `modify_device` so it can never lag a committed mutation. Turns every
17    /// `get_device_snapshot` into an Arc refcount bump instead of a full
18    /// Device clone (the snapshot is read on every inbound message).
19    device_snapshot: std::sync::RwLock<Arc<Device>>,
20    backend: Arc<dyn Backend>,
21    dirty: Arc<AtomicBool>,
22    save_notify: Arc<Event>,
23    /// Set to true when the background saver halts due to repeated flush failures.
24    saver_halted: Arc<AtomicBool>,
25    #[cfg(test)]
26    fail_sender_key_device_clears: AtomicBool,
27    #[cfg(test)]
28    fail_sender_key_device_status_writes: AtomicBool,
29}
30
31impl PersistenceManager {
32    /// Create a PersistenceManager with a backend implementation.
33    ///
34    /// Note: The backend should already be configured with the correct device_id
35    /// (via SqliteStore::new_for_device for multi-account scenarios).
36    pub async fn new(backend: Arc<dyn Backend>) -> Result<Self, StoreError> {
37        debug!("PersistenceManager: Ensuring device row exists.");
38        // Ensure a device row exists for this backend's device_id; create it if not.
39        let exists = backend.exists().await?;
40        if !exists {
41            debug!("PersistenceManager: No device row found. Creating new device row.");
42            let id = backend.create().await?;
43            debug!("PersistenceManager: Created device row with id={id}.");
44        }
45
46        debug!("PersistenceManager: Attempting to load device data via Backend.");
47        let device_data_opt = backend.load().await?;
48
49        let device = if let Some(serializable_device) = device_data_opt {
50            debug!(
51                "PersistenceManager: Loaded existing device data (PushName: '{}'). Initializing Device.",
52                serializable_device.push_name
53            );
54            let mut dev = Device::new(backend.clone());
55            dev.load_from_serializable(serializable_device);
56            dev
57        } else {
58            debug!("PersistenceManager: No data yet; initializing default Device in memory.");
59            Device::new(backend.clone())
60        };
61
62        let snapshot = Arc::new(device.clone());
63        Ok(Self {
64            device: Arc::new(RwLock::new(device)),
65            device_snapshot: std::sync::RwLock::new(snapshot),
66            backend,
67            dirty: Arc::new(AtomicBool::new(false)),
68            save_notify: Arc::new(Event::new()),
69            saver_halted: Arc::new(AtomicBool::new(false)),
70            #[cfg(test)]
71            fail_sender_key_device_clears: AtomicBool::new(false),
72            #[cfg(test)]
73            fail_sender_key_device_status_writes: AtomicBool::new(false),
74        })
75    }
76
77    /// Handle for store adapters that need `&mut Device` trait access.
78    /// For plain reads, prefer [`get_device_snapshot`](Self::get_device_snapshot).
79    pub async fn get_device_arc(&self) -> Arc<RwLock<Device>> {
80        self.device.clone()
81    }
82
83    /// Cheap point-in-time view of the device state: an Arc refcount bump,
84    /// no locking against writers and no Device clone. Always reflects the
85    /// last committed `modify_device`/`process_command` mutation.
86    pub fn get_device_snapshot(&self) -> Arc<Device> {
87        self.device_snapshot
88            .read()
89            .unwrap_or_else(|p| p.into_inner())
90            .clone()
91    }
92
93    pub fn backend(&self) -> Arc<dyn Backend> {
94        self.backend.clone()
95    }
96
97    /// Returns true if the background saver halted due to repeated flush failures.
98    pub fn is_saver_halted(&self) -> bool {
99        self.saver_halted.load(Ordering::Acquire)
100    }
101
102    pub async fn modify_device<F, R>(&self, modifier: F) -> R
103    where
104        F: FnOnce(&mut Device) -> R,
105    {
106        let mut device_guard = self.device.write().await;
107        let result = modifier(&mut device_guard);
108
109        // Dirty BEFORE the snapshot rebuild: a shutdown flush racing this
110        // window must see the store dirty, or it would exit clean and drop
111        // the committed mutation (the clone below is not free).
112        self.dirty.store(true, Ordering::Relaxed);
113
114        // Rebuild while still holding the write guard so no reader can
115        // observe post-mutation effects with a pre-mutation snapshot.
116        *self
117            .device_snapshot
118            .write()
119            .unwrap_or_else(|p| p.into_inner()) = Arc::new(device_guard.clone());
120        drop(device_guard);
121
122        self.save_notify.notify(1);
123
124        result
125    }
126
127    /// Flush any dirty device state to the backend immediately.
128    pub async fn flush(&self) -> Result<(), StoreError> {
129        self.save_to_disk().await
130    }
131
132    async fn save_to_disk(&self) -> Result<(), StoreError> {
133        if self.dirty.swap(false, Ordering::AcqRel) {
134            debug!("Device state is dirty, saving to disk.");
135            let device_guard = self.device.read().await;
136            let serializable_device = device_guard.to_serializable();
137            drop(device_guard);
138
139            if let Err(e) = self.backend.save(&serializable_device).await {
140                // Restore dirty flag so the next tick retries the save
141                self.dirty.store(true, Ordering::Release);
142                return Err(e);
143            }
144            debug!("Device state saved successfully.");
145        }
146        Ok(())
147    }
148
149    /// Triggers a snapshot of the underlying storage backend.
150    /// Useful for debugging critical errors like crypto state corruption.
151    pub async fn create_snapshot(
152        &self,
153        name: &str,
154        extra_content: Option<&[u8]>,
155    ) -> Result<(), StoreError> {
156        #[cfg(feature = "debug-snapshots")]
157        {
158            // Ensure pending changes are saved first
159            self.save_to_disk().await?;
160            self.backend.snapshot_db(name, extra_content).await
161        }
162        #[cfg(not(feature = "debug-snapshots"))]
163        {
164            let _ = name;
165            let _ = extra_content;
166            log::warn!("Snapshot requested but 'debug-snapshots' feature is disabled");
167            Ok(())
168        }
169    }
170
171    /// Spawn the background saver. The task wakes on `save_notify`, the
172    /// interval tick, or the `shutdown` signal; runs `save_to_disk` after
173    /// each wake (no-op when the dirty flag is clear); and performs a final
174    /// flush before exiting on shutdown.
175    ///
176    /// Caller must keep the returned [`AbortHandle`] — dropping it aborts
177    /// the task. [`ShutdownSignal`] is sticky (see [`ShutdownNotifier`](wacore::runtime::ShutdownNotifier)):
178    /// a notify that races the task's first [`listen()`](event_listener::Event::listen)
179    /// is observed via the flag on the first iteration, so no data is stranded.
180    pub fn run_background_saver(
181        self: Arc<Self>,
182        runtime: Arc<dyn Runtime>,
183        interval: Duration,
184        shutdown: ShutdownSignal,
185    ) -> AbortHandle {
186        const MAX_CONSECUTIVE_FAILURES: u32 = 10;
187
188        let rt = runtime.clone();
189        let weak = Arc::downgrade(&self);
190        drop(self);
191        debug!("Background saver started (interval {interval:?})");
192        runtime.spawn(Box::pin(async move {
193            let mut consecutive_failures: u32 = 0;
194
195            // Flush any state dirtied during construction. save_notify is
196            // edge-triggered and fires from SetDeviceProps etc. before Bot::build
197            // spawns this task, so the dirty flag is our sticky catch for
198            // pre-spawn writes.
199            if let Some(this) = weak.upgrade()
200                && let Err(e) = this.save_to_disk().await
201            {
202                error!("Background saver: initial flush failed: {e}");
203                consecutive_failures = 1;
204            }
205
206            loop {
207                let Some(this) = weak.upgrade() else {
208                    debug!("PersistenceManager dropped, exiting background saver.");
209                    return;
210                };
211                let save_listener = this.save_notify.listen();
212                drop(this);
213
214                let should_exit = futures::select! {
215                    _ = save_listener.fuse() => false,
216                    _ = rt.sleep(interval).fuse() => false,
217                    _ = wait_for_shutdown(&shutdown).fuse() => true,
218                };
219
220                let Some(this) = weak.upgrade() else {
221                    debug!("PersistenceManager dropped, exiting background saver.");
222                    return;
223                };
224                let flush_result = this.save_to_disk().await;
225
226                // On the shutdown path the task is terminating either way; a failed
227                // final flush should not permanently flag the store as halted.
228                if should_exit {
229                    match &flush_result {
230                        Err(e) => {
231                            error!("Background saver: final flush on shutdown failed: {e}");
232                        }
233                        Ok(()) => {
234                            debug!("Background saver received shutdown; final flush complete.");
235                        }
236                    }
237                    return;
238                }
239
240                if let Err(e) = flush_result {
241                    consecutive_failures += 1;
242                    if consecutive_failures >= MAX_CONSECUTIVE_FAILURES {
243                        this.saver_halted.store(true, Ordering::Release);
244                        error!(
245                            "Background saver: {consecutive_failures} consecutive flush failures, \
246                             halting to prevent silent data loss. Last error: {e}"
247                        );
248                        return;
249                    }
250                    error!(
251                        "Background saver flush failed ({consecutive_failures}/{MAX_CONSECUTIVE_FAILURES}): {e}"
252                    );
253                } else {
254                    consecutive_failures = 0;
255                }
256            }
257        }))
258    }
259}
260
261use super::commands::{DeviceCommand, apply_command_to_device};
262
263impl PersistenceManager {
264    pub async fn process_command(&self, command: DeviceCommand) {
265        self.modify_device(|device| {
266            apply_command_to_device(device, command);
267        })
268        .await;
269    }
270}
271
272impl PersistenceManager {
273    pub async fn get_sender_key_devices(
274        &self,
275        group_jid: &str,
276    ) -> Result<Vec<(String, bool)>, StoreError> {
277        self.backend.get_sender_key_devices(group_jid).await
278    }
279
280    pub async fn set_sender_key_status(
281        &self,
282        group_jid: &str,
283        entries: &[(&str, bool)],
284    ) -> Result<(), StoreError> {
285        #[cfg(test)]
286        if self
287            .fail_sender_key_device_status_writes
288            .load(Ordering::Acquire)
289        {
290            return Err(StoreError::Io(std::io::Error::other(
291                "injected sender-key tracker status-write failure",
292            )));
293        }
294        self.backend.set_sender_key_status(group_jid, entries).await
295    }
296
297    pub async fn clear_sender_key_devices(&self, group_jid: &str) -> Result<(), StoreError> {
298        #[cfg(test)]
299        if self.fail_sender_key_device_clears.load(Ordering::Acquire) {
300            return Err(StoreError::Io(std::io::Error::other(
301                "injected sender-key tracker clear failure",
302            )));
303        }
304        self.backend.clear_sender_key_devices(group_jid).await
305    }
306
307    #[cfg(test)]
308    pub(crate) fn fail_sender_key_device_clears_for_tests(&self, fail: bool) {
309        self.fail_sender_key_device_clears
310            .store(fail, Ordering::Release);
311    }
312
313    #[cfg(test)]
314    pub(crate) fn fail_sender_key_device_status_writes_for_tests(&self, fail: bool) {
315        self.fail_sender_key_device_status_writes
316            .store(fail, Ordering::Release);
317    }
318
319    pub async fn delete_sender_key_device_rows(
320        &self,
321        device_jids: &[&str],
322    ) -> Result<(), StoreError> {
323        self.backend
324            .delete_sender_key_device_rows(device_jids)
325            .await
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::runtime_impl::TokioRuntime;
333    use wacore::time::Instant;
334
335    // Saver must observe shutdown.notify, run a final flush, and exit so the
336    // AbortHandle-backed task doesn't outlive the Bot.
337    #[tokio::test]
338    async fn saver_flushes_and_exits_on_shutdown() {
339        let backend = crate::test_utils::create_test_backend().await;
340        let pm = Arc::new(
341            PersistenceManager::new(backend.clone())
342                .await
343                .expect("pm init"),
344        );
345
346        let notifier = wacore::runtime::ShutdownNotifier::new();
347        let shutdown_signal = notifier.subscribe();
348
349        let runtime: Arc<dyn Runtime> = Arc::new(TokioRuntime);
350        // Interval far in the future so only shutdown can wake the saver.
351        let handle =
352            pm.clone()
353                .run_background_saver(runtime, Duration::from_secs(3600), shutdown_signal);
354
355        // Let the task enter its select before mutating.
356        tokio::time::sleep(Duration::from_millis(50)).await;
357
358        pm.modify_device(|d| {
359            d.push_name = "shutdown-flush".to_string();
360        })
361        .await;
362
363        notifier.notify();
364
365        let deadline = Instant::now() + Duration::from_secs(2);
366        loop {
367            if let Ok(Some(d)) = backend.load().await
368                && d.push_name == "shutdown-flush"
369            {
370                break;
371            }
372            if Instant::now() > deadline {
373                panic!("final flush did not reach backend after shutdown");
374            }
375            tokio::time::sleep(Duration::from_millis(20)).await;
376        }
377
378        // Dropping the handle must be a no-op when the task already exited.
379        drop(handle);
380    }
381
382    // Drop of the AbortHandle must actually terminate the task — not merely
383    // "not panic." Use the runtime Arc's strong count as the observable:
384    // the spawned task captures one reference via `rt = runtime.clone()`,
385    // which is released when the task's state machine is dropped.
386    #[tokio::test]
387    async fn saver_exits_when_abort_handle_dropped_without_signal() {
388        let backend = crate::test_utils::create_test_backend().await;
389        let pm = Arc::new(PersistenceManager::new(backend).await.expect("pm init"));
390
391        let runtime: Arc<dyn Runtime> = Arc::new(TokioRuntime);
392        let baseline = Arc::strong_count(&runtime);
393
394        let handle = pm.clone().run_background_saver(
395            Arc::clone(&runtime),
396            Duration::from_secs(3600),
397            ShutdownSignal::never(),
398        );
399
400        tokio::time::sleep(Duration::from_millis(50)).await;
401        assert!(
402            Arc::strong_count(&runtime) > baseline,
403            "running saver should hold a captured runtime Arc"
404        );
405
406        drop(handle);
407
408        let deadline = Instant::now() + Duration::from_secs(1);
409        while Arc::strong_count(&runtime) > baseline {
410            if Instant::now() > deadline {
411                panic!(
412                    "saver task did not release the runtime Arc within 1s of AbortHandle drop \
413                     (strong_count={}, baseline={})",
414                    Arc::strong_count(&runtime),
415                    baseline
416                );
417            }
418            tokio::time::sleep(Duration::from_millis(10)).await;
419        }
420    }
421
422    // Regression guard for the Client-lifetime-tie fix: storing the saver's
423    // AbortHandle inside a struct held by Arc means the handle survives Arc
424    // clones and only runs abort when the LAST strong ref drops. If the
425    // handle were held by Bot alone, extracting Arc<Client> and dropping
426    // Bot would leave the Client without periodic persistence.
427    //
428    // Tested at the primitive level (Arc<T> + OnceLock<AbortHandle>) because
429    // Client's internal detached tasks hold their own strong refs and would
430    // keep Client alive regardless. Rust's Drop semantics guarantee the
431    // chain Arc::drop -> T::drop -> OnceLock::drop -> AbortHandle::drop.
432    #[tokio::test]
433    async fn abort_handle_in_arc_drops_only_when_last_ref_released() {
434        use std::sync::atomic::{AtomicBool, Ordering};
435
436        struct Owner(std::sync::OnceLock<AbortHandle>);
437
438        let owner = Arc::new(Owner(std::sync::OnceLock::new()));
439
440        let aborted = Arc::new(AtomicBool::new(false));
441        let aborted_clone = Arc::clone(&aborted);
442        owner
443            .0
444            .set(AbortHandle::new(move || {
445                aborted_clone.store(true, Ordering::SeqCst);
446            }))
447            .ok()
448            .expect("first set");
449
450        let owner_clone = Arc::clone(&owner);
451        drop(owner);
452        assert!(
453            !aborted.load(Ordering::SeqCst),
454            "handle must survive while another Arc ref is held"
455        );
456
457        drop(owner_clone);
458        assert!(
459            aborted.load(Ordering::SeqCst),
460            "last Arc drop must release the handle and fire abort"
461        );
462    }
463}