1#[cfg(test)]
4mod tests;
5
6use futures::{StreamExt, stream};
7use std::{collections::HashSet, sync::Arc};
8use xmtp_common::{
9 ErrorCode, RetryableError,
10 time::{Duration, Instant, sleep},
11};
12use xmtp_db::{
13 StorageError,
14 consent_record::ConsentState,
15 group::GroupQueryArgs,
16 incoming_envelope::{NetworkEntityKind, QueryIncomingEnvelope, StreamTopic, TopicProgress},
17 prelude::*,
18};
19use xmtp_proto::types::{Cursor, GroupId, Topic, TopicCursor, TopicKind};
20
21use super::incoming::{
22 IncomingCoordinator, IncomingError, IncomingProcessing, IncomingReceivePolicy, IncomingScope,
23};
24use crate::{
25 context::XmtpSharedContext,
26 groups::{GroupError, MlsGroup},
27};
28
29#[derive(Debug, Clone)]
31pub enum BarrierCause {
32 TargetPending,
34 ReceiptPending,
36 ProcessingPending,
38 Blocked(String),
39 Storage(Arc<StorageError>),
40 Receiver(Arc<IncomingError>),
41 InvalidTopic,
42}
43
44#[derive(Debug, Clone)]
46pub struct BarrierTopic {
47 pub topic: Topic,
49 pub scope_generation: Option<u64>,
51 pub target: Option<Cursor>,
53 pub received: Cursor,
55 pub processed: Cursor,
57 pub unresolved_welcomes: Vec<Cursor>,
59 pub inactive: bool,
61 pub cause: Option<BarrierCause>,
63}
64
65impl BarrierTopic {
66 pub(crate) fn capture_scope_generation(&mut self, snapshot: &super::incoming::IncomingStatus) {
67 self.scope_generation = snapshot
68 .topics
69 .iter()
70 .find(|entry| entry.topic == self.topic)
71 .map(|entry| entry.scope_generation)
72 .or_else(|| self.target.map(|_| snapshot.scope_generation));
73 }
74
75 pub fn complete(&self) -> bool {
77 self.cause.is_none()
78 }
79
80 fn blocked(&self) -> bool {
81 matches!(
82 self.cause,
83 Some(BarrierCause::Blocked(_) | BarrierCause::InvalidTopic)
84 ) || matches!(&self.cause, Some(BarrierCause::Storage(error)) if !error.is_retryable())
85 || matches!(&self.cause, Some(BarrierCause::Receiver(error)) if !error.is_retryable())
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct BarrierSnapshot {
92 pub topics: Vec<BarrierTopic>,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum BarrierFailure {
99 Blocked,
100 Deadline,
101 Cancelled,
102}
103
104#[derive(Debug, thiserror::Error, ErrorCode)]
105pub enum BarrierError {
106 #[error("Processing barrier did not complete: {reason:?}; {} unfinished topics", unfinished.len())]
108 Incomplete {
109 reason: BarrierFailure,
111 unfinished: Vec<BarrierTopic>,
113 },
114}
115
116impl RetryableError for BarrierError {
117 fn is_retryable(&self) -> bool {
118 matches!(
119 self,
120 Self::Incomplete {
121 reason: BarrierFailure::Deadline,
122 ..
123 }
124 )
125 }
126}
127
128impl crate::worker::NeedsDbReconnect for BarrierError {
129 fn needs_db_reconnect(&self) -> bool {
130 let Self::Incomplete { unfinished, .. } = self;
131 unfinished.iter().any(|topic| match &topic.cause {
132 Some(BarrierCause::Storage(error)) => error.db_needs_connection(),
133 Some(BarrierCause::Receiver(error)) => error.needs_db_reconnect(),
134 _ => false,
135 })
136 }
137}
138
139pub(crate) fn durable_complete(
142 kind: NetworkEntityKind,
143 target: Option<Cursor>,
144 progress: TopicProgress,
145 pending_count: usize,
146) -> bool {
147 target.is_some_and(|target| {
148 progress.received >= target
149 && match kind {
150 NetworkEntityKind::Welcome => pending_count == 0,
151 NetworkEntityKind::Group | NetworkEntityKind::Identity => {
152 progress.processed >= target
153 }
154 }
155 })
156}
157
158fn read_topic<C: XmtpSharedContext>(context: &C, topic: &Topic, target: Cursor) -> BarrierTopic {
159 let mut status = BarrierTopic {
160 topic: topic.clone(),
161 scope_generation: None,
162 target: Some(target),
163 received: Cursor::default(),
164 processed: Cursor::default(),
165 unresolved_welcomes: Vec::new(),
166 inactive: false,
167 cause: Some(BarrierCause::ReceiptPending),
168 };
169 let kind = match topic.kind() {
170 TopicKind::GroupMessagesV1 => NetworkEntityKind::Group,
171 TopicKind::WelcomeMessagesV1 => NetworkEntityKind::Welcome,
172 TopicKind::IdentityUpdatesV1 => NetworkEntityKind::Identity,
173 _ => {
174 status.cause = Some(BarrierCause::InvalidTopic);
175 return status;
176 }
177 };
178 let db_topic = StreamTopic {
179 entity_id: topic.identifier().to_vec(),
180 kind,
181 };
182 let db = context.db();
183 let progress = match db.topic_progress(&db_topic) {
184 Ok(progress) => progress,
185 Err(error) => {
186 status.cause = Some(BarrierCause::Storage(Arc::new(error)));
187 return status;
188 }
189 };
190 status.received = progress.received;
191 status.processed = progress.processed;
192 if kind == NetworkEntityKind::Group {
193 match GroupId::try_from(topic.identifier()) {
194 Ok(id) => match MlsGroup::new_cached(context.context_ref().clone(), &id)
195 .map_err(GroupError::from)
196 .and_then(|(group, _)| group.is_active())
197 {
198 Ok(false) => {
199 status.inactive = true;
200 status.cause = None;
201 return status;
202 }
203 Ok(true) => {}
204 Err(error) => {
205 status.cause = Some(BarrierCause::Receiver(Arc::new(IncomingError::Group(
206 error,
207 ))));
208 return status;
209 }
210 },
211 Err(_) => {
212 status.cause = Some(BarrierCause::InvalidTopic);
213 return status;
214 }
215 }
216 }
217 let pending = match db.pending_states_through(&db_topic, target) {
218 Ok(pending) => pending,
219 Err(error) => {
220 status.cause = Some(BarrierCause::Storage(Arc::new(error)));
221 return status;
222 }
223 };
224 if kind == NetworkEntityKind::Welcome {
225 status.unresolved_welcomes = pending.iter().map(|row| row.sequence_id).collect();
226 }
227 status.cause = if progress.received < target {
228 Some(BarrierCause::ReceiptPending)
229 } else if durable_complete(kind, Some(target), progress, pending.len()) {
230 None
231 } else if !pending.is_empty()
232 && (kind != NetworkEntityKind::Welcome && pending[0].blocked
233 || kind == NetworkEntityKind::Welcome && pending.iter().all(|row| row.blocked))
234 {
235 Some(BarrierCause::Blocked(
236 pending[0]
237 .error_code
238 .clone()
239 .unwrap_or_else(|| "processing_blocked".into()),
240 ))
241 } else {
242 Some(BarrierCause::ProcessingPending)
243 };
244 status
245}
246
247pub async fn receive_through_current<C: XmtpSharedContext>(
250 context: &C,
251 topics: Vec<Topic>,
252) -> Result<BarrierSnapshot, GroupError> {
253 let deadline = Instant::now() + context.incoming_runtime().policy().barrier_timeout;
254 Ok(receive_through_current_until(context, topics, deadline).await?)
255}
256
257pub async fn receive_through_current_until<C: XmtpSharedContext>(
259 context: &C,
260 topics: Vec<Topic>,
261 deadline: Instant,
262) -> Result<BarrierSnapshot, BarrierError> {
263 let coordinator = IncomingCoordinator::for_context(context);
264 let _receipt = coordinator.acquire(IncomingScope::Topics(topics.clone()));
265 let (targets, unavailable) = capture_targets(context, topics, deadline).await;
266 wait_for_targets(
267 context,
268 targets,
269 unavailable,
270 None,
271 IncomingReceivePolicy::ImmediateQuery,
272 deadline,
273 &mut HashSet::new(),
274 )
275 .await
276}
277
278pub struct GroupBarrierRun {
280 pub group_ids: Vec<GroupId>,
282 pub result: Result<BarrierSnapshot, BarrierError>,
284}
285
286struct WelcomeDiscovery {
288 topic: Topic,
289 target: Cursor,
290 consent_states: Option<Vec<ConsentState>>,
291}
292
293pub async fn receive_with_welcomes_until<C: XmtpSharedContext>(
296 context: &C,
297 starting_groups: Vec<GroupId>,
298 consent_states: Option<Vec<ConsentState>>,
299 deadline: Instant,
300) -> GroupBarrierRun {
301 let (group_ids, snapshot, failure) =
302 receive_with_welcomes_snapshot_until(context, starting_groups, consent_states, deadline)
303 .await;
304 let result = finish_snapshot(snapshot, failure);
305 GroupBarrierRun { group_ids, result }
306}
307
308pub(crate) async fn receive_with_welcomes_snapshot_until<C: XmtpSharedContext>(
310 context: &C,
311 starting_groups: Vec<GroupId>,
312 consent_states: Option<Vec<ConsentState>>,
313 deadline: Instant,
314) -> (Vec<GroupId>, BarrierSnapshot, Option<BarrierFailure>) {
315 let mut groups: HashSet<_> = starting_groups.into_iter().collect();
316 let welcome_topic = Topic::new_welcome_message(context.installation_id());
317 let mut topics: Vec<_> = groups.iter().map(Topic::new_group_message).collect();
318 topics.push(welcome_topic.clone());
319 let coordinator = IncomingCoordinator::for_context(context);
320 let _receipt = coordinator.acquire(IncomingScope::Topics(topics.clone()));
321 let (targets, unavailable) = capture_targets(context, topics, deadline).await;
322 let discovery = targets
323 .get(&welcome_topic)
324 .copied()
325 .map(|target| WelcomeDiscovery {
326 topic: welcome_topic,
327 target,
328 consent_states,
329 });
330 let (snapshot, failure) = wait_for_targets_snapshot(
331 context,
332 targets,
333 unavailable,
334 discovery,
335 IncomingReceivePolicy::ImmediateQuery,
336 deadline,
337 &mut groups,
338 )
339 .await;
340 let mut group_ids: Vec<_> = groups.into_iter().collect();
341 group_ids.sort();
342 (group_ids, snapshot, failure)
343}
344
345async fn capture_targets<C: XmtpSharedContext>(
347 context: &C,
348 topics: Vec<Topic>,
349 deadline: Instant,
350) -> (TopicCursor, Vec<BarrierTopic>) {
351 let mut topics = topics;
352 topics.sort_by_key(Topic::cloned_vec);
353 topics.dedup();
354 let chunks: Vec<_> = topics
355 .chunks(context.api().limits().max_query_topics)
356 .map(<[Topic]>::to_vec)
357 .collect();
358 let results = stream::iter(chunks)
359 .map(|topics| async move {
360 let remaining = deadline.saturating_duration_since(Instant::now());
361 let result = xmtp_common::time::timeout(
362 remaining,
363 context.api().newest_topic_cursors(topics.clone()),
364 )
365 .await;
366 (topics, result)
367 })
368 .buffer_unordered(context.incoming_runtime().policy().max_dependency_requests)
369 .collect::<Vec<_>>()
370 .await;
371 let mut targets = TopicCursor::new();
372 let mut unavailable = Vec::new();
373 for (topics, result) in results {
374 let cause = match result {
375 Ok(Ok(captured)) => {
376 targets.extend(captured);
377 continue;
378 }
379 Ok(Err(error)) => BarrierCause::Receiver(Arc::new(IncomingError::Transport(
380 xmtp_proto::api::NetworkError::new(error),
381 ))),
382 Err(_) => BarrierCause::TargetPending,
383 };
384 for topic in topics {
385 let mut status = read_topic(context, &topic, Cursor::default());
386 status.target = None;
387 status.cause = Some(cause.clone());
388 unavailable.push(status);
389 }
390 }
391 (targets, unavailable)
392}
393
394pub async fn wait_through<C: XmtpSharedContext>(
397 context: &C,
398 targets: TopicCursor,
399 timeout: Option<Duration>,
400) -> Result<BarrierSnapshot, BarrierError> {
401 let deadline =
402 Instant::now() + timeout.unwrap_or(context.incoming_runtime().policy().barrier_timeout);
403 wait_for_targets(
404 context,
405 targets,
406 Vec::new(),
407 None,
408 IncomingReceivePolicy::StreamFirst,
409 deadline,
410 &mut HashSet::new(),
411 )
412 .await
413}
414
415async fn wait_for_targets<C: XmtpSharedContext>(
417 context: &C,
418 targets: TopicCursor,
419 unavailable: Vec<BarrierTopic>,
420 discovery: Option<WelcomeDiscovery>,
421 receive_policy: IncomingReceivePolicy,
422 deadline: Instant,
423 groups: &mut HashSet<GroupId>,
424) -> Result<BarrierSnapshot, BarrierError> {
425 let (snapshot, failure) = wait_for_targets_snapshot(
426 context,
427 targets,
428 unavailable,
429 discovery,
430 receive_policy,
431 deadline,
432 groups,
433 )
434 .await;
435 finish_snapshot(snapshot, failure)
436}
437
438fn finish_snapshot(
439 snapshot: BarrierSnapshot,
440 failure: Option<BarrierFailure>,
441) -> Result<BarrierSnapshot, BarrierError> {
442 match failure {
443 None => Ok(snapshot),
444 Some(reason) => Err(BarrierError::Incomplete {
445 reason,
446 unfinished: snapshot
447 .topics
448 .into_iter()
449 .filter(|topic| !topic.complete())
450 .collect(),
451 }),
452 }
453}
454
455async fn wait_for_targets_snapshot<C: XmtpSharedContext>(
456 context: &C,
457 mut targets: TopicCursor,
458 mut unavailable: Vec<BarrierTopic>,
459 discovery: Option<WelcomeDiscovery>,
460 receive_policy: IncomingReceivePolicy,
461 deadline: Instant,
462 groups: &mut HashSet<GroupId>,
463) -> (BarrierSnapshot, Option<BarrierFailure>) {
464 let coordinator = IncomingCoordinator::for_context(context);
465 let lease = coordinator.acquire(IncomingScope::Barrier {
466 targets: targets.clone(),
467 deadline,
468 receive_policy,
469 });
470 loop {
471 #[cfg(test)]
472 tests::before_progress_read();
473 let mut topics: Vec<_> = targets
476 .iter()
477 .map(|(topic, target)| read_topic(context, topic, *target))
478 .collect();
479 let mut discovery_failure = None;
480 if let Some(discovery) = &discovery {
481 let discovered = (|| -> Result<Vec<GroupId>, StorageError> {
482 let db = context.db();
483 let eligible = db.group_ids_discovered_through(discovery.target)?;
484 let selected: HashSet<_> = db
485 .find_groups(GroupQueryArgs {
486 consent_states: discovery.consent_states.clone(),
487 include_sync_groups: true,
488 include_duplicate_dms: true,
489 ..Default::default()
490 })?
491 .into_iter()
492 .map(|group| group.id)
493 .collect();
494 Ok(eligible
495 .into_iter()
496 .filter(|id| selected.contains(id))
497 .collect())
498 })();
499 match discovered {
500 Ok(discovered) => {
501 let new_topics: Vec<_> = discovered
502 .into_iter()
503 .filter(|id| groups.insert(*id))
504 .map(Topic::new_group_message)
505 .collect();
506 if !new_topics.is_empty() {
507 let (captured, failures) =
508 capture_targets(context, new_topics, deadline).await;
509 targets.extend(captured);
510 unavailable.extend(failures);
511 lease.replace_scope(IncomingScope::Barrier {
512 targets: targets.clone(),
513 deadline,
514 receive_policy,
515 });
516 continue;
518 }
519 }
520 Err(error) => {
521 let mut status = read_topic(context, &discovery.topic, discovery.target);
522 status.cause = Some(BarrierCause::Storage(Arc::new(error)));
523 discovery_failure = Some(status);
524 }
525 }
526 }
527 let receiver = lease.snapshot();
528 for topic in &mut topics {
529 if topic.complete() {
530 continue;
531 }
532 if let Some(incoming) = receiver
533 .topics
534 .iter()
535 .find(|entry| entry.topic == topic.topic)
536 && incoming.processing == IncomingProcessing::Blocked
537 && let Some(error) = incoming.error.as_ref()
538 {
539 topic.cause = Some(BarrierCause::Receiver(error.clone()));
540 }
541 }
542 for status in &unavailable {
543 let mut fresh = read_topic(context, &status.topic, Cursor::default());
544 fresh.target = None;
545 fresh.cause = status.cause.clone();
546 topics.push(fresh);
547 }
548 if let Some(failure) = discovery_failure {
549 if let Some(status) = topics
550 .iter_mut()
551 .find(|status| status.topic == failure.topic)
552 {
553 *status = failure;
554 } else {
555 topics.push(failure);
556 }
557 }
558 for topic in &mut topics {
559 topic.capture_scope_generation(&receiver);
560 }
561 if topics.iter().all(BarrierTopic::complete) {
562 return (BarrierSnapshot { topics }, None);
563 }
564 let reason = if context.is_closed() {
565 Some(BarrierFailure::Cancelled)
566 } else if topics.iter().all(|topic| {
567 topic.complete()
568 || topic.blocked()
569 && (topic.target.is_none()
570 || receiver
571 .topics
572 .iter()
573 .any(|entry| entry.topic == topic.topic))
574 }) {
575 Some(BarrierFailure::Blocked)
576 } else if Instant::now() >= deadline {
577 Some(BarrierFailure::Deadline)
578 } else {
579 None
580 };
581 if let Some(reason) = reason {
582 return (BarrierSnapshot { topics }, Some(reason));
583 }
584 tokio::select! {
585 _ = context.cancellation_token().cancelled() => {},
586 _ = lease.changed() => {},
587 _ = sleep(context.incoming_runtime().policy().active_database_poll_interval.min(deadline.saturating_duration_since(Instant::now()))) => {},
588 }
589 }
590}