1use diesel::{
4 prelude::*,
5 sql_types::{BigInt, Binary, Integer},
6};
7use xmtp_proto::types::{Cursor, GroupId};
8
9use super::{
10 refresh_state::EntityKind,
11 schema::{
12 group_welcome_discovery as discovery, incoming_envelopes as incoming,
13 refresh_state as progress,
14 },
15 stream_storage::{BudgetScope, StreamStorageError, stream_transaction},
16};
17use crate::{ConnectionExt, StorageError};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum NetworkEntityKind {
22 Welcome,
24 Group,
26 Identity,
28}
29
30impl NetworkEntityKind {
31 pub fn entity_kind(self) -> EntityKind {
32 match self {
33 Self::Welcome => EntityKind::Welcome,
34 Self::Group => EntityKind::ApplicationMessage,
35 Self::Identity => EntityKind::Identity,
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct StreamTopic {
43 pub entity_id: Vec<u8>,
45 pub kind: NetworkEntityKind,
47}
48
49impl StreamTopic {
50 pub fn group(group_id: GroupId) -> Self {
51 Self {
52 entity_id: group_id.as_ref().to_vec(),
53 kind: NetworkEntityKind::Group,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
61pub struct NewIncomingEnvelope {
62 pub sequence_id: Cursor,
64 pub envelope: Vec<u8>,
66}
67
68#[derive(Debug, Clone, Queryable, Selectable)]
70#[diesel(table_name = incoming)]
71pub struct StoredIncomingEnvelope {
72 pub entity_id: Vec<u8>,
73 pub entity_kind: EntityKind,
74 pub sequence_id: i64,
75 pub envelope: Vec<u8>,
77 pub retry_at_ns: i64,
79 pub blocked: bool,
81 pub error_code: Option<String>,
83 pub retry_expires_at_ns: Option<i64>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct PendingEnvelopeState {
90 pub sequence_id: Cursor,
91 pub blocked: bool,
92 pub error_code: Option<String>,
93 pub retry_at_ns: i64,
94}
95
96#[derive(Debug, Clone, Copy)]
98pub struct PendingBudget {
99 pub rows: u64,
101 pub bytes: u64,
103}
104
105#[derive(Debug, Clone, Copy)]
107pub struct IncomingLimits {
108 pub batch: PendingBudget,
110 pub topic: PendingBudget,
112 pub kind: PendingBudget,
114}
115
116#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118pub struct TopicProgress {
119 pub processed: Cursor,
121 pub received: Cursor,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum JoinAnchorMode {
128 Advance,
130 InactiveReadd,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct AdmissionResult {
138 pub received: Cursor,
140 pub inserted: usize,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct TerminalRejection {
147 pub sequence_id: Cursor,
148 pub code: String,
149}
150
151#[derive(Debug, Clone, QueryableByName)]
153pub struct PendingTopicUsage {
154 #[diesel(sql_type = Binary)]
155 pub entity_id: Vec<u8>,
156 #[diesel(sql_type = BigInt)]
157 pub rows: i64,
158 #[diesel(sql_type = BigInt)]
159 pub bytes: i64,
160}
161
162#[derive(Debug, Clone)]
164pub struct IncomingRetry {
165 pub retry_at_ns: i64,
167 pub blocked: bool,
169 pub error_code: Option<String>,
171 pub retry_expires_at_ns: Option<i64>,
173}
174
175pub trait QueryIncomingEnvelope: ConnectionExt + Sized {
177 fn record_welcome_discovery(
180 &self,
181 group_id: GroupId,
182 welcome_cursor: Cursor,
183 ) -> Result<(), StorageError> {
184 let sequence = i64::try_from(welcome_cursor.0)
185 .ok()
186 .filter(|sequence| *sequence > 0)
187 .ok_or(StreamStorageError::InvalidBatch)?;
188 stream_transaction(self, |conn| {
189 diesel::insert_into(discovery::table)
190 .values((
191 discovery::group_id.eq(group_id),
192 discovery::first_welcome_sequence_id.eq(sequence),
193 ))
194 .on_conflict(discovery::group_id)
195 .do_nothing()
196 .execute(conn)?;
197 Ok(())
198 })
199 }
200
201 fn group_ids_discovered_through(&self, target: Cursor) -> Result<Vec<GroupId>, StorageError> {
204 let target = i64::try_from(target.0).map_err(|_| StreamStorageError::InvalidBatch)?;
205 self.raw_query(|conn| {
206 discovery::table
207 .filter(discovery::first_welcome_sequence_id.le(target))
208 .order((
209 discovery::first_welcome_sequence_id.asc(),
210 discovery::group_id.asc(),
211 ))
212 .select(discovery::group_id)
213 .load(conn)
214 })
215 .map_err(Into::into)
216 }
217
218 fn record_terminal_rejection(
222 &self,
223 topic: &StreamTopic,
224 sequence: Cursor,
225 code: &'static str,
226 ) -> Result<(), StorageError> {
227 let sequence_id = i64::try_from(sequence.0)
228 .ok()
229 .filter(|id| *id > 0)
230 .ok_or(StreamStorageError::InvalidBatch)?;
231 if code.is_empty() {
232 return Err(StreamStorageError::InvalidBatch.into());
233 }
234 stream_transaction(self, |conn| {
235 if sequence > load_progress(conn, topic)?.received {
236 return Err(StreamStorageError::InvalidBatch.into());
237 }
238 let pending = incoming::table
239 .find((&topic.entity_id, topic.kind.entity_kind(), sequence_id))
240 .select(incoming::sequence_id)
241 .first::<i64>(conn)
242 .optional()?
243 .is_some();
244 if !pending {
245 return Err(StreamStorageError::HeadChanged.into());
246 }
247 if topic.kind != NetworkEntityKind::Welcome
248 && first_pending(conn, topic)?.is_none_or(|head| head.sequence_id != sequence_id)
249 {
250 return Err(StreamStorageError::HeadChanged.into());
251 }
252 diesel::update(progress::table.find((&topic.entity_id, topic.kind.entity_kind())))
253 .set((
254 progress::last_rejection_sequence_id.eq(sequence_id),
255 progress::last_rejection_code.eq(code),
256 ))
257 .execute(conn)?;
258 Ok(())
259 })
260 }
261
262 fn read_last_rejection(
264 &self,
265 topic: &StreamTopic,
266 ) -> Result<Option<TerminalRejection>, StorageError> {
267 let row = self.raw_query(|conn| {
268 progress::table
269 .find((&topic.entity_id, topic.kind.entity_kind()))
270 .select((
271 progress::last_rejection_sequence_id,
272 progress::last_rejection_code,
273 ))
274 .first::<(Option<i64>, Option<String>)>(conn)
275 .optional()
276 })?;
277 match row {
278 None | Some((None, None)) => Ok(None),
279 Some((Some(sequence), Some(code))) => Ok(Some(TerminalRejection {
280 sequence_id: Cursor(sequence as u64),
281 code,
282 })),
283 _ => Err(StorageError::DbDeserialize),
284 }
285 }
286
287 fn admit_ordered_batch(
291 &self,
292 topic: &StreamTopic,
293 after: Cursor,
294 envelopes: &[NewIncomingEnvelope],
295 limits: IncomingLimits,
296 ) -> Result<AdmissionResult, StorageError> {
297 let mut previous = after.0;
298 let mut bytes = 0u64;
299 for envelope in envelopes {
300 if envelope.sequence_id.0 <= previous
301 || envelope.envelope.is_empty()
302 || envelope.sequence_id.0 > i64::MAX as u64
303 {
304 return Err(StreamStorageError::InvalidBatch.into());
305 }
306 previous = envelope.sequence_id.0;
307 bytes = bytes
308 .checked_add(envelope.envelope.len() as u64)
309 .ok_or(StreamStorageError::InvalidBatch)?;
310 }
311 check_budget(
312 envelopes.len() as u64,
313 bytes,
314 limits.batch,
315 BudgetScope::Batch,
316 )?;
317 stream_transaction(self, |conn| {
318 initialize_progress(conn, topic)?;
319 let state = load_progress(conn, topic)?;
320 if after.0 > state.received.0 {
321 return Err(StreamStorageError::MissingPrefix {
322 after: after.0,
323 received: state.received.0,
324 }
325 .into());
326 }
327 let new: Vec<_> = envelopes
328 .iter()
329 .filter(|envelope| envelope.sequence_id.0 > state.received.0)
330 .collect();
331 if new.is_empty() {
332 return Ok(AdmissionResult {
333 received: state.received,
334 inserted: 0,
335 });
336 }
337 let new_bytes = new
338 .iter()
339 .map(|envelope| envelope.envelope.len() as u64)
340 .sum::<u64>();
341 let usages = pending_usage(conn, topic.kind)?;
342 let own = usages
343 .iter()
344 .find(|usage| usage.entity_id == topic.entity_id);
345 let topic_rows = own.map_or(0, |usage| usage.rows as u64);
346 let topic_bytes = own.map_or(0, |usage| usage.bytes as u64);
347 check_budget(
348 topic_rows + new.len() as u64,
349 topic_bytes + new_bytes,
350 limits.topic,
351 BudgetScope::Topic,
352 )?;
353 let kind_rows: u64 = usages.iter().map(|usage| usage.rows as u64).sum();
354 let kind_bytes: u64 = usages.iter().map(|usage| usage.bytes as u64).sum();
355 check_budget(
356 kind_rows + new.len() as u64,
357 kind_bytes + new_bytes,
358 limits.kind,
359 BudgetScope::Kind,
360 )?;
361 for envelope in &new {
362 diesel::insert_into(incoming::table)
363 .values((
364 incoming::entity_id.eq(&topic.entity_id),
365 incoming::entity_kind.eq(topic.kind.entity_kind()),
366 incoming::sequence_id.eq(envelope.sequence_id.0 as i64),
367 incoming::envelope.eq(&envelope.envelope),
368 ))
369 .execute(conn)?;
370 }
371 let received = new
372 .last()
373 .ok_or(StreamStorageError::InvalidBatch)?
374 .sequence_id;
375 diesel::update(progress::table.find((&topic.entity_id, topic.kind.entity_kind())))
376 .set(progress::received_sequence_id.eq(received.0 as i64))
377 .execute(conn)?;
378 Ok(AdmissionResult {
379 received,
380 inserted: new.len(),
381 })
382 })
383 }
384
385 fn topic_progress(&self, topic: &StreamTopic) -> Result<TopicProgress, StorageError> {
387 self.raw_query(|conn| Ok(load_progress(conn, topic)))?
388 }
389
390 fn first_pending_envelope(
392 &self,
393 topic: &StreamTopic,
394 ) -> Result<Option<StoredIncomingEnvelope>, StorageError> {
395 self.raw_query(|conn| first_pending(conn, topic))
396 .map_err(Into::into)
397 }
398
399 fn pending_envelope(
401 &self,
402 topic: &StreamTopic,
403 sequence: Cursor,
404 ) -> Result<Option<StoredIncomingEnvelope>, StorageError> {
405 let sequence = i64::try_from(sequence.0).map_err(|_| StreamStorageError::InvalidBatch)?;
406 self.raw_query(|conn| {
407 incoming::table
408 .find((&topic.entity_id, topic.kind.entity_kind(), sequence))
409 .first::<StoredIncomingEnvelope>(conn)
410 .optional()
411 })
412 .map_err(Into::into)
413 }
414
415 fn pending_states_through(
418 &self,
419 topic: &StreamTopic,
420 target: Cursor,
421 ) -> Result<Vec<PendingEnvelopeState>, StorageError> {
422 let target = i64::try_from(target.0).map_err(|_| StreamStorageError::InvalidBatch)?;
423 let rows = self.raw_query(|conn| {
424 incoming::table
425 .filter(incoming::entity_id.eq(&topic.entity_id))
426 .filter(incoming::entity_kind.eq(topic.kind.entity_kind()))
427 .filter(incoming::sequence_id.le(target))
428 .order(incoming::sequence_id.asc())
429 .select((
430 incoming::sequence_id,
431 incoming::blocked,
432 incoming::error_code,
433 incoming::retry_at_ns,
434 ))
435 .load::<(i64, bool, Option<String>, i64)>(conn)
436 })?;
437 rows.into_iter()
438 .map(|(sequence, blocked, error_code, retry_at_ns)| {
439 Ok(PendingEnvelopeState {
440 sequence_id: Cursor(
441 u64::try_from(sequence).map_err(|_| StorageError::DbDeserialize)?,
442 ),
443 blocked,
444 error_code,
445 retry_at_ns,
446 })
447 })
448 .collect()
449 }
450
451 fn complete_pending_envelope(
455 &self,
456 topic: &StreamTopic,
457 sequence: Cursor,
458 ) -> Result<bool, StorageError> {
459 let sequence = i64::try_from(sequence.0).map_err(|_| StreamStorageError::InvalidBatch)?;
460 stream_transaction(self, |conn| {
461 let Some(head) = first_pending(conn, topic)? else {
462 return Ok(false);
463 };
464 if topic.kind != NetworkEntityKind::Welcome && head.sequence_id != sequence {
465 return Err(StreamStorageError::HeadChanged.into());
466 }
467 let deleted = diesel::delete(incoming::table.find((
468 &topic.entity_id,
469 topic.kind.entity_kind(),
470 sequence,
471 )))
472 .execute(conn)?;
473 if deleted == 0 {
474 return Ok(false);
475 }
476 let handled = if topic.kind == NetworkEntityKind::Welcome {
477 match first_pending(conn, topic)? {
478 Some(pending) => pending.sequence_id - 1,
479 None => load_progress(conn, topic)?.received.0 as i64,
480 }
481 } else {
482 sequence
483 };
484 diesel::update(progress::table.find((&topic.entity_id, topic.kind.entity_kind())))
485 .set(progress::sequence_id.eq(handled))
486 .execute(conn)?;
487 Ok(true)
488 })
489 }
490
491 fn set_incoming_retry(
493 &self,
494 topic: &StreamTopic,
495 sequence: Cursor,
496 retry: &IncomingRetry,
497 ) -> Result<bool, StorageError> {
498 let sequence = i64::try_from(sequence.0).map_err(|_| StreamStorageError::InvalidBatch)?;
499 stream_transaction(self, |conn| {
500 if topic.kind != NetworkEntityKind::Welcome
501 && first_pending(conn, topic)?.is_none_or(|head| head.sequence_id != sequence)
502 {
503 return Ok(false);
504 }
505 let target =
506 incoming::table.find((&topic.entity_id, topic.kind.entity_kind(), sequence));
507 let deadline = target
508 .select(incoming::retry_expires_at_ns)
509 .first::<Option<i64>>(conn)
510 .optional()?;
511 let Some(deadline) = deadline else {
512 return Ok(false);
513 };
514 Ok(diesel::update(target)
515 .set((
516 incoming::retry_at_ns.eq(retry.retry_at_ns),
517 incoming::blocked.eq(retry.blocked),
518 incoming::error_code.eq(&retry.error_code),
519 incoming::retry_expires_at_ns.eq(deadline.or(retry.retry_expires_at_ns)),
520 ))
521 .execute(conn)?
522 > 0)
523 })
524 }
525
526 fn ready_welcomes(
528 &self,
529 now_ns: i64,
530 limit: u32,
531 ) -> Result<Vec<StoredIncomingEnvelope>, StorageError> {
532 self.ready_welcomes_bounded(now_ns, limit, u64::MAX)
533 }
534
535 fn ready_welcomes_bounded(
538 &self,
539 now_ns: i64,
540 limit: u32,
541 max_bytes: u64,
542 ) -> Result<Vec<StoredIncomingEnvelope>, StorageError> {
543 self.raw_query(|conn| {
544 Ok(conn.transaction::<_, StorageError, _>(|conn| {
545 let due = || {
546 incoming::table
547 .filter(incoming::entity_kind.eq(EntityKind::Welcome))
548 .filter(incoming::blocked.eq(false))
549 .filter(incoming::retry_at_ns.le(now_ns))
550 .order((incoming::sequence_id.asc(), incoming::entity_id.asc()))
551 };
552 let sizes = due()
553 .select(diesel::dsl::sql::<BigInt>("length(envelope)"))
554 .limit(i64::from(limit))
555 .load::<i64>(conn)?;
556 let mut selected_rows = 0_i64;
557 let mut selected_bytes = 0_u64;
558 for bytes in sizes {
559 let bytes = u64::try_from(bytes).map_err(|_| StorageError::DbDeserialize)?;
560 if bytes > max_bytes.saturating_sub(selected_bytes) {
561 if selected_rows == 0 {
562 return Err(StreamStorageError::Capacity {
563 scope: BudgetScope::Batch,
564 }
565 .into());
566 }
567 break;
568 }
569 selected_rows += 1;
570 selected_bytes += bytes;
571 }
572 Ok(due()
573 .limit(selected_rows)
574 .select(StoredIncomingEnvelope::as_select())
575 .load(conn)?)
576 }))
577 })?
578 }
579
580 fn blocked_welcomes_bounded(
582 &self,
583 topic: &StreamTopic,
584 after: Cursor,
585 limit: u32,
586 max_bytes: u64,
587 ) -> Result<Vec<StoredIncomingEnvelope>, StorageError> {
588 if topic.kind != NetworkEntityKind::Welcome {
589 return Err(StreamStorageError::InvalidBatch.into());
590 }
591 let after = i64::try_from(after.0).map_err(|_| StreamStorageError::InvalidBatch)?;
592 self.raw_query(|conn| {
593 Ok(conn.transaction::<_, StorageError, _>(|conn| {
594 let pending = || {
595 incoming::table
596 .filter(incoming::entity_id.eq(&topic.entity_id))
597 .filter(incoming::entity_kind.eq(EntityKind::Welcome))
598 .filter(incoming::blocked.eq(true))
599 .filter(incoming::sequence_id.gt(after))
600 .order(incoming::sequence_id.asc())
601 };
602 let sizes = pending()
603 .select(diesel::dsl::sql::<BigInt>("length(envelope)"))
604 .limit(i64::from(limit))
605 .load::<i64>(conn)?;
606 let mut rows = 0_i64;
607 let mut bytes_used = 0_u64;
608 for bytes in sizes {
609 let bytes = u64::try_from(bytes).map_err(|_| StorageError::DbDeserialize)?;
610 if bytes > max_bytes.saturating_sub(bytes_used) {
611 if rows == 0 {
612 return Err(StreamStorageError::Capacity {
613 scope: BudgetScope::Batch,
614 }
615 .into());
616 }
617 break;
618 }
619 rows += 1;
620 bytes_used += bytes;
621 }
622 Ok(pending().limit(rows).load(conn)?)
623 }))
624 })?
625 }
626
627 fn welcome_barrier_complete(
629 &self,
630 topic: &StreamTopic,
631 target: Cursor,
632 ) -> Result<bool, StorageError> {
633 if topic.kind != NetworkEntityKind::Welcome {
634 return Err(StreamStorageError::InvalidBatch.into());
635 }
636 let target_id = i64::try_from(target.0).map_err(|_| StreamStorageError::InvalidBatch)?;
637 self.raw_query(|conn| {
638 Ok(conn.transaction::<_, StorageError, _>(|conn| {
639 if load_progress(conn, topic)?.received < target {
640 return Ok(false);
641 }
642 let count = incoming::table
643 .filter(incoming::entity_id.eq(&topic.entity_id))
644 .filter(incoming::entity_kind.eq(EntityKind::Welcome))
645 .filter(incoming::sequence_id.le(target_id))
646 .select(diesel::dsl::count_star())
647 .first::<i64>(conn)?;
648 Ok(count == 0)
649 }))
650 })?
651 }
652
653 fn has_pending_welcomes(&self) -> Result<bool, StorageError> {
655 self.raw_query(|conn| {
656 incoming::table
657 .filter(incoming::entity_kind.eq(EntityKind::Welcome))
658 .select(diesel::dsl::count_star())
659 .first::<i64>(conn)
660 .map(|count| count > 0)
661 })
662 .map_err(Into::into)
663 }
664
665 fn install_group_anchor(
668 &self,
669 group_id: GroupId,
670 anchor: Cursor,
671 mode: JoinAnchorMode,
672 ) -> Result<(), StorageError> {
673 let anchor = i64::try_from(anchor.0).map_err(|_| StreamStorageError::InvalidBatch)?;
674 let topic = StreamTopic::group(group_id);
675 stream_transaction(self, |conn| {
676 let state = progress::table
677 .find((&topic.entity_id, EntityKind::ApplicationMessage))
678 .select((progress::sequence_id, progress::received_sequence_id))
679 .first::<(i64, Option<i64>)>(conn)
680 .optional()?;
681 match state {
682 None => {
683 if mode == JoinAnchorMode::InactiveReadd {
684 return Err(StreamStorageError::StaleJoinAnchor.into());
685 }
686 diesel::insert_into(progress::table)
687 .values((
688 progress::entity_id.eq(&topic.entity_id),
689 progress::entity_kind.eq(EntityKind::ApplicationMessage),
690 progress::sequence_id.eq(anchor),
691 progress::received_sequence_id.eq(anchor),
692 ))
693 .execute(conn)?;
694 }
695 Some((processed, received)) => {
696 let valid = match mode {
697 JoinAnchorMode::Advance => anchor > processed,
698 JoinAnchorMode::InactiveReadd => anchor == processed,
699 };
700 if !valid {
701 return Err(StreamStorageError::StaleJoinAnchor.into());
702 }
703 let changed = diesel::update(
704 progress::table
705 .find((&topic.entity_id, EntityKind::ApplicationMessage))
706 .filter(progress::sequence_id.eq(processed)),
707 )
708 .set((
709 progress::sequence_id.eq(anchor),
710 progress::received_sequence_id.eq(received.unwrap_or(0).max(anchor)),
711 ))
712 .execute(conn)?;
713 if changed == 0 {
714 return Err(StreamStorageError::StaleJoinAnchor.into());
715 }
716 }
717 }
718 diesel::delete(
719 incoming::table
720 .filter(incoming::entity_id.eq(&topic.entity_id))
721 .filter(incoming::entity_kind.eq(EntityKind::ApplicationMessage))
722 .filter(incoming::sequence_id.le(anchor)),
723 )
724 .execute(conn)?;
725 Ok(())
726 })
727 }
728
729 fn pending_topic_usage(
731 &self,
732 kind: NetworkEntityKind,
733 ) -> Result<Vec<PendingTopicUsage>, StorageError> {
734 self.raw_query(|conn| pending_usage(conn, kind))
735 .map_err(Into::into)
736 }
737}
738
739impl<C: ConnectionExt> QueryIncomingEnvelope for C {}
740
741fn check_budget(
742 rows: u64,
743 bytes: u64,
744 limit: PendingBudget,
745 scope: BudgetScope,
746) -> Result<(), StorageError> {
747 if rows > limit.rows || bytes > limit.bytes {
748 return Err(StreamStorageError::Capacity { scope }.into());
749 }
750 Ok(())
751}
752
753fn initialize_progress(
754 conn: &mut diesel::SqliteConnection,
755 topic: &StreamTopic,
756) -> Result<(), StorageError> {
757 diesel::insert_or_ignore_into(progress::table)
758 .values((
759 progress::entity_id.eq(&topic.entity_id),
760 progress::entity_kind.eq(topic.kind.entity_kind()),
761 progress::sequence_id.eq(0i64),
762 progress::received_sequence_id.eq(0i64),
763 ))
764 .execute(conn)?;
765 diesel::update(
767 progress::table
768 .find((&topic.entity_id, topic.kind.entity_kind()))
769 .filter(progress::sequence_id.eq(0))
770 .filter(progress::received_sequence_id.is_null()),
771 )
772 .set(progress::received_sequence_id.eq(0i64))
773 .execute(conn)?;
774 Ok(())
775}
776
777fn load_progress(
778 conn: &mut diesel::SqliteConnection,
779 topic: &StreamTopic,
780) -> Result<TopicProgress, StorageError> {
781 let row = progress::table
782 .find((&topic.entity_id, topic.kind.entity_kind()))
783 .select((progress::sequence_id, progress::received_sequence_id))
784 .first::<(i64, Option<i64>)>(conn)
785 .optional()?;
786 match row {
787 None | Some((0, None)) => Ok(TopicProgress::default()),
788 Some((processed, Some(received))) => Ok(TopicProgress {
789 processed: Cursor(processed as u64),
790 received: Cursor(received as u64),
791 }),
792 Some(_) => Err(StreamStorageError::UninitializedNetworkProgress.into()),
793 }
794}
795
796fn first_pending(
797 conn: &mut diesel::SqliteConnection,
798 topic: &StreamTopic,
799) -> QueryResult<Option<StoredIncomingEnvelope>> {
800 incoming::table
801 .filter(incoming::entity_id.eq(&topic.entity_id))
802 .filter(incoming::entity_kind.eq(topic.kind.entity_kind()))
803 .order(incoming::sequence_id.asc())
804 .first(conn)
805 .optional()
806}
807
808fn pending_usage(
809 conn: &mut diesel::SqliteConnection,
810 kind: NetworkEntityKind,
811) -> QueryResult<Vec<PendingTopicUsage>> {
812 diesel::sql_query("SELECT entity_id, COUNT(*) AS rows, SUM(length(envelope)) AS bytes FROM incoming_envelopes WHERE entity_kind = ? GROUP BY entity_id ORDER BY rows DESC, entity_id ASC")
813 .bind::<Integer, _>(kind.entity_kind() as i32).load(conn)
814}
815
816#[cfg(test)]
817mod tests;