1use super::*;
4use xmtp_common::snippet::Snippet;
5
6fn log_publish_error(error: &GroupError) {
7 if let GroupError::FailedToVerifyInstallations(failed) = error {
8 tracing::error!(
9 error_kind = "FailedToVerifyInstallations",
10 failed_installation_count = failed.0.len(),
11 "Sync: error publishing intents"
12 );
13 } else {
14 tracing::error!("Sync: error publishing intents {error:?}");
15 }
16}
17
18impl<Context> MlsGroup<Context>
19where
20 Context: XmtpSharedContext,
21{
22 #[xmtp_common::mls_span]
23 pub async fn sync(&self) -> Result<SyncSummary, GroupError> {
24 self.sync_once()
25 .await
26 .map_err(|error| self.context.server_configuration().blocked_or(error))
27 }
28
29 async fn sync_once(&self) -> Result<SyncSummary, GroupError> {
39 self.context.server_configuration().check()?;
41
42 let conn = self.context.db();
43 let result = async {
44 if conn
45 .find_group(&self.group_id)?
46 .is_some_and(|group| group.membership_state == GroupMembershipState::Restored)
47 {
48 return Err(GroupError::GroupInactive);
49 }
50 let epoch = self.epoch().await?;
51 tracing::debug!(
52 inbox_id = self.context.inbox_id(),
53 installation_id = self.context.installation_id().as_slice().snippet(),
54 group_id = self.group_id.short_hex(),
55 epoch,
56 "syncing group",
57 );
58 let sync_summary = self.sync_with_conn().await.map_err(GroupError::from)?;
59 self.maybe_update_installations(None).await?;
60 Ok(sync_summary)
61 }
62 .await;
63
64 let siblings = match conn.other_dms(&self.group_id) {
65 Ok(siblings) => siblings,
66 Err(error) => {
69 result?;
70 return Err(error.into());
71 }
72 };
73 for sibling in siblings {
74 if sibling.membership_state == GroupMembershipState::Restored {
75 continue;
76 }
77 let sibling = Self::new_from_arc(
78 self.context.clone(),
79 sibling.id,
80 sibling.dm_id,
81 sibling.conversation_type,
82 sibling.created_at_ns,
83 );
84 if let Err(error) = sibling.sync_with_conn().await {
85 tracing::warn!(
86 group_id = sibling.group_id.short_hex(),
87 ?error,
88 "stitched DM sibling sync failed"
89 );
90 continue;
91 }
92 if let Err(error) = sibling.maybe_update_installations(None).await {
93 tracing::warn!(
94 group_id = sibling.group_id.short_hex(),
95 ?error,
96 "stitched DM sibling installation update failed"
97 );
98 }
99 }
100
101 result
102 }
103
104 fn handle_group_paused(&self) -> Result<(), GroupError> {
105 let group_id_typed = self.group_id;
107 if let Some(required_min_version_str) = self
108 .context
109 .db()
110 .get_group_paused_version(&group_id_typed)?
111 {
112 tracing::info!(
113 "Group is paused until version: {}",
114 required_min_version_str
115 );
116 let current_version_str = self.context.version_info().pkg_version();
117 let current_version = self.context.version_info().pkg_semver();
118 let required_min_version = LibXMTPVersion::parse(&required_min_version_str)
119 .map_err(CommitValidationError::from)?;
120
121 if required_min_version <= *current_version {
122 tracing::info!(
123 "Unpausing group since version requirements are met. \
124 Group ID: {}",
125 hex::encode(self.group_id),
126 );
127 self.context.db().unpause_group(&group_id_typed)?;
128 } else {
129 tracing::warn!(
130 "Skipping sync for paused group since version requirements are not met. \
131 Group ID: {}, \
132 Required version: {}, \
133 Current version: {}",
134 hex::encode(self.group_id),
135 required_min_version_str,
136 current_version_str
137 );
138 return Err(GroupError::GroupPausedUntilUpdate(required_min_version_str));
140 }
141 }
142 Ok(())
143 }
144
145 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(err, fields(inbox_id = %self.context.inbox_id(), operation = "sync_with_conn")))]
149 #[cfg_attr(not(any(test, feature = "test-utils")), xmtp_common::mls_span)]
150 pub async fn sync_with_conn(&self) -> Result<SyncSummary, SyncSummary> {
151 let mut app_data_changes = Vec::new();
157 let result = self.sync_with_conn_locked(&mut app_data_changes).await;
158 self.dispatch_app_data_changes(app_data_changes).await;
159 result
160 }
161
162 async fn sync_with_conn_locked(
165 &self,
166 app_data_changes: &mut Vec<AppDataChange>,
167 ) -> Result<SyncSummary, SyncSummary> {
168 let _mutex = self.mutex.lock().await;
169 let mut summary = SyncSummary::default();
170
171 if self
172 .context
173 .db()
174 .find_group(&self.group_id)
175 .map_err(|error| SyncSummary::other(error.into()))?
176 .is_some_and(|group| group.membership_state == GroupMembershipState::Restored)
177 {
178 return Err(SyncSummary::other(GroupError::GroupInactive));
179 }
180
181 if !self.is_active().map_err(SyncSummary::other)? {
182 log_event!(
183 Event::GroupSyncGroupInactive,
184 self.context.installation_id(),
185 group_id = self.group_id
186 );
187 return Ok(summary);
188 }
189
190 if let Err(e) = self.handle_group_paused() {
191 return Err(SyncSummary::other(e));
192 }
193
194 let result = self.publish_intents().await;
196 if let Err(e) = result {
197 log_publish_error(&e);
198 summary.add_publish_err(e);
199 }
200
201 let result = self.receive().await;
204 match result {
205 Ok(mut s) => {
206 app_data_changes.append(&mut s.app_data_changes);
207 summary.add_process(s)
208 }
209 Err(e) => {
210 summary.add_other(e);
211 }
215 }
216
217 let result = self.post_commit().await;
218 if let Err(e) = result {
219 tracing::error!("post commit error {e:?}",);
220 summary.add_post_commit_err(e);
221 }
222
223 if summary.is_errored() {
224 Err(summary)
225 } else {
226 Ok(summary)
227 }
228 }
229
230 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip_all))]
231 #[cfg_attr(not(any(test, feature = "test-utils")), xmtp_common::mls_span)]
232 pub(crate) async fn sync_until_last_intent_resolved(&self) -> Result<SyncSummary, GroupError> {
233 let intents = self.context.db().find_group_intents(
237 self.group_id,
238 Some(vec![
239 IntentState::ToPublish,
240 IntentState::Published,
241 IntentState::Committed,
242 ]),
243 Some(IntentKind::all().collect()),
244 )?;
245
246 let Some(intent) = intents.last() else {
247 return Ok(Default::default());
248 };
249
250 self.sync_until_intent_resolved(intent.id).await
251 }
252
253 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(err, level = "info", fields(inbox_id = %self.context.inbox_id(), operation = "intent"), skip(self)))]
254 #[cfg_attr(not(any(test, feature = "test-utils")), xmtp_common::mls_span)]
255 #[cfg_attr(any(test, feature = "test-utils"), tracing::instrument(level = "info", fields(inbox_id = %self.context.inbox_id()), skip(self)))]
266 #[cfg_attr(
267 not(any(test, feature = "test-utils")),
268 tracing::instrument(level = "trace", skip(self))
269 )]
270 pub(crate) async fn sync_until_intent_resolved(
271 &self,
272 intent_id: ID,
273 ) -> Result<SyncSummary, GroupError> {
274 log_event!(
275 Event::GroupSyncStart,
276 self.context.installation_id(),
277 group_id = self.group_id
278 );
279
280 let result = async {
282 let waited = async {
283 let mut summary =
284 Box::pin(self.sync_until_intent_resolved_inner(intent_id)).await?;
285 for child in self.pending_membership_children(intent_id)? {
288 summary.extend(Box::pin(self.sync_until_intent_resolved_inner(child)).await?);
289 }
290 Ok::<_, GroupError>(summary)
291 }
292 .await;
293 if self.membership_child_follow_up_failed(intent_id)? {
299 return Err(super::publish::OutgoingPreparationError::WelcomesUnsendable.into());
300 }
301 waited
302 }
303 .await;
304 let summary = match &result {
305 Ok(summary) => Some(summary),
306 Err(GroupError::Sync(summary)) => Some(&**summary),
307 Err(GroupError::SyncFailedToWait(summary)) => Some(&**summary),
308 _ => None,
309 };
310
311 log_event!(
312 Event::GroupSyncFinished,
313 self.context.installation_id(),
314 group_id = self.group_id,
315 summary = ?summary,
316 success = result.is_ok()
317 );
318
319 result
320 }
321
322 async fn sync_until_intent_resolved_inner(
323 &self,
324 intent_id: ID,
325 ) -> Result<SyncSummary, GroupError> {
326 let mut summary = SyncSummary::default();
327 let db = self.context.db();
328
329 let time_spent = xmtp_common::time::Instant::now();
330 let backoff = ExponentialBackoff::builder()
331 .duration(Duration::from_millis(SYNC_BACKOFF_WAIT_MS.into()))
332 .total_wait_max(Duration::from_secs(SYNC_BACKOFF_TOTAL_WAIT_MAX_SECS.into()))
333 .max_jitter(Duration::from_millis(SYNC_JITTER_MS.into()))
334 .build();
335
336 let mut attempt = 0;
338 while attempt < MAX_GROUP_SYNC_RETRIES {
339 let remaining = self
340 .context
341 .incoming_runtime()
342 .policy()
343 .barrier_timeout
344 .saturating_sub(time_spent.elapsed());
345 if remaining.is_zero() {
346 break;
347 }
348 let predecessor = db
349 .find_group_intents(
350 self.group_id,
351 Some(vec![
352 IntentState::ToPublish,
353 IntentState::Published,
354 IntentState::Committed,
355 ]),
356 Some(IntentKind::all().collect()),
357 )?
358 .into_iter()
359 .find(|intent| intent.id < intent_id && intent.kind != IntentKind::SendMessage)
360 .map(|intent| intent.id);
361 let wait_for = backoff
362 .backoff(attempt + 1, time_spent)
363 .unwrap_or(Duration::from_millis(50));
364 let membership_before = match Fetch::<StoredGroupIntent>::fetch(&db, &intent_id)? {
365 Some(intent)
366 if intent.kind == IntentKind::UpdateGroupMembership
367 && intent.state == IntentState::ToPublish =>
368 {
369 Some((
370 UpdateGroupMembershipIntentData::try_from(intent.data.as_slice())?,
371 self.with_group_snapshot(|group| {
372 extract_group_membership(group.extensions()).map_err(GroupError::from)
373 })?,
374 ))
375 }
376 _ => None,
377 };
378
379 log_event!(
380 Event::GroupSyncAttempt,
381 self.context.installation_id(),
382 group_id = self.group_id,
383 attempt,
384 backoff = ?wait_for
385 );
386
387 let mut round_succeeded = false;
391 let rate_limit_delay = match xmtp_common::time::timeout(
394 remaining,
395 xmtp_common::retry::with_retry_deadline(
396 xmtp_common::time::Instant::now() + remaining,
397 self.sync_intent_round(intent_id, predecessor, remaining),
398 ),
399 )
400 .await
401 {
402 Ok(Ok(s)) => {
403 round_succeeded = !s.is_errored();
404 let delay = s.rate_limit_delay();
405 summary.extend(s);
406 delay
407 }
408 Ok(Err(error @ GroupError::PublishedButUnconfirmed { .. })) => return Err(error),
409 Ok(Err(error)) => {
410 let delay = error.rate_limit_delay();
411 summary.add_other(error);
412 delay
413 }
414 Err(_) => break,
415 };
416 let current = Fetch::<StoredGroupIntent>::fetch(&db, &intent_id);
417 let waiting_to_publish = matches!(
418 ¤t,
419 Ok(Some(intent)) if intent.state == IntentState::ToPublish
420 );
421 match current {
422 Ok(Some(StoredGroupIntent {
423 state: IntentState::Processed,
424 ..
425 })) => {
426 return Ok(summary);
428 }
429 Ok(None) => {
430 return Err(NotFound::IntentById(intent_id).into());
431 }
432
433 Ok(Some(StoredGroupIntent {
439 state: IntentState::Superseded,
440 kind,
441 ..
442 })) => {
443 log_event!(
444 Event::GroupSyncIntentErrored,
445 self.context.installation_id(),
446 level = warn,
447 group_id = self.group_id, intent_id = intent_id,
448 intent_kind = ?kind
449 );
450 return Err(GroupError::from(summary));
451 }
452
453 Ok(Some(StoredGroupIntent {
454 state: IntentState::Error,
455 kind,
456 ..
457 })) => {
458 log_event!(
461 Event::GroupSyncIntentErrored,
462 self.context.installation_id(),
463 level = warn,
464 group_id = self.group_id, intent_id = intent_id,
465 intent_kind = ?kind
466 );
467 summary.extend(self.rejected_intent_summary(intent_id)?);
468 return Err(GroupError::from(summary));
469 }
470 Ok(Some(StoredGroupIntent {
474 state: IntentState::FollowUpFailed,
475 kind,
476 ..
477 })) => {
478 log_event!(
479 Event::GroupSyncIntentErrored,
480 self.context.installation_id(),
481 level = warn,
482 group_id = self.group_id, intent_id = intent_id,
483 intent_kind = ?kind
484 );
485 return Err(super::publish::OutgoingPreparationError::WelcomesUnsendable.into());
486 }
487 Ok(Some(StoredGroupIntent { state, kind, .. })) => {
488 log_event!(
489 Event::GroupSyncIntentRetry,
490 self.context.installation_id(),
491 level = warn, group_id = self.group_id,
492 intent_id = intent_id, state = ?state, intent_kind = ?kind
493 );
494 }
495 Err(err) => {
496 tracing::error!(
497 group_id = %self.group_id,
498 intent_id,
499 attempt,
500 "database error fetching intent {err:?}"
501 );
502 summary.add_other(GroupError::Storage(err));
503 }
504 };
505 if round_succeeded && waiting_to_publish {
508 let predecessor_finished = if let Some(predecessor) = predecessor {
509 Fetch::<StoredGroupIntent>::fetch(&db, &predecessor)?
510 .is_some_and(|intent| intent.state == IntentState::Processed)
511 } else {
512 false
513 };
514 let membership_advanced = if let Some((target, before)) = membership_before {
518 let after = self.with_group_snapshot(|group| {
519 extract_group_membership(group.extensions()).map_err(GroupError::from)
520 })?;
521 target.membership_updates.iter().any(|(inbox, target)| {
522 before.get(inbox).is_none_or(|old| old < target)
523 && after.get(inbox).is_some_and(|new| new >= target)
524 }) || target
525 .removed_members
526 .iter()
527 .any(|inbox| before.get(inbox).is_some() && after.get(inbox).is_none())
528 } else {
529 false
530 };
531 if predecessor_finished || membership_advanced {
532 continue;
533 }
534 }
535 attempt += 1;
536 if attempt < MAX_GROUP_SYNC_RETRIES {
537 let remaining = self
538 .context
539 .incoming_runtime()
540 .policy()
541 .barrier_timeout
542 .saturating_sub(time_spent.elapsed());
543 if let Some(delay) = rate_limit_delay {
547 let Some(wait) = rate_limited_wait(wait_for, delay, remaining) else {
548 break;
549 };
550 xmtp_common::time::sleep(wait).await;
551 continue;
552 }
553 xmtp_common::time::sleep(wait_for.min(remaining)).await;
554 }
555 }
556 match Fetch::<StoredGroupIntent>::fetch(&db, &intent_id)?.map(|intent| intent.state) {
558 Some(IntentState::Processed) => return Ok(summary),
559 Some(IntentState::FollowUpFailed) => {
561 return Err(super::publish::OutgoingPreparationError::WelcomesUnsendable.into());
562 }
563 _ => {}
564 }
565 if self.published_request_target(intent_id)?.is_some() {
566 return Err(GroupError::PublishedButUnconfirmed {
567 intent_id,
568 cause: None,
569 rate_limit_delay: summary.rate_limit_delay(),
570 });
571 }
572 Err(GroupError::SyncFailedToWait(Box::new(summary)))
573 }
574
575 async fn sync_intent_round(
579 &self,
580 intent_id: ID,
581 predecessor: Option<ID>,
582 timeout: Duration,
583 ) -> Result<SyncSummary, GroupError> {
584 use xmtp_proto::types::Topic;
585 let started = xmtp_common::time::Instant::now();
586 let mut summary = SyncSummary::default();
587 match xmtp_common::time::timeout(timeout, self.publish_intents()).await {
588 Ok(Ok(())) => {}
589 Ok(Err(error)) => summary.add_publish_err(error),
590 Err(_) => return Err(GroupError::SyncFailedToWait(Box::new(summary))),
591 }
592 let publish_delay = summary.rate_limit_delay();
596 let receipt = self.request_read_target(intent_id)?;
597 let blocker = match receipt {
598 Some(_) => None,
599 None => self.blocking_target(predecessor)?,
600 };
601 let topic = Topic::new_group_message(self.group_id);
602 let targets = match receipt.or(blocker) {
603 Some(target) => [(topic, target)].into(),
604 None => match self.context.api().newest_topic_cursors(vec![topic]).await {
605 Ok(targets) => targets,
606 Err(error) if publish_delay.is_some() => {
607 summary.add_other(error.into());
608 return Ok(summary);
609 }
610 Err(error) => return Err(error.into()),
611 },
612 };
613 if let Err(cause) = crate::subscriptions::barrier::wait_through(
614 &self.context,
615 targets,
616 Some(timeout.saturating_sub(started.elapsed())),
617 )
618 .await
619 {
620 if self.published_request_target(intent_id)?.is_some() {
621 return Err(GroupError::PublishedButUnconfirmed {
622 intent_id,
623 cause: Some(Box::new(cause)),
624 rate_limit_delay: publish_delay,
625 });
626 }
627 if publish_delay.is_some() {
628 summary.add_other(cause.into());
629 return Ok(summary);
630 }
631 return Err(cause.into());
632 }
633 if let Err(error) = self.post_commit().await {
634 summary.add_post_commit_err(error);
635 }
636 Ok(summary)
637 }
638
639 fn blocking_target(&self, predecessor: Option<ID>) -> Result<Option<Cursor>, GroupError> {
646 let published = self
647 .context
648 .db()
649 .find_group_intents(
650 self.group_id,
651 Some(vec![IntentState::Published]),
652 Some(
653 IntentKind::all()
654 .filter(|kind| *kind != IntentKind::SendMessage)
655 .collect(),
656 ),
657 )?
658 .into_iter()
659 .map(|intent| intent.id)
660 .min();
661 match published.or(predecessor) {
662 Some(blocker) => self.published_intent_target(blocker),
663 None => Ok(None),
664 }
665 }
666
667 pub(super) fn validate_message_epoch(
668 inbox_id: InboxIdRef<'_>,
669 intent_id: i32,
670 group_epoch: GroupEpoch,
671 message_epoch: GroupEpoch,
672 max_past_epochs: usize,
673 ) -> Result<(), GroupMessageProcessingError> {
674 #[cfg(any(test, feature = "test-hooks"))]
675 super::test_mocks::maybe_mock_future_epoch_for_tests()?;
676
677 if message_epoch.as_u64() + max_past_epochs as u64 <= group_epoch.as_u64() {
678 tracing::warn!(
679 inbox_id,
680 message_epoch = message_epoch.as_u64(),
681 group_epoch = group_epoch.as_u64(),
682 intent_id,
683 "[{}] message epoch {} is {} or more less than the group epoch {} for intent {}. Retrying message",
684 inbox_id,
685 message_epoch,
686 max_past_epochs,
687 group_epoch.as_u64(),
688 intent_id
689 );
690 return Err(GroupMessageProcessingError::OldEpoch(
691 message_epoch.as_u64(),
692 group_epoch.as_u64(),
693 ));
694 } else if message_epoch.as_u64() > group_epoch.as_u64() {
695 tracing::error!(
697 inbox_id,
698 message_epoch = message_epoch.as_u64(),
699 group_epoch = group_epoch.as_u64(),
700 intent_id,
701 "[{}] message epoch {} is greater than group epoch {} for intent {}. Retrying message",
702 inbox_id,
703 message_epoch,
704 group_epoch,
705 intent_id
706 );
707 return Err(GroupMessageProcessingError::FutureEpoch(
708 message_epoch.as_u64(),
709 group_epoch.as_u64(),
710 ));
711 }
712 Ok(())
713 }
714}
715
716fn rate_limited_wait(backoff: Duration, delay: Duration, remaining: Duration) -> Option<Duration> {
721 (delay < remaining).then(|| backoff.max(delay).min(remaining))
722}
723
724#[cfg(test)]
725mod rate_limited_wait_tests {
726 use super::*;
727
728 #[xmtp_common::test]
731 fn a_rate_limited_wait_stays_inside_the_call() {
732 let ms = Duration::from_millis;
733 assert_eq!(rate_limited_wait(ms(500), ms(100), ms(200)), Some(ms(200)));
734 assert_eq!(rate_limited_wait(ms(50), ms(100), ms(200)), Some(ms(100)));
735 assert_eq!(rate_limited_wait(ms(50), ms(200), ms(200)), None);
736 }
737}