xmtp_mls/worker/device_sync/
worker.rs1use super::{
2 DeviceSyncClient, DeviceSyncError, decode_supported_content,
3 preference_sync::{PreferenceUpdate, store_preference_updates},
4};
5use crate::{
6 context::XmtpSharedContext,
7 subscriptions::{
8 LocalEvents, SyncWorkerEvent,
9 incoming::{IncomingCoordinator, IncomingScope},
10 },
11 worker::{
12 BoxedWorker, DynMetrics, MetricsCasting, NeedsDbReconnect, Worker, WorkerFactory,
13 WorkerKind, WorkerResult, metrics::WorkerMetrics,
14 },
15};
16use futures::TryFutureExt;
17use prost::Message;
18use std::{sync::Arc, time::Duration};
19use tokio::sync::{OnceCell, broadcast};
20use tracing::instrument;
21use xmtp_common::Event;
22use xmtp_db::group_message::StoredGroupMessage;
23use xmtp_db::prelude::*;
24use xmtp_macro::log_event;
25use xmtp_proto::xmtp::{
26 device_sync::content::{
27 DeviceSyncAcknowledge, PreferenceUpdates as PreferenceUpdatesProto,
28 device_sync_content::Content as ContentProto,
29 },
30 mls::message_contents::EncodedContent,
31};
32
33const MAX_ATTEMPTS: i32 = 3;
34
35pub struct SyncWorker<Context> {
36 client: DeviceSyncClient<Context>,
37 receiver: broadcast::Receiver<SyncWorkerEvent>,
38 init: OnceCell<()>,
39 metrics: Arc<WorkerMetrics<SyncMetric>>,
40}
41
42impl<Context> SyncWorker<Context>
43where
44 Context: XmtpSharedContext + 'static,
45{
46 pub fn new(context: Context, metrics: Option<DynMetrics>) -> Self {
47 let receiver = context.worker_events().subscribe();
48 let metrics = metrics
49 .and_then(|m| m.as_sync_metrics())
50 .unwrap_or(Arc::new(WorkerMetrics::new(context.installation_id())));
51 let client = DeviceSyncClient::new(context, metrics.clone());
52
53 Self {
54 client,
55 receiver,
56 init: OnceCell::new(),
57 metrics,
58 }
59 }
60}
61
62struct Factory<Context> {
63 context: Context,
64}
65
66impl<Context> WorkerFactory for Factory<Context>
67where
68 Context: XmtpSharedContext + 'static,
69{
70 fn create(&self, metrics: Option<DynMetrics>) -> (BoxedWorker, Option<DynMetrics>) {
71 let worker = SyncWorker::new(self.context.clone(), metrics);
72 let metrics = worker.metrics.clone();
73
74 (Box::new(worker) as Box<_>, Some(metrics as Arc<_>))
75 }
76
77 fn kind(&self) -> WorkerKind {
78 WorkerKind::DeviceSync
79 }
80}
81
82#[xmtp_common::async_trait]
83impl<Context> Worker for SyncWorker<Context>
84where
85 Context: XmtpSharedContext + 'static,
86{
87 fn kind(&self) -> WorkerKind {
88 WorkerKind::DeviceSync
89 }
90
91 fn metrics(&self) -> Option<DynMetrics> {
92 Some(self.metrics.clone())
93 }
94
95 fn factory<C>(context: C) -> impl WorkerFactory + 'static
96 where
97 C: XmtpSharedContext + 'static,
98 {
99 Factory { context }
100 }
101
102 async fn run_tasks(&mut self) -> WorkerResult<()> {
103 self.run().map_err(|e| Box::new(e) as Box<_>).await
104 }
105}
106
107impl<Context> SyncWorker<Context>
108where
109 Context: XmtpSharedContext + 'static,
110{
111 async fn run(&mut self) -> Result<(), DeviceSyncError> {
112 self.sync_init().await?;
113 let _receipt = IncomingCoordinator::for_context(&self.client.context)
115 .acquire(IncomingScope::DeviceSyncGroups);
116 self.metrics.increment_metric(SyncMetric::Init);
117
118 let tick_fut = Self::tick(self.client.context.clone());
119 let run_fut = self.run_internal();
120
121 tokio::select! {
122 _ = tick_fut => Ok(()),
123 res = run_fut => res,
124 }
125 }
126
127 async fn run_internal(&mut self) -> Result<(), DeviceSyncError> {
128 use tokio::sync::broadcast::error::RecvError;
129 loop {
130 let event = match self.receiver.recv().await {
131 Ok(event) => event,
132 Err(RecvError::Lagged(skipped)) => {
133 tracing::warn!(
139 skipped,
140 "sync worker receiver lagged; re-scheduling installation reconciliation"
141 );
142 self.client.schedule_add_installations_to_groups()?;
143 self.evt_new_sync_group_msg(true).await?;
144 continue;
145 }
146 Err(RecvError::Closed) => break,
147 };
148
149 if matches!(event, SyncWorkerEvent::Tick) {
152 self.evt_new_sync_group_msg(true).await?;
153 continue;
154 }
155
156 tracing::info!(
157 installation_id = %self.client.context.installation_id(),
158 "new sync worker event: {event:?}",
159 );
160 self.handle_event(event).await?;
161 }
162 Ok(())
163 }
164
165 #[tracing::instrument(skip_all, fields(worker = ?self.kind(), operation = "worker_turn", event = ?event))]
166 async fn handle_event(&mut self, event: SyncWorkerEvent) -> Result<(), DeviceSyncError> {
167 match event {
168 SyncWorkerEvent::NewSyncGroupFromWelcome(_group_id) => {
169 self.evt_new_sync_group_from_welcome().await
170 }
171 SyncWorkerEvent::NewSyncGroupMsg => self.evt_new_sync_group_msg(false).await,
172 SyncWorkerEvent::SyncPreferences(preference_updates) => {
173 self.evt_sync_preferences(preference_updates).await
174 }
175 SyncWorkerEvent::CycleHMAC => self.evt_cycle_hmac().await,
176 SyncWorkerEvent::Tick => unreachable!("Tick is handled before dispatch"),
179 }
180 }
181
182 async fn tick(ctx: Context) {
183 use futures::StreamExt;
184 let (base, jitter) = ctx.worker_interval(
185 crate::worker::WorkerKind::DeviceSync,
186 Duration::from_secs(20),
187 );
188 let mut intervals = xmtp_common::time::jittered_interval_stream(base, jitter);
189 let _ = intervals.next().await;
193 while intervals.next().await.is_some() {
194 let _ = ctx.worker_events().send(SyncWorkerEvent::Tick);
199 }
200 }
201
202 #[instrument(level = "trace", skip_all)]
204 async fn sync_init(&mut self) -> Result<(), DeviceSyncError> {
205 let Self { init, client, .. } = &self;
206
207 init.get_or_try_init(|| async {
208 let conn = self.client.context.db();
209 log_event!(
210 Event::DeviceSyncInitializing,
211 self.client.context.installation_id()
212 );
213
214 if conn.primary_sync_group()?.is_none() {
216 log_event!(
217 Event::DeviceSyncNoPrimarySyncGroup,
218 self.client.context.installation_id()
219 );
220 let sync_group = client.get_sync_group().await?;
221 log_event!(
222 Event::DeviceSyncCreatedPrimarySyncGroup,
223 self.client.context.installation_id(),
224 group_id = sync_group.group_id
225 );
226 }
227
228 log_event!(
229 Event::DeviceSyncInitializingFinished,
230 self.client.context.installation_id()
231 );
232
233 Ok(())
234 })
235 .await
236 .copied()
237 }
238
239 async fn evt_new_sync_group_from_welcome(&self) -> Result<(), DeviceSyncError> {
240 tracing::info!("New sync group from welcome detected.");
241
242 self.client.schedule_add_installations_to_groups()?;
246
247 self.metrics
248 .increment_metric(SyncMetric::SyncGroupWelcomesProcessed);
249
250 self.client.cycle_hmac().await?;
252
253 Ok(())
254 }
255
256 async fn evt_new_sync_group_msg(&self, is_tick: bool) -> Result<(), DeviceSyncError> {
257 let unprocessed_messages = self.client.context.db().unprocessed_sync_group_messages()?;
258
259 if !is_tick || !unprocessed_messages.is_empty() {
260 tracing::info!("Processing {} messages.", unprocessed_messages.len());
261 }
262
263 self.client
264 .process_sync_group_messages(&self.metrics, unprocessed_messages)
265 .await
266 }
267
268 async fn evt_sync_preferences(
269 &self,
270 updates: Vec<PreferenceUpdate>,
271 ) -> Result<(), DeviceSyncError> {
272 let updates = self.client.sync_preferences(updates).await?;
273
274 updates.iter().for_each(|update| match update {
275 PreferenceUpdate::Consent(_) => self.metrics.increment_metric(SyncMetric::ConsentSent),
276 PreferenceUpdate::Hmac { .. } => self.metrics.increment_metric(SyncMetric::HmacSent),
277 });
278 Ok(())
279 }
280
281 async fn evt_cycle_hmac(&self) -> Result<(), DeviceSyncError> {
282 self.client.cycle_hmac().await?;
283 Ok(())
284 }
285}
286
287impl<Context> DeviceSyncClient<Context>
288where
289 Context: XmtpSharedContext,
290{
291 async fn process_sync_group_messages(
292 &self,
293 handle: &WorkerMetrics<SyncMetric>,
294 messages: Vec<StoredGroupMessage>,
295 ) -> Result<(), DeviceSyncError>
296 where
297 Context::Db: 'static,
298 {
299 let installation_id = self.installation_id();
300
301 for msg in messages {
302 let content = EncodedContent::decode(&*msg.decrypted_message_bytes)
303 .ok()
304 .and_then(|content| decode_supported_content(&content.content));
305 let Some(content) = content else {
306 self.context
310 .db()
311 .mark_device_sync_msg_as_processed(&msg.id)?;
312 continue;
313 };
314 let is_external = msg.sender_installation_id != installation_id;
315
316 let msg_type = match &content {
317 ContentProto::PreferenceUpdates(_) => "PreferenceUpdates",
318 ContentProto::Acknowledge(_) => "Acknowledge",
319 };
320
321 log_event!(
322 Event::DeviceSyncProcessingMessages,
323 self.context.installation_id(),
324 msg_type,
325 external = is_external,
326 message_id = #msg.id,
327 group_id = msg.group_id
328 );
329
330 if let Err(err) = self.process_message(handle, &msg, content).await {
331 if err.needs_db_reconnect() {
334 return Err(err);
335 }
336 log_event!(
337 Event::DeviceSyncMessageProcessingError,
338 self.context.installation_id(),
339 error = %err,
340 message_id = #msg.id
341 );
342 self.context
343 .db()
344 .increment_device_sync_msg_attempt(&msg.id, MAX_ATTEMPTS)?;
345 } else {
346 self.context
347 .db()
348 .mark_device_sync_msg_as_processed(&msg.id)?;
349 }
350 }
351
352 Ok(())
353 }
354
355 async fn process_message(
356 &self,
357 handle: &WorkerMetrics<SyncMetric>,
358 msg: &StoredGroupMessage,
359 content: ContentProto,
360 ) -> Result<(), DeviceSyncError>
361 where
362 Context::Db: 'static,
363 {
364 let conn = self.context.db();
365 let installation_id = self.context.installation_id();
366 let is_external = msg.sender_installation_id != installation_id;
367
368 match content {
369 ContentProto::PreferenceUpdates(PreferenceUpdatesProto { updates }) => {
370 if is_external {
371 tracing::info!("Incoming preference updates: {updates:?}");
372 }
373 tracing::info!(
374 "{} storing preference updates",
375 self.context.installation_id()
376 );
377 let updated = store_preference_updates(updates.clone(), &conn, handle)?;
379 if !updated.is_empty() {
380 self.context.task_channels().wake_notifications();
381 let _ = self
382 .context
383 .local_events()
384 .send(LocalEvents::PreferencesChanged(updated));
385 }
386 }
387 ContentProto::Acknowledge(DeviceSyncAcknowledge { .. }) => {
388 return Ok(());
389 }
390 }
391
392 Ok(())
393 }
394}
395
396#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
397pub enum SyncMetric {
398 Init,
399 SyncGroupCreated,
400 SyncGroupWelcomesProcessed,
401 HmacSent,
402 HmacReceived,
403 ConsentSent,
404 ConsentReceived,
405}
406
407impl WorkerMetrics<SyncMetric> {
408 pub async fn wait_for_init(&self) -> Result<(), xmtp_common::time::Expired> {
409 self.register_interest(SyncMetric::Init, 1).wait().await
410 }
411}