1mod acknowledgement;
4mod types;
5
6pub use acknowledgement::DeliveryAcknowledgement;
7pub(crate) use types::LocalDeliveryConfig;
8pub use types::{LocalDeliveryError, LocalDeliveryFilter};
9pub use xmtp_db::delivery::{DeliveryCursor, DeliveryScope, DeliverySnapshot};
10
11use acknowledgement::{AcknowledgementState, DeliverySession, PendingAcknowledgement};
12use futures::{Stream, StreamExt};
13use parking_lot::Mutex;
14use std::{borrow::Cow, collections::VecDeque, sync::Arc};
15use tokio::sync::Notify;
16use xmtp_common::{
17 StreamHandle,
18 time::{now_ns, sleep},
19};
20use xmtp_db::{
21 Fetch, StorageError,
22 consent_record::{ConsentState, ConsentType},
23 delivery::{DeliveryMessage, QueryDelivery},
24 group::StoredGroup,
25 group_message::StoredGroupMessage,
26 prelude::*,
27};
28use xmtp_proto::types::GroupId;
29
30use super::{DEFAULT_STREAM_CONSENT_STATES, SubscribeError, internal::InternalEvent};
31use crate::context::XmtpSharedContext;
32use xmtp_events::{EventFilter, Subscription};
33
34type Result<T> = std::result::Result<T, LocalDeliveryError>;
35
36pub struct LocalDeliveryItem<Context: XmtpSharedContext> {
38 pub message: StoredGroupMessage,
40 pub cursor: DeliveryCursor,
42 pub acknowledgement: DeliveryAcknowledgement<Context>,
44}
45
46#[derive(Clone)]
47struct Selection {
48 scope: DeliveryScope,
49 filter: LocalDeliveryFilter,
50 revision: u64,
51}
52
53#[derive(Clone)]
55pub struct LocalDeliveryControl {
56 selection: Arc<Mutex<Selection>>,
57 changed: Arc<Notify>,
58 closer: Arc<dyn CloseDelivery>,
59}
60
61trait CloseDelivery: xmtp_common::MaybeSend + xmtp_common::MaybeSync {
62 fn close_delivery(&self);
63}
64
65impl<C: XmtpSharedContext> CloseDelivery for DeliverySession<C> {
66 fn close_delivery(&self) {
67 self.close();
68 }
69}
70
71impl LocalDeliveryControl {
72 pub fn close(&self) {
73 self.closer.close_delivery();
74 }
75
76 pub fn update_scope(&self, scope: DeliveryScope) {
78 let mut selection = self.selection.lock();
79 selection.scope = scope;
80 selection.revision = selection.revision.wrapping_add(1);
81 self.changed.notify_one();
82 }
83
84 pub fn update_filter(&self, filter: LocalDeliveryFilter) {
86 let mut selection = self.selection.lock();
87 selection.filter = filter;
88 selection.revision = selection.revision.wrapping_add(1);
89 self.changed.notify_one();
90 }
91
92 pub fn scope(&self) -> DeliveryScope {
93 self.selection.lock().scope.clone()
94 }
95}
96
97pub struct LocalDelivery<Context: XmtpSharedContext> {
99 session: Arc<DeliverySession<Context>>,
100 config: LocalDeliveryConfig,
101 control: LocalDeliveryControl,
102 events: Subscription<InternalEvent>,
103 candidates: VecDeque<DeliveryMessage>,
104 candidate_revision: u64,
105 pending: Option<Arc<PendingAcknowledgement>>,
107 replay_position: Option<DeliveryCursor>,
109 renewal: Option<Box<dyn StreamHandle<StreamOutput = ()>>>,
110}
111
112impl<Context> LocalDelivery<Context>
113where
114 Context: XmtpSharedContext + 'static,
115{
116 pub(crate) fn new(
118 context: Context,
119 scope: DeliveryScope,
120 filter: LocalDeliveryFilter,
121 from: Option<DeliveryCursor>,
122 config: LocalDeliveryConfig,
123 ) -> Result<Self> {
124 config.validate()?;
125 if context.is_closed() {
126 return Err(LocalDeliveryError::Closed);
127 }
128 let events = context.events().subscribe(
130 EventFilter::default().with_internal(|event| {
131 matches!(event, InternalEvent::MessageStored { is_sync: false, .. })
132 }),
133 Some(10),
134 );
135 let now = now_ns();
136 let database_id = context.db().stream_database_id()?;
137 let owner = if let Some(cursor) = from {
138 context
139 .db()
140 .replay_delivery_messages(cursor, &scope, now, 0)?;
141 None
142 } else {
143 let mut registered = context.delivery_owner().lock();
144 if context.is_closed() {
145 return Err(LocalDeliveryError::Closed);
146 }
147 let mut retired = context.incoming_runtime().retired_delivery_owner.lock();
148 if let Some(owner) = *retired {
149 context.db().release_delivery_owner(owner)?;
151 if *registered == Some(owner) {
152 *registered = None;
153 }
154 *retired = None;
155 }
156 let owner = context
157 .db()
158 .acquire_delivery_owner_with_clock(config.lease_duration_ns()?, now_ns)?;
159 *registered = Some(owner);
160 Some(owner)
161 };
162 let session = Arc::new(DeliverySession::new(context, owner, database_id));
163 let renewal = if owner.is_some() {
164 let session = Arc::clone(&session);
165 Some(Box::new(xmtp_common::spawn(None, async move {
166 loop {
167 tokio::select! {
168 _ = session.cancel.cancelled() => { session.close(); break; },
169 _ = sleep(config.renew_interval) => {}
170 }
171 let result = config
172 .lease_duration_ns()
173 .and_then(|lease_ns| session.renew(lease_ns).map_err(Into::into));
174 if let Err(error) = result {
175 session.fail(error);
176 break;
177 }
178 }
179 })) as Box<dyn StreamHandle<StreamOutput = ()>>)
180 } else {
181 None
182 };
183 Ok(Self {
184 session: Arc::clone(&session),
185 config,
186 control: LocalDeliveryControl {
187 selection: Arc::new(Mutex::new(Selection {
188 scope,
189 filter,
190 revision: 0,
191 })),
192 changed: Arc::new(Notify::new()),
193 closer: session,
194 },
195 events,
196 candidates: VecDeque::new(),
197 candidate_revision: 0,
198 pending: None,
199 replay_position: from,
200 renewal,
201 })
202 }
203
204 pub fn control(&self) -> LocalDeliveryControl {
205 self.control.clone()
206 }
207
208 pub fn close(&mut self) {
210 self.session.close();
211 }
212
213 pub async fn next_delivery(&mut self) -> Result<Option<LocalDeliveryItem<Context>>> {
217 let result = self.next_inner().await;
218 if result.is_err() {
219 self.session.close();
220 }
221 result
222 }
223
224 async fn next_inner(&mut self) -> Result<Option<LocalDeliveryItem<Context>>> {
226 if let Some(pending) = self.pending.clone() {
227 loop {
228 let notified = pending.changed.notified();
229 {
230 let mut state = pending.state.lock();
231 if let Some(error) = self.session.background_error() {
233 return Err(error);
234 }
235 if matches!(*state, AcknowledgementState::Waiting)
236 && self.control.selection.lock().revision != pending.revision
237 {
238 *state = AcknowledgementState::Reselect;
239 }
240 match *state {
241 AcknowledgementState::Acknowledged => {
242 if self.replay_position.is_some() {
243 self.replay_position = Some(pending.cursor);
244 }
245 self.pending = None;
246 break;
247 }
248 AcknowledgementState::Reselect => {
249 self.pending = None;
250 self.candidates.clear();
251 break;
252 }
253 AcknowledgementState::Rejected => {
254 return Err(LocalDeliveryError::AcknowledgementRejected);
255 }
256 AcknowledgementState::Failed => {
257 return Err(LocalDeliveryError::AcknowledgementFailed);
258 }
259 AcknowledgementState::Waiting | AcknowledgementState::Dispatched => {}
260 }
261 }
262 tokio::select! {
263 _ = notified => {},
264 _ = self.control.changed.notified() => {},
265 _ = self.session.cancel.cancelled() => return self.session.end_result(),
266 }
267 }
268 }
269 loop {
270 if self.session.is_closed() {
271 return self.session.end_result();
272 }
273 let selection = self.control.selection.lock().clone();
274 if selection.revision != self.candidate_revision {
275 self.candidates.clear();
276 self.candidate_revision = selection.revision;
277 }
278 if self.candidates.is_empty() {
279 let now = now_ns();
280 self.candidates = {
281 let db = self.session.context.db();
282 if let Some(position) = self.replay_position {
283 db.replay_delivery_messages_bounded(
284 position,
285 &selection.scope,
286 now,
287 self.config.batch_size,
288 self.config.max_bytes,
289 )?
290 } else {
291 db.default_delivery_messages_bounded(
292 self.session.owner().ok_or(LocalDeliveryError::Closed)?,
293 &selection.scope,
294 now,
295 self.config.batch_size,
296 self.config.max_bytes,
297 )?
298 }
299 .into()
300 };
301 if self.candidates.is_empty() {
302 tokio::select! {
303 _ = self.session.cancel.cancelled() => return self.session.end_result(),
304 _ = self.control.changed.notified() => {},
305 _ = self.events.next() => {},
306 _ = sleep(self.config.poll_interval) => {},
307 }
308 continue;
309 }
310 }
311 let mut scanned = 0;
312 while let Some(candidate) = self.candidates.pop_front() {
313 let selection = self.control.selection.lock().clone();
315 if selection.revision != self.candidate_revision {
316 self.candidates.clear();
317 self.candidate_revision = selection.revision;
318 break;
319 }
320 self.session.check_owner()?;
321 let retained = self.session.context.db().delivery_message_is_retained(
322 &candidate.message.id,
323 candidate.cursor,
324 now_ns(),
325 )?;
326 if !retained
327 || !matches_filter(
328 &self.session.context,
329 candidate.message.group_id,
330 &selection.scope,
331 &selection.filter,
332 )?
333 {
334 if !self.skip_candidate(&candidate, selection.revision)? {
335 self.candidates.clear();
336 break;
337 }
338 scanned += 1;
339 if scanned >= self.config.batch_size {
340 break;
341 }
342 continue;
343 }
344 self.session.check_owner()?;
346 let pending = Arc::new(PendingAcknowledgement::new(
347 candidate.cursor,
348 selection.revision,
349 ));
350 let acknowledgement = DeliveryAcknowledgement::new(
351 Arc::clone(&self.session),
352 Arc::clone(&pending),
353 candidate.message.group_id,
354 self.control.clone(),
355 candidate.message.id.clone(),
356 );
357 self.pending = Some(pending);
358 return Ok(Some(LocalDeliveryItem {
359 message: candidate.message,
360 cursor: candidate.cursor,
361 acknowledgement,
362 }));
363 }
364 tokio::task::yield_now().await;
365 }
366 }
367
368 fn skip_candidate(&mut self, candidate: &DeliveryMessage, revision: u64) -> Result<bool> {
371 let selection = self.control.selection.lock();
372 if selection.revision != revision {
373 return Ok(false);
374 }
375 self.session.check_owner()?;
376 if let Some(owner) = self.session.owner() {
377 self.session.context.db().acknowledge_delivery_with_clock(
378 owner,
379 candidate.message.group_id,
380 candidate.cursor,
381 now_ns,
382 )?;
383 } else {
384 self.replay_position = Some(candidate.cursor);
385 }
386 Ok(true)
387 }
388
389 pub fn into_stream(self) -> impl Stream<Item = super::Result<StoredGroupMessage>> {
392 self.into_cursor_stream()
393 .map(|item| item.map(|item| item.message).map_err(SubscribeError::from))
394 }
395
396 pub fn into_cursor_stream(self) -> impl Stream<Item = Result<DeliveryMessage>> {
399 futures::stream::unfold(
400 Some((self, None::<DeliveryAcknowledgement<Context>>)),
401 |state| async move {
402 let (mut reader, previous) = state?;
403 if let Some(previous) = previous
404 && let Err(error) = previous.acknowledge()
405 {
406 return Some((Err(error), None));
407 }
408 loop {
409 match reader.next_delivery().await {
410 Ok(Some(LocalDeliveryItem {
411 message,
412 cursor,
413 acknowledgement,
414 })) => match acknowledgement.check_owner() {
415 Ok(()) => {
416 return Some((
417 Ok(DeliveryMessage { message, cursor }),
418 Some((reader, Some(acknowledgement))),
419 ));
420 }
421 Err(LocalDeliveryError::SelectionChanged) => continue,
422 Err(error) => return Some((Err(error), None)),
423 },
424 Ok(None) => return None,
425 Err(error) => return Some((Err(error), None)),
426 }
427 }
428 },
429 )
430 }
431
432 pub fn enriched_history_snapshot(
434 context: &Context,
435 scope: &DeliveryScope,
436 filter: &LocalDeliveryFilter,
437 limit: u32,
438 ) -> Result<EnrichedDeliverySnapshot> {
439 let settings = context.incoming_runtime().policy();
440 let filter = effective_filter(scope, filter);
441 Ok(context
442 .db()
443 .delivery_history_snapshot_projected_with_clock(
444 scope,
445 &filter,
446 now_ns,
447 limit.min(settings.max_local_read_rows),
448 settings.max_local_read_bytes,
449 |conn, snapshot| {
450 let storage = conn.key_store();
451 enrich_delivery_snapshot(storage.db(), snapshot)
452 },
453 )?)
454 }
455
456 pub fn history_snapshot(
458 context: &Context,
459 scope: &DeliveryScope,
460 filter: &LocalDeliveryFilter,
461 limit: u32,
462 ) -> Result<DeliverySnapshot> {
463 let settings = context.incoming_runtime().policy();
464 let filter = effective_filter(scope, filter);
465 Ok(context
466 .db()
467 .delivery_history_snapshot_projected_with_clock(
468 scope,
469 &filter,
470 now_ns,
471 limit.min(settings.max_local_read_rows),
472 settings.max_local_read_bytes,
473 |_, snapshot| Ok(snapshot),
474 )?)
475 }
476}
477
478impl<Context: XmtpSharedContext> Drop for LocalDelivery<Context> {
479 fn drop(&mut self) {
480 self.session.close();
481 if let Some(task) = &self.renewal {
482 task.end();
483 }
484 }
485}
486
487fn effective_consent_states<'a>(
489 scope: &DeliveryScope,
490 filter: &'a LocalDeliveryFilter,
491) -> Option<&'a [ConsentState]> {
492 match (&filter.consent_states, scope) {
493 (Some(states), _) => Some(states),
494 (None, DeliveryScope::All) => Some(&DEFAULT_STREAM_CONSENT_STATES),
495 (None, _) => None,
496 }
497}
498
499fn effective_filter<'a>(
500 scope: &DeliveryScope,
501 filter: &'a LocalDeliveryFilter,
502) -> Cow<'a, LocalDeliveryFilter> {
503 match effective_consent_states(scope, filter) {
504 Some(states) if filter.consent_states.is_none() => Cow::Owned(LocalDeliveryFilter {
505 conversation_type: filter.conversation_type,
506 consent_states: Some(states.to_vec()),
507 }),
508 _ => Cow::Borrowed(filter),
509 }
510}
511
512fn matches_filter<Context: XmtpSharedContext>(
513 context: &Context,
514 group_id: GroupId,
515 scope: &DeliveryScope,
516 filter: &LocalDeliveryFilter,
517) -> Result<bool> {
518 let db = context.db();
519 if let Some(kind) = filter.conversation_type {
520 let group: Option<StoredGroup> = db.fetch(&group_id)?;
521 if group.is_none_or(|group| group.conversation_type != kind) {
522 return Ok(false);
523 }
524 }
525 if let Some(states) = effective_consent_states(scope, filter) {
527 let consent = db
528 .get_consent_record(hex::encode(group_id), ConsentType::ConversationId)
529 .map_err(StorageError::from)?
530 .map_or(ConsentState::Unknown, |record| record.state);
531 if !states.contains(&consent) {
532 return Ok(false);
533 }
534 }
535 Ok(true)
536}
537
538#[cfg(test)]
539mod tests;
540
541pub struct EnrichedDeliverySnapshot {
543 pub messages: Vec<crate::messages::enrichment::EnrichedStoredMessage>,
544 pub cursor: DeliveryCursor,
545}
546
547fn enrich_delivery_snapshot(
548 db: impl xmtp_db::DbQuery,
549 snapshot: DeliverySnapshot,
550) -> std::result::Result<EnrichedDeliverySnapshot, StorageError> {
551 use crate::messages::enrichment::{EnrichMessageError, enrich_messages_with_stored};
552 let mut groups = std::collections::HashMap::new();
553 let mut projected: Vec<_> = (0..snapshot.messages.len()).map(|_| None).collect();
554 for (index, item) in snapshot.messages.into_iter().enumerate() {
555 groups
556 .entry(item.message.group_id)
557 .or_insert_with(Vec::new)
558 .push(((index, item.cursor), item.message));
559 }
560 for (group_id, items) in groups {
561 let (positions, rows): (Vec<_>, Vec<_>) = items.into_iter().unzip();
562 let enriched =
563 enrich_messages_with_stored(&db, &group_id, rows).map_err(|error| match error {
564 EnrichMessageError::Storage(error) => error,
565 EnrichMessageError::DbConnection(error) => StorageError::from(error),
566 })?;
567 if enriched.len() != positions.len() {
568 return Err(StorageError::DbDeserialize);
569 }
570 for ((index, cursor), mut row) in positions.into_iter().zip(enriched) {
571 row.delivery_cursor = Some(cursor);
572 projected[index] = Some(row);
573 }
574 }
575 Ok(EnrichedDeliverySnapshot {
576 messages: projected
577 .into_iter()
578 .map(|row| row.ok_or(StorageError::DbDeserialize))
579 .collect::<std::result::Result<_, _>>()?,
580 cursor: snapshot.cursor,
581 })
582}