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