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 target: Option<Cursor>,
51 pub received: Cursor,
53 pub processed: Cursor,
55 pub unresolved_welcomes: Vec<Cursor>,
57 pub inactive: bool,
59 pub cause: Option<BarrierCause>,
61}
62
63impl BarrierTopic {
64 pub fn complete(&self) -> bool {
66 self.cause.is_none()
67 }
68
69 fn blocked(&self) -> bool {
70 matches!(
71 self.cause,
72 Some(BarrierCause::Blocked(_) | BarrierCause::InvalidTopic)
73 ) || matches!(&self.cause, Some(BarrierCause::Storage(error)) if !error.is_retryable())
74 || matches!(&self.cause, Some(BarrierCause::Receiver(error)) if !error.is_retryable())
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct BarrierSnapshot {
81 pub topics: Vec<BarrierTopic>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum BarrierFailure {
88 Blocked,
89 Deadline,
90 Cancelled,
91}
92
93#[derive(Debug, thiserror::Error, ErrorCode)]
94pub enum BarrierError {
95 #[error("Processing barrier did not complete: {reason:?}; {} unfinished topics", unfinished.len())]
97 Incomplete {
98 reason: BarrierFailure,
100 unfinished: Vec<BarrierTopic>,
102 },
103}
104
105impl RetryableError for BarrierError {
106 fn is_retryable(&self) -> bool {
107 matches!(
108 self,
109 Self::Incomplete {
110 reason: BarrierFailure::Deadline,
111 ..
112 }
113 )
114 }
115}
116
117impl crate::worker::NeedsDbReconnect for BarrierError {
118 fn needs_db_reconnect(&self) -> bool {
119 let Self::Incomplete { unfinished, .. } = self;
120 unfinished.iter().any(|topic| match &topic.cause {
121 Some(BarrierCause::Storage(error)) => error.db_needs_connection(),
122 Some(BarrierCause::Receiver(error)) => error.needs_db_reconnect(),
123 _ => false,
124 })
125 }
126}
127
128pub(crate) fn durable_complete(
131 kind: NetworkEntityKind,
132 target: Option<Cursor>,
133 progress: TopicProgress,
134 pending_count: usize,
135) -> bool {
136 target.is_some_and(|target| {
137 progress.received >= target
138 && match kind {
139 NetworkEntityKind::Welcome => pending_count == 0,
140 NetworkEntityKind::Group | NetworkEntityKind::Identity => {
141 progress.processed >= target
142 }
143 }
144 })
145}
146
147fn read_topic<C: XmtpSharedContext>(context: &C, topic: &Topic, target: Cursor) -> BarrierTopic {
148 let mut status = BarrierTopic {
149 topic: topic.clone(),
150 target: Some(target),
151 received: Cursor::default(),
152 processed: Cursor::default(),
153 unresolved_welcomes: Vec::new(),
154 inactive: false,
155 cause: Some(BarrierCause::ReceiptPending),
156 };
157 let kind = match topic.kind() {
158 TopicKind::GroupMessagesV1 => NetworkEntityKind::Group,
159 TopicKind::WelcomeMessagesV1 => NetworkEntityKind::Welcome,
160 TopicKind::IdentityUpdatesV1 => NetworkEntityKind::Identity,
161 _ => {
162 status.cause = Some(BarrierCause::InvalidTopic);
163 return status;
164 }
165 };
166 let db_topic = StreamTopic {
167 entity_id: topic.identifier().to_vec(),
168 kind,
169 };
170 let db = context.db();
171 let progress = match db.topic_progress(&db_topic) {
172 Ok(progress) => progress,
173 Err(error) => {
174 status.cause = Some(BarrierCause::Storage(Arc::new(error)));
175 return status;
176 }
177 };
178 status.received = progress.received;
179 status.processed = progress.processed;
180 if kind == NetworkEntityKind::Group {
181 match GroupId::try_from(topic.identifier()) {
182 Ok(id) => match MlsGroup::new_cached(context.context_ref().clone(), &id)
183 .map_err(GroupError::from)
184 .and_then(|(group, _)| group.is_active())
185 {
186 Ok(false) => {
187 status.inactive = true;
188 status.cause = None;
189 return status;
190 }
191 Ok(true) => {}
192 Err(error) => {
193 status.cause = Some(BarrierCause::Receiver(Arc::new(IncomingError::Group(
194 error,
195 ))));
196 return status;
197 }
198 },
199 Err(_) => {
200 status.cause = Some(BarrierCause::InvalidTopic);
201 return status;
202 }
203 }
204 }
205 let pending = match db.pending_states_through(&db_topic, target) {
206 Ok(pending) => pending,
207 Err(error) => {
208 status.cause = Some(BarrierCause::Storage(Arc::new(error)));
209 return status;
210 }
211 };
212 if kind == NetworkEntityKind::Welcome {
213 status.unresolved_welcomes = pending.iter().map(|row| row.sequence_id).collect();
214 }
215 status.cause = if progress.received < target {
216 Some(BarrierCause::ReceiptPending)
217 } else if durable_complete(kind, Some(target), progress, pending.len()) {
218 None
219 } else if !pending.is_empty()
220 && (kind != NetworkEntityKind::Welcome && pending[0].blocked
221 || kind == NetworkEntityKind::Welcome && pending.iter().all(|row| row.blocked))
222 {
223 Some(BarrierCause::Blocked(
224 pending[0]
225 .error_code
226 .clone()
227 .unwrap_or_else(|| "processing_blocked".into()),
228 ))
229 } else {
230 Some(BarrierCause::ProcessingPending)
231 };
232 status
233}
234
235pub async fn receive_through_current<C: XmtpSharedContext>(
238 context: &C,
239 topics: Vec<Topic>,
240) -> Result<BarrierSnapshot, GroupError> {
241 let deadline = Instant::now() + context.incoming_runtime().policy().barrier_timeout;
242 Ok(receive_through_current_until(context, topics, deadline).await?)
243}
244
245pub async fn receive_through_current_until<C: XmtpSharedContext>(
247 context: &C,
248 topics: Vec<Topic>,
249 deadline: Instant,
250) -> Result<BarrierSnapshot, BarrierError> {
251 let coordinator = IncomingCoordinator::for_context(context);
252 let _receipt = coordinator.acquire(IncomingScope::Topics(topics.clone()));
253 let (targets, unavailable) = capture_targets(context, topics, deadline).await;
254 wait_for_targets(
255 context,
256 targets,
257 unavailable,
258 None,
259 IncomingReceivePolicy::ImmediateQuery,
260 deadline,
261 &mut HashSet::new(),
262 )
263 .await
264}
265
266pub struct GroupBarrierRun {
268 pub group_ids: Vec<GroupId>,
270 pub result: Result<BarrierSnapshot, BarrierError>,
272}
273
274struct WelcomeDiscovery {
276 topic: Topic,
277 target: Cursor,
278 consent_states: Option<Vec<ConsentState>>,
279}
280
281pub async fn receive_with_welcomes_until<C: XmtpSharedContext>(
284 context: &C,
285 starting_groups: Vec<GroupId>,
286 consent_states: Option<Vec<ConsentState>>,
287 deadline: Instant,
288) -> GroupBarrierRun {
289 let mut groups: HashSet<_> = starting_groups.into_iter().collect();
290 let welcome_topic = Topic::new_welcome_message(context.installation_id());
291 let mut topics: Vec<_> = groups.iter().map(Topic::new_group_message).collect();
292 topics.push(welcome_topic.clone());
293 let coordinator = IncomingCoordinator::for_context(context);
294 let _receipt = coordinator.acquire(IncomingScope::Topics(topics.clone()));
295 let (targets, unavailable) = capture_targets(context, topics, deadline).await;
296 let discovery = targets
297 .get(&welcome_topic)
298 .copied()
299 .map(|target| WelcomeDiscovery {
300 topic: welcome_topic,
301 target,
302 consent_states,
303 });
304 let result = wait_for_targets(
305 context,
306 targets,
307 unavailable,
308 discovery,
309 IncomingReceivePolicy::ImmediateQuery,
310 deadline,
311 &mut groups,
312 )
313 .await;
314 let mut group_ids: Vec<_> = groups.into_iter().collect();
315 group_ids.sort();
316 GroupBarrierRun { group_ids, result }
317}
318
319async fn capture_targets<C: XmtpSharedContext>(
321 context: &C,
322 topics: Vec<Topic>,
323 deadline: Instant,
324) -> (TopicCursor, Vec<BarrierTopic>) {
325 let mut topics = topics;
326 topics.sort_by_key(Topic::cloned_vec);
327 topics.dedup();
328 let chunks: Vec<_> = topics
329 .chunks(context.api().limits().max_query_topics)
330 .map(<[Topic]>::to_vec)
331 .collect();
332 let results = stream::iter(chunks)
333 .map(|topics| async move {
334 let remaining = deadline.saturating_duration_since(Instant::now());
335 let result = xmtp_common::time::timeout(
336 remaining,
337 context.api().newest_topic_cursors(topics.clone()),
338 )
339 .await;
340 (topics, result)
341 })
342 .buffer_unordered(context.incoming_runtime().policy().max_dependency_requests)
343 .collect::<Vec<_>>()
344 .await;
345 let mut targets = TopicCursor::new();
346 let mut unavailable = Vec::new();
347 for (topics, result) in results {
348 let cause = match result {
349 Ok(Ok(captured)) => {
350 targets.extend(captured);
351 continue;
352 }
353 Ok(Err(error)) => BarrierCause::Receiver(Arc::new(IncomingError::Transport(
354 xmtp_proto::api::NetworkError::new(error),
355 ))),
356 Err(_) => BarrierCause::TargetPending,
357 };
358 for topic in topics {
359 let mut status = read_topic(context, &topic, Cursor::default());
360 status.target = None;
361 status.cause = Some(cause.clone());
362 unavailable.push(status);
363 }
364 }
365 (targets, unavailable)
366}
367
368pub async fn wait_through<C: XmtpSharedContext>(
371 context: &C,
372 targets: TopicCursor,
373 timeout: Option<Duration>,
374) -> Result<BarrierSnapshot, BarrierError> {
375 let deadline =
376 Instant::now() + timeout.unwrap_or(context.incoming_runtime().policy().barrier_timeout);
377 wait_for_targets(
378 context,
379 targets,
380 Vec::new(),
381 None,
382 IncomingReceivePolicy::StreamFirst,
383 deadline,
384 &mut HashSet::new(),
385 )
386 .await
387}
388
389async fn wait_for_targets<C: XmtpSharedContext>(
390 context: &C,
391 mut targets: TopicCursor,
392 mut unavailable: Vec<BarrierTopic>,
393 discovery: Option<WelcomeDiscovery>,
394 receive_policy: IncomingReceivePolicy,
395 deadline: Instant,
396 groups: &mut HashSet<GroupId>,
397) -> Result<BarrierSnapshot, BarrierError> {
398 let coordinator = IncomingCoordinator::for_context(context);
399 let lease = coordinator.acquire(IncomingScope::Barrier {
400 targets: targets.clone(),
401 deadline,
402 receive_policy,
403 });
404 loop {
405 #[cfg(test)]
406 tests::before_progress_read();
407 let mut topics: Vec<_> = targets
410 .iter()
411 .map(|(topic, target)| read_topic(context, topic, *target))
412 .collect();
413 let mut discovery_failure = None;
414 if let Some(discovery) = &discovery {
415 let discovered = (|| -> Result<Vec<GroupId>, StorageError> {
416 let db = context.db();
417 let eligible = db.group_ids_discovered_through(discovery.target)?;
418 let selected: HashSet<_> = db
419 .find_groups(GroupQueryArgs {
420 consent_states: discovery.consent_states.clone(),
421 include_sync_groups: true,
422 include_duplicate_dms: true,
423 ..Default::default()
424 })?
425 .into_iter()
426 .map(|group| group.id)
427 .collect();
428 Ok(eligible
429 .into_iter()
430 .filter(|id| selected.contains(id))
431 .collect())
432 })();
433 match discovered {
434 Ok(discovered) => {
435 let new_topics: Vec<_> = discovered
436 .into_iter()
437 .filter(|id| groups.insert(*id))
438 .map(Topic::new_group_message)
439 .collect();
440 if !new_topics.is_empty() {
441 let (captured, failures) =
442 capture_targets(context, new_topics, deadline).await;
443 targets.extend(captured);
444 unavailable.extend(failures);
445 lease.replace_scope(IncomingScope::Barrier {
446 targets: targets.clone(),
447 deadline,
448 receive_policy,
449 });
450 continue;
452 }
453 }
454 Err(error) => {
455 let mut status = read_topic(context, &discovery.topic, discovery.target);
456 status.cause = Some(BarrierCause::Storage(Arc::new(error)));
457 discovery_failure = Some(status);
458 }
459 }
460 }
461 let receiver = lease.snapshot();
462 for topic in &mut topics {
463 if topic.complete() {
464 continue;
465 }
466 if let Some(incoming) = receiver
467 .topics
468 .iter()
469 .find(|entry| entry.topic == topic.topic)
470 && incoming.processing == IncomingProcessing::Blocked
471 && let Some(error) = incoming.error.as_ref()
472 {
473 topic.cause = Some(BarrierCause::Receiver(error.clone()));
474 }
475 }
476 for status in &unavailable {
477 let mut fresh = read_topic(context, &status.topic, Cursor::default());
478 fresh.target = None;
479 fresh.cause = status.cause.clone();
480 topics.push(fresh);
481 }
482 if let Some(failure) = discovery_failure {
483 if let Some(status) = topics
484 .iter_mut()
485 .find(|status| status.topic == failure.topic)
486 {
487 *status = failure;
488 } else {
489 topics.push(failure);
490 }
491 }
492 if topics.iter().all(BarrierTopic::complete) {
493 return Ok(BarrierSnapshot { topics });
494 }
495 let reason = if context.is_closed() {
496 Some(BarrierFailure::Cancelled)
497 } else if topics.iter().all(|topic| {
498 topic.complete()
499 || topic.blocked()
500 && (topic.target.is_none()
501 || receiver
502 .topics
503 .iter()
504 .any(|entry| entry.topic == topic.topic))
505 }) {
506 Some(BarrierFailure::Blocked)
507 } else if Instant::now() >= deadline {
508 Some(BarrierFailure::Deadline)
509 } else {
510 None
511 };
512 if let Some(reason) = reason {
513 return Err(BarrierError::Incomplete {
514 reason,
515 unfinished: topics
516 .into_iter()
517 .filter(|topic| !topic.complete())
518 .collect(),
519 });
520 }
521 tokio::select! {
522 _ = context.cancellation_token().cancelled() => {},
523 _ = lease.changed() => {},
524 _ = sleep(context.incoming_runtime().policy().active_database_poll_interval.min(deadline.saturating_duration_since(Instant::now()))) => {},
525 }
526 }
527}