xmtp_mls/subscriptions/incoming/stream_api.rs
1//! The receiver operations that application streams use.
2//!
3//! A stream acquires its lease with [`IncomingCoordinator::acquire_stream`],
4//! which gives that lease its own recovery budget. While it waits, the stream
5//! rechecks the budget with [`IncomingLease::check_recovery`] on a timer as
6//! well as on lease changes, and ends with the [`RecoveryFailure`] it returns.
7//! A stream reports its last status with [`IncomingStatus::cancel`] applied
8//! when it closes, and a conversation stream skips rediscovery with a
9//! [`GroupSetGate`]. The order and timing of these checks belong to each stream.
10//!
11//! On native targets, the `shared_wires` child module owns the shared Bidi
12//! transports and their suspension, which the app's suspend and resume calls
13//! drive.
14
15use super::{
16 IncomingConnection, IncomingCoordinator, IncomingError, IncomingLease, IncomingProcessing,
17 IncomingRegistration, IncomingScope, IncomingStatus,
18};
19use std::sync::Arc;
20use xmtp_common::time::{Duration, Instant};
21
22#[cfg(not(target_arch = "wasm32"))]
23pub mod shared_wires;
24
25/// How often a waiting message reader rechecks its recovery budget without
26/// another wake, so a budget that runs out by time alone still ends the stream.
27pub const RECOVERY_POLL: Duration = Duration::from_secs(1);
28
29/// Why a stream's recovery budget ended the stream.
30#[derive(Clone, Debug)]
31pub enum RecoveryFailure {
32 /// The stream failed too often, or stayed disconnected too long, to retry.
33 Exhausted {
34 /// Failed attempts counted against this stream's budget.
35 attempts: u32,
36 /// The latest failure, or `None` when the deadline passed without one.
37 source: Option<Arc<IncomingError>>,
38 },
39 /// A failure that retrying cannot fix ended the stream at once.
40 Terminal(Arc<IncomingError>),
41}
42
43/// The group-set generation (`QueryGroup::group_set_generation`) of an
44/// operation's last successful discovery read, so a rediscovery is skipped
45/// while the stored group set is unchanged.
46///
47/// Read the generation before the discovery query and record it only after
48/// the query succeeds. A write that commits between the two reads then changes
49/// the generation the next pass sees, so the next pass reads again. Reading the
50/// generation after the query could cache an old set under a new generation.
51#[derive(Debug, Default)]
52pub struct GroupSetGate(Option<i64>);
53
54impl GroupSetGate {
55 /// Whether `current` differs from the recorded generation, or none is
56 /// recorded, so the operation must read again.
57 pub fn changed(&self, current: i64) -> bool {
58 self.0 != Some(current)
59 }
60
61 /// Record the generation read before a discovery read that succeeded.
62 pub fn record(&mut self, current: i64) {
63 self.0 = Some(current);
64 }
65}
66
67impl IncomingCoordinator {
68 /// Keep this scope active for an application stream until the returned
69 /// lease closes or drops.
70 ///
71 /// Application streams own a finite outage budget. Internal workers and
72 /// bounded sync operations keep their existing lifetime and deadline rules.
73 pub fn acquire_stream(self: &Arc<Self>, scope: IncomingScope) -> IncomingLease {
74 self.acquire_with_recovery(scope, true)
75 }
76}
77
78impl IncomingLease {
79 /// The failure that ended this lease's recovery budget, if one did.
80 ///
81 /// A lease from [`IncomingCoordinator::acquire`] has no budget, and a
82 /// closed lease no longer reports one, so both return `Ok`.
83 pub fn check_recovery(&self) -> Result<(), RecoveryFailure> {
84 let mut consumers = self.coordinator.state.consumer_recovery.lock();
85 match consumers.get_mut(&self.id) {
86 Some(state) => state.check(Instant::now()),
87 None => Ok(()),
88 }
89 }
90}
91
92impl IncomingStatus {
93 /// Report this status as ended: the connection closed, processing and
94 /// every topic cancelled, every topic registration removed, and no
95 /// discovery pending.
96 pub fn cancel(&mut self) {
97 self.connection = IncomingConnection::Closed;
98 self.processing = IncomingProcessing::Cancelled;
99 self.discovery_pending = false;
100 for topic in &mut self.topics {
101 topic.registration = IncomingRegistration::Removed;
102 topic.processing = IncomingProcessing::Cancelled;
103 }
104 }
105}