1use super::*;
4use xmtp_common::snippet::Snippet;
5
6fn log_publish_error(error: &GroupError) {
8 if let GroupError::FailedToVerifyInstallations(failed) = error {
9 tracing::error!(
10 error_kind = "FailedToVerifyInstallations",
11 failed_installation_count = failed.0.len(),
12 "Sync: error publishing intents"
13 );
14 } else {
15 tracing::error!("Sync: error publishing intents {error:?}");
16 }
17}
18
19impl<Context> MlsGroup<Context>
20where
21 Context: XmtpSharedContext,
22{
23 #[xmtp_common::mls_span]
24 pub async fn sync(&self) -> Result<SyncSummary, GroupError> {
25 self.sync_once()
26 .await
27 .map_err(|error| self.context.server_configuration().blocked_or(error))
28 }
29
30 async fn sync_once(&self) -> Result<SyncSummary, GroupError> {
31 let conn = self.context.db();
32 if conn
33 .find_group(&self.group_id)?
34 .is_some_and(|group| group.membership_state == GroupMembershipState::Restored)
35 {
36 return Err(GroupError::GroupInactive);
37 }
38 self.context.server_configuration().check()?;
40
41 let epoch = self.epoch().await?;
42 tracing::debug!(
43 inbox_id = self.context.inbox_id(),
44 installation_id = self.context.installation_id().as_slice().snippet(),
45 group_id = self.group_id.short_hex(),
46 epoch,
47 "syncing group",
48 );
49
50 for other_dm in conn.other_dms(&self.group_id)? {
52 if other_dm.membership_state == GroupMembershipState::Restored {
53 continue;
54 }
55 let other_dm = Self::new_from_arc(
56 self.context.clone(),
57 other_dm.id,
58 other_dm.dm_id.clone(),
59 other_dm.conversation_type,
60 other_dm.created_at_ns,
61 );
62
63 other_dm.sync_with_conn().await?;
64 other_dm.maybe_update_installations(None).await?;
65 }
66
67 let sync_summary = self.sync_with_conn().await.map_err(GroupError::from)?;
68 self.maybe_update_installations(None).await?;
69 Ok(sync_summary)
70 }
71
72 fn handle_group_paused(&self) -> Result<(), GroupError> {
73 let group_id_typed = self.group_id;
75 if let Some(required_min_version_str) = self
76 .context
77 .db()
78 .get_group_paused_version(&group_id_typed)?
79 {
80 tracing::info!(
81 "Group is paused until version: {}",
82 required_min_version_str
83 );
84 let current_version_str = self.context.version_info().pkg_version();
85 let current_version = self.context.version_info().pkg_semver();
86 let required_min_version = LibXMTPVersion::parse(&required_min_version_str)
87 .map_err(CommitValidationError::from)?;
88
89 if required_min_version <= *current_version {
90 tracing::info!(
91 "Unpausing group since version requirements are met. \
92 Group ID: {}",
93 hex::encode(self.group_id),
94 );
95 self.context.db().unpause_group(&group_id_typed)?;
96 } else {
97 tracing::warn!(
98 "Skipping sync for paused group since version requirements are not met. \
99 Group ID: {}, \
100 Required version: {}, \
101 Current version: {}",
102 hex::encode(self.group_id),
103 required_min_version_str,
104 current_version_str
105 );
106 return Err(GroupError::GroupPausedUntilUpdate(required_min_version_str));
108 }
109 }
110 Ok(())
111 }
112
113 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(err, fields(inbox_id = %self.context.inbox_id(), operation = "sync_with_conn")))]
117 #[cfg_attr(not(any(test, feature = "test-utils")), xmtp_common::mls_span)]
118 pub async fn sync_with_conn(&self) -> Result<SyncSummary, SyncSummary> {
119 let mut app_data_changes = Vec::new();
125 let result = self.sync_with_conn_locked(&mut app_data_changes).await;
126 self.dispatch_app_data_changes(app_data_changes).await;
127 result
128 }
129
130 async fn sync_with_conn_locked(
133 &self,
134 app_data_changes: &mut Vec<AppDataChange>,
135 ) -> Result<SyncSummary, SyncSummary> {
136 let _mutex = self.mutex.lock().await;
137 let mut summary = SyncSummary::default();
138
139 if self
140 .context
141 .db()
142 .find_group(&self.group_id)
143 .map_err(|error| SyncSummary::other(error.into()))?
144 .is_some_and(|group| group.membership_state == GroupMembershipState::Restored)
145 {
146 return Err(SyncSummary::other(GroupError::GroupInactive));
147 }
148
149 if !self.is_active().map_err(SyncSummary::other)? {
150 log_event!(
151 Event::GroupSyncGroupInactive,
152 self.context.installation_id(),
153 group_id = self.group_id
154 );
155 return Ok(summary);
156 }
157
158 if let Err(e) = self.handle_group_paused() {
159 return Err(SyncSummary::other(e));
160 }
161
162 let result = self.publish_intents().await;
164 if let Err(e) = result {
165 log_publish_error(&e);
166 summary.add_publish_err(e);
167 }
168
169 let result = self.receive().await;
172 match result {
173 Ok(mut s) => {
174 app_data_changes.append(&mut s.app_data_changes);
175 summary.add_process(s)
176 }
177 Err(e) => {
178 summary.add_other(e);
179 }
183 }
184
185 let result = self.post_commit().await;
186 if let Err(e) = result {
187 tracing::error!("post commit error {e:?}",);
188 summary.add_post_commit_err(e);
189 }
190
191 if summary.is_errored() {
192 Err(summary)
193 } else {
194 Ok(summary)
195 }
196 }
197
198 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip_all))]
199 #[cfg_attr(not(any(test, feature = "test-utils")), xmtp_common::mls_span)]
200 pub(crate) async fn sync_until_last_intent_resolved(&self) -> Result<SyncSummary, GroupError> {
201 let intents = self.context.db().find_group_intents(
205 self.group_id,
206 Some(vec![
207 IntentState::ToPublish,
208 IntentState::Published,
209 IntentState::Committed,
210 ]),
211 Some(IntentKind::all().collect()),
212 )?;
213
214 let Some(intent) = intents.last() else {
215 return Ok(Default::default());
216 };
217
218 self.sync_until_intent_resolved(intent.id).await
219 }
220
221 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(err, level = "info", fields(inbox_id = %self.context.inbox_id(), operation = "intent"), skip(self)))]
222 #[cfg_attr(not(any(test, feature = "test-utils")), xmtp_common::mls_span)]
223 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
234 #[cfg_attr(
235 not(any(test, feature = "test-utils")),
236 tracing::instrument(level = "trace", skip(self))
237 )]
238 pub(crate) async fn sync_until_intent_resolved(
239 &self,
240 intent_id: ID,
241 ) -> Result<SyncSummary, GroupError> {
242 log_event!(
243 Event::GroupSyncStart,
244 self.context.installation_id(),
245 group_id = self.group_id
246 );
247
248 let result = Box::pin(self.sync_until_intent_resolved_inner(intent_id)).await;
250 let summary = match &result {
251 Ok(summary) => Some(summary),
252 Err(GroupError::Sync(summary)) => Some(&**summary),
253 Err(GroupError::SyncFailedToWait(summary)) => Some(&**summary),
254 _ => None,
255 };
256
257 log_event!(
258 Event::GroupSyncFinished,
259 self.context.installation_id(),
260 group_id = self.group_id,
261 summary = ?summary,
262 success = result.is_ok()
263 );
264
265 result
266 }
267
268 async fn sync_until_intent_resolved_inner(
269 &self,
270 intent_id: ID,
271 ) -> Result<SyncSummary, GroupError> {
272 let mut summary = SyncSummary::default();
273 let db = self.context.db();
274
275 let time_spent = xmtp_common::time::Instant::now();
276 let backoff = ExponentialBackoff::builder()
277 .duration(Duration::from_millis(SYNC_BACKOFF_WAIT_MS.into()))
278 .total_wait_max(Duration::from_secs(SYNC_BACKOFF_TOTAL_WAIT_MAX_SECS.into()))
279 .max_jitter(Duration::from_millis(SYNC_JITTER_MS.into()))
280 .build();
281
282 let mut attempt = 0;
284 while attempt < MAX_GROUP_SYNC_RETRIES {
285 let remaining = self
286 .context
287 .incoming_runtime()
288 .policy()
289 .barrier_timeout
290 .saturating_sub(time_spent.elapsed());
291 if remaining.is_zero() {
292 break;
293 }
294 let predecessor = db
295 .find_group_intents(
296 self.group_id,
297 Some(vec![
298 IntentState::ToPublish,
299 IntentState::Published,
300 IntentState::Committed,
301 ]),
302 Some(IntentKind::all().collect()),
303 )?
304 .into_iter()
305 .find(|intent| intent.id < intent_id && intent.kind != IntentKind::SendMessage)
306 .map(|intent| intent.id);
307 let wait_for = backoff
308 .backoff(attempt + 1, time_spent)
309 .unwrap_or(Duration::from_millis(50));
310
311 log_event!(
312 Event::GroupSyncAttempt,
313 self.context.installation_id(),
314 group_id = self.group_id,
315 attempt,
316 backoff = ?wait_for
317 );
318
319 let mut round_succeeded = false;
323 match xmtp_common::time::timeout(
324 remaining,
325 self.sync_intent_round(intent_id, predecessor, remaining),
326 )
327 .await
328 {
329 Ok(Ok(s)) => {
330 round_succeeded = !s.is_errored();
331 summary.extend(s);
332 }
333 Ok(Err(error @ GroupError::PublishedButUnconfirmed { .. })) => return Err(error),
334 Ok(Err(error)) => summary.add_other(error),
335 Err(_) => break,
336 }
337 let current = Fetch::<StoredGroupIntent>::fetch(&db, &intent_id);
338 let waiting_to_publish = matches!(
339 ¤t,
340 Ok(Some(intent)) if intent.state == IntentState::ToPublish
341 );
342 match current {
343 Ok(Some(StoredGroupIntent {
344 state: IntentState::Processed,
345 ..
346 })) => {
347 return Ok(summary);
349 }
350 Ok(None) => {
351 return Err(NotFound::IntentById(intent_id).into());
352 }
353
354 Ok(Some(StoredGroupIntent {
360 state: IntentState::Superseded,
361 kind,
362 ..
363 })) => {
364 log_event!(
365 Event::GroupSyncIntentErrored,
366 self.context.installation_id(),
367 level = warn,
368 group_id = self.group_id, intent_id = intent_id,
369 intent_kind = ?kind
370 );
371 return Err(GroupError::from(summary));
372 }
373
374 Ok(Some(StoredGroupIntent {
375 state: IntentState::Error,
376 kind,
377 ..
378 })) => {
379 log_event!(
382 Event::GroupSyncIntentErrored,
383 self.context.installation_id(),
384 level = warn,
385 group_id = self.group_id, intent_id = intent_id,
386 intent_kind = ?kind
387 );
388 summary.extend(self.rejected_intent_summary(intent_id)?);
389 return Err(GroupError::from(summary));
390 }
391 Ok(Some(StoredGroupIntent { state, kind, .. })) => {
392 log_event!(
393 Event::GroupSyncIntentRetry,
394 self.context.installation_id(),
395 level = warn, group_id = self.group_id,
396 intent_id = intent_id, state = ?state, intent_kind = ?kind
397 );
398 }
399 Err(err) => {
400 tracing::error!(
401 group_id = %self.group_id,
402 intent_id,
403 attempt,
404 "database error fetching intent {err:?}"
405 );
406 summary.add_other(GroupError::Storage(err));
407 }
408 };
409 if round_succeeded
412 && waiting_to_publish
413 && let Some(predecessor) = predecessor
414 && Fetch::<StoredGroupIntent>::fetch(&db, &predecessor)?
415 .is_some_and(|intent| intent.state == IntentState::Processed)
416 {
417 continue;
418 }
419 attempt += 1;
420 if attempt < MAX_GROUP_SYNC_RETRIES {
421 let remaining = self
422 .context
423 .incoming_runtime()
424 .policy()
425 .barrier_timeout
426 .saturating_sub(time_spent.elapsed());
427 xmtp_common::time::sleep(wait_for.min(remaining)).await;
428 }
429 }
430 if Fetch::<StoredGroupIntent>::fetch(&db, &intent_id)?
431 .is_some_and(|intent| intent.state == IntentState::Processed)
432 {
433 return Ok(summary);
434 }
435 if self.published_intent_target(intent_id)?.is_some() {
436 return Err(GroupError::PublishedButUnconfirmed {
437 intent_id,
438 cause: None,
439 });
440 }
441 Err(GroupError::SyncFailedToWait(Box::new(summary)))
442 }
443
444 async fn sync_intent_round(
448 &self,
449 intent_id: ID,
450 predecessor: Option<ID>,
451 timeout: Duration,
452 ) -> Result<SyncSummary, GroupError> {
453 use xmtp_proto::types::Topic;
454 let started = xmtp_common::time::Instant::now();
455 let mut summary = SyncSummary::default();
456 match xmtp_common::time::timeout(timeout, self.publish_intents()).await {
457 Ok(Ok(())) => {}
458 Ok(Err(error)) => summary.add_publish_err(error),
459 Err(_) => return Err(GroupError::SyncFailedToWait(Box::new(summary))),
460 }
461 let receipt = self.published_intent_target(intent_id)?;
462 let blocker = match (receipt, predecessor) {
463 (None, Some(predecessor)) => self.published_intent_target(predecessor)?,
464 _ => None,
465 };
466 let topic = Topic::new_group_message(self.group_id);
467 let targets = match receipt.or(blocker) {
468 Some(target) => [(topic, target)].into(),
469 None => self.context.api().newest_topic_cursors(vec![topic]).await?,
470 };
471 if let Err(cause) = crate::subscriptions::barrier::wait_through(
472 &self.context,
473 targets,
474 Some(timeout.saturating_sub(started.elapsed())),
475 )
476 .await
477 {
478 return Err(if receipt.is_some() {
479 GroupError::PublishedButUnconfirmed {
480 intent_id,
481 cause: Some(Box::new(cause)),
482 }
483 } else {
484 cause.into()
485 });
486 }
487 if let Err(error) = self.post_commit().await {
488 summary.add_post_commit_err(error);
489 }
490 Ok(summary)
491 }
492
493 pub(super) fn validate_message_epoch(
494 inbox_id: InboxIdRef<'_>,
495 intent_id: i32,
496 group_epoch: GroupEpoch,
497 message_epoch: GroupEpoch,
498 max_past_epochs: usize,
499 ) -> Result<(), GroupMessageProcessingError> {
500 #[cfg(any(test, feature = "test-utils"))]
501 crate::utils::test_mocks_helpers::maybe_mock_future_epoch_for_tests()?;
502
503 if message_epoch.as_u64() + max_past_epochs as u64 <= group_epoch.as_u64() {
504 tracing::warn!(
505 inbox_id,
506 message_epoch = message_epoch.as_u64(),
507 group_epoch = group_epoch.as_u64(),
508 intent_id,
509 "[{}] message epoch {} is {} or more less than the group epoch {} for intent {}. Retrying message",
510 inbox_id,
511 message_epoch,
512 max_past_epochs,
513 group_epoch.as_u64(),
514 intent_id
515 );
516 return Err(GroupMessageProcessingError::OldEpoch(
517 message_epoch.as_u64(),
518 group_epoch.as_u64(),
519 ));
520 } else if message_epoch.as_u64() > group_epoch.as_u64() {
521 tracing::error!(
523 inbox_id,
524 message_epoch = message_epoch.as_u64(),
525 group_epoch = group_epoch.as_u64(),
526 intent_id,
527 "[{}] message epoch {} is greater than group epoch {} for intent {}. Retrying message",
528 inbox_id,
529 message_epoch,
530 group_epoch,
531 intent_id
532 );
533 return Err(GroupMessageProcessingError::FutureEpoch(
534 message_epoch.as_u64(),
535 group_epoch.as_u64(),
536 ));
537 }
538 Ok(())
539 }
540}