1use parking_lot::Mutex;
4use std::sync::Arc;
5
6use super::{
7 incoming::{IncomingCoordinator, IncomingLease, IncomingScope, IncomingStatus},
8 local_delivery::{
9 DeliveryCursor, DeliveryScope, LocalDelivery, LocalDeliveryConfig, LocalDeliveryControl,
10 LocalDeliveryError, LocalDeliveryFilter, LocalDeliveryItem,
11 },
12};
13use crate::context::XmtpSharedContext;
14
15pub struct MessageReader<C: XmtpSharedContext> {
17 delivery: LocalDelivery<C>,
18 control: MessageReaderControl,
19 configuration: crate::server_configuration::ServerConfigurationHandle,
20}
21
22#[derive(Clone)]
24pub struct MessageReaderControl {
25 delivery: LocalDeliveryControl,
26 lease: Arc<Mutex<Option<Arc<IncomingLease>>>>,
27 changes: Arc<tokio::sync::Mutex<tokio::sync::watch::Receiver<u64>>>,
28 last_status: Arc<Mutex<IncomingStatus>>,
29 closed: tokio_util::sync::CancellationToken,
30}
31
32pub struct MessageReaderObserver {
34 changes: Option<tokio::sync::watch::Receiver<u64>>,
35 closed: tokio_util::sync::CancellationToken,
36}
37
38impl MessageReaderObserver {
39 pub async fn changed(&mut self) {
40 if let Some(changes) = &mut self.changes {
41 tokio::select! {
42 _ = self.closed.cancelled() => {},
43 _ = changes.changed() => {},
44 }
45 }
46 }
47}
48
49fn incoming_scope(scope: &DeliveryScope) -> IncomingScope {
50 match scope {
51 DeliveryScope::All => IncomingScope::AllGroups,
52 DeliveryScope::Groups(groups) => IncomingScope::Groups(groups.clone()),
53 }
54}
55
56impl<C: XmtpSharedContext + 'static> MessageReader<C> {
57 pub fn new(
59 context: C,
60 scope: DeliveryScope,
61 filter: LocalDeliveryFilter,
62 from: Option<DeliveryCursor>,
63 ) -> Result<Self, LocalDeliveryError> {
64 context
65 .server_configuration()
66 .check()
67 .map_err(|error| LocalDeliveryError::Configuration(Box::new(error)))?;
68 let configuration = context.server_configuration().clone();
69 let config = LocalDeliveryConfig::from(context.incoming_runtime().policy());
70 let coordinator = IncomingCoordinator::for_context(&context);
71 let lease = Arc::new(coordinator.acquire_stream(incoming_scope(&scope)));
72 let delivery = LocalDelivery::new(context, scope, filter, from, config)?;
73 let control = MessageReaderControl {
74 delivery: delivery.control(),
75 last_status: Arc::new(Mutex::new(lease.snapshot())),
76 changes: Arc::new(tokio::sync::Mutex::new(lease.subscribe_changes())),
77 lease: Arc::new(Mutex::new(Some(lease))),
78 closed: tokio_util::sync::CancellationToken::new(),
79 };
80 Ok(Self {
81 delivery,
82 control,
83 configuration,
84 })
85 }
86
87 pub fn control(&self) -> MessageReaderControl {
88 self.control.clone()
89 }
90
91 pub async fn next_delivery(
93 &mut self,
94 ) -> Result<Option<LocalDeliveryItem<C>>, LocalDeliveryError> {
95 let lease = self.control.lease.lock().clone();
96 let Some(lease) = lease else { return Ok(None) };
97 let configuration = self.configuration.clone();
98 let result = {
99 let delivery = self.delivery.next_delivery();
102 futures::pin_mut!(delivery);
103 loop {
104 if let Err(error) = configuration.check() {
105 break Err(LocalDeliveryError::Configuration(Box::new(error)));
106 }
107 if let Err(error) = lease.check_recovery() {
108 break Err(error.error());
109 }
110 tokio::select! {
111 biased;
112 _ = self.control.closed.cancelled() => {
113 break configuration.check()
114 .map(|()| None)
115 .map_err(|error| LocalDeliveryError::Configuration(Box::new(error)));
116 },
117 item = &mut delivery => {
118 if let Err(error) = configuration.check() {
119 break Err(LocalDeliveryError::Configuration(Box::new(error)));
120 }
121 if matches!(&item, Ok(Some(_)))
124 && let Err(error) = lease.check_recovery()
125 {
126 break Err(error.error());
127 }
128 break item;
129 },
130 _ = lease.changed() => {},
131 _ = xmtp_common::time::sleep(super::recovery::RECOVERY_POLL) => {},
132 }
133 }
134 };
135 if !matches!(&result, Ok(Some(_))) {
136 self.close();
139 }
140 result
141 }
142
143 pub fn catch_up_snapshot(&self) -> IncomingStatus {
145 self.control.catch_up_snapshot()
146 }
147
148 pub fn into_stream(
150 self,
151 ) -> impl futures::Stream<Item = super::Result<xmtp_db::group_message::StoredGroupMessage>>
152 {
153 futures::stream::unfold(
154 Some((
155 self,
156 None::<super::local_delivery::DeliveryAcknowledgement<C>>,
157 )),
158 |state| async move {
159 let (mut reader, previous) = state?;
160 if let Some(previous) = previous
161 && let Err(error) = previous.acknowledge()
162 {
163 return Some((Err(error.into()), None));
164 }
165 loop {
166 match reader.next_delivery().await {
167 Ok(Some(item)) => match item.acknowledgement.check_owner() {
168 Ok(()) => {
169 return Some((
170 Ok(item.message),
171 Some((reader, Some(item.acknowledgement))),
172 ));
173 }
174 Err(LocalDeliveryError::SelectionChanged) => continue,
175 Err(error) => return Some((Err(error.into()), None)),
176 },
177 Ok(None) => return None,
178 Err(error) => return Some((Err(error.into()), None)),
179 }
180 }
181 },
182 )
183 }
184
185 pub fn close(&mut self) {
187 self.delivery.close();
188 self.control.close();
189 }
190}
191
192impl MessageReaderControl {
193 #[cfg(any(test, feature = "test-utils"))]
194 pub fn lease_holder_count_for_test(&self) -> usize {
195 Arc::strong_count(&self.lease)
196 }
197
198 #[cfg(test)]
199 pub(crate) fn notify_change_for_test(&self) {
200 if let Some(lease) = self.lease.lock().as_ref() {
201 lease.notify_change_for_test();
202 }
203 }
204
205 pub fn observer(&self) -> MessageReaderObserver {
207 MessageReaderObserver {
208 changes: self
209 .lease
210 .lock()
211 .as_ref()
212 .map(|lease| lease.subscribe_changes()),
213 closed: self.closed.clone(),
214 }
215 }
216
217 pub fn update_scope(&self, scope: DeliveryScope) {
219 self.delivery.update_scope(scope.clone());
220 if let Some(lease) = self.lease.lock().as_ref() {
221 lease.replace_scope(incoming_scope(&scope));
222 }
223 }
224
225 pub fn update_filter(&self, filter: LocalDeliveryFilter) {
227 self.delivery.update_filter(filter);
228 }
229
230 pub fn catch_up_snapshot(&self) -> IncomingStatus {
232 if let Some(lease) = self.lease.lock().as_ref() {
233 let status = lease.snapshot();
234 *self.last_status.lock() = status.clone();
235 status
236 } else {
237 self.last_status.lock().clone()
238 }
239 }
240
241 pub async fn changed(&self) {
243 let mut changes = self.changes.lock().await;
244 tokio::select! {
245 _ = self.closed.cancelled() => {},
246 _ = changes.changed() => {},
247 }
248 }
249
250 pub fn close(&self) {
252 self.delivery.close();
253 self.closed.cancel();
254 if let Some(lease) = self.lease.lock().take() {
255 let mut status = lease.snapshot();
256 status.cancel();
257 *self.last_status.lock() = status;
258 lease.close();
259 }
260 }
261}
262
263impl<C: XmtpSharedContext> Drop for MessageReader<C> {
264 fn drop(&mut self) {
265 self.control.close();
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::{
273 groups::send_message_opts::SendMessageOpts, test::mock::generate_stored_msg, tester,
274 };
275 use xmtp_common::time::{Duration, Instant};
276 use xmtp_db::Store;
277 use xmtp_proto::types::Cursor;
278
279 #[xmtp_common::test(unwrap_try = true)]
281 async fn one_dm_reader_receives_a_later_explicitly_joined_duplicate() {
282 tester!(alix, disable_workers);
283 tester!(bo, disable_workers);
284 let first = alix
285 .find_or_create_dm(bo.inbox_id().to_string(), None)
286 .await?;
287 let mut reader = MessageReader::new(
288 alix.context.clone(),
289 DeliveryScope::Groups(vec![first.group_id]),
290 LocalDeliveryFilter::default(),
291 None,
292 )?;
293 let second = bo
294 .find_or_create_dm(alix.inbox_id().to_string(), None)
295 .await?;
296 assert_ne!(first.group_id, second.group_id);
297 alix.sync_welcomes().await?;
299 let installed: Option<xmtp_db::group::StoredGroup> =
300 xmtp_db::Fetch::fetch(&alix.context.db(), &second.group_id)?;
301 assert!(installed.is_some());
302 let message_id = second
303 .send_message(b"later duplicate", SendMessageOpts::default())
304 .await?;
305
306 let observed_group = xmtp_common::time::timeout(Duration::from_secs(15), async {
307 loop {
308 let item = reader
309 .next_delivery()
310 .await?
311 .expect("the reader must remain open");
312 let matched = item.message.id == message_id;
313 let group_id = item.message.group_id;
314 item.acknowledgement.acknowledge()?;
315 if matched {
316 break Ok::<_, LocalDeliveryError>(group_id);
317 }
318 }
319 })
320 .await??;
321 assert_eq!(observed_group, second.group_id);
322 }
323
324 #[xmtp_common::test(unwrap_try = true)]
326 async fn blocked_connection_ends_a_pending_message_read_with_its_cause() {
327 use crate::{client::ClientError, server_configuration::BlockedConnection};
328 tester!(alix, disable_workers);
329 let mut reader = MessageReader::new(
330 alix.context.clone(),
331 DeliveryScope::All,
332 LocalDeliveryFilter::default(),
333 None,
334 )?;
335 let error = {
336 let pending = reader.next_delivery();
337 futures::pin_mut!(pending);
338 assert!(futures::poll!(&mut pending).is_pending());
339 alix.context.server_configuration().block_connection(
340 BlockedConnection::ClientVersionTooOld {
341 client: "1.0.0".into(),
342 minimum: "9999.0.0".into(),
343 },
344 );
345 alix.context.cancellation_token().cancel();
346 pending
347 .await
348 .err()
349 .expect("blocked connection must fail the read")
350 };
351 assert!(matches!(
352 error,
353 LocalDeliveryError::Configuration(error)
354 if matches!(*error, ClientError::ClientVersionTooOld { ref minimum, .. }
355 if minimum == "9999.0.0")
356 ));
357 assert!(reader.next_delivery().await?.is_none());
358 }
359
360 #[xmtp_common::test(unwrap_try = true)]
362 async fn exhaustion_wins_when_a_pending_local_item_is_ready_too() {
363 tester!(alix, disable_workers);
364 let group = alix.create_group(None, None)?;
365 for sequence in [100, 200] {
366 generate_stored_msg(Cursor(sequence), group.group_id).store(&alix.context.db())?;
367 }
368 let create = || {
369 MessageReader::new(
370 alix.context.clone(),
371 DeliveryScope::Groups(vec![group.group_id]),
372 LocalDeliveryFilter::default(),
373 None,
374 )
375 };
376 let mut reader = create()?;
377 let lease = reader.control.lease.lock().clone().unwrap();
378 let first = reader.next_delivery().await?.unwrap();
379 first.acknowledgement.check_owner()?;
380 let pending = reader.next_delivery();
381 futures::pin_mut!(pending);
382 assert!(futures::poll!(&mut pending).is_pending());
383 lease.fail_recovery_for_test(super::super::recovery::RecoveryFailure::Exhausted {
384 attempts: 10,
385 source: None,
386 });
387 first.acknowledgement.acknowledge()?;
390 assert!(matches!(
391 pending.await,
392 Err(LocalDeliveryError::NetworkRecoveryExhausted { .. })
393 ));
394 let mut replacement = create()?;
395 let second = replacement.next_delivery().await?.unwrap();
396 assert_ne!(second.message.id, first.message.id);
397 }
398
399 #[xmtp_common::test(unwrap_try = true)]
401 async fn twice_exhausted_readers_release_ownership_before_replacement_on_same_client() {
402 tester!(alix, disable_workers);
403 let group = alix.create_group(None, None)?;
404 let create = || {
405 MessageReader::new(
406 alix.context.clone(),
407 DeliveryScope::Groups(vec![group.group_id]),
408 LocalDeliveryFilter::default(),
409 None,
410 )
411 };
412 for sequence in [100, 200] {
413 let message = generate_stored_msg(Cursor(sequence), group.group_id);
414 message.store(&alix.context.db())?;
415 let mut expired = create()?;
416 let lease = expired.control.lease.lock().clone().unwrap();
417 let outage = super::super::recovery::RecoverySnapshot::default();
420 let now = Instant::now();
421 let mut recovery = super::super::recovery::RecoveryBudget::new(&outage, now);
422 let failure = recovery
423 .check(&outage, now + Duration::from_secs(3600))
424 .unwrap_err();
425 assert!(matches!(
426 failure,
427 super::super::recovery::RecoveryFailure::Exhausted { .. }
428 ));
429 lease.fail_recovery_for_test(failure);
430 assert!(matches!(
431 expired.next_delivery().await,
432 Err(LocalDeliveryError::NetworkRecoveryExhausted { .. })
433 ));
434 let mut replacement = create()?;
435 expired.close();
436 let delivered = replacement.next_delivery().await?.unwrap();
437 assert_eq!(delivered.message.id, message.id);
438 delivered.acknowledgement.check_owner()?;
439 delivered.acknowledgement.acknowledge()?;
440 replacement.close();
441 }
442 }
443}