1use std::collections::{HashMap, HashSet, VecDeque};
31use std::sync::{
32 Arc,
33 atomic::{AtomicBool, Ordering},
34};
35
36use prost::Message;
37use tokio::sync::{mpsc, oneshot};
38use xmtp_common::rate_limit::Bucket;
39use xmtp_common::time::Instant;
40use xmtp_common::{BoxDynFuture, MaybeSend, MaybeSync, RetryableError};
41#[cfg(not(test))]
42use xmtp_configuration::AUTH_LOCKOUT_COOLDOWN;
43use xmtp_proto::api::ApiClientError;
44use xmtp_proto::{
45 backend_v1::ServerEnvelope,
46 types::{
47 Cursor, IncomingBatchLimits, IncomingEvent, IncomingSubscription, OrderedEnvelopeBatch,
48 Topic, TopicCursor,
49 },
50};
51
52#[cfg(test)]
55const AUTH_LOCKOUT_COOLDOWN: std::time::Duration = std::time::Duration::from_millis(500);
56
57use super::bidi::{BidiBinding, Connection, Event, TryMutateError};
58use super::chunk_by_budget;
59
60pub const DEFAULT_LEASE_DEPTH: usize = 64;
62const OUTBOX_RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_millis(25);
64const INCOMING_PAUSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
68
69fn update_budget() -> Bucket {
70 Bucket::new(
71 xmtp_configuration::BACKEND_DEFAULT_MAX_UPDATE_FRAMES_PER_SECOND,
72 xmtp_configuration::BACKEND_DEFAULT_MAX_UPDATE_BURST,
73 )
74}
75
76pub(crate) const MAX_MUTATE_TOPICS: usize = xmtp_configuration::BACKEND_DEFAULT_MAX_UPDATE_ADDS;
78pub(crate) const MAX_MUTATE_BYTES: usize =
80 xmtp_configuration::BACKEND_DEFAULT_MAX_REQUEST_BYTES - PER_ENTRY_OVERHEAD;
81const PER_ENTRY_OVERHEAD: usize = 64;
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct MutateLimits {
88 pub add_cap: usize,
89 pub remove_cap: usize,
90 pub byte_cap: usize,
91}
92
93impl Default for MutateLimits {
94 fn default() -> Self {
95 Self {
96 add_cap: MAX_MUTATE_TOPICS,
97 remove_cap: xmtp_configuration::BACKEND_DEFAULT_MAX_UPDATE_REMOVES,
98 byte_cap: MAX_MUTATE_BYTES,
99 }
100 }
101}
102
103impl MutateLimits {
104 pub fn from_limits(limits: &xmtp_configuration::LimitsConfiguration) -> Self {
107 Self {
108 add_cap: limits.max_update_adds.max(1),
109 remove_cap: limits.max_update_removes.max(1),
110 byte_cap: limits
111 .max_request_bytes
112 .saturating_sub(PER_ENTRY_OVERHEAD)
113 .max(1),
114 }
115 }
116}
117
118fn topic_wire_cost(topic: &Topic) -> usize {
119 1 + topic.identifier().len() + PER_ENTRY_OVERHEAD
120}
121
122pub fn chunk_mutate_adds<C>(adds: Vec<(Topic, C)>) -> Vec<Vec<(Topic, C)>> {
125 chunk_by_budget(adds, MAX_MUTATE_TOPICS, MAX_MUTATE_BYTES, |(topic, _)| {
126 topic_wire_cost(topic)
127 })
128}
129
130const RECONNECT_INITIAL_DELAY: std::time::Duration = std::time::Duration::from_millis(100);
131const RECONNECT_MAX_DELAY: std::time::Duration = std::time::Duration::from_secs(30);
132const MIN_STABLE_UPTIME: std::time::Duration = std::time::Duration::from_secs(10);
133const GRACEFUL_CLOSE_BUDGET: std::time::Duration = std::time::Duration::from_secs(5);
135
136#[derive(Debug)]
138pub struct OpenError {
139 retryable: bool,
140 locked_out: bool,
141 attempted: bool,
142 source: Box<dyn std::error::Error + Send + Sync + 'static>,
143}
144
145impl OpenError {
146 pub fn new<E>(e: E) -> Self
147 where
148 E: std::error::Error + xmtp_common::RetryableError + Send + Sync + 'static,
149 {
150 Self {
151 retryable: e.is_retryable(),
152 locked_out: Self::locked_out_of(&e),
153 attempted: !Self::cooldown_refusal(&e),
154 source: Box::new(e),
155 }
156 }
157
158 fn is_locked_out(&self) -> bool {
161 self.locked_out
162 }
163
164 fn attempted(&self) -> bool {
165 self.attempted
166 }
167
168 #[cfg(test)]
169 pub fn retryable(e: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
170 Self {
171 retryable: true,
172 locked_out: false,
173 attempted: true,
174 source: e.into(),
175 }
176 }
177
178 #[cfg(test)]
179 pub fn unretryable(e: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>) -> Self {
180 Self {
181 retryable: false,
182 locked_out: false,
183 attempted: true,
184 source: e.into(),
185 }
186 }
187}
188
189impl OpenError {
190 fn cooldown_refusal<E: 'static>(error: &E) -> bool {
191 (error as &dyn std::any::Any)
192 .downcast_ref::<ApiClientError>()
193 .is_some_and(|error| {
194 matches!(
195 error,
196 ApiClientError::Auth(xmtp_proto::api::AuthError::Exhausted)
197 )
198 })
199 }
200
201 fn locked_out_of<E: 'static>(error: &E) -> bool {
203 (error as &dyn std::any::Any)
204 .downcast_ref::<ApiClientError>()
205 .is_some_and(ApiClientError::is_locked_out)
206 }
207}
208
209impl std::fmt::Display for OpenError {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 self.source.fmt(f)
212 }
213}
214
215impl std::error::Error for OpenError {
216 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
217 Some(&*self.source)
218 }
219}
220
221impl xmtp_common::RetryableError for OpenError {
222 fn is_retryable(&self) -> bool {
223 self.retryable
224 }
225}
226
227pub trait TransportBinding: BidiBinding
229where
230 Self::GroupMessage: Clone,
231 Self::WelcomeMessage: Clone,
232{
233 type Cursor: Copy + Send + std::fmt::Debug + From<u64> + Into<u64> + 'static;
235
236 fn build_mutate(
238 adds: impl IntoIterator<Item = (Topic, Self::Cursor)>,
239 removes: impl IntoIterator<Item = Topic>,
240 mutate_id: u64,
241 ) -> Self::Mutate;
242
243 fn group_topic(msg: &Self::GroupMessage) -> Option<Topic>;
244 fn welcome_topic(msg: &Self::WelcomeMessage) -> Option<Topic>;
245
246 fn group_cursor(msg: &Self::GroupMessage) -> Option<Self::Cursor>;
247 fn welcome_cursor(msg: &Self::WelcomeMessage) -> Option<Self::Cursor>;
248 fn group_envelope(msg: &Self::GroupMessage) -> &ServerEnvelope;
249 fn welcome_envelope(msg: &Self::WelcomeMessage) -> &ServerEnvelope;
250 fn advance(position: &mut Self::Cursor, delivered: Self::Cursor);
251 fn covers(position: &Self::Cursor, delivered: &Self::Cursor) -> bool;
253 fn meet(a: Self::Cursor, b: Self::Cursor) -> Self::Cursor;
255}
256
257#[derive(Debug, thiserror::Error)]
258pub enum TransportError {
259 #[error("the bidi transport is closed")]
260 Closed,
261 #[error("opening the bidi wire failed: {0}")]
262 Open(#[source] OpenError),
263 #[error("a lease must name at least one topic")]
264 Empty,
265 #[error("the bidi wire topic limit would be exceeded")]
267 TooManyTopics,
268 #[error("invalid subscription frame: {0}")]
269 Protocol(&'static str),
270 #[error("incoming subscription envelope exceeds its receive limit")]
272 Capacity,
273 #[error("incoming subscription delivery queue is full")]
275 Backpressure,
276 #[error(transparent)]
277 Wire(#[from] std::sync::Arc<super::bidi::ConnectionFailure>),
278}
279
280impl TransportError {
281 pub fn is_cooldown_refusal(&self) -> bool {
283 matches!(self, Self::Open(error) if !error.attempted())
284 }
285}
286
287type IncomingFailure = std::sync::Arc<parking_lot::Mutex<Option<TransportError>>>;
288type IncomingFrame = Result<Vec<IncomingEvent>, TransportError>;
290
291impl xmtp_common::RetryableError for TransportError {
292 fn is_retryable(&self) -> bool {
293 match self {
294 Self::Backpressure | Self::Capacity => true,
295 Self::Open(e) => e.is_retryable(),
296 Self::Wire(e) => e.is_retryable(),
297 Self::Closed | Self::Empty | Self::TooManyTopics | Self::Protocol(_) => false,
298 }
299 }
300}
301
302pub enum LeaseEvent<B: TransportBinding>
304where
305 B::GroupMessage: Clone,
306 B::WelcomeMessage: Clone,
307{
308 CatchUpComplete,
310 GroupMessages(Vec<B::GroupMessage>),
311 WelcomeMessages(Vec<B::WelcomeMessage>),
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
315struct LeaseId(u64);
316
317pub struct BidiTransport<B: TransportBinding>
319where
320 B::GroupMessage: Clone,
321 B::WelcomeMessage: Clone,
322{
323 cmds: mpsc::UnboundedSender<Cmd<B>>,
324}
325
326impl<B: TransportBinding> Clone for BidiTransport<B>
327where
328 B::GroupMessage: Clone,
329 B::WelcomeMessage: Clone,
330{
331 fn clone(&self) -> Self {
332 Self {
333 cmds: self.cmds.clone(),
334 }
335 }
336}
337
338pub struct TopicLease<B: TransportBinding>
340where
341 B::GroupMessage: Clone,
342 B::WelcomeMessage: Clone,
343{
344 id: LeaseId,
345 topics: Vec<Topic>,
346 events: mpsc::Receiver<LeaseEvent<B>>,
347 incoming: Option<mpsc::Receiver<IncomingFrame>>,
348 incoming_pending: std::vec::IntoIter<IncomingEvent>,
349 incoming_failure: IncomingFailure,
350 connected: Arc<AtomicBool>,
351 cmds: mpsc::UnboundedSender<Cmd<B>>,
352}
353
354impl<B: TransportBinding> TopicLease<B>
355where
356 B::GroupMessage: Clone,
357 B::WelcomeMessage: Clone,
358{
359 pub async fn next(&mut self) -> Option<LeaseEvent<B>> {
361 self.events.recv().await
362 }
363
364 pub fn topics(&self) -> &[Topic] {
365 &self.topics
366 }
367
368 pub async fn next_incoming(&mut self) -> Option<Result<IncomingEvent, TransportError>> {
370 loop {
371 if let Some(event) = self.incoming_pending.next() {
372 return Some(Ok(event));
373 }
374 match self.incoming.as_mut()?.recv().await {
375 Some(Ok(events)) => self.incoming_pending = events.into_iter(),
376 Some(Err(error)) => return Some(Err(error)),
377 None => return self.incoming_failure.lock().take().map(Err),
378 }
379 }
380 }
381
382 pub fn acknowledge_received(&self, cursors: TopicCursor) {
384 let _ = self.cmds.send(Cmd::Received {
385 id: self.id,
386 cursors,
387 });
388 }
389
390 pub fn into_incoming_subscription(self) -> IncomingSubscription<TransportError> {
392 let cmds = self.cmds.clone();
393 let id = self.id;
394 let connected = self.connected.clone();
395 let events = futures::stream::unfold(self, |mut lease| async move {
396 lease.next_incoming().await.map(|event| (event, lease))
397 });
398 IncomingSubscription::new(Box::pin(events), move |cursors| {
399 let _ = cmds.send(Cmd::Received { id, cursors });
400 })
401 .with_connection_source(move || connected.load(Ordering::Acquire))
402 }
403}
404
405impl<B: TransportBinding> Drop for TopicLease<B>
406where
407 B::GroupMessage: Clone,
408 B::WelcomeMessage: Clone,
409{
410 fn drop(&mut self) {
411 let _ = self.cmds.send(Cmd::Deref(self.id));
412 }
413}
414
415impl<B: TransportBinding> BidiTransport<B>
416where
417 B::GroupMessage: Clone,
418 B::WelcomeMessage: Clone,
419{
420 pub fn new<O, Fut>(opener: O, initially_suspended: bool) -> Self
422 where
423 O: Fn(B::Mutate) -> Fut + MaybeSend + MaybeSync + 'static,
424 Fut: Future<Output = Result<Connection<B>, OpenError>> + MaybeSend + 'static,
425 {
426 Self::spawn(opener, initially_suspended, MutateLimits::default())
427 }
428
429 pub fn new_within<O, Fut>(opener: O, initially_suspended: bool, mutate: MutateLimits) -> Self
431 where
432 O: Fn(B::Mutate) -> Fut + MaybeSend + MaybeSync + 'static,
433 Fut: Future<Output = Result<Connection<B>, OpenError>> + MaybeSend + 'static,
434 {
435 Self::spawn(opener, initially_suspended, mutate)
436 }
437
438 #[cfg(test)]
439 pub(crate) fn new_with_chunk_limits<O, Fut>(
440 opener: O,
441 initially_suspended: bool,
442 chunk_cap: usize,
443 chunk_bytes: usize,
444 ) -> Self
445 where
446 O: Fn(B::Mutate) -> Fut + MaybeSend + MaybeSync + 'static,
447 Fut: Future<Output = Result<Connection<B>, OpenError>> + MaybeSend + 'static,
448 {
449 Self::spawn(
450 opener,
451 initially_suspended,
452 MutateLimits {
453 add_cap: chunk_cap,
454 remove_cap: chunk_cap,
455 byte_cap: chunk_bytes,
456 },
457 )
458 }
459
460 fn spawn<O, Fut>(opener: O, initially_suspended: bool, mutate: MutateLimits) -> Self
461 where
462 O: Fn(B::Mutate) -> Fut + MaybeSend + MaybeSync + 'static,
463 Fut: Future<Output = Result<Connection<B>, OpenError>> + MaybeSend + 'static,
464 {
465 let (cmds, cmds_rx) = mpsc::unbounded_channel();
466 let opener: Opener<B> = Box::new(
467 move |initial| -> BoxDynFuture<'static, Result<Connection<B>, OpenError>> {
468 Box::pin(opener(initial))
469 },
470 );
471 xmtp_common::spawn(
472 None,
473 run_ledger::<B>(
474 opener,
475 cmds_rx,
476 cmds.clone().downgrade(),
477 initially_suspended,
478 mutate,
479 ),
480 );
481 Self { cmds }
482 }
483
484 pub async fn lease(
488 &self,
489 subs: Vec<(Topic, B::Cursor)>,
490 depth: usize,
491 ) -> Result<TopicLease<B>, TransportError> {
492 if subs.is_empty() {
493 return Err(TransportError::Empty);
494 }
495 let (reply, response) = oneshot::channel();
496 self.cmds
497 .send(Cmd::Lease { subs, depth, reply })
498 .map_err(|_| TransportError::Closed)?;
499 response.await.map_err(|_| TransportError::Closed)?
500 }
501
502 pub async fn lease_ordered(
504 &self,
505 subs: Vec<(Topic, B::Cursor)>,
506 depth: usize,
507 limits: IncomingBatchLimits,
508 ) -> Result<TopicLease<B>, TransportError> {
509 if subs.is_empty() {
510 return Err(TransportError::Empty);
511 }
512 if limits.max_rows == 0 || limits.max_bytes == 0 {
513 return Err(TransportError::Capacity);
514 }
515 let (reply, response) = oneshot::channel();
516 self.cmds
517 .send(Cmd::LeaseOrdered {
518 subs,
519 depth,
520 limits,
521 reply,
522 })
523 .map_err(|_| TransportError::Closed)?;
524 response.await.map_err(|_| TransportError::Closed)?
525 }
526
527 #[xmtp_common::span(prefix = "bidi")]
529 pub async fn suspend(&self) -> Result<(), TransportError> {
530 self.enqueue_suspend()?
531 .await
532 .map_err(|_| TransportError::Closed)
533 }
534
535 pub fn enqueue_suspend(&self) -> Result<oneshot::Receiver<()>, TransportError> {
537 let (reply, response) = oneshot::channel();
538 self.cmds
539 .send(Cmd::Suspend { reply })
540 .map_err(|_| TransportError::Closed)?;
541 Ok(response)
542 }
543
544 #[xmtp_common::span(prefix = "bidi")]
547 pub async fn resume(&self) -> Result<(), TransportError> {
548 self.enqueue_resume()?
549 .await
550 .map_err(|_| TransportError::Closed)
551 }
552
553 pub fn enqueue_resume(&self) -> Result<oneshot::Receiver<()>, TransportError> {
555 let (reply, response) = oneshot::channel();
556 self.cmds
557 .send(Cmd::Resume { reply })
558 .map_err(|_| TransportError::Closed)?;
559 Ok(response)
560 }
561}
562
563enum Cmd<B: TransportBinding>
564where
565 B::GroupMessage: Clone,
566 B::WelcomeMessage: Clone,
567{
568 Lease {
569 subs: Vec<(Topic, B::Cursor)>,
570 depth: usize,
571 reply: oneshot::Sender<Result<TopicLease<B>, TransportError>>,
572 },
573 LeaseOrdered {
574 subs: Vec<(Topic, B::Cursor)>,
575 depth: usize,
576 limits: IncomingBatchLimits,
577 reply: oneshot::Sender<Result<TopicLease<B>, TransportError>>,
578 },
579 Received {
580 id: LeaseId,
581 cursors: TopicCursor,
582 },
583 Deref(LeaseId),
584 Suspend {
585 reply: oneshot::Sender<()>,
586 },
587 Resume {
588 reply: oneshot::Sender<()>,
589 },
590}
591
592trait OpenWire<B: BidiBinding>: MaybeSend + MaybeSync {
593 fn open(&self, initial: B::Mutate) -> BoxDynFuture<'static, Result<Connection<B>, OpenError>>;
594}
595
596impl<B: BidiBinding, F> OpenWire<B> for F
597where
598 F: Fn(B::Mutate) -> BoxDynFuture<'static, Result<Connection<B>, OpenError>>
599 + MaybeSend
600 + MaybeSync,
601{
602 fn open(&self, initial: B::Mutate) -> BoxDynFuture<'static, Result<Connection<B>, OpenError>> {
603 self(initial)
604 }
605}
606
607type Opener<B> = Box<dyn OpenWire<B>>;
608#[derive(Debug, Clone, Copy)]
609enum DeliveryKind {
610 Group,
611 Welcome,
612}
613
614struct PendingUpdate<C> {
616 adds: Vec<(Topic, C)>,
617 removes: Vec<Topic>,
618}
619
620struct TopicRegistration<C> {
622 target: Option<u64>,
623 delivered: C,
624 holders: HashSet<LeaseId>,
625 state: RegistrationState,
626}
627
628enum RegistrationState {
629 Adding,
630 Active,
631 Removing,
633}
634
635struct LeaseObligation {
637 satisfied: bool,
638}
639
640struct LeaseState<B: TransportBinding>
641where
642 B::GroupMessage: Clone,
643 B::WelcomeMessage: Clone,
644{
645 floors: HashMap<Topic, B::Cursor>,
646 delivered: HashMap<Topic, B::Cursor>,
647 obligations: HashMap<Topic, LeaseObligation>,
648 unmet: usize,
649 notified: bool,
650 events: mpsc::Sender<LeaseEvent<B>>,
651 incoming: Option<(mpsc::Sender<IncomingFrame>, IncomingBatchLimits)>,
652 incoming_failure: IncomingFailure,
653 connected: Arc<AtomicBool>,
654 incoming_pending: VecDeque<Vec<IncomingEvent>>,
655 paused_at: Option<Instant>,
656 pause_entries: u64,
657}
658
659impl<B: TransportBinding> LeaseState<B>
660where
661 B::GroupMessage: Clone,
662 B::WelcomeMessage: Clone,
663{
664 fn finish_pause(&mut self, now: Instant, reason: &'static str) {
665 if let Some(started) = self.paused_at.take() {
666 tracing::info!(
667 pause_entries = self.pause_entries,
668 pause_duration_ms = now.saturating_duration_since(started).as_millis() as u64,
669 backlog_depth = self.incoming_pending.len(),
670 reason,
671 "bidi incoming: pause ended"
672 );
673 }
674 }
675
676 fn flush_incoming(&mut self, now: Instant) -> bool {
678 let Some((sender, _)) = &self.incoming else {
679 return false;
680 };
681 let sender = sender.clone();
682 while !self.incoming_pending.is_empty() {
683 let permit = match sender.try_reserve() {
684 Ok(permit) => permit,
685 Err(mpsc::error::TrySendError::Full(_)) => {
686 if self.paused_at.is_some_and(|started| {
687 now.saturating_duration_since(started) >= INCOMING_PAUSE_TIMEOUT
688 }) {
689 tracing::warn!(
690 backlog_depth = self.incoming_pending.len(),
691 "bidi incoming: pause deadline exceeded; closing lease"
692 );
693 *self.incoming_failure.lock() = Some(TransportError::Backpressure);
694 self.finish_pause(now, "deadline");
695 return true;
696 }
697 if self.paused_at.is_none() {
698 self.paused_at = Some(now);
699 self.pause_entries += 1;
700 tracing::info!(
701 pause_entries = self.pause_entries,
702 backlog_depth = self.incoming_pending.len(),
703 "bidi incoming: paused"
704 );
705 }
706 return false;
707 }
708 Err(mpsc::error::TrySendError::Closed(_)) => {
709 *self.incoming_failure.lock() = Some(TransportError::Backpressure);
710 self.finish_pause(now, "consumer_closed");
711 return true;
712 }
713 };
714 let Some(events) = self.incoming_pending.pop_front() else {
715 unreachable!()
716 };
717 let positions: Vec<_> = events
718 .iter()
719 .filter_map(|event| {
720 let IncomingEvent::OrderedBatch(batch) = event else {
721 return None;
722 };
723 let last = batch
724 .envelopes
725 .last()?
726 .meta
727 .as_ref()?
728 .cursor
729 .as_ref()?
730 .sequence_id;
731 Some((batch.topic.clone(), last))
732 })
733 .collect();
734 permit.send(Ok(events));
737 for (topic, last) in positions {
738 if let Some(delivered) = self.delivered.get_mut(&topic) {
739 B::advance(delivered, last.into());
740 }
741 }
742 self.finish_pause(now, "progress");
744 }
745 self.finish_pause(now, "drained");
746 false
747 }
748}
749
750struct Ledger<B: TransportBinding>
751where
752 B::GroupMessage: Clone,
753 B::WelcomeMessage: Clone,
754{
755 leases: HashMap<LeaseId, LeaseState<B>>,
756 by_topic: HashMap<Topic, HashSet<LeaseId>>,
757 last_seen: HashMap<Topic, B::Cursor>,
758 registrations: HashMap<Topic, TopicRegistration<B::Cursor>>,
759 pending_updates: HashMap<u64, PendingUpdate<B::Cursor>>,
760 dirty_topics: HashSet<Topic>,
761 failed_incoming: HashSet<LeaseId>,
762 next_lease: u64,
763 next_update: u64,
764 mutate: MutateLimits,
765}
766
767impl<B: TransportBinding> Default for Ledger<B>
768where
769 B::GroupMessage: Clone,
770 B::WelcomeMessage: Clone,
771{
772 fn default() -> Self {
773 Self {
774 leases: HashMap::new(),
775 by_topic: HashMap::new(),
776 last_seen: HashMap::new(),
777 registrations: HashMap::new(),
778 pending_updates: HashMap::new(),
779 dirty_topics: HashSet::new(),
780 failed_incoming: HashSet::new(),
781 next_lease: 0,
782 next_update: 0,
783 mutate: MutateLimits::default(),
784 }
785 }
786}
787
788impl<B: TransportBinding> Ledger<B>
789where
790 B::GroupMessage: Clone,
791 B::WelcomeMessage: Clone,
792{
793 fn has_pending_incoming(&self) -> bool {
794 self.leases
795 .values()
796 .any(|lease| !lease.incoming_pending.is_empty())
797 }
798
799 fn flush_incoming(&mut self, now: Instant) -> Vec<LeaseId> {
800 self.leases
801 .iter_mut()
802 .filter_map(|(id, lease)| {
803 if !lease.incoming_pending.is_empty() {
804 self.dirty_topics.extend(lease.floors.keys().cloned());
805 }
806 lease.flush_incoming(now).then_some(*id)
807 })
808 .collect()
809 }
810
811 fn received(&mut self, id: LeaseId, cursors: TopicCursor) {
813 let Some(lease) = self.leases.get_mut(&id) else {
814 return;
815 };
816 if lease.incoming.is_none() {
817 return;
818 }
819 for (topic, cursor) in cursors {
820 if cursor.0 > i64::MAX as u64 {
821 continue;
822 }
823 if let Some(floor) = lease.floors.get_mut(&topic) {
824 B::advance(floor, cursor.0.into());
825 if let Some(delivered) = lease.delivered.get_mut(&topic) {
826 B::advance(delivered, cursor.0.into());
827 }
828 self.dirty_topics.insert(topic);
829 }
830 }
831 }
832
833 fn incoming_registered(&mut self, id: LeaseId, topics: impl IntoIterator<Item = Topic>) {
835 let Some(lease) = self.leases.get_mut(&id) else {
836 return;
837 };
838 let Some(_) = &lease.incoming else {
839 return;
840 };
841 lease.connected.store(
843 lease.floors.keys().all(|topic| {
844 self.registrations.get(topic).is_some_and(|registration| {
845 matches!(registration.state, RegistrationState::Active)
846 })
847 }),
848 Ordering::Release,
849 );
850 let mut starts = TopicCursor::new();
851 let mut targets = TopicCursor::new();
852 for topic in topics {
853 let Some(registration) = self.registrations.get(&topic) else {
854 continue;
855 };
856 let Some(target) = registration.target else {
857 continue;
858 };
859 let Some(start) = lease.delivered.get(&topic) else {
860 continue;
861 };
862 starts.insert(topic.clone(), Cursor((*start).into()));
863 targets.insert(topic, Cursor(target));
864 }
865 if !starts.is_empty() {
866 lease
867 .incoming_pending
868 .push_back(vec![IncomingEvent::Registered { starts, targets }]);
869 if lease.flush_incoming(Instant::now()) {
870 self.failed_incoming.insert(id);
871 }
872 }
873 }
874
875 fn valid_targets(&self, id: u64, targets: &[(Topic, u64)]) -> bool {
877 let Some(update) = self.pending_updates.get(&id) else {
878 return false;
879 };
880 let unique: HashSet<_> = targets.iter().map(|(topic, _)| topic).collect();
881 targets.len() == update.adds.len()
882 && unique.len() == targets.len()
883 && targets.iter().all(|(topic, target)| {
884 *target <= i64::MAX as u64 && update.adds.iter().any(|(added, _)| added == topic)
885 })
886 }
887
888 fn demux_incoming<M>(
890 &mut self,
891 messages: &[M],
892 envelope_of: impl Fn(&M) -> &ServerEnvelope,
893 ) -> Result<Vec<LeaseId>, &'static str> {
894 let mut positions = HashMap::new();
895 for message in messages {
896 let envelope = envelope_of(message);
897 let meta = envelope.meta.as_ref().ok_or("metadata")?;
898 let topic =
899 Topic::parse(&meta.topic.as_ref().ok_or("topic")?.topic).map_err(|_| "topic")?;
900 let (_, cursor, _) =
901 crate::envelope::metadata(meta, topic.kind()).map_err(|_| "metadata")?;
902 let Some(registration) = self.registrations.get(&topic) else {
903 continue;
904 };
905 if registration.target.is_none() {
906 return Err("messages before Applied");
907 }
908 let previous = positions
909 .entry(topic)
910 .or_insert_with(|| Cursor(registration.delivered.into()));
911 if cursor <= *previous {
912 return Err("cursor order");
913 }
914 *previous = cursor;
915 }
916 let mut dropped = Vec::new();
917 for (id, lease) in &mut self.leases {
918 let Some((_, limits)) = &lease.incoming else {
919 continue;
920 };
921 let mut selected = Vec::new();
922 let mut impossible = false;
923 for message in messages {
924 let envelope = envelope_of(message);
925 let meta = envelope.meta.as_ref().ok_or("metadata")?;
926 let topic = Topic::parse(&meta.topic.as_ref().ok_or("topic")?.topic)
927 .map_err(|_| "topic")?;
928 let Some(registration) = self.registrations.get(&topic) else {
929 continue;
930 };
931 if !registration.holders.contains(id) {
932 continue;
933 }
934 let Some(delivered) = lease.delivered.get(&topic) else {
935 continue;
936 };
937 if meta.cursor.as_ref().ok_or("cursor")?.sequence_id <= (*delivered).into() {
938 continue;
939 }
940 impossible |= limits.max_rows == 0 || envelope.encoded_len() > limits.max_bytes;
943 selected.push((topic, envelope));
944 }
945 if impossible {
946 tracing::error!(
947 max_rows = limits.max_rows,
948 max_bytes = limits.max_bytes,
949 "bidi incoming: envelope cannot fit receive limits; closing lease"
950 );
951 *lease.incoming_failure.lock() = Some(TransportError::Capacity);
952 dropped.push(*id);
953 continue;
954 }
955 if selected.is_empty() {
956 continue;
957 }
958 let mut projected = lease.delivered.clone();
962 for chunk in chunk_by_budget(
963 selected,
964 limits.max_rows,
965 limits.max_bytes,
966 |(_, envelope)| envelope.encoded_len(),
967 ) {
968 let mut by_topic: HashMap<Topic, Vec<ServerEnvelope>> = HashMap::new();
969 for (topic, envelope) in chunk {
970 by_topic.entry(topic).or_default().push(envelope.clone());
971 }
972 let mut events = Vec::with_capacity(by_topic.len());
973 for (topic, envelopes) in by_topic {
974 let position = projected.get_mut(&topic).ok_or("lease topic")?;
975 let after = Cursor((*position).into());
976 let last = envelopes
977 .last()
978 .and_then(|envelope| envelope.meta.as_ref()?.cursor.as_ref())
979 .ok_or("cursor")?
980 .sequence_id;
981 B::advance(position, last.into());
982 events.push(IncomingEvent::OrderedBatch(OrderedEnvelopeBatch {
983 topic,
984 after,
985 envelopes,
986 }));
987 }
988 lease.incoming_pending.push_back(events);
989 }
990 if lease.flush_incoming(Instant::now()) {
991 dropped.push(*id);
992 }
993 }
994 Ok(dropped)
995 }
996
997 fn next_update_id(&mut self) -> u64 {
998 self.next_update += 1;
999 self.next_update
1000 }
1001
1002 fn register(
1004 &mut self,
1005 subs: &[(Topic, B::Cursor)],
1006 events: mpsc::Sender<LeaseEvent<B>>,
1007 ) -> LeaseId {
1008 self.next_lease += 1;
1009 let id = LeaseId(self.next_lease);
1010 let floors: HashMap<_, _> = subs.iter().cloned().collect();
1011 for topic in floors.keys() {
1012 self.by_topic.entry(topic.clone()).or_default().insert(id);
1013 }
1014 self.leases.insert(
1015 id,
1016 LeaseState {
1017 delivered: floors.clone(),
1018 obligations: floors
1019 .keys()
1020 .map(|topic| (topic.clone(), LeaseObligation { satisfied: false }))
1021 .collect(),
1022 unmet: floors.len(),
1023 floors,
1024 notified: false,
1025 events,
1026 incoming: None,
1027 incoming_failure: IncomingFailure::default(),
1028 connected: Arc::new(AtomicBool::new(false)),
1029 incoming_pending: VecDeque::new(),
1030 paused_at: None,
1031 pause_entries: 0,
1032 },
1033 );
1034 id
1035 }
1036
1037 fn reset_wire(&mut self) {
1039 self.registrations.clear();
1040 self.pending_updates.clear();
1041 self.dirty_topics.clear();
1042 for lease in self.leases.values_mut() {
1043 lease.connected.store(false, Ordering::Release);
1044 lease.obligations.clear();
1045 lease.unmet = lease.floors.len();
1046 if lease.incoming.is_some() {
1047 lease.finish_pause(Instant::now(), "wire_reset");
1048 lease.incoming_pending.clear();
1049 lease.delivered.clone_from(&lease.floors);
1050 }
1051 }
1052 }
1053
1054 fn reset_obligation(&mut self, id: LeaseId, topic: &Topic) {
1055 if let Some(lease) = self.leases.get_mut(&id) {
1056 if let Some(obligation) = lease.obligations.get_mut(topic) {
1057 if obligation.satisfied {
1058 obligation.satisfied = false;
1059 lease.unmet += 1;
1060 }
1061 } else {
1062 lease
1063 .obligations
1064 .insert(topic.clone(), LeaseObligation { satisfied: false });
1065 }
1066 }
1067 }
1068
1069 fn resume_cursor(&self, topic: &Topic) -> Option<B::Cursor> {
1074 let holders = self.by_topic.get(topic)?;
1075 holders
1076 .iter()
1077 .filter_map(|id| self.leases.get(id)?.floors.get(topic).copied())
1078 .reduce(B::meet)
1079 }
1080
1081 fn resume_adds(&self) -> Vec<(Topic, B::Cursor)> {
1082 self.by_topic
1083 .keys()
1084 .filter_map(|topic| Some((topic.clone(), self.resume_cursor(topic)?)))
1085 .collect()
1086 }
1087
1088 fn prepare_adds(&mut self, adds: Vec<(Topic, B::Cursor)>) -> Vec<(u64, B::Mutate)> {
1090 let chunks = chunk_by_budget(
1091 adds,
1092 self.mutate.add_cap,
1093 self.mutate.byte_cap,
1094 |(topic, _)| topic_wire_cost(topic),
1095 );
1096 chunks
1097 .into_iter()
1098 .map(|adds| {
1099 for (topic, cursor) in &adds {
1100 self.dirty_topics.insert(topic.clone());
1101 let holders = self.by_topic.get(topic).cloned().unwrap_or_default();
1102 for id in &holders {
1103 self.reset_obligation(*id, topic);
1104 }
1105 self.registrations
1106 .entry(topic.clone())
1107 .or_insert_with(|| TopicRegistration {
1108 target: None,
1109 delivered: *cursor,
1110 holders,
1111 state: RegistrationState::Adding,
1112 });
1113 }
1114 let id = self.next_update_id();
1115 let update = B::build_mutate(adds.clone(), [], id);
1116 self.pending_updates.insert(
1117 id,
1118 PendingUpdate {
1119 adds,
1120 removes: vec![],
1121 },
1122 );
1123 (id, update)
1124 })
1125 .collect()
1126 }
1127
1128 fn prepare_removes(&mut self, removes: Vec<Topic>) -> Vec<(u64, B::Mutate)> {
1129 chunk_by_budget(
1130 removes,
1131 self.mutate.remove_cap,
1132 self.mutate.byte_cap,
1133 topic_wire_cost,
1134 )
1135 .into_iter()
1136 .map(|removes| {
1137 let id = self.next_update_id();
1138 let update = B::build_mutate([], removes.clone(), id);
1139 self.pending_updates.insert(
1140 id,
1141 PendingUpdate {
1142 adds: vec![],
1143 removes,
1144 },
1145 );
1146 (id, update)
1147 })
1148 .collect()
1149 }
1150
1151 fn join(&mut self, id: LeaseId, subs: Vec<(Topic, B::Cursor)>) -> Vec<(u64, B::Mutate)> {
1153 let mut adds = Vec::new();
1154 let mut removes = Vec::new();
1155 let mut joined = Vec::new();
1156 for (topic, floor) in subs {
1157 self.dirty_topics.insert(topic.clone());
1158 let Some(registration) = self.registrations.get_mut(&topic) else {
1159 adds.push((topic, floor));
1160 continue;
1161 };
1162 match &mut registration.state {
1163 RegistrationState::Removing => {}
1164 _ if !B::covers(&floor, ®istration.delivered) => {
1165 registration.state = RegistrationState::Removing;
1166 for holder in ®istration.holders {
1167 if let Some(lease) = self.leases.get(holder) {
1168 lease.connected.store(false, Ordering::Release);
1169 }
1170 }
1171 removes.push(topic);
1172 }
1173 _ => {
1174 registration.holders.insert(id);
1175 joined.push(topic);
1176 }
1177 }
1178 }
1179 let mut updates = self.prepare_removes(removes);
1180 updates.extend(self.prepare_adds(adds));
1181 self.incoming_registered(id, joined);
1182 updates
1183 }
1184
1185 fn applied(&mut self, id: u64, targets: Vec<(Topic, u64)>) -> Vec<(Topic, B::Cursor)> {
1187 let Some(update) = self.pending_updates.remove(&id) else {
1188 tracing::warn!(id, "received Applied for an unknown update");
1189 return Vec::new();
1190 };
1191 let targets: HashMap<_, _> = targets.into_iter().collect();
1192 let mut readds = Vec::new();
1193 for topic in update.removes {
1194 self.registrations.remove(&topic);
1195 if let Some(cursor) = self.resume_cursor(&topic) {
1196 readds.push((topic, cursor));
1197 }
1198 }
1199 let mut registered: HashMap<LeaseId, Vec<Topic>> = HashMap::new();
1200 for (topic, _) in update.adds {
1201 self.dirty_topics.insert(topic.clone());
1202 if let Some(registration) = self.registrations.get_mut(&topic) {
1203 if let Some(target) = targets.get(&topic) {
1204 registration.target = Some(*target);
1205 }
1206 if matches!(registration.state, RegistrationState::Adding) {
1208 registration.state = RegistrationState::Active;
1209 }
1210 for holder in ®istration.holders {
1211 registered.entry(*holder).or_default().push(topic.clone());
1212 }
1213 }
1214 }
1215 for (holder, topics) in registered {
1216 self.incoming_registered(holder, topics);
1217 }
1218 readds
1219 }
1220
1221 fn recheck(&mut self) -> Vec<LeaseId> {
1223 let failed: Vec<_> = self.failed_incoming.drain().collect();
1224 let mut candidates = HashSet::new();
1225 for topic in self.dirty_topics.drain() {
1226 let Some(registration) = self.registrations.get(&topic) else {
1227 continue;
1228 };
1229 let Some(target) = registration.target else {
1230 continue;
1231 };
1232 let target_cursor = B::Cursor::from(target);
1233 for id in ®istration.holders {
1234 candidates.insert(*id);
1235 let Some(lease) = self.leases.get_mut(id) else {
1236 continue;
1237 };
1238 let Some(obligation) = lease.obligations.get_mut(&topic) else {
1239 continue;
1240 };
1241 if !obligation.satisfied
1242 && (target == 0
1243 || if lease.incoming.is_some() {
1244 lease
1245 .delivered
1246 .get(&topic)
1247 .is_some_and(|position| B::covers(position, &target_cursor))
1248 } else {
1249 B::covers(®istration.delivered, &target_cursor)
1250 }
1251 || lease
1252 .floors
1253 .get(&topic)
1254 .is_some_and(|floor| B::covers(floor, &target_cursor)))
1255 {
1256 obligation.satisfied = true;
1257 lease.unmet -= 1;
1258 }
1259 }
1260 }
1261 candidates
1262 .into_iter()
1263 .filter_map(|id| {
1264 let lease = self.leases.get_mut(&id)?;
1265 if !lease.notified && lease.unmet == 0 {
1266 if lease.incoming.is_none()
1267 && lease.events.try_send(LeaseEvent::CatchUpComplete).is_err()
1268 {
1269 return Some(id);
1270 }
1271 lease.notified = true;
1272 }
1273 None
1274 })
1275 .chain(failed)
1276 .collect()
1277 }
1278
1279 fn deref(&mut self, id: LeaseId) -> Vec<Topic> {
1281 let Some(mut lease) = self.leases.remove(&id) else {
1282 return vec![];
1283 };
1284 lease.finish_pause(Instant::now(), "lease_removed");
1285 let mut removes = Vec::new();
1286 for topic in lease.floors.keys() {
1287 if let Some(holders) = self.by_topic.get_mut(topic) {
1288 holders.remove(&id);
1289 if holders.is_empty() {
1290 self.by_topic.remove(topic);
1291 self.last_seen.remove(topic);
1292 removes.push(topic.clone());
1293 }
1294 }
1295 if let Some(registration) = self.registrations.get_mut(topic) {
1296 registration.holders.remove(&id);
1297 }
1298 }
1299 removes
1300 }
1301
1302 fn demux<M: Clone>(
1305 &mut self,
1306 messages: Vec<M>,
1307 kind: DeliveryKind,
1308 topic_of: impl Fn(&M) -> Option<Topic>,
1309 cursor_of: impl Fn(&M) -> Option<B::Cursor>,
1310 event: impl Fn(Vec<M>) -> LeaseEvent<B>,
1311 ) -> Vec<LeaseId> {
1312 let mut batches: HashMap<LeaseId, Vec<&M>> = HashMap::new();
1313 for message in &messages {
1314 let Some(topic) = topic_of(message) else {
1315 continue;
1316 };
1317 let Some(registration) = self.registrations.get_mut(&topic) else {
1318 continue;
1319 };
1320 if registration.target.is_none() {
1322 continue;
1323 }
1324 self.dirty_topics.insert(topic.clone());
1325 let cursor = cursor_of(message);
1326 if let Some(cursor) = cursor {
1327 B::advance(&mut registration.delivered, cursor);
1328 B::advance(
1329 self.last_seen.entry(topic.clone()).or_insert(cursor),
1330 cursor,
1331 );
1332 }
1333 for id in ®istration.holders {
1334 let Some(lease) = self.leases.get_mut(id) else {
1335 continue;
1336 };
1337 if lease.incoming.is_some() {
1338 continue;
1339 }
1340 if let Some(cursor) = cursor {
1341 let Some(position) = lease.delivered.get_mut(&topic) else {
1342 continue;
1343 };
1344 if B::covers(position, &cursor) {
1345 continue;
1346 }
1347 B::advance(position, cursor);
1348 }
1349 batches.entry(*id).or_default().push(message);
1350 }
1351 }
1352 batches
1353 .into_iter()
1354 .filter_map(|(id, batch)| {
1355 let lease = self.leases.get(&id)?;
1356 if let Ok(permit) = lease.events.try_reserve() {
1357 permit.send(event(batch.into_iter().cloned().collect()));
1358 None
1359 } else {
1360 tracing::warn!(
1361 lease = id.0,
1362 ?kind,
1363 "closing a lease whose delivery channel is full"
1364 );
1365 Some(id)
1366 }
1367 })
1368 .collect()
1369 }
1370
1371 fn caught_up(&self) -> bool {
1372 self.pending_updates.is_empty()
1373 && !self.has_pending_incoming()
1374 && self.leases.values().all(|lease| lease.unmet == 0)
1375 }
1376}
1377
1378enum Step<B: TransportBinding>
1381where
1382 B::GroupMessage: Clone,
1383 B::WelcomeMessage: Clone,
1384{
1385 Cmd(Option<Cmd<B>>),
1386 Wire(Option<Event<B::GroupMessage, B::WelcomeMessage>>),
1387 Retry,
1388 Reconnect,
1389}
1390
1391async fn run_ledger<B: TransportBinding>(
1392 opener: Opener<B>,
1393 cmds: mpsc::UnboundedReceiver<Cmd<B>>,
1394 lease_cmds: mpsc::WeakUnboundedSender<Cmd<B>>,
1395 initially_suspended: bool,
1396 mutate: MutateLimits,
1397) where
1398 B::GroupMessage: Clone,
1399 B::WelcomeMessage: Clone,
1400{
1401 LedgerTask {
1402 opener,
1403 cmds,
1404 lease_cmds,
1405 ledger: Ledger {
1406 mutate,
1407 ..Ledger::default()
1408 },
1409 conn: None,
1410 reconnect_delay: RECONNECT_INITIAL_DELAY,
1411 reconnect_at: tokio::time::Instant::now(),
1412 wire_opened_at: None,
1413 wire_span: None,
1414 suspended: initially_suspended,
1415 wire_opens: 0,
1416 resume_notify: Vec::new(),
1417 outbox: Outbox::default(),
1418 update_budget: update_budget(),
1419 deferred: std::collections::VecDeque::new(),
1420 }
1421 .run()
1422 .await
1423}
1424
1425enum Flow {
1426 Continue,
1427 Shutdown,
1428}
1429
1430struct LedgerTask<B: TransportBinding>
1431where
1432 B::GroupMessage: Clone,
1433 B::WelcomeMessage: Clone,
1434{
1435 opener: Opener<B>,
1436 cmds: mpsc::UnboundedReceiver<Cmd<B>>,
1437 lease_cmds: mpsc::WeakUnboundedSender<Cmd<B>>,
1438 ledger: Ledger<B>,
1439 conn: Option<Connection<B>>,
1440 reconnect_delay: std::time::Duration,
1441 reconnect_at: tokio::time::Instant,
1442 wire_opened_at: Option<tokio::time::Instant>,
1443 wire_span: Option<tracing::Span>,
1444 suspended: bool,
1445 wire_opens: u64,
1446 resume_notify: Vec<oneshot::Sender<()>>,
1447 outbox: Outbox<B::Mutate>,
1448 update_budget: Bucket,
1449 deferred: std::collections::VecDeque<Cmd<B>>,
1450}
1451
1452impl<B: TransportBinding> LedgerTask<B>
1453where
1454 B::GroupMessage: Clone,
1455 B::WelcomeMessage: Clone,
1456{
1457 async fn run(mut self) {
1458 loop {
1459 let queued = self.deferred.len() + self.cmds.len();
1462 for _ in 0..queued {
1463 let cmd = self
1464 .deferred
1465 .pop_front()
1466 .or_else(|| self.cmds.try_recv().ok());
1467 let Some(cmd) = cmd else { break };
1468 if let Flow::Shutdown = self.command(cmd).await {
1469 return;
1470 }
1471 }
1472 let mut dropped = self.ledger.flush_incoming(Instant::now());
1473 dropped.extend(self.ledger.recheck());
1474 let removes = self.drop_leases(dropped);
1475 self.retire(removes);
1476 self.settle_idle_waiters();
1477 self.settle_caught_up_waiters();
1478 self.flush_outbox();
1479 let flow = match self.next_step().await {
1480 Step::Retry => Flow::Continue,
1481 Step::Cmd(None) => self.shutdown(),
1482 Step::Cmd(Some(cmd)) => self.command(cmd).await,
1483 Step::Wire(Some(event)) => self.wire_event(event),
1484 Step::Wire(None) => self.wire_died(),
1485 Step::Reconnect => self.reconnect().await,
1486 };
1487 if let Flow::Shutdown = flow {
1488 return;
1489 }
1490 }
1491 }
1492
1493 async fn command(&mut self, cmd: Cmd<B>) -> Flow {
1494 match cmd {
1495 Cmd::Lease { subs, depth, reply } => self.lease(subs, depth, None, reply).await,
1496 Cmd::LeaseOrdered {
1497 subs,
1498 depth,
1499 limits,
1500 reply,
1501 } => self.lease(subs, depth, Some(limits), reply).await,
1502 Cmd::Received { id, cursors } => {
1503 self.ledger.received(id, cursors);
1504 Flow::Continue
1505 }
1506 Cmd::Deref(id) => self.deref(id),
1507 Cmd::Suspend { reply } => self.suspend(reply),
1508 Cmd::Resume { reply } => self.resume(reply).await,
1509 }
1510 }
1511
1512 async fn next_step(&mut self) -> Step<B> {
1513 if let Some(cmd) = self.deferred.pop_front() {
1514 return Step::Cmd(Some(cmd));
1515 }
1516 let retry_after = self.update_budget.wait().max(OUTBOX_RETRY_INTERVAL);
1517 if self.ledger.has_pending_incoming() {
1518 let pending: Vec<_> = self
1521 .ledger
1522 .leases
1523 .values()
1524 .filter(|lease| !lease.incoming_pending.is_empty())
1525 .filter_map(|lease| {
1526 let (sender, _) = lease.incoming.as_ref()?;
1527 Some(Box::pin(sender.clone().reserve_owned()))
1528 })
1529 .collect();
1530 let until_deadline = self
1531 .ledger
1532 .leases
1533 .values()
1534 .filter_map(|lease| lease.paused_at)
1535 .map(|started| INCOMING_PAUSE_TIMEOUT.saturating_sub(started.elapsed()))
1536 .min()
1537 .unwrap_or(INCOMING_PAUSE_TIMEOUT);
1538 return tokio::select! {
1539 cmd = self.cmds.recv() => Step::Cmd(cmd),
1540 _ = futures::future::select_all(pending) => Step::Retry,
1541 _ = xmtp_common::time::sleep(until_deadline) => Step::Retry,
1542 _ = xmtp_common::time::sleep(retry_after), if !self.outbox.is_empty() => Step::Retry,
1543 };
1544 }
1545 match self.conn.as_mut() {
1546 Some(wire) => tokio::select! {
1547 cmd = self.cmds.recv() => Step::Cmd(cmd),
1548 event = wire.next() => Step::Wire(event),
1549 _ = xmtp_common::time::sleep(retry_after), if !self.outbox.is_empty() => Step::Retry,
1550 },
1551 None if !self.ledger.leases.is_empty() && !self.suspended => tokio::select! {
1552 cmd = self.cmds.recv() => Step::Cmd(cmd),
1553 _ = tokio::time::sleep_until(self.reconnect_at) => Step::Reconnect,
1554 },
1555 None => Step::Cmd(self.cmds.recv().await),
1556 }
1557 }
1558
1559 fn shutdown(&mut self) -> Flow {
1560 self.outbox.clear();
1561 self.close_wire_span("shutdown");
1562 if let Some(wire) = self.conn.take() {
1563 close_gracefully(wire);
1564 }
1565 Flow::Shutdown
1566 }
1567
1568 fn open_wire_span(&mut self) {
1569 self.update_budget = update_budget();
1570 self.update_budget.take();
1572 self.wire_opened_at = Some(tokio::time::Instant::now());
1573 self.wire_span = Some(tracing::info_span!(
1574 parent: None,
1575 "bidi_wire",
1576 operation = "bidi.wire_session",
1577 reason = tracing::field::Empty,
1578 ));
1579 }
1580
1581 fn close_wire_span(&mut self, reason: &'static str) {
1582 if let Some(span) = self.wire_span.take() {
1583 span.record("reason", reason);
1584 }
1585 }
1586
1587 async fn lease(
1588 &mut self,
1589 subs: Vec<(Topic, B::Cursor)>,
1590 depth: usize,
1591 incoming_limits: Option<IncomingBatchLimits>,
1592 reply: oneshot::Sender<Result<TopicLease<B>, TransportError>>,
1593 ) -> Flow {
1594 let mut positions = HashMap::new();
1595 let mut subs_unique: Vec<(Topic, B::Cursor)> = Vec::new();
1596 for (topic, floor) in subs {
1597 if let Some(index) = positions.get(&topic).copied() {
1598 let (_, cursor): &mut (Topic, B::Cursor) = &mut subs_unique[index];
1599 *cursor = B::meet(*cursor, floor);
1600 } else {
1601 positions.insert(topic.clone(), subs_unique.len());
1602 subs_unique.push((topic, floor));
1603 }
1604 }
1605 let subs = subs_unique;
1606 let added = subs
1607 .iter()
1608 .filter(|(topic, _)| !self.ledger.by_topic.contains_key(topic))
1609 .count();
1610 if self.ledger.by_topic.len() + added
1611 > xmtp_configuration::BACKEND_DEFAULT_MAX_STREAM_TOPICS
1612 {
1613 let _ = reply.send(Err(TransportError::TooManyTopics));
1614 return Flow::Continue;
1615 }
1616 let cold = self.conn.is_none()
1617 && self.ledger.leases.is_empty()
1618 && !self.suspended
1619 && tokio::time::Instant::now() >= self.reconnect_at;
1620 let topics = subs.iter().map(|(topic, _)| topic.clone()).collect();
1621 let (tx, events) = mpsc::channel(depth.max(1));
1622 let id = self.ledger.register(&subs, tx);
1623 let incoming = incoming_limits.map(|limits| {
1624 let (sender, receiver) = mpsc::channel(depth.max(1));
1625 if let Some(lease) = self.ledger.leases.get_mut(&id) {
1626 lease.incoming = Some((sender, limits));
1627 }
1628 receiver
1629 });
1630 let incoming_failure = self
1631 .ledger
1632 .leases
1633 .get(&id)
1634 .map(|lease| lease.incoming_failure.clone())
1635 .unwrap_or_default();
1636 let connected = self.ledger.leases[&id].connected.clone();
1637 if cold {
1638 self.outbox.updates.extend(self.ledger.prepare_adds(subs));
1639 let Some((_, initial)) = self.outbox.updates.pop_front() else {
1640 return Flow::Shutdown;
1641 };
1642 match self.open_preemptibly(initial).await {
1643 OpenOutcome::Opened(wire) => {
1644 self.conn = Some(wire);
1645 self.open_wire_span();
1646 self.wire_opens += 1;
1647 }
1648 OpenOutcome::Failed(error) => {
1649 self.ledger.deref(id);
1650 self.ledger.reset_wire();
1651 self.outbox.clear();
1652 self.reconnect_delay = if error.is_locked_out() {
1653 AUTH_LOCKOUT_COOLDOWN
1654 } else {
1655 (self.reconnect_delay * 2).min(RECONNECT_MAX_DELAY)
1656 };
1657 self.arm_reconnect();
1658 let _ = reply.send(Err(TransportError::Open(error)));
1659 return Flow::Continue;
1660 }
1661 OpenOutcome::Suspended(ack) => {
1662 self.ledger.reset_wire();
1663 self.outbox.clear();
1664 self.suspend_preempted(ack);
1665 }
1666 OpenOutcome::Shutdown => return Flow::Shutdown,
1667 }
1668 } else if self.conn.is_some() {
1669 self.outbox.updates.extend(self.ledger.join(id, subs));
1670 let dropped = self.ledger.recheck();
1671 let removes = self.drop_leases(dropped);
1672 self.retire(removes);
1673 }
1674 let Some(cmds) = self.lease_cmds.upgrade() else {
1675 return Flow::Continue;
1676 };
1677 let _ = reply.send(Ok(TopicLease {
1678 id,
1679 topics,
1680 events,
1681 incoming,
1682 incoming_pending: Vec::new().into_iter(),
1683 incoming_failure,
1684 connected,
1685 cmds,
1686 }));
1687 Flow::Continue
1688 }
1689
1690 fn deref(&mut self, id: LeaseId) -> Flow {
1691 let removes = self.drop_leases(vec![id]);
1692 self.retire(removes);
1693 self.settle_idle_waiters();
1694 self.settle_caught_up_waiters();
1695 Flow::Continue
1696 }
1697
1698 fn drop_leases(&mut self, dropped: Vec<LeaseId>) -> Vec<Topic> {
1699 let mut removes = Vec::new();
1700 for lease in dropped {
1701 removes.extend(self.ledger.deref(lease));
1702 }
1703 let unused: HashSet<_> = self
1705 .ledger
1706 .pending_updates
1707 .iter()
1708 .filter_map(|(id, update)| {
1709 (!update.adds.is_empty()
1710 && update
1711 .adds
1712 .iter()
1713 .all(|(topic, _)| !self.ledger.by_topic.contains_key(topic)))
1714 .then_some(*id)
1715 })
1716 .collect();
1717 for id in self.outbox.purge(&unused) {
1718 if let Some(update) = self.ledger.pending_updates.remove(&id) {
1719 for (topic, _) in update.adds {
1720 self.ledger.dirty_topics.insert(topic.clone());
1721 if !self
1724 .ledger
1725 .registrations
1726 .get(&topic)
1727 .is_some_and(|registration| {
1728 matches!(registration.state, RegistrationState::Removing)
1729 })
1730 {
1731 self.ledger.registrations.remove(&topic);
1732 }
1733 }
1734 }
1735 }
1736 removes
1737 }
1738
1739 fn suspend(&mut self, reply: oneshot::Sender<()>) -> Flow {
1740 tracing::info!(
1741 leases = self.ledger.leases.len(),
1742 had_wire = self.conn.is_some(),
1743 "bidi transport: suspending — going off the network"
1744 );
1745 self.suspended = true;
1746 self.outbox.clear();
1747 self.ledger.reset_wire();
1748 self.close_wire_span("suspend");
1749 if let Some(wire) = self.conn.take() {
1750 close_gracefully(wire);
1751 }
1752 for waiter in self.resume_notify.drain(..) {
1753 let _ = waiter.send(());
1754 }
1755 let _ = reply.send(());
1756 Flow::Continue
1757 }
1758
1759 async fn resume(&mut self, reply: oneshot::Sender<()>) -> Flow {
1762 tracing::info!(
1763 leases = self.ledger.leases.len(),
1764 "bidi transport: resuming — catch up, then done"
1765 );
1766 let was_suspended = self.suspended;
1767 self.suspended = false;
1768 self.resume_notify.push(reply);
1769 if self.conn.is_some() {
1770 self.settle_caught_up_waiters();
1771 return Flow::Continue;
1772 }
1773 if self.ledger.leases.is_empty() {
1774 self.settle_idle_waiters();
1775 return Flow::Continue;
1776 }
1777 if was_suspended {
1778 self.reconnect_delay = RECONNECT_INITIAL_DELAY;
1779 self.reconnect().await
1780 } else {
1781 Flow::Continue
1782 }
1783 }
1784
1785 fn wire_event(&mut self, event: Event<B::GroupMessage, B::WelcomeMessage>) -> Flow {
1786 let has_incoming = self
1787 .ledger
1788 .leases
1789 .values()
1790 .any(|lease| lease.incoming.is_some());
1791 let incoming_dropped = if has_incoming {
1792 match &event {
1793 Event::Applied { id, targets } if !self.ledger.valid_targets(*id, targets) => {
1794 return self.fail_incoming("Applied targets");
1795 }
1796 Event::GroupMessages { messages } => {
1797 self.ledger.demux_incoming(messages, B::group_envelope)
1798 }
1799 Event::WelcomeMessages { messages } => {
1800 self.ledger.demux_incoming(messages, B::welcome_envelope)
1801 }
1802 _ => Ok(Vec::new()),
1803 }
1804 } else {
1805 Ok(Vec::new())
1806 };
1807 let incoming_dropped = match incoming_dropped {
1808 Ok(dropped) => dropped,
1809 Err(reason) => return self.fail_incoming(reason),
1810 };
1811 let mut dropped = match event {
1812 Event::Started { .. } => Vec::new(),
1813 Event::Applied { id, targets } => {
1814 let readds = self.ledger.applied(id, targets);
1815 self.outbox.updates.extend(self.ledger.prepare_adds(readds));
1816 Vec::new()
1817 }
1818 Event::GroupMessages { messages } => self.ledger.demux(
1819 messages,
1820 DeliveryKind::Group,
1821 B::group_topic,
1822 B::group_cursor,
1823 LeaseEvent::GroupMessages,
1824 ),
1825 Event::WelcomeMessages { messages } => self.ledger.demux(
1826 messages,
1827 DeliveryKind::Welcome,
1828 B::welcome_topic,
1829 B::welcome_cursor,
1830 LeaseEvent::WelcomeMessages,
1831 ),
1832 };
1833 dropped.extend(incoming_dropped);
1834 dropped.extend(self.ledger.recheck());
1835 let removes = self.drop_leases(dropped);
1836 self.retire(removes);
1837 self.settle_idle_waiters();
1838 self.settle_caught_up_waiters();
1839 Flow::Continue
1840 }
1841
1842 fn fail_incoming(&mut self, reason: &'static str) -> Flow {
1843 let dropped = self
1844 .ledger
1845 .leases
1846 .iter()
1847 .filter_map(|(id, lease)| {
1848 lease.incoming.as_ref()?;
1849 *lease.incoming_failure.lock() = Some(TransportError::Protocol(reason));
1850 Some(*id)
1851 })
1852 .collect();
1853 let removes = self.drop_leases(dropped);
1854 self.retire(removes);
1855 self.settle_idle_waiters();
1856 Flow::Continue
1857 }
1858
1859 fn arm_reconnect(&mut self) -> std::time::Duration {
1860 let delay = self.reconnect_delay + xmtp_common::time::rand_offset(self.reconnect_delay);
1861 self.reconnect_at = tokio::time::Instant::now() + delay;
1862 delay
1863 }
1864
1865 fn wire_died(&mut self) -> Flow {
1866 self.outbox.clear();
1867 let failure = self.conn.as_ref().and_then(Connection::failure);
1868 drop(self.conn.take());
1869 let mut dropped: Vec<_> = self
1870 .ledger
1871 .leases
1872 .iter()
1873 .filter_map(|(id, lease)| {
1874 let (sender, _) = lease.incoming.as_ref()?;
1875 let event = failure
1876 .as_ref()
1877 .map_or(Ok(vec![IncomingEvent::Disconnected]), |error| {
1878 Err(TransportError::Wire(error.clone()))
1879 });
1880 let full = match sender.try_send(event) {
1881 Ok(()) => false,
1882 Err(error) => {
1883 *lease.incoming_failure.lock() = Some(match error.into_inner() {
1884 Err(error) => error,
1885 Ok(_) => TransportError::Backpressure,
1886 });
1887 true
1888 }
1889 };
1890 (full || failure.as_ref().is_some_and(|error| !error.is_retryable())).then_some(*id)
1891 })
1892 .collect();
1893 if failure.as_ref().is_some_and(|error| !error.is_retryable()) {
1894 dropped.extend(self.ledger.leases.keys().copied());
1897 }
1898 self.drop_leases(dropped);
1899 self.ledger.reset_wire();
1900 self.close_wire_span("wire_end");
1901 let stable = self
1902 .wire_opened_at
1903 .take()
1904 .is_some_and(|opened| opened.elapsed() >= MIN_STABLE_UPTIME);
1905 self.reconnect_delay = if stable {
1906 RECONNECT_INITIAL_DELAY
1907 } else {
1908 (self.reconnect_delay * 2).min(RECONNECT_MAX_DELAY)
1909 };
1910 let retry_in = self.arm_reconnect();
1911 if !self.ledger.leases.is_empty() {
1912 tracing::warn!(
1913 leases = self.ledger.leases.len(),
1914 pending_updates = self.ledger.pending_updates.len(),
1915 retry_in_ms = retry_in.as_millis() as u64,
1916 "bidi transport: wire died; reopening from lease floors"
1917 );
1918 }
1919 self.settle_idle_waiters();
1920 Flow::Continue
1921 }
1922
1923 #[tracing::instrument(skip_all, fields(operation = "bidi.reconnect"))]
1924 async fn reconnect(&mut self) -> Flow {
1925 match self.reopen().await {
1926 AfterReopen::Proceed => Flow::Continue,
1927 AfterReopen::Suspended(ack) => {
1928 self.suspend_preempted(ack);
1929 Flow::Continue
1930 }
1931 AfterReopen::Shutdown => Flow::Shutdown,
1932 }
1933 }
1934
1935 fn suspend_preempted(&mut self, ack: oneshot::Sender<()>) {
1936 self.suspended = true;
1937 self.park_deferred_resumes();
1938 for waiter in self.resume_notify.drain(..) {
1939 let _ = waiter.send(());
1940 }
1941 let _ = ack.send(());
1942 }
1943
1944 async fn reopen(&mut self) -> AfterReopen {
1945 self.ledger.reset_wire();
1946 let adds = self.ledger.resume_adds();
1947 if adds.is_empty() {
1948 self.settle_idle_waiters();
1949 return AfterReopen::Proceed;
1950 }
1951 self.outbox.updates.extend(self.ledger.prepare_adds(adds));
1952 let Some((_, initial)) = self.outbox.updates.pop_front() else {
1953 return AfterReopen::Proceed;
1954 };
1955 match self.open_preemptibly(initial).await {
1956 OpenOutcome::Opened(wire) => {
1957 self.conn = Some(wire);
1958 self.open_wire_span();
1959 self.wire_opens += 1;
1960 AfterReopen::Proceed
1961 }
1962 OpenOutcome::Failed(error) => {
1963 self.outbox.clear();
1964 self.ledger.reset_wire();
1965 let locked_out = error.is_locked_out();
1968 let attempted = error.attempted();
1969 if !locked_out && !error.is_retryable() {
1970 tracing::error!("bidi reconnect failed permanently: {error}");
1971 let error = std::sync::Arc::new(super::bidi::ConnectionFailure::Wire(
1972 xmtp_proto::api::NetworkError::new(error),
1973 ));
1974 for lease in self.ledger.leases.values() {
1975 if lease.incoming.is_some() {
1976 *lease.incoming_failure.lock() =
1977 Some(TransportError::Wire(error.clone()));
1978 }
1979 }
1980 let ids = self.ledger.leases.keys().copied().collect();
1981 self.drop_leases(ids);
1982 self.reconnect_delay = (self.reconnect_delay * 2).min(RECONNECT_MAX_DELAY);
1983 self.arm_reconnect();
1984 self.park_deferred_resumes();
1985 return AfterReopen::Proceed;
1986 }
1987 let failure = std::sync::Arc::new(super::bidi::ConnectionFailure::Wire(
1991 xmtp_proto::api::NetworkError::new(error),
1992 ));
1993 let mut dropped = Vec::new();
1994 for (id, lease) in &self.ledger.leases {
1995 if let Some((sender, _)) = &lease.incoming
1996 && attempted
1997 && sender
1998 .try_send(Err(TransportError::Wire(failure.clone())))
1999 .is_err()
2000 {
2001 *lease.incoming_failure.lock() =
2002 Some(TransportError::Wire(failure.clone()));
2003 dropped.push(*id);
2004 }
2005 }
2006 self.drop_leases(dropped);
2007 self.reconnect_delay = if locked_out {
2008 AUTH_LOCKOUT_COOLDOWN
2009 } else {
2010 (self.reconnect_delay * 2).min(RECONNECT_MAX_DELAY)
2011 };
2012 self.arm_reconnect();
2013 self.park_deferred_resumes();
2014 AfterReopen::Proceed
2015 }
2016 OpenOutcome::Suspended(ack) => {
2017 self.outbox.clear();
2018 self.ledger.reset_wire();
2019 AfterReopen::Suspended(ack)
2020 }
2021 OpenOutcome::Shutdown => AfterReopen::Shutdown,
2022 }
2023 }
2024
2025 async fn open_preemptibly(&mut self, mutate: B::Mutate) -> OpenOutcome<B> {
2028 let open = self.opener.open(mutate);
2029 tokio::pin!(open);
2030 loop {
2031 tokio::select! {
2032 result = &mut open => {
2033 return match result {
2034 Ok(wire) => OpenOutcome::Opened(wire),
2035 Err(e) => OpenOutcome::Failed(e),
2036 };
2037 }
2038 cmd = self.cmds.recv() => match cmd {
2039 Some(Cmd::Suspend { reply }) => return OpenOutcome::Suspended(reply),
2040 Some(cmd) => self.deferred.push_back(cmd),
2041 None => return OpenOutcome::Shutdown,
2042 },
2043 }
2044 }
2045 }
2046
2047 fn park_deferred_resumes(&mut self) {
2049 for cmd in std::mem::take(&mut self.deferred) {
2050 match cmd {
2051 Cmd::Resume { reply } => self.resume_notify.push(reply),
2052 other => self.deferred.push_back(other),
2053 }
2054 }
2055 }
2056
2057 fn settle_idle_waiters(&mut self) {
2058 if !self.ledger.leases.is_empty() {
2059 return;
2060 }
2061 self.ledger.reset_wire();
2062 for waiter in self.resume_notify.drain(..) {
2063 let _ = waiter.send(());
2064 }
2065 }
2066
2067 fn settle_caught_up_waiters(&mut self) {
2068 if self.conn.is_none() || !self.ledger.caught_up() || !self.outbox.is_empty() {
2069 return;
2070 }
2071 for waiter in self.resume_notify.drain(..) {
2072 let _ = waiter.send(());
2073 }
2074 }
2075
2076 fn retire(&mut self, removes: Vec<Topic>) {
2077 if self.ledger.leases.is_empty() {
2078 self.outbox.clear();
2079 self.ledger.reset_wire();
2080 self.close_wire_span("idle");
2081 if let Some(wire) = self.conn.take() {
2082 close_gracefully(wire);
2083 }
2084 return;
2085 }
2086 if self.conn.is_none() {
2087 return;
2088 }
2089 let mut to_remove = Vec::new();
2090 for topic in removes {
2091 if let Some(registration) = self.ledger.registrations.get_mut(&topic)
2092 && !matches!(registration.state, RegistrationState::Removing)
2093 {
2094 registration.state = RegistrationState::Removing;
2095 to_remove.push(topic);
2096 }
2097 }
2098 self.outbox
2099 .updates
2100 .extend(self.ledger.prepare_removes(to_remove));
2101 }
2102
2103 fn coalesced_prefix(&self) -> Option<(usize, PendingUpdate<B::Cursor>)> {
2106 if self.outbox.updates.len() < 2 {
2107 return None;
2108 }
2109 let (first_id, _) = self.outbox.updates.front()?;
2110 let first = self.ledger.pending_updates.get(first_id)?;
2111 let adds_only = !first.adds.is_empty() && first.removes.is_empty();
2112 let removes_only = first.adds.is_empty() && !first.removes.is_empty();
2113 if !adds_only && !removes_only {
2114 return None;
2115 }
2116 let topic_cap = if adds_only {
2122 self.ledger.mutate.add_cap
2123 } else {
2124 self.ledger.mutate.remove_cap
2125 };
2126 let mut topics: HashSet<_> = first
2127 .adds
2128 .iter()
2129 .map(|(topic, _)| topic)
2130 .chain(first.removes.iter())
2131 .collect();
2132 let mut bytes: usize = topics.iter().map(|topic| topic_wire_cost(topic)).sum();
2133 let mut count = 1;
2134 for (id, _) in self.outbox.updates.iter().skip(1) {
2135 let Some(next) = self.ledger.pending_updates.get(id) else {
2136 break;
2137 };
2138 if (adds_only && (next.adds.is_empty() || !next.removes.is_empty()))
2139 || (removes_only && (next.removes.is_empty() || !next.adds.is_empty()))
2140 {
2141 break;
2142 }
2143 let next_topics: Vec<_> = next
2144 .adds
2145 .iter()
2146 .map(|(topic, _)| topic)
2147 .chain(next.removes.iter())
2148 .collect();
2149 let next_bytes: usize = next_topics.iter().map(|topic| topic_wire_cost(topic)).sum();
2150 if topics.len() + next_topics.len() > topic_cap
2151 || bytes + next_bytes > self.ledger.mutate.byte_cap
2152 || next_topics.iter().any(|topic| topics.contains(topic))
2153 {
2154 break;
2155 }
2156 topics.extend(next_topics);
2157 bytes += next_bytes;
2158 count += 1;
2159 }
2160 if count == 1 {
2161 return None;
2162 }
2163 let mut merged = PendingUpdate {
2164 adds: Vec::new(),
2165 removes: Vec::new(),
2166 };
2167 for (id, _) in self.outbox.updates.iter().take(count) {
2168 let update = self.ledger.pending_updates.get(id)?;
2169 merged.adds.extend(update.adds.iter().cloned());
2170 merged.removes.extend(update.removes.iter().cloned());
2171 }
2172 Some((count, merged))
2173 }
2174
2175 fn flush_outbox(&mut self) {
2178 let Some(wire) = self.conn.as_ref() else {
2179 return;
2180 };
2181 while let Some((id, _)) = self.outbox.updates.front() {
2182 if !self.update_budget.take() {
2183 return;
2184 }
2185 let id = *id;
2186 if let Some((count, merged)) = self.coalesced_prefix() {
2187 let update = B::build_mutate(merged.adds.clone(), merged.removes.clone(), id);
2188 if wire.try_mutate(update).is_err() {
2189 self.update_budget.refund();
2190 return;
2191 }
2192 for (old_id, _) in self.outbox.updates.drain(..count) {
2193 self.ledger.pending_updates.remove(&old_id);
2194 }
2195 self.ledger.pending_updates.insert(id, merged);
2196 continue;
2197 }
2198 let Some((id, update)) = self.outbox.updates.pop_front() else {
2199 break;
2200 };
2201 match wire.try_mutate(update) {
2202 Ok(()) => {}
2203 Err(TryMutateError::Full(update)) | Err(TryMutateError::Closed(update)) => {
2204 self.update_budget.refund();
2205 self.outbox.updates.push_front((id, update));
2206 return;
2207 }
2208 }
2209 }
2210 }
2211}
2212
2213enum OpenOutcome<B: BidiBinding> {
2214 Opened(Connection<B>),
2215 Failed(OpenError),
2216 Suspended(oneshot::Sender<()>),
2217 Shutdown,
2218}
2219
2220enum AfterReopen {
2221 Proceed,
2222 Suspended(oneshot::Sender<()>),
2223 Shutdown,
2224}
2225
2226struct Outbox<M> {
2227 updates: std::collections::VecDeque<(u64, M)>,
2228}
2229
2230impl<M> Default for Outbox<M> {
2231 fn default() -> Self {
2232 Self {
2233 updates: std::collections::VecDeque::new(),
2234 }
2235 }
2236}
2237
2238impl<M> Outbox<M> {
2239 fn is_empty(&self) -> bool {
2240 self.updates.is_empty()
2241 }
2242 fn clear(&mut self) {
2243 self.updates.clear();
2244 }
2245
2246 fn purge(&mut self, ids: &HashSet<u64>) -> Vec<u64> {
2247 let mut purged = Vec::new();
2248 self.updates.retain(|(id, _)| {
2249 if ids.contains(id) {
2250 purged.push(*id);
2251 false
2252 } else {
2253 true
2254 }
2255 });
2256 purged
2257 }
2258}
2259
2260fn close_gracefully<B: TransportBinding>(wire: Connection<B>)
2263where
2264 B::GroupMessage: Clone,
2265 B::WelcomeMessage: Clone,
2266{
2267 xmtp_common::spawn(None, async move {
2268 let mut wire = wire;
2269 let drained = xmtp_common::time::timeout(GRACEFUL_CLOSE_BUDGET, async {
2270 if wire.finish().await.is_err() {
2271 return; }
2273 while wire.next().await.is_some() {}
2274 })
2275 .await;
2276 if drained.is_err() {
2277 tracing::debug!(
2278 budget_ms = GRACEFUL_CLOSE_BUDGET.as_millis() as u64,
2279 "bidi transport: graceful-close drain budget expired; dropping the wire"
2280 );
2281 }
2282 });
2283}
2284
2285#[cfg(test)]
2286mod tests;