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