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.server_configuration().check()?;
65 let configuration = context.server_configuration().clone();
66 let config = LocalDeliveryConfig::from(context.stream_policy());
67 let coordinator = IncomingCoordinator::for_context(&context);
68 let lease = Arc::new(coordinator.acquire_stream(incoming_scope(&scope)));
69 let delivery = LocalDelivery::new(context, scope, filter, from, config)?;
70 let control = MessageReaderControl {
71 delivery: delivery.control(),
72 last_status: Arc::new(Mutex::new(lease.snapshot())),
73 changes: Arc::new(tokio::sync::Mutex::new(lease.subscribe_changes())),
74 lease: Arc::new(Mutex::new(Some(lease))),
75 closed: tokio_util::sync::CancellationToken::new(),
76 };
77 Ok(Self {
78 delivery,
79 control,
80 configuration,
81 })
82 }
83
84 pub fn control(&self) -> MessageReaderControl {
85 self.control.clone()
86 }
87
88 pub async fn next_delivery(
90 &mut self,
91 ) -> Result<Option<LocalDeliveryItem<C>>, LocalDeliveryError> {
92 let lease = self.control.lease.lock().clone();
93 let Some(lease) = lease else { return Ok(None) };
94 let configuration = self.configuration.clone();
95 let result = {
96 let delivery = self.delivery.next_delivery();
99 futures::pin_mut!(delivery);
100 loop {
101 if let Err(error) = configuration.check() {
102 break Err(error.into());
103 }
104 if let Err(error) = lease.check_recovery() {
105 break Err(LocalDeliveryError::from(error));
106 }
107 tokio::select! {
108 biased;
109 _ = self.control.closed.cancelled() => {
110 break configuration.check()
111 .map(|()| None)
112 .map_err(LocalDeliveryError::from);
113 },
114 item = &mut delivery => {
115 if let Err(error) = configuration.check() {
116 break Err(error.into());
117 }
118 if matches!(&item, Ok(Some(_)))
121 && let Err(error) = lease.check_recovery()
122 {
123 break Err(LocalDeliveryError::from(error));
124 }
125 break item;
126 },
127 _ = lease.changed() => {},
128 _ = xmtp_common::time::sleep(super::incoming::stream_api::RECOVERY_POLL) => {},
129 }
130 }
131 };
132 if !matches!(&result, Ok(Some(_))) {
133 self.close();
136 }
137 result
138 }
139
140 pub fn catch_up_snapshot(&self) -> IncomingStatus {
142 self.control.catch_up_snapshot()
143 }
144
145 pub fn into_stream(
147 self,
148 ) -> impl futures::Stream<Item = super::Result<xmtp_db::group_message::StoredGroupMessage>>
149 {
150 futures::stream::unfold(
151 Some((
152 self,
153 None::<super::local_delivery::DeliveryAcknowledgement<C>>,
154 )),
155 |state| async move {
156 let (mut reader, previous) = state?;
157 if let Some(previous) = previous
158 && let Err(error) = previous.acknowledge()
159 {
160 return Some((Err(error.into()), None));
161 }
162 loop {
163 match reader.next_delivery().await {
164 Ok(Some(item)) => match item.acknowledgement.check_owner() {
165 Ok(()) => {
166 return Some((
167 Ok(item.message),
168 Some((reader, Some(item.acknowledgement))),
169 ));
170 }
171 Err(LocalDeliveryError::SelectionChanged) => continue,
172 Err(error) => return Some((Err(error.into()), None)),
173 },
174 Ok(None) => return None,
175 Err(error) => return Some((Err(error.into()), None)),
176 }
177 }
178 },
179 )
180 }
181
182 pub fn close(&mut self) {
184 self.delivery.close();
185 self.control.close();
186 }
187}
188
189impl MessageReaderControl {
190 #[cfg(any(test, feature = "test-utils"))]
191 pub fn lease_holder_count_for_test(&self) -> usize {
192 Arc::strong_count(&self.lease)
193 }
194
195 #[cfg(test)]
196 pub(crate) fn notify_change_for_test(&self) {
197 if let Some(lease) = self.lease.lock().as_ref() {
198 lease.notify_change_for_test();
199 }
200 }
201
202 pub fn observer(&self) -> MessageReaderObserver {
204 MessageReaderObserver {
205 changes: self
206 .lease
207 .lock()
208 .as_ref()
209 .map(|lease| lease.subscribe_changes()),
210 closed: self.closed.clone(),
211 }
212 }
213
214 pub fn update_scope(&self, scope: DeliveryScope) {
216 self.delivery.update_scope(scope.clone());
217 if let Some(lease) = self.lease.lock().as_ref() {
218 lease.replace_scope(incoming_scope(&scope));
219 }
220 }
221
222 pub fn update_filter(&self, filter: LocalDeliveryFilter) {
224 self.delivery.update_filter(filter);
225 }
226
227 pub fn catch_up_snapshot(&self) -> IncomingStatus {
229 if let Some(lease) = self.lease.lock().as_ref() {
230 let status = lease.snapshot();
231 *self.last_status.lock() = status.clone();
232 status
233 } else {
234 self.last_status.lock().clone()
235 }
236 }
237
238 pub async fn changed(&self) {
240 let mut changes = self.changes.lock().await;
241 tokio::select! {
242 _ = self.closed.cancelled() => {},
243 _ = changes.changed() => {},
244 }
245 }
246
247 pub fn close(&self) {
249 self.delivery.close();
250 self.closed.cancel();
251 if let Some(lease) = self.lease.lock().take() {
252 let mut status = lease.snapshot();
253 status.cancel();
254 *self.last_status.lock() = status;
255 lease.close();
256 }
257 }
258}
259
260impl<C: XmtpSharedContext> Drop for MessageReader<C> {
261 fn drop(&mut self) {
262 self.control.close();
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 #[cfg(not(target_arch = "wasm32"))]
269 mod preflight;
270
271 use super::*;
272 use crate::prelude::*;
273 use crate::{
274 groups::send_message_opts::SendMessageOpts, test::mock::generate_stored_msg, tester,
275 };
276 use xmtp_common::time::Duration;
277 use xmtp_db::Store;
278 use xmtp_proto::types::Cursor;
279
280 #[xmtp_common::test(unwrap_try = true)]
282 async fn one_dm_reader_receives_a_later_explicitly_joined_duplicate() {
283 tester!(alix, disable_workers);
284 tester!(bo, disable_workers);
285 let first = alix
286 .find_or_create_dm(bo.inbox_id().to_string(), None)
287 .await?;
288 let mut reader = MessageReader::new(
289 alix.context.clone(),
290 DeliveryScope::Groups(vec![first.group_id]),
291 LocalDeliveryFilter::default(),
292 None,
293 )?;
294 let second = bo
295 .find_or_create_dm(alix.inbox_id().to_string(), None)
296 .await?;
297 assert_ne!(first.group_id, second.group_id);
298 alix.sync_welcomes().await?;
300 let installed: Option<xmtp_db::group::StoredGroup> =
301 xmtp_db::Fetch::fetch(&alix.context.db(), &second.group_id)?;
302 assert!(installed.is_some());
303 let message_id = second
304 .send_message(b"later duplicate", SendMessageOpts::default())
305 .await?;
306
307 let observed_group = xmtp_common::time::timeout(Duration::from_secs(15), async {
308 loop {
309 let item = reader
310 .next_delivery()
311 .await?
312 .expect("the reader must remain open");
313 let matched = item.message.id == message_id;
314 let group_id = item.message.group_id;
315 item.acknowledgement.acknowledge()?;
316 if matched {
317 break Ok::<_, LocalDeliveryError>(group_id);
318 }
319 }
320 })
321 .await??;
322 assert_eq!(observed_group, second.group_id);
323 }
324
325 #[xmtp_common::test(unwrap_try = true)]
327 async fn blocked_connection_ends_a_pending_message_read_with_its_cause() {
328 use crate::server_configuration::BlockedConnection;
329 tester!(alix, disable_workers);
330 let mut reader = MessageReader::new(
331 alix.context.clone(),
332 DeliveryScope::All,
333 LocalDeliveryFilter::default(),
334 None,
335 )?;
336 let error = {
337 let pending = reader.next_delivery();
338 futures::pin_mut!(pending);
339 assert!(futures::poll!(&mut pending).is_pending());
340 alix.context.server_configuration().block_connection(
341 BlockedConnection::ClientVersionTooOld {
342 client: "1.0.0".into(),
343 minimum: "9999.0.0".into(),
344 },
345 );
346 alix.context.cancellation_token().cancel();
347 pending
348 .await
349 .err()
350 .expect("blocked connection must fail the read")
351 };
352 assert!(matches!(
353 error,
354 LocalDeliveryError::Blocked(BlockedConnection::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.exhaust_recovery_for_test(10, Duration::ZERO);
384 first.acknowledgement.acknowledge()?;
387 assert!(matches!(
388 pending.await,
389 Err(LocalDeliveryError::NetworkRecoveryExhausted { .. })
390 ));
391 let mut replacement = create()?;
392 let second = replacement.next_delivery().await?.unwrap();
393 assert_ne!(second.message.id, first.message.id);
394 }
395
396 #[xmtp_common::test(unwrap_try = true)]
398 async fn twice_exhausted_readers_release_ownership_before_replacement_on_same_client() {
399 tester!(alix, disable_workers);
400 let group = alix.create_group(None, None)?;
401 let create = || {
402 MessageReader::new(
403 alix.context.clone(),
404 DeliveryScope::Groups(vec![group.group_id]),
405 LocalDeliveryFilter::default(),
406 None,
407 )
408 };
409 for sequence in [100, 200] {
410 let message = generate_stored_msg(Cursor(sequence), group.group_id);
411 message.store(&alix.context.db())?;
412 let mut expired = create()?;
413 let lease = expired.control.lease.lock().clone().unwrap();
414 assert!(lease.exhaust_recovery_for_test(0, Duration::from_secs(3600)));
417 assert!(matches!(
418 expired.next_delivery().await,
419 Err(LocalDeliveryError::NetworkRecoveryExhausted { .. })
420 ));
421 let mut replacement = create()?;
422 expired.close();
423 let delivered = replacement.next_delivery().await?.unwrap();
424 assert_eq!(delivered.message.id, message.id);
425 delivered.acknowledgement.check_owner()?;
426 delivered.acknowledgement.acknowledge()?;
427 replacement.close();
428 }
429 }
430}