whatsapp_rust/client/adapters.rs
1//! Signal/sender-key store adapters, per-session locks and noise socket access.
2
3use super::*;
4use anyhow::Context as _;
5
6impl Client {
7 /// Build a [`SignalProtocolStoreAdapter`] from the current device state and signal cache.
8 pub(crate) async fn signal_adapter(
9 &self,
10 ) -> crate::store::signal_adapter::SignalProtocolStoreAdapter {
11 let device_store = self.persistence_manager.get_device_arc().await;
12 self.signal_adapter_from(device_store)
13 }
14
15 /// Build a standalone [`SenderKeyAdapter`] from the current device state and
16 /// signal cache, avoiding the full five-store adapter on the SKDM path.
17 pub(crate) async fn sender_key_adapter(
18 &self,
19 ) -> crate::store::signal_adapter::SenderKeyAdapter {
20 crate::store::signal_adapter::SenderKeyAdapter::new(
21 self.persistence_manager.get_device_arc().await,
22 self.signal_cache.clone(),
23 )
24 }
25
26 /// Build a [`SignalProtocolStoreAdapter`] from a pre-fetched device arc.
27 pub(crate) fn signal_adapter_from(
28 &self,
29 device_store: Arc<RwLock<crate::store::Device>>,
30 ) -> crate::store::signal_adapter::SignalProtocolStoreAdapter {
31 crate::store::signal_adapter::SignalProtocolStoreAdapter::new(
32 device_store,
33 self.signal_cache.clone(),
34 )
35 }
36
37 /// Get the per-address session mutex from the lock cache.
38 pub(crate) async fn session_lock_for(&self, signal_addr_str: &str) -> Arc<Mutex<()>> {
39 self.session_locks
40 .get_with_by_ref(signal_addr_str, async { Arc::new(Mutex::new(())) })
41 .await
42 }
43
44 /// Acquire the per-group cold-distribution single-flight guard.
45 pub(crate) async fn group_distribution_lock(
46 &self,
47 group: &Jid,
48 ) -> async_lock::MutexGuardArc<()> {
49 self.group_distribution_locks
50 .get_with_by_ref(group, async { Arc::new(Mutex::new(())) })
51 .await
52 .lock_arc()
53 .await
54 }
55
56 /// Get the active noise socket, or error if not connected.
57 pub(crate) async fn get_noise_socket(&self) -> Result<Arc<NoiseSocket>, ClientError> {
58 self.noise_socket
59 .lock()
60 .await
61 .clone()
62 .ok_or(ClientError::NotConnected)
63 }
64
65 /// Force any pending write-behind Signal cache state to the backend,
66 /// returning once the flush completes (or fails).
67 ///
68 /// The live receive path schedules a coalesced flush (see `signal_flush.rs`)
69 /// instead of writing through, and lease-covered sends do the same. Only a
70 /// send that raises a session or sender-key counter lease flushes
71 /// synchronously. On success, the
72 /// backend normally trails the cache by about the coalescing window, but
73 /// that is not a hard wall-clock bound — the timer can slip under runtime
74 /// starvation and the flush can wait on locks or slow/failing storage (a
75 /// backend outage extends it until the retry loop succeeds). Use this to
76 /// settle durability deterministically before reading persisted state or
77 /// ahead of a non-graceful shutdown — and check the returned `Result`, as a
78 /// failure leaves state pending.
79 ///
80 /// Call from a control task, never from inside an event handler or an
81 /// [`InboundDurabilityHook`]: during an offline-sync drain those run while
82 /// the processing permit is held, and settling routes through that same
83 /// permit — re-entering it would deadlock.
84 ///
85 /// [`InboundDurabilityHook`]: crate::types::durability_hook::InboundDurabilityHook
86 pub async fn flush_pending_signal_state(&self) -> Result<(), SignalMaintenanceError> {
87 self.flush_signal_cache_batch_safe().await
88 }
89
90 /// Flush the in-memory signal cache to the database backend. Invoked by the
91 /// send path (synchronously, pre-wire), the receive-path coalescer, and the
92 /// drain/retry/teardown recovery paths — not unconditionally per message.
93 pub(crate) async fn flush_signal_cache(&self) -> Result<(), anyhow::Error> {
94 // Clone the backend before awaiting so a slow write cannot retain the
95 // device guard and stall every concurrent Device write.
96 let backend = self
97 .persistence_manager
98 .get_device_snapshot()
99 .backend
100 .clone();
101 self.signal_cache
102 .flush(&*backend)
103 .await
104 .context("Failed to flush signal cache")
105 }
106
107 /// Signal-cache flush that is safe while the offline drain is active.
108 ///
109 /// [`flush_signal_cache`](Self::flush_signal_cache) is safe only when the
110 /// caller holds the message processing permit or the batcher is known
111 /// inactive: it persists the WHOLE cache, including ratchet advances of
112 /// drain entries that may not have a durable buffered row yet. Everything
113 /// else must go through this `_batch_safe` variant.
114 ///
115 /// During the drain, decrypted messages accumulate in the commit batcher
116 /// with no durable buffered copy; flushing the cache from an unrelated
117 /// path (a retry receipt, a send, an identity change) would persist their
118 /// ratchet advances, and a crash/teardown that then drops the entries
119 /// turns each redelivery into an ackable duplicate — silent loss for hook
120 /// consumers. So in drain mode this routes through the batcher: commit
121 /// the pending entries (rows first) and flush under the processing
122 /// permit. Outside the drain it is exactly [`Self::flush_signal_cache`].
123 ///
124 /// Must NOT be called while holding the processing permit (it acquires
125 /// it); permit-holding paths commit via the batcher directly.
126 pub(crate) async fn flush_signal_cache_batch_safe(&self) -> Result<(), SignalMaintenanceError> {
127 // Under the permit the commit ALWAYS flushes the Signal cache (an empty
128 // batch still flushes — see flush_inbound_commits_under_permit), so a
129 // successful call is proof the out-of-band advance is persisted; no
130 // stale is_active() re-check needed even if the drain finisher
131 // deactivated while we waited for the permit.
132 let drain_active = self.inbound_commit_batch.is_active();
133 if drain_active && let Some(client) = self.self_weak.get().and_then(|w| w.upgrade()) {
134 return if client
135 .flush_inbound_commits_under_permit(false, None, None)
136 .await
137 {
138 Ok(())
139 } else {
140 Err(SignalMaintenanceError::DrainCommitFailed)
141 };
142 } else if drain_active {
143 // Drain active but no live client to route the permit-held flush
144 // through (practically unreachable — the run loop holds a strong
145 // Arc). Fail closed regardless of has_entries(): an empty drain can
146 // still carry dirty SKDM-only advances with no rows, and a raw
147 // flush here would persist them rowless — the exact loss this
148 // batch-safe path exists to prevent. Leaving the cache unflushed
149 // makes the server redeliver.
150 return Err(SignalMaintenanceError::DrainShuttingDown);
151 }
152 self.flush_signal_cache()
153 .await
154 .map_err(SignalMaintenanceError::Storage)
155 }
156
157 /// Pre-wire durability gate for the send path. Flushes synchronously only
158 /// when an outbound crypto advance actually demands it — a raised session
159 /// counter lease or a raised sender-key lease not yet persisted (see
160 /// `SignalStoreCache::needs_pre_wire_flush`). Otherwise the dirty state is
161 /// covered by an existing durable lease, so it only needs to land
162 /// eventually: it rides the same coalesced write-behind as the receive
163 /// path instead of paying a serialize + storage transaction per message.
164 /// A failure must abort the send — transmitting a ciphertext whose lease
165 /// could not be persisted reintroduces the counter-reuse window.
166 ///
167 /// The gate is deliberately a GLOBAL predicate, not one scoped to the
168 /// addresses this stanza encrypted for: the send path does not carry them
169 /// back up, and erring toward an extra flush is the safe direction (it can
170 /// over-flush, never under-flush). The cost is a sharp edge under a failing
171 /// backend: another session's pending lease makes this send flush, and that
172 /// flush's failure aborts this send too — the same collateral every send
173 /// took before leases existed, now limited to the window where some lease
174 /// is actually unpersisted. So "a lease-covered send needs no flush" is a
175 /// statement about what this send REQUIRES, not a promise it never flushes.
176 pub(crate) async fn persist_signal_state_pre_wire(&self) -> Result<(), anyhow::Error> {
177 if self.signal_cache.needs_pre_wire_flush().await {
178 return self
179 .flush_signal_cache_batch_safe()
180 .await
181 .map_err(Into::into);
182 }
183 self.schedule_signal_flush(self.connection_generation.load(Ordering::Acquire));
184 Ok(())
185 }
186
187 /// [`flush_signal_cache_batch_safe`](Self::flush_signal_cache_batch_safe)
188 /// with error logging instead of propagation.
189 pub(crate) async fn flush_signal_cache_batch_safe_logged(
190 &self,
191 context: &str,
192 id: Option<&str>,
193 ) {
194 if let Err(e) = self.flush_signal_cache_batch_safe().await {
195 log_signal_flush_error(context, id, &e);
196 }
197 }
198}
199
200fn log_signal_flush_error(context: &str, id: Option<&str>, e: &SignalMaintenanceError) {
201 if let Some(id) = id {
202 log::error!("Failed to flush signal cache ({context} {id}): {e:?}");
203 } else {
204 log::error!("Failed to flush signal cache ({context}): {e:?}");
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use wacore::store::in_memory::InMemoryBackend;
212 use wacore_binary::{Jid, Server};
213
214 #[tokio::test]
215 async fn signal_flush_context_preserves_the_backend_error_chain() {
216 let backend = Arc::new(InMemoryBackend::new());
217 let client = crate::test_utils::create_test_client_with_backend(backend.clone()).await;
218 let peer = Jid::new("12025550111", Server::Pn).with_device(1);
219 crate::test_utils::seed_peer_session(&client, &peer).await;
220 backend.set_fail_session_writes(true);
221
222 let error = client
223 .flush_signal_cache()
224 .await
225 .expect_err("injected backend failure must propagate");
226 let chain: Vec<String> = error.chain().map(ToString::to_string).collect();
227
228 assert_eq!(
229 chain.first().map(String::as_str),
230 Some("Failed to flush signal cache")
231 );
232 assert!(
233 chain
234 .iter()
235 .any(|cause| cause.contains("put_sessions_batch failing (test hook)")),
236 "typed backend cause missing from {chain:?}"
237 );
238 }
239
240 #[tokio::test]
241 async fn flush_pending_signal_state_settles_a_healthy_backend() {
242 let backend = Arc::new(InMemoryBackend::new());
243 let client = crate::test_utils::create_test_client_with_backend(backend.clone()).await;
244 let peer = Jid::new("12025550112", Server::Pn).with_device(1);
245 crate::test_utils::seed_peer_session(&client, &peer).await;
246
247 client
248 .flush_pending_signal_state()
249 .await
250 .expect("a healthy backend must settle");
251 }
252
253 #[tokio::test]
254 async fn flush_pending_signal_state_reports_a_backend_failure_as_storage() {
255 let backend = Arc::new(InMemoryBackend::new());
256 let client = crate::test_utils::create_test_client_with_backend(backend.clone()).await;
257 let peer = Jid::new("12025550113", Server::Pn).with_device(1);
258 crate::test_utils::seed_peer_session(&client, &peer).await;
259 backend.set_fail_session_writes(true);
260
261 let error = client
262 .flush_pending_signal_state()
263 .await
264 .expect_err("injected backend failure must propagate");
265 assert!(matches!(error, SignalMaintenanceError::Storage(_)));
266 assert!(std::error::Error::source(&error).is_some());
267 }
268}