1use std::collections::{HashMap, VecDeque};
71use std::sync::{Arc, Condvar, Mutex, Weak};
72use std::time::Duration;
73
74use bytes::Bytes;
75use didbot_data::{dag_cbor, Cid, Value};
76use didbot_pds::journal::Mark;
77use didbot_pds::wal::DEFAULT_SYNC_INTERVAL;
78use didbot_pds::{RepoAccount, RepoCommit, RepoEventSink, RepoIdentity, RepoSync, Sequence, Wal};
79use tokio::sync::watch;
80use tracing::{debug, info, warn};
81
82pub const DEFAULT_REPLAY_FRAMES: usize = 4096;
90
91pub const DEFAULT_REPLAY_BYTES: usize = 64 * 1024 * 1024;
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct ReplayBounds {
111 pub frames: usize,
113 pub bytes: usize,
115}
116
117impl Default for ReplayBounds {
118 fn default() -> Self {
119 Self {
120 frames: DEFAULT_REPLAY_FRAMES,
121 bytes: DEFAULT_REPLAY_BYTES,
122 }
123 }
124}
125
126impl ReplayBounds {
127 #[must_use]
129 pub fn frames(frames: usize) -> Self {
130 Self {
131 frames,
132 ..Self::default()
133 }
134 }
135}
136
137const OP_MESSAGE: i64 = 1;
139
140const OP_ERROR: i64 = -1;
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum FrameKind {
149 Commit,
151 Identity,
153 Account,
155 Sync,
157}
158
159impl FrameKind {
160 #[must_use]
162 pub fn tag(&self) -> &'static str {
163 match self {
164 Self::Commit => "#commit",
165 Self::Identity => "#identity",
166 Self::Account => "#account",
167 Self::Sync => "#sync",
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct Frame {
185 pub seq: u64,
187 pub kind: FrameKind,
189 pub bytes: Bytes,
191}
192
193#[must_use]
198pub fn encode_message(tag: &str, body: &Value) -> Vec<u8> {
199 let mut bytes = dag_cbor::encode(&map([
200 ("op", Value::Integer(OP_MESSAGE)),
201 ("t", Value::String(tag.to_owned())),
202 ]));
203 bytes.extend(dag_cbor::encode(body));
204 bytes
205}
206
207#[must_use]
209pub fn encode_error(error: &str, message: &str) -> Vec<u8> {
210 let mut bytes = dag_cbor::encode(&map([("op", Value::Integer(OP_ERROR))]));
211 bytes.extend(dag_cbor::encode(&map([
212 ("error", Value::String(error.to_owned())),
213 ("message", Value::String(message.to_owned())),
214 ])));
215 bytes
216}
217
218fn clamp(seq: u64) -> i64 {
225 seq.min(i64::MAX as u64) as i64
226}
227
228fn map<const N: usize>(pairs: [(&str, Value); N]) -> Value {
230 Value::Map(
231 pairs
232 .into_iter()
233 .map(|(key, value)| (key.to_owned(), value))
234 .collect(),
235 )
236}
237
238fn link(cid: Option<&str>) -> Value {
245 match cid.map(Cid::parse) {
246 Some(Ok(cid)) => Value::Link(cid),
247 Some(Err(error)) => {
248 warn!(%error, "a commit named a CID that will not parse; sending null");
249 Value::Null
250 }
251 None => Value::Null,
252 }
253}
254
255fn commit_body(seq: u64, event: &RepoCommit, time: &str) -> Value {
261 let ops = event
262 .ops
263 .iter()
264 .map(|op| {
265 let mut fields = map([
266 ("action", Value::String(op.action().to_owned())),
267 ("cid", link(op.cid.as_deref())),
268 ("path", Value::String(op.path.clone())),
269 ]);
270 if let (Some(prev), Value::Map(fields)) = (&op.prev, &mut fields) {
276 fields.insert("prev".to_owned(), link(Some(prev)));
277 }
278 fields
279 })
280 .collect();
281 let mut body = map([
282 ("blobs", Value::List(Vec::new())),
283 ("blocks", Value::Bytes(event.blocks.clone())),
284 ("commit", link(Some(&event.commit))),
285 ("ops", Value::List(ops)),
286 ("rebase", Value::Bool(false)),
287 ("repo", Value::String(event.did.clone())),
288 ("rev", Value::String(event.rev.clone())),
289 ("seq", Value::Integer(clamp(seq))),
290 (
291 "since",
292 match &event.since {
293 Some(rev) => Value::String(rev.clone()),
294 None => Value::Null,
295 },
296 ),
297 ("time", Value::String(time.to_owned())),
298 ("tooBig", Value::Bool(false)),
299 ]);
300 if let Some(prev) = &event.prev_data {
303 if let Value::Map(fields) = &mut body {
304 fields.insert("prevData".to_owned(), link(Some(prev)));
305 }
306 }
307 body
308}
309
310fn identity_body(seq: u64, event: &RepoIdentity, time: &str) -> Value {
312 let mut body = map([
313 ("did", Value::String(event.did.clone())),
314 ("seq", Value::Integer(clamp(seq))),
315 ("time", Value::String(time.to_owned())),
316 ]);
317 if let Some(handle) = &event.handle {
320 if let Value::Map(fields) = &mut body {
321 fields.insert("handle".to_owned(), Value::String(handle.clone()));
322 }
323 }
324 body
325}
326
327fn account_body(seq: u64, event: &RepoAccount, time: &str) -> Value {
329 let mut body = map([
330 ("active", Value::Bool(event.active)),
331 ("did", Value::String(event.did.clone())),
332 ("seq", Value::Integer(clamp(seq))),
333 ("time", Value::String(time.to_owned())),
334 ]);
335 if let Some(status) = event.status {
339 if let Value::Map(fields) = &mut body {
340 fields.insert(
341 "status".to_owned(),
342 Value::String(status.as_str().to_owned()),
343 );
344 }
345 }
346 body
347}
348
349fn sync_body(seq: u64, event: &RepoSync, time: &str) -> Value {
351 map([
352 ("blocks", Value::Bytes(event.blocks.clone())),
353 ("did", Value::String(event.did.clone())),
354 ("rev", Value::String(event.rev.clone())),
355 ("seq", Value::Integer(clamp(seq))),
356 ("time", Value::String(time.to_owned())),
357 ])
358}
359
360fn info_body(name: &str, message: &str) -> Value {
362 map([
363 ("message", Value::String(message.to_owned())),
364 ("name", Value::String(name.to_owned())),
365 ])
366}
367
368pub const OUTDATED_CURSOR: &str = "OutdatedCursor";
370
371pub const FUTURE_CURSOR: &str = "FutureCursor";
373
374#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
383pub struct Exclusions {
384 pub commit: bool,
386 pub identity: bool,
388 pub account: bool,
390}
391
392impl Exclusions {
393 #[must_use]
398 pub fn wants(&self, kind: FrameKind) -> bool {
399 match kind {
400 FrameKind::Commit => !self.commit,
401 FrameKind::Identity => !self.identity,
402 FrameKind::Account => !self.account,
403 FrameKind::Sync => true,
404 }
405 }
406
407 #[must_use]
409 pub fn any(&self) -> bool {
410 self.commit || self.identity || self.account
411 }
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416enum Resume {
417 Live,
419 After(u64),
421 Future(u64),
423}
424
425struct Start {
428 oldest: u64,
430 outdated: bool,
433 after: u64,
435 stream: u64,
437}
438
439#[derive(Debug, Clone)]
446pub struct Repos {
447 inner: Arc<Inner>,
448}
449
450#[derive(Debug)]
451struct Inner {
452 sequence: Arc<Sequence>,
453 bounds: ReplayBounds,
454 ring: Mutex<Ring>,
461 published: watch::Sender<u64>,
464 held: Option<Arc<Held>>,
467 closing: watch::Sender<bool>,
470}
471
472#[derive(Debug)]
475struct Held {
476 log: Arc<Wal>,
477 commits: Mutex<VecDeque<(Mark, RepoCommit)>>,
481 arrived: Condvar,
483}
484
485impl Held {
486 fn over(log: Arc<Wal>) -> Self {
487 Self {
488 log,
489 commits: Mutex::new(VecDeque::new()),
490 arrived: Condvar::new(),
491 }
492 }
493
494 fn lock(&self) -> std::sync::MutexGuard<'_, VecDeque<(Mark, RepoCommit)>> {
495 self.commits
496 .lock()
497 .unwrap_or_else(|poisoned| poisoned.into_inner())
498 }
499
500 fn hold(&self, event: &RepoCommit) {
503 let mut commits = self.lock();
504 commits.push_back((self.log.mark(), event.clone()));
505 self.arrived.notify_one();
506 }
507
508 fn oldest(&self, timeout: Duration) -> Option<Mark> {
511 let commits = self.lock();
512 let (commits, _) = self
513 .arrived
514 .wait_timeout_while(commits, timeout, |commits| commits.is_empty())
515 .unwrap_or_else(|poisoned| poisoned.into_inner());
516 commits.front().map(|(mark, _)| *mark)
517 }
518
519 fn take_through(&self, synced: Mark) -> Option<RepoCommit> {
522 let mut commits = self.lock();
523 if commits.front().is_some_and(|(mark, _)| *mark <= synced) {
524 return commits.pop_front().map(|(_, event)| event);
525 }
526 None
527 }
528}
529
530fn publish_as_synced(inner: &Arc<Inner>, held: Arc<Held>) -> std::io::Result<()> {
537 let producer = Arc::downgrade(inner);
538 std::thread::Builder::new()
539 .name("didbot-firehose-held".to_owned())
540 .spawn(move || loop {
541 let oldest = held.oldest(DEFAULT_SYNC_INTERVAL);
542 if producer.strong_count() == 0 {
543 return;
544 }
545 let Some(oldest) = oldest else {
546 continue;
547 };
548 let synced = held.log.wait_synced(oldest, DEFAULT_SYNC_INTERVAL);
549 let Some(inner) = producer.upgrade() else {
550 return;
551 };
552 if synced {
553 Repos { inner }.release_synced();
554 }
555 })
556 .map(drop)
557}
558
559#[derive(Debug)]
561struct Ring {
562 frames: VecDeque<Frame>,
563 bytes: usize,
565 bound: Option<ReplayBounds>,
568 slack: ReplayBounds,
570 evicted: u64,
573 streams: HashMap<u64, u64>,
576 next_stream: u64,
578}
579
580impl Ring {
581 fn push(&mut self, frame: Frame) {
586 if let Some(bound) = self.bound {
587 let incoming = frame.bytes.len();
588 let within = |ring: &Self, frames: usize, bytes: usize| {
589 ring.frames.len() < frames && ring.bytes.saturating_add(incoming) <= bytes
590 };
591 let hard_frames = bound.frames.saturating_add(self.slack.frames);
592 let hard_bytes = bound.bytes.saturating_add(self.slack.bytes);
593 while let Some(oldest) = self.frames.front() {
594 if within(self, bound.frames, bound.bytes) {
595 break;
596 }
597 let unsent = self.pinned().is_some_and(|after| oldest.seq > after);
598 if unsent && within(self, hard_frames, hard_bytes) {
599 break;
600 }
601 self.bytes -= oldest.bytes.len();
602 self.evicted = oldest.seq;
603 self.frames.pop_front();
604 }
605 }
606 self.bytes += frame.bytes.len();
607 self.frames.push_back(frame);
608 }
609
610 fn pinned(&self) -> Option<u64> {
614 self.streams
615 .values()
616 .copied()
617 .filter(|after| *after >= self.evicted)
618 .min()
619 }
620}
621
622impl Inner {
623 fn ring(&self) -> std::sync::MutexGuard<'_, Ring> {
630 self.ring
631 .lock()
632 .unwrap_or_else(|poisoned| poisoned.into_inner())
633 }
634}
635
636impl Repos {
637 #[must_use]
640 pub fn new(sequence: Arc<Sequence>, bounds: ReplayBounds) -> Self {
641 Self::build(sequence, bounds, false, None)
642 }
643
644 pub fn booting(
654 sequence: Arc<Sequence>,
655 bounds: ReplayBounds,
656 log: Option<Arc<Wal>>,
657 ) -> std::io::Result<Self> {
658 let held = log.map(|log| Arc::new(Held::over(log)));
659 let repos = Self::build(sequence, bounds, true, held.clone());
660 if let Some(held) = held {
661 publish_as_synced(&repos.inner, held)?;
662 }
663 Ok(repos)
664 }
665
666 fn build(
667 sequence: Arc<Sequence>,
668 bounds: ReplayBounds,
669 booting: bool,
670 held: Option<Arc<Held>>,
671 ) -> Self {
672 let bounds = ReplayBounds {
673 frames: bounds.frames.max(1),
674 ..bounds
675 };
676 Self {
677 inner: Arc::new(Inner {
678 sequence,
679 bounds,
680 ring: Mutex::new(Ring {
681 frames: VecDeque::new(),
682 bytes: 0,
683 bound: (!booting).then_some(bounds),
684 slack: bounds,
685 evicted: 0,
686 streams: HashMap::new(),
687 next_stream: 0,
688 }),
689 published: watch::Sender::new(0),
690 held,
691 closing: watch::Sender::new(false),
692 }),
693 }
694 }
695
696 pub fn listening(&self) {
705 let mut ring = self.ring();
706 if ring.bound.is_none() {
707 let held = ring.frames.len();
708 let held_bytes = ring.bytes;
709 let bounds = self.inner.bounds;
710 ring.bound = Some(ReplayBounds {
711 frames: held.saturating_add(bounds.frames),
712 bytes: held_bytes.saturating_add(bounds.bytes),
713 });
714 info!(
715 held,
716 held_bytes,
717 frames = bounds.frames,
718 bytes = bounds.bytes,
719 "the replay buffer holds every frame the boot numbered"
720 );
721 }
722 }
723
724 #[must_use]
727 pub fn newest(&self) -> u64 {
728 self.inner.sequence.latest()
729 }
730
731 #[must_use]
740 pub fn subscribers(&self) -> usize {
741 self.inner.published.receiver_count()
742 }
743
744 #[must_use]
746 pub fn bounds(&self) -> ReplayBounds {
747 self.inner.bounds
748 }
749
750 fn ring(&self) -> std::sync::MutexGuard<'_, Ring> {
751 self.inner.ring()
752 }
753
754 #[cfg(test)]
759 fn oldest(&self) -> u64 {
760 self.ring()
761 .frames
762 .front()
763 .map(|frame| frame.seq)
764 .unwrap_or(0)
765 }
766
767 #[cfg(test)]
772 fn since(&self, seq: u64) -> Vec<Frame> {
773 self.ring()
774 .frames
775 .iter()
776 .filter(|frame| frame.seq > seq)
777 .cloned()
778 .collect()
779 }
780
781 #[cfg(test)]
783 fn all(&self) -> Vec<Frame> {
784 self.ring().frames.iter().cloned().collect()
785 }
786
787 fn start_for(&self, resume: Resume, newest: u64) -> Start {
800 let mut ring = self.ring();
801 let oldest = ring.frames.front().map(|frame| frame.seq).unwrap_or(0);
802 let (outdated, after) = match resume {
803 Resume::Live => (false, ring.frames.back().map_or(0, |frame| frame.seq)),
804 Resume::After(seq) if oldest == 0 => (seq < newest, seq),
809 Resume::After(seq) if seq + 1 >= oldest => (false, seq),
812 Resume::After(_) => (true, oldest - 1),
814 Resume::Future(_) => unreachable!("refused before the buffer is read"),
815 };
816 let stream = ring.next_stream;
817 ring.next_stream += 1;
818 ring.streams.insert(stream, after);
819 Start {
820 oldest,
821 outdated,
822 after,
823 stream,
824 }
825 }
826
827 pub fn publish_commit(&self, event: &RepoCommit) -> Option<Frame> {
834 let time = now_rfc3339();
835 self.publish(FrameKind::Commit, |seq| commit_body(seq, event, &time))
836 }
837
838 pub fn publish_identity(&self, event: &RepoIdentity) -> Option<Frame> {
840 let time = now_rfc3339();
841 self.publish(FrameKind::Identity, |seq| identity_body(seq, event, &time))
842 }
843
844 pub fn publish_account(&self, event: &RepoAccount) -> Option<Frame> {
846 let time = now_rfc3339();
847 self.publish(FrameKind::Account, |seq| account_body(seq, event, &time))
848 }
849
850 pub fn publish_sync(&self, event: &RepoSync) -> Option<Frame> {
852 let time = now_rfc3339();
853 self.publish(FrameKind::Sync, |seq| sync_body(seq, event, &time))
854 }
855
856 fn publish(&self, kind: FrameKind, body: impl FnOnce(u64) -> Value) -> Option<Frame> {
865 let mut ring = self.ring();
866 self.publish_locked(&mut ring, kind, body)
867 }
868
869 fn publish_locked(
871 &self,
872 ring: &mut Ring,
873 kind: FrameKind,
874 body: impl FnOnce(u64) -> Value,
875 ) -> Option<Frame> {
876 let Some(seq) = self.inner.sequence.next() else {
878 warn!(
879 ?kind,
880 "an event was not put on subscribeRepos: it could not be numbered"
881 );
882 return None;
883 };
884 let frame = Frame {
885 seq,
886 kind,
887 bytes: Bytes::from(encode_message(kind.tag(), &body(seq))),
888 };
889 ring.push(frame.clone());
890
891 self.inner.published.send_replace(seq);
895 debug!(
896 seq,
897 ?kind,
898 bytes = frame.bytes.len(),
899 held = ring.frames.len(),
900 held_bytes = ring.bytes,
901 subscribers = self.inner.published.receiver_count(),
902 "firehose frame published"
903 );
904 Some(frame)
905 }
906
907 pub fn release(&self) {
913 self.sync_held_log();
914 self.release_synced();
915 }
916
917 fn release_synced(&self) {
919 let mut ring = self.ring();
920 self.release_locked(&mut ring);
921 }
922
923 fn release_locked(&self, ring: &mut Ring) {
924 let Some(held) = &self.inner.held else {
925 return;
926 };
927 let synced = held.log.synced();
928 while let Some(event) = held.take_through(synced) {
929 let time = now_rfc3339();
930 self.publish_locked(ring, FrameKind::Commit, |seq| {
931 commit_body(seq, &event, &time)
932 });
933 }
934 }
935
936 fn sync_held_log(&self) {
938 if let Some(held) = &self.inner.held {
939 if let Err(error) = held.log.sync() {
940 warn!(%error, "the log did not sync; the commits held for it stay held");
941 }
942 }
943 }
944
945 fn publish_after_held(
948 &self,
949 kind: FrameKind,
950 body: impl FnOnce(u64) -> Value,
951 ) -> Option<Frame> {
952 self.sync_held_log();
953 let mut ring = self.ring();
954 self.release_locked(&mut ring);
955 self.publish_locked(&mut ring, kind, body)
956 }
957
958 pub fn close(&self) {
966 self.inner.closing.send_replace(true);
967 }
968
969 pub async fn detached(&self, timeout: Duration) -> usize {
972 let deadline = tokio::time::Instant::now() + timeout;
973 while self.subscribers() > 0 && tokio::time::Instant::now() < deadline {
974 tokio::time::sleep(Duration::from_millis(10)).await;
975 }
976 self.subscribers()
977 }
978
979 pub fn subscribe(
986 &self,
987 cursor: Option<i64>,
988 exclude: Exclusions,
989 ) -> Result<ReposStream, Vec<u8>> {
990 let newest = self.newest();
991 let resume = match cursor {
992 None => Resume::Live,
993 Some(seq) if seq < 0 => Resume::After(0),
997 Some(seq) if (seq as u64) > newest => Resume::Future(seq as u64),
998 Some(seq) => Resume::After(seq as u64),
999 };
1000
1001 if let Resume::Future(seq) = resume {
1002 warn!(
1003 cursor = seq,
1004 newest,
1005 "a consumer asked to resume from a frame this server has not sent; if this data \
1006 directory was restored from a snapshot, the restore skipped creating {}",
1007 didbot_pds::RESTORED_FILE
1008 );
1009 return Err(encode_error(
1010 FUTURE_CURSOR,
1011 &format!("cursor {seq} is ahead of this stream; the newest frame sent is {newest}"),
1012 ));
1013 }
1014
1015 let Start {
1016 oldest,
1017 outdated,
1018 after,
1019 stream,
1020 } = self.start_for(resume, newest);
1021
1022 if outdated {
1023 warn!(
1024 ?cursor,
1025 oldest, newest, "a consumer asked for a cursor this server no longer holds"
1026 );
1027 }
1028 let info = outdated.then(|| {
1029 Bytes::from(encode_message(
1030 "#info",
1031 &info_body(
1032 OUTDATED_CURSOR,
1033 &format!(
1034 "this cursor is outside the replay buffer, which holds {oldest} to \
1035 {newest}. Everything before it must be read from the repositories"
1036 ),
1037 ),
1038 ))
1039 });
1040 info!(
1041 ?cursor,
1042 outdated,
1043 excluding = exclude.any(),
1044 after,
1045 "subscribeRepos consumer connected"
1046 );
1047
1048 Ok(ReposStream {
1049 info,
1050 producer: Arc::downgrade(&self.inner),
1051 stream,
1052 after,
1053 published: self.inner.published.subscribe(),
1054 exclude,
1055 lagged: 0,
1056 closing: self.inner.closing.subscribe(),
1057 })
1058 }
1059}
1060
1061impl RepoEventSink for Repos {
1064 fn commit(&self, event: &RepoCommit) {
1065 match &self.inner.held {
1066 Some(held) => held.hold(event),
1067 None => {
1068 self.publish_commit(event);
1069 }
1070 }
1071 }
1072
1073 fn identity(&self, event: &RepoIdentity) {
1074 let time = now_rfc3339();
1075 self.publish_after_held(FrameKind::Identity, |seq| identity_body(seq, event, &time));
1076 }
1077
1078 fn account(&self, event: &RepoAccount) -> bool {
1079 let time = now_rfc3339();
1080 self.publish_after_held(FrameKind::Account, |seq| account_body(seq, event, &time))
1081 .is_some()
1082 }
1083
1084 fn sync(&self, event: &RepoSync) -> bool {
1085 let time = now_rfc3339();
1086 self.publish_after_held(FrameKind::Sync, |seq| sync_body(seq, event, &time))
1087 .is_some()
1088 }
1089}
1090
1091impl Default for Repos {
1092 fn default() -> Self {
1093 Self::new(Arc::new(Sequence::in_memory()), ReplayBounds::default())
1094 }
1095}
1096
1097#[derive(Debug)]
1103pub struct ReposStream {
1104 info: Option<Bytes>,
1106 producer: Weak<Inner>,
1108 stream: u64,
1110 after: u64,
1114 published: watch::Receiver<u64>,
1116 exclude: Exclusions,
1118 lagged: u64,
1121 closing: watch::Receiver<bool>,
1123}
1124
1125enum Read {
1127 Frame(Bytes),
1129 Behind(u64),
1132 Nothing,
1134}
1135
1136impl ReposStream {
1137 #[must_use]
1139 pub fn lagged(&self) -> u64 {
1140 self.lagged
1141 }
1142
1143 pub async fn next(&mut self) -> Option<Bytes> {
1151 if let Some(info) = self.info.take() {
1152 return Some(info);
1153 }
1154 loop {
1155 self.published.borrow_and_update();
1158 let producer = self.producer.upgrade()?;
1159 match self.read(&producer) {
1160 Read::Frame(bytes) => return Some(bytes),
1161 Read::Behind(skipped) => {
1162 self.lagged = self.lagged.saturating_add(skipped);
1163 warn!(
1164 skipped,
1165 after = self.after,
1166 "a subscribeRepos consumer fell behind"
1167 );
1168 return None;
1169 }
1170 Read::Nothing => {}
1171 }
1172 drop(producer);
1173 if *self.closing.borrow() {
1175 return None;
1176 }
1177 tokio::select! {
1178 changed = self.published.changed() => {
1179 if changed.is_err() {
1180 return None;
1181 }
1182 }
1183 _ = self.closing.wait_for(|closing| *closing) => {}
1184 }
1185 }
1186 }
1187
1188 fn read(&mut self, producer: &Inner) -> Read {
1191 let mut ring = producer.ring();
1192 if ring.evicted > self.after {
1193 return Read::Behind(ring.evicted - self.after);
1194 }
1195 let start = ring.frames.partition_point(|frame| frame.seq <= self.after);
1196 let mut found = Read::Nothing;
1197 for frame in ring.frames.range(start..) {
1198 self.after = frame.seq;
1199 if self.exclude.wants(frame.kind) {
1203 found = Read::Frame(frame.bytes.clone());
1204 break;
1205 }
1206 }
1207 ring.streams.insert(self.stream, self.after);
1208 found
1209 }
1210}
1211
1212impl Drop for ReposStream {
1214 fn drop(&mut self) {
1215 if let Some(producer) = self.producer.upgrade() {
1216 producer.ring().streams.remove(&self.stream);
1217 }
1218 }
1219}
1220
1221fn now_rfc3339() -> String {
1223 time::OffsetDateTime::now_utc()
1224 .replace_nanosecond(0)
1225 .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
1226 .format(&time::format_description::well_known::Rfc3339)
1227 .unwrap_or_default()
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233 use didbot_pds::{AccountStatus, RepoAction, RepoOp, RepoSync};
1234
1235 fn commit(rev: &str, since: Option<&str>) -> RepoCommit {
1236 RepoCommit {
1237 did: "did:web:a.agents.localhost%3A3000".to_owned(),
1238 commit: "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm".to_owned(),
1239 rev: rev.to_owned(),
1240 since: since.map(ToOwned::to_owned),
1241 prev_data: None,
1242 blocks: vec![0x0a, 0x0b],
1243 ops: vec![RepoOp {
1244 action: RepoAction::Create,
1245 path: "com.example.thing/3lq".to_owned(),
1246 cid: Some("bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm".to_owned()),
1247 prev: None,
1248 }],
1249 }
1250 }
1251
1252 fn decode(bytes: &[u8]) -> (Value, Value) {
1255 for split in 1..bytes.len() {
1260 let (Ok(header), Ok(body)) = (
1261 dag_cbor::decode(&bytes[..split]),
1262 dag_cbor::decode(&bytes[split..]),
1263 ) else {
1264 continue;
1265 };
1266 return (header, body);
1267 }
1268 panic!("a frame did not decode as two concatenated DAG-CBOR values");
1269 }
1270
1271 fn seq_of(bytes: &[u8]) -> u64 {
1273 let body = decode(bytes).1;
1274 let Value::Integer(seq) = field(&body, "seq") else {
1275 panic!("seq is not an integer");
1276 };
1277 *seq as u64
1278 }
1279
1280 fn field<'a>(value: &'a Value, key: &str) -> &'a Value {
1281 let Value::Map(fields) = value else {
1282 panic!("not a map");
1283 };
1284 fields.get(key).unwrap_or_else(|| panic!("no {key}"))
1285 }
1286
1287 #[test]
1288 fn a_commit_frame_is_a_header_then_the_body_the_lexicon_names() {
1289 let repos = Repos::default();
1290 let frame = repos
1291 .publish_commit(&commit("3lqaaa", None))
1292 .expect("the frame was numbered");
1293 let (header, body) = decode(&frame.bytes);
1294 assert_eq!(field(&header, "op"), &Value::Integer(1));
1295 assert_eq!(field(&header, "t"), &Value::String("#commit".to_owned()));
1296 assert_eq!(field(&body, "seq"), &Value::Integer(1));
1297 assert_eq!(
1298 field(&body, "repo"),
1299 &Value::String(commit("3lqaaa", None).did)
1300 );
1301 assert_eq!(field(&body, "rev"), &Value::String("3lqaaa".to_owned()));
1302 assert_eq!(field(&body, "blocks"), &Value::Bytes(vec![0x0a, 0x0b]));
1303 assert_eq!(field(&body, "rebase"), &Value::Bool(false));
1304 assert_eq!(field(&body, "tooBig"), &Value::Bool(false));
1305 assert_eq!(field(&body, "blobs"), &Value::List(Vec::new()));
1306 }
1307
1308 #[test]
1311 fn a_first_commit_sends_a_null_since_rather_than_no_since() {
1312 let repos = Repos::default();
1313 let first = repos
1314 .publish_commit(&commit("3lqaaa", None))
1315 .expect("the frame was numbered");
1316 assert_eq!(field(&decode(&first.bytes).1, "since"), &Value::Null);
1317 let second = repos
1318 .publish_commit(&commit("3lqbbb", Some("3lqaaa")))
1319 .expect("the frame was numbered");
1320 assert_eq!(
1321 field(&decode(&second.bytes).1, "since"),
1322 &Value::String("3lqaaa".to_owned())
1323 );
1324 }
1325
1326 #[test]
1327 fn an_op_carries_its_action_path_and_cid_link() {
1328 let repos = Repos::default();
1329 let frame = repos
1330 .publish_commit(&commit("3lqaaa", None))
1331 .expect("the frame was numbered");
1332 let body = decode(&frame.bytes).1;
1333 let Value::List(ops) = field(&body, "ops") else {
1334 panic!("ops is not a list");
1335 };
1336 assert_eq!(ops.len(), 1);
1337 assert_eq!(
1338 field(&ops[0], "action"),
1339 &Value::String("create".to_owned())
1340 );
1341 assert_eq!(
1342 field(&ops[0], "path"),
1343 &Value::String("com.example.thing/3lq".to_owned())
1344 );
1345 assert!(matches!(field(&ops[0], "cid"), Value::Link(_)));
1346 }
1347
1348 #[test]
1349 fn a_deletion_sends_a_null_cid_and_names_what_it_removed() {
1350 let repos = Repos::default();
1351 let mut event = commit("3lqaaa", None);
1352 event.ops[0] = RepoOp {
1353 action: RepoAction::Delete,
1354 path: "com.example.thing/3lq".to_owned(),
1355 cid: None,
1356 prev: Some("bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm".to_owned()),
1357 };
1358 let frame = repos
1359 .publish_commit(&event)
1360 .expect("the frame was numbered");
1361 let body = decode(&frame.bytes).1;
1362 let Value::List(ops) = field(&body, "ops") else {
1363 panic!("ops is not a list");
1364 };
1365 assert_eq!(
1366 field(&ops[0], "action"),
1367 &Value::String("delete".to_owned())
1368 );
1369 assert_eq!(field(&ops[0], "cid"), &Value::Null);
1370 assert!(
1371 matches!(field(&ops[0], "prev"), Value::Link(_)),
1372 "a delete op must name the record it removed, or nothing can invert it"
1373 );
1374 }
1375
1376 #[test]
1380 fn a_create_leaves_prev_off_the_wire_entirely() {
1381 let repos = Repos::default();
1382 let frame = repos
1383 .publish_commit(&commit("3lqaaa", None))
1384 .expect("the frame was numbered");
1385 let body = decode(&frame.bytes).1;
1386 let Value::List(ops) = field(&body, "ops") else {
1387 panic!("ops is not a list");
1388 };
1389 let Value::Map(fields) = &ops[0] else {
1390 panic!("an op is not a map");
1391 };
1392 assert!(!fields.contains_key("prev"), "a create defined `prev`");
1393 }
1394
1395 #[test]
1396 fn sequence_numbers_start_at_one_and_do_not_skip() {
1397 let repos = Repos::default();
1398 let seqs: Vec<u64> = (0..5)
1399 .map(|_| {
1400 repos
1401 .publish_commit(&commit("3lqaaa", None))
1402 .expect("the frame was numbered")
1403 .seq
1404 })
1405 .collect();
1406 assert_eq!(seqs, vec![1, 2, 3, 4, 5]);
1407 assert_eq!(repos.newest(), 5);
1408 }
1409
1410 #[tokio::test]
1417 async fn a_replayed_frame_points_at_the_buffer_rather_than_a_copy() {
1418 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(8));
1419 for _ in 0..3 {
1420 repos
1421 .publish_commit(&commit("3lqaaa", None))
1422 .expect("the frame was numbered");
1423 }
1424 let held: Vec<*const u8> = repos
1425 .ring()
1426 .frames
1427 .iter()
1428 .map(|frame| frame.bytes.as_ptr())
1429 .collect();
1430
1431 let mut stream = repos
1432 .subscribe(Some(0), Exclusions::default())
1433 .expect("a cursor of 0 replays everything held");
1434 let mut replayed = Vec::new();
1435 for _ in 0..held.len() {
1436 replayed.push(stream.next().await.expect("a replayed frame").as_ptr());
1437 }
1438 assert_eq!(
1439 replayed, held,
1440 "every replayed frame is the buffer's own allocation"
1441 );
1442 }
1443
1444 #[tokio::test]
1447 async fn a_live_frame_reaches_every_subscriber_as_one_allocation() {
1448 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(8));
1449 let mut streams: Vec<ReposStream> = (0..3)
1450 .map(|_| {
1451 repos
1452 .subscribe(None, Exclusions::default())
1453 .expect("a live subscription refuses nothing")
1454 })
1455 .collect();
1456
1457 let published = repos
1458 .publish_commit(&commit("3lqaaa", None))
1459 .expect("the frame was numbered")
1460 .bytes
1461 .as_ptr();
1462 for stream in &mut streams {
1463 let got = stream.next().await.expect("the live frame");
1464 assert_eq!(
1465 got.as_ptr(),
1466 published,
1467 "a live frame is shared with every receiver, not copied per receiver"
1468 );
1469 }
1470 }
1471
1472 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1482 async fn live_frames_arrive_in_sequence_order_under_concurrent_writes() {
1483 const WRITERS: usize = 16;
1484 const EACH: usize = 200;
1485 let total = WRITERS * EACH;
1486 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(total));
1487 let mut stream = repos
1488 .subscribe(None, Exclusions::default())
1489 .expect("a live subscription refuses nothing");
1490
1491 let writer = repos.clone();
1492 crate::race::under_pressure(WRITERS, EACH, move || {
1493 writer
1494 .publish_commit(&commit("3lqaaa", None))
1495 .expect("the frame was numbered");
1496 });
1497
1498 let mut arrived = Vec::with_capacity(total);
1499 for _ in 0..total {
1500 let bytes = stream.next().await.expect("every published frame arrives");
1501 arrived.push(seq_of(&bytes));
1502 }
1503 let expected: Vec<u64> = (1..=total as u64).collect();
1504 assert_eq!(
1505 arrived, expected,
1506 "a live subscriber received a frame out of sequence order"
1507 );
1508 assert_eq!(stream.lagged(), 0);
1509 }
1510
1511 fn sync(did: &str) -> RepoSync {
1512 RepoSync {
1513 did: did.to_owned(),
1514 rev: "3lqaaa".to_owned(),
1515 blocks: vec![0x0a, 0x0b],
1516 }
1517 }
1518
1519 #[test]
1521 fn a_sync_frame_carries_the_did_the_rev_and_the_commit_block() {
1522 let repos = Repos::default();
1523 let frame = repos
1524 .publish_sync(&sync("did:web:a"))
1525 .expect("the frame was numbered");
1526 let (header, body) = decode(&frame.bytes);
1527 assert_eq!(field(&header, "t"), &Value::String("#sync".to_owned()));
1528 let Value::Map(fields) = &body else {
1529 panic!("not a map");
1530 };
1531 assert_eq!(
1532 fields.keys().map(String::as_str).collect::<Vec<_>>(),
1533 ["blocks", "did", "rev", "seq", "time"]
1534 );
1535 assert_eq!(field(&body, "blocks"), &Value::Bytes(vec![0x0a, 0x0b]));
1536 assert_eq!(field(&body, "did"), &Value::String("did:web:a".to_owned()));
1537 assert_eq!(field(&body, "rev"), &Value::String("3lqaaa".to_owned()));
1538 assert_eq!(field(&body, "seq"), &Value::Integer(1));
1539 }
1540
1541 #[tokio::test]
1544 async fn no_exclusion_drops_a_sync() {
1545 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(8));
1546 repos
1547 .publish_commit(&commit("3lqaaa", None))
1548 .expect("the frame was numbered");
1549 repos
1550 .publish_sync(&sync("did:web:a"))
1551 .expect("the frame was numbered");
1552 let mut stream = repos
1553 .subscribe(
1554 Some(0),
1555 Exclusions {
1556 commit: true,
1557 identity: true,
1558 account: true,
1559 },
1560 )
1561 .expect("a resumable cursor");
1562 let (header, _) = decode(&stream.next().await.expect("the #sync"));
1563 assert_eq!(field(&header, "t"), &Value::String("#sync".to_owned()));
1564 }
1565
1566 #[tokio::test]
1570 async fn a_boot_keeps_every_frame_it_numbered_and_its_capacity_on_top() {
1571 let repos = Repos::booting(
1572 Arc::new(Sequence::in_memory()),
1573 ReplayBounds::frames(3),
1574 None,
1575 )
1576 .expect("a producer");
1577 for _ in 0..10 {
1578 repos
1579 .publish_account(&account("did:web:a", true))
1580 .expect("the frame was numbered");
1581 }
1582 assert_eq!(repos.all().len(), 10, "a boot evicts nothing");
1583
1584 repos.listening();
1585 for _ in 0..3 {
1586 repos
1587 .publish_commit(&commit("3lqaaa", None))
1588 .expect("the frame was numbered");
1589 }
1590 assert_eq!(repos.all().len(), 13, "the capacity is on top of the boot");
1591 repos
1592 .publish_commit(&commit("3lqbbb", None))
1593 .expect("the frame was numbered");
1594 assert_eq!(repos.all().len(), 13, "and bounded after it");
1595 assert_eq!(repos.oldest(), 2);
1596
1597 let mut stream = repos
1598 .subscribe(Some(1), Exclusions::default())
1599 .expect("a resumable cursor");
1600 let mut seqs = Vec::new();
1601 for _ in 0..13 {
1602 seqs.push(seq_of(&stream.next().await.expect("a replayed frame")));
1603 }
1604 assert_eq!(seqs, (2..=14).collect::<Vec<u64>>());
1605 }
1606
1607 #[test]
1608 fn the_buffer_drops_its_oldest_rather_than_growing() {
1609 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(3));
1610 for _ in 0..6 {
1611 repos
1612 .publish_commit(&commit("3lqaaa", None))
1613 .expect("the frame was numbered");
1614 }
1615 assert_eq!(repos.oldest(), 4);
1616 assert_eq!(repos.all().len(), 3);
1617 }
1618
1619 #[test]
1620 fn resuming_replays_exactly_what_came_after_the_cursor() {
1621 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(16));
1622 for _ in 0..5 {
1623 repos
1624 .publish_commit(&commit("3lqaaa", None))
1625 .expect("the frame was numbered");
1626 }
1627 let seqs: Vec<u64> = repos.since(2).iter().map(|frame| frame.seq).collect();
1628 assert_eq!(seqs, vec![3, 4, 5]);
1629 }
1630
1631 #[test]
1634 fn a_future_cursor_is_the_error_the_lexicon_declares() {
1635 let repos = Repos::default();
1636 repos
1637 .publish_commit(&commit("3lqaaa", None))
1638 .expect("the frame was numbered");
1639 let refused = repos
1640 .subscribe(Some(9), Exclusions::default())
1641 .expect_err("a future cursor errors");
1642 let (header, body) = decode(&refused);
1643 assert_eq!(field(&header, "op"), &Value::Integer(-1));
1644 assert_eq!(
1645 field(&body, "error"),
1646 &Value::String(FUTURE_CURSOR.to_owned())
1647 );
1648 assert!(
1649 repos.subscribe(Some(1), Exclusions::default()).is_ok(),
1650 "the newest frame is not future"
1651 );
1652 }
1653
1654 fn identity(did: &str) -> RepoIdentity {
1655 RepoIdentity {
1656 did: did.to_owned(),
1657 handle: Some("kestrel.agents.localhost".to_owned()),
1658 }
1659 }
1660
1661 fn account(did: &str, active: bool) -> RepoAccount {
1662 RepoAccount {
1663 did: did.to_owned(),
1664 active,
1665 status: (!active).then_some(AccountStatus::Deleted),
1666 }
1667 }
1668
1669 #[test]
1670 fn an_identity_frame_names_the_did_and_the_handle_it_answers_to() {
1671 let repos = Repos::default();
1672 let frame = repos
1673 .publish_identity(&identity("did:web:a"))
1674 .expect("the frame was numbered");
1675 let (header, body) = decode(&frame.bytes);
1676 assert_eq!(field(&header, "t"), &Value::String("#identity".to_owned()));
1677 assert_eq!(field(&body, "did"), &Value::String("did:web:a".to_owned()));
1678 assert_eq!(
1679 field(&body, "handle"),
1680 &Value::String("kestrel.agents.localhost".to_owned())
1681 );
1682 assert_eq!(field(&body, "seq"), &Value::Integer(1));
1683 }
1684
1685 #[test]
1688 fn an_active_account_carries_no_status_and_a_deleted_one_says_deleted() {
1689 let repos = Repos::default();
1690 let live = decode(
1691 &repos
1692 .publish_account(&account("did:web:a", true))
1693 .expect("the frame was numbered")
1694 .bytes,
1695 )
1696 .1;
1697 assert_eq!(field(&live, "active"), &Value::Bool(true));
1698 let Value::Map(fields) = &live else {
1699 panic!("not a map");
1700 };
1701 assert!(!fields.contains_key("status"), "an active account has none");
1702
1703 let gone = decode(
1704 &repos
1705 .publish_account(&account("did:web:a", false))
1706 .expect("the frame was numbered")
1707 .bytes,
1708 )
1709 .1;
1710 assert_eq!(field(&gone, "active"), &Value::Bool(false));
1711 assert_eq!(field(&gone, "status"), &Value::String("deleted".to_owned()));
1712 }
1713
1714 #[tokio::test]
1717 async fn all_three_kinds_share_one_sequence_and_replay_together() {
1718 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(64));
1719 repos
1720 .publish_identity(&identity("did:web:a"))
1721 .expect("the frame was numbered");
1722 repos
1723 .publish_account(&account("did:web:a", true))
1724 .expect("the frame was numbered");
1725 repos
1726 .publish_commit(&commit("3lqaaa", None))
1727 .expect("the frame was numbered");
1728 repos
1729 .publish_commit(&commit("3lqbbb", Some("3lqaaa")))
1730 .expect("the frame was numbered");
1731 repos
1732 .publish_account(&account("did:web:a", false))
1733 .expect("the frame was numbered");
1734
1735 let mut stream = repos
1736 .subscribe(Some(1), Exclusions::default())
1737 .expect("a resumable cursor");
1738 let mut seen = Vec::new();
1739 for _ in 0..4 {
1740 let bytes = stream.next().await.expect("a replayed frame");
1741 let (header, _) = decode(&bytes);
1742 let Value::String(tag) = field(&header, "t") else {
1743 panic!("no tag");
1744 };
1745 seen.push((tag.clone(), seq_of(&bytes)));
1746 }
1747 assert_eq!(
1748 seen,
1749 vec![
1750 ("#account".to_owned(), 2),
1751 ("#commit".to_owned(), 3),
1752 ("#commit".to_owned(), 4),
1753 ("#account".to_owned(), 5),
1754 ],
1755 "the three kinds must share one gapless, ordered sequence"
1756 );
1757 }
1758
1759 #[tokio::test]
1763 async fn an_exclusion_drops_frames_without_disturbing_the_sequence() {
1764 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(64));
1765 repos
1766 .publish_identity(&identity("did:web:a"))
1767 .expect("the frame was numbered");
1768 repos
1769 .publish_commit(&commit("3lqaaa", None))
1770 .expect("the frame was numbered");
1771 repos
1772 .publish_identity(&identity("did:web:b"))
1773 .expect("the frame was numbered");
1774 repos
1775 .publish_account(&account("did:web:b", true))
1776 .expect("the frame was numbered");
1777
1778 let exclude = Exclusions {
1779 identity: true,
1780 ..Exclusions::default()
1781 };
1782 let mut stream = repos.subscribe(Some(0), exclude).expect("from the start");
1783 let first = stream.next().await.expect("a frame");
1784 assert_eq!(seq_of(&first), 2, "the identity at 1 was not delivered");
1785 let second = stream.next().await.expect("a frame");
1786 assert_eq!(seq_of(&second), 4, "the identity at 3 was not delivered");
1787
1788 repos
1791 .publish_commit(&commit("3lqbbb", Some("3lqaaa")))
1792 .expect("the frame was numbered");
1793 let mut again = repos.subscribe(Some(4), exclude).expect("a live cursor");
1794 assert_eq!(seq_of(&again.next().await.expect("a frame")), 5);
1795 }
1796
1797 #[tokio::test]
1800 async fn excluding_every_kind_leaves_a_stream_that_is_merely_quiet() {
1801 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(64));
1802 repos
1803 .publish_commit(&commit("3lqaaa", None))
1804 .expect("the frame was numbered");
1805 let mut stream = repos
1806 .subscribe(
1807 Some(0),
1808 Exclusions {
1809 commit: true,
1810 identity: true,
1811 account: true,
1812 },
1813 )
1814 .expect("excluding everything is not an error");
1815 assert!(
1816 tokio::time::timeout(std::time::Duration::from_millis(50), stream.next())
1817 .await
1818 .is_err(),
1819 "a frame arrived on a stream that excluded every kind"
1820 );
1821 }
1822
1823 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1836 async fn a_cursor_evicted_while_the_buffer_is_read_is_told_it_is_outdated() {
1837 use std::sync::atomic::{AtomicBool, Ordering};
1838
1839 const CAPACITY: usize = 4;
1840 const ROUNDS: usize = 4096;
1841 let repos = Repos::new(
1842 Arc::new(Sequence::in_memory()),
1843 ReplayBounds::frames(CAPACITY),
1844 );
1845 repos
1848 .publish_commit(&commit("3lqaaa", None))
1849 .expect("the frame was numbered");
1850
1851 let writing = Arc::new(AtomicBool::new(true));
1852 let writer = repos.clone();
1853 let publishing = writing.clone();
1854 let publisher = std::thread::spawn(move || {
1855 while publishing.load(Ordering::Relaxed) {
1856 writer
1857 .publish_commit(&commit("3lqaaa", None))
1858 .expect("the frame was numbered");
1859 }
1860 });
1861
1862 for _ in 0..ROUNDS {
1863 let cursor = repos.oldest().saturating_sub(1);
1866 let mut stream = repos
1867 .subscribe(Some(cursor as i64), Exclusions::default())
1868 .expect("a cursor inside the buffer is not ahead of the stream");
1869 let Some(frame) = stream.next().await else {
1872 continue;
1873 };
1874 let (header, body) = decode(&frame);
1875 if field(&header, "t") == &Value::String("#info".to_owned()) {
1876 assert_eq!(
1877 field(&body, "name"),
1878 &Value::String(OUTDATED_CURSOR.to_owned()),
1879 "the only `#info` this server sends is an outdated cursor"
1880 );
1881 continue;
1882 }
1883 assert_eq!(
1884 seq_of(&frame),
1885 cursor + 1,
1886 "a consumer whose cursor was evicted while the buffer was read \
1887 was served a later frame with nothing in front of it saying so"
1888 );
1889 }
1890
1891 writing.store(false, Ordering::Relaxed);
1892 publisher.join().expect("the publisher does not panic");
1893 }
1894
1895 #[tokio::test]
1896 async fn a_cursor_outside_the_buffer_gets_an_info_frame_and_then_what_is_left() {
1897 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(3));
1898 for _ in 0..6 {
1899 repos
1900 .publish_commit(&commit("3lqaaa", None))
1901 .expect("the frame was numbered");
1902 }
1903 let mut stream = repos
1904 .subscribe(Some(1), Exclusions::default())
1905 .expect("1 is not a future cursor");
1906 let (header, body) = decode(&stream.next().await.expect("an info frame"));
1907 assert_eq!(field(&header, "t"), &Value::String("#info".to_owned()));
1908 assert_eq!(
1909 field(&body, "name"),
1910 &Value::String(OUTDATED_CURSOR.to_owned())
1911 );
1912 let mut seqs = Vec::new();
1914 for _ in 0..3 {
1915 seqs.push(seq_of(&stream.next().await.expect("a replayed frame")));
1916 }
1917 assert_eq!(seqs, vec![4u64, 5, 6]);
1918 }
1919
1920 #[tokio::test]
1930 async fn a_cursor_from_before_a_restart_is_told_it_is_outdated() {
1931 let floor = didbot_pds::RESERVATION;
1934 let sequence = Sequence::new(Arc::new(didbot_pds::MemorySequenceLog), floor);
1935 let repos = Repos::new(Arc::new(sequence), ReplayBounds::frames(64));
1936 assert_eq!(repos.newest(), floor, "the restart resumed at the floor");
1937 assert_eq!(repos.all().len(), 0, "and with an empty replay buffer");
1938
1939 let mut stream = repos
1940 .subscribe(Some(7), Exclusions::default())
1941 .expect("a cursor from before a restart is behind, not ahead");
1942 let frame = tokio::time::timeout(std::time::Duration::from_secs(10), stream.next())
1946 .await
1947 .expect("a consumer from before the restart was quietly treated as caught up")
1948 .expect("the stream is open");
1949 let (header, body) = decode(&frame);
1950 assert_eq!(field(&header, "t"), &Value::String("#info".to_owned()));
1951 assert_eq!(
1952 field(&body, "name"),
1953 &Value::String(OUTDATED_CURSOR.to_owned())
1954 );
1955
1956 assert!(
1958 repos
1959 .publish_commit(&commit("3lqaaa", None))
1960 .expect("the frame was numbered")
1961 .seq
1962 > floor,
1963 "a frame repeated a number a previous run could have handed out"
1964 );
1965 }
1966
1967 #[tokio::test]
1972 async fn a_stream_that_fell_behind_ends_rather_than_skipping_to_the_present() {
1973 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(4));
1974 let mut stream = repos
1975 .subscribe(None, Exclusions::default())
1976 .expect("a live consumer");
1977 for _ in 0..12 {
1980 repos
1981 .publish_commit(&commit("3lqaaa", None))
1982 .expect("the frame was numbered");
1983 }
1984 assert_eq!(
1985 stream.next().await,
1986 None,
1987 "a consumer that fell behind was handed a frame with a hole in front of it"
1988 );
1989 assert!(
1990 stream.lagged() > 0,
1991 "the stream ended without recording that anything was dropped"
1992 );
1993 }
1994
1995 #[tokio::test]
2000 async fn publishing_does_not_wait_on_a_subscriber_that_never_reads() {
2001 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(4));
2002 let _idle = repos
2003 .subscribe(None, Exclusions::default())
2004 .expect("a live consumer");
2005 let published = tokio::task::spawn_blocking({
2006 let repos = repos.clone();
2007 move || {
2008 (0..64)
2012 .map(|_| {
2013 repos
2014 .publish_commit(&commit("3lqaaa", None))
2015 .expect("the frame was numbered")
2016 .seq
2017 })
2018 .collect::<Vec<u64>>()
2019 }
2020 });
2021 let published = tokio::time::timeout(std::time::Duration::from_secs(10), published)
2022 .await
2023 .expect("publishing blocked on a subscriber that was not reading")
2024 .expect("the publishing thread did not panic");
2025 assert_eq!(
2026 published,
2027 (1..=64).collect::<Vec<u64>>(),
2028 "every write was numbered, in order, with the reader stalled"
2029 );
2030 assert_eq!(repos.all().len(), 4, "and the buffer stayed at its bound");
2031 }
2032
2033 #[tokio::test]
2037 async fn the_oldest_frame_held_is_reachable_and_the_one_before_it_is_not() {
2038 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(3));
2039 for _ in 0..6 {
2040 repos
2041 .publish_commit(&commit("3lqaaa", None))
2042 .expect("the frame was numbered");
2043 }
2044 let oldest = repos.oldest();
2045 assert_eq!(oldest, 4, "the buffer holds 4, 5 and 6");
2046
2047 let mut reachable = repos
2048 .subscribe(Some(oldest as i64 - 1), Exclusions::default())
2049 .expect("not a future cursor");
2050 assert_eq!(
2051 seq_of(&reachable.next().await.expect("a frame")),
2052 oldest,
2053 "a cursor one below the oldest frame held must replay it, not admit a gap"
2054 );
2055
2056 let mut lost = repos
2057 .subscribe(Some(oldest as i64 - 2), Exclusions::default())
2058 .expect("not a future cursor");
2059 let (header, body) = decode(&lost.next().await.expect("a frame"));
2060 assert_eq!(field(&header, "t"), &Value::String("#info".to_owned()));
2061 assert_eq!(
2062 field(&body, "name"),
2063 &Value::String(OUTDATED_CURSOR.to_owned()),
2064 "a cursor two below the oldest frame held has a hole and must be told so"
2065 );
2066 }
2067
2068 #[tokio::test]
2072 async fn a_resumed_consumer_sees_every_frame_after_its_cursor_exactly_once() {
2073 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(64));
2074 for _ in 0..3 {
2075 repos
2076 .publish_commit(&commit("3lqaaa", None))
2077 .expect("the frame was numbered");
2078 }
2079 let mut first = repos
2080 .subscribe(None, Exclusions::default())
2081 .expect("a live consumer");
2082 for _ in 0..2 {
2083 repos
2084 .publish_commit(&commit("3lqbbb", None))
2085 .expect("the frame was numbered");
2086 }
2087 let mut seen = Vec::new();
2088 for _ in 0..2 {
2089 seen.push(seq_of(&first.next().await.expect("a live frame")));
2090 }
2091 assert_eq!(seen, vec![4, 5]);
2092
2093 drop(first);
2096 for _ in 0..2 {
2097 repos
2098 .publish_commit(&commit("3lqccc", None))
2099 .expect("the frame was numbered");
2100 }
2101 let mut again = repos
2102 .subscribe(Some(5), Exclusions::default())
2103 .expect("a resumable cursor");
2104 for _ in 0..2 {
2105 seen.push(seq_of(&again.next().await.expect("a replayed frame")));
2106 }
2107 assert_eq!(seen, vec![4, 5, 6, 7], "no gaps and no repeats");
2108 }
2109
2110 fn largest_commit() -> RepoCommit {
2112 RepoCommit {
2113 blocks: vec![0; 2_000_000],
2114 ..commit("3lqaaa", None)
2115 }
2116 }
2117
2118 #[test]
2121 fn a_burst_of_large_frames_stays_under_the_byte_bound_and_keeps_the_newest() {
2122 const BYTES: usize = 16 * 1024 * 1024;
2123 let repos = Repos::new(
2124 Arc::new(Sequence::in_memory()),
2125 ReplayBounds {
2126 frames: 4096,
2127 bytes: BYTES,
2128 },
2129 );
2130 for _ in 0..64 {
2131 repos
2132 .publish_commit(&largest_commit())
2133 .expect("the frame was numbered");
2134 }
2135 let held = repos.all();
2136 let total: usize = held.iter().map(|frame| frame.bytes.len()).sum();
2137 assert!(
2138 total <= BYTES,
2139 "the buffer holds {total} bytes, past {BYTES}"
2140 );
2141 assert_eq!(
2142 repos.ring().bytes,
2143 total,
2144 "the running total is the real one"
2145 );
2146 assert_eq!(
2147 held.len(),
2148 BYTES / held[0].bytes.len(),
2149 "as many frames as fit are kept"
2150 );
2151 let seqs: Vec<u64> = held.iter().map(|frame| frame.seq).collect();
2152 let first = 64 - held.len() as u64 + 1;
2153 assert_eq!(
2154 seqs,
2155 (first..=64).collect::<Vec<u64>>(),
2156 "the oldest went first"
2157 );
2158 }
2159
2160 #[test]
2163 fn a_frame_larger_than_the_byte_bound_is_still_held_alone() {
2164 let repos = Repos::new(
2165 Arc::new(Sequence::in_memory()),
2166 ReplayBounds {
2167 frames: 64,
2168 bytes: 1024,
2169 },
2170 );
2171 repos
2172 .publish_commit(&commit("3lqaaa", None))
2173 .expect("the frame was numbered");
2174 repos
2175 .publish_commit(&largest_commit())
2176 .expect("the frame was numbered");
2177 let seqs: Vec<u64> = repos.all().iter().map(|frame| frame.seq).collect();
2178 assert_eq!(seqs, vec![2]);
2179 }
2180
2181 #[tokio::test]
2182 async fn a_cursor_the_byte_bound_evicted_is_told_it_is_outdated() {
2183 let size = Repos::default()
2184 .publish_commit(&commit("3lqaaa", None))
2185 .expect("the frame was numbered")
2186 .bytes
2187 .len();
2188 let repos = Repos::new(
2189 Arc::new(Sequence::in_memory()),
2190 ReplayBounds {
2191 frames: 64,
2192 bytes: 3 * size,
2193 },
2194 );
2195 for _ in 0..6 {
2196 repos
2197 .publish_commit(&commit("3lqaaa", None))
2198 .expect("the frame was numbered");
2199 }
2200 assert_eq!(repos.oldest(), 4, "three frames fit in the byte bound");
2201
2202 let mut stream = repos
2203 .subscribe(Some(1), Exclusions::default())
2204 .expect("1 is not a future cursor");
2205 let (header, body) = decode(&stream.next().await.expect("an info frame"));
2206 assert_eq!(field(&header, "t"), &Value::String("#info".to_owned()));
2207 assert_eq!(
2208 field(&body, "name"),
2209 &Value::String(OUTDATED_CURSOR.to_owned())
2210 );
2211 let mut seqs = Vec::new();
2212 for _ in 0..3 {
2213 seqs.push(seq_of(&stream.next().await.expect("a replayed frame")));
2214 }
2215 assert_eq!(seqs, vec![4u64, 5, 6]);
2216 }
2217
2218 #[tokio::test]
2222 async fn a_stalled_consumer_is_ended_once_the_byte_bound_evicts_past_it() {
2223 const BYTES: usize = 8 * 1024 * 1024;
2224 let repos = Repos::new(
2225 Arc::new(Sequence::in_memory()),
2226 ReplayBounds {
2227 frames: 4096,
2228 bytes: BYTES,
2229 },
2230 );
2231 let mut stalled = repos
2232 .subscribe(None, Exclusions::default())
2233 .expect("a live consumer");
2234 for _ in 0..16 {
2235 repos
2236 .publish_commit(&largest_commit())
2237 .expect("the frame was numbered");
2238 assert!(
2239 repos.ring().bytes <= 2 * BYTES,
2240 "a stream's unsent frames held the buffer past twice its bound"
2241 );
2242 }
2243 assert!(
2244 repos.ring().bytes <= BYTES,
2245 "a stream that had already lost a frame kept the buffer past its bound"
2246 );
2247 assert_eq!(
2248 stalled.next().await,
2249 None,
2250 "a consumer the buffer evicted past was handed a frame with a hole in front of it"
2251 );
2252 assert!(stalled.lagged() > 0);
2253 }
2254
2255 #[tokio::test]
2260 async fn a_cursor_at_the_eviction_edge_replays_everything_after_it() {
2261 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(4));
2262 for _ in 0..4 {
2263 repos
2264 .publish_commit(&commit("3lqaaa", None))
2265 .expect("the frame was numbered");
2266 }
2267 let mut stream = repos
2268 .subscribe(Some(0), Exclusions::default())
2269 .expect("a cursor inside the buffer");
2270 repos
2271 .publish_commit(&commit("3lqbbb", None))
2272 .expect("the frame was numbered");
2273
2274 let mut seqs = Vec::new();
2275 for _ in 0..5 {
2276 let frame = stream
2277 .next()
2278 .await
2279 .expect("a stream with a valid cursor was ended");
2280 seqs.push(seq_of(&frame));
2281 }
2282 assert_eq!(seqs, vec![1, 2, 3, 4, 5]);
2283 assert_eq!(stream.lagged(), 0);
2284
2285 repos
2287 .publish_commit(&commit("3lqccc", None))
2288 .expect("the frame was numbered");
2289 assert_eq!(repos.all().len(), 4, "the bound applies again");
2290 }
2291
2292 #[tokio::test]
2296 async fn an_outdated_stream_starting_at_the_oldest_frame_is_not_cut_off() {
2297 let repos = Repos::new(Arc::new(Sequence::in_memory()), ReplayBounds::frames(4));
2298 for _ in 0..6 {
2299 repos
2300 .publish_commit(&commit("3lqaaa", None))
2301 .expect("the frame was numbered");
2302 }
2303 let mut stream = repos
2304 .subscribe(Some(0), Exclusions::default())
2305 .expect("not a future cursor");
2306 for _ in 0..3 {
2307 repos
2308 .publish_commit(&commit("3lqbbb", None))
2309 .expect("the frame was numbered");
2310 }
2311 let (header, _) = decode(&stream.next().await.expect("an info frame"));
2312 assert_eq!(field(&header, "t"), &Value::String("#info".to_owned()));
2313 let mut seqs = Vec::new();
2314 for _ in 0..7 {
2315 seqs.push(seq_of(&stream.next().await.expect("a frame")));
2316 }
2317 assert_eq!(seqs, (3..=9).collect::<Vec<u64>>());
2318 }
2319
2320 #[tokio::test]
2323 async fn a_stream_survives_the_eviction_of_a_frame_it_already_sent() {
2324 let size = Repos::default()
2325 .publish_commit(&commit("3lqaaa", None))
2326 .expect("the frame was numbered")
2327 .bytes
2328 .len();
2329 let repos = Repos::new(
2330 Arc::new(Sequence::in_memory()),
2331 ReplayBounds {
2332 frames: 4096,
2333 bytes: 2 * size,
2334 },
2335 );
2336 let mut stream = repos
2337 .subscribe(None, Exclusions::default())
2338 .expect("a live consumer");
2339 repos
2340 .publish_commit(&commit("3lqaaa", None))
2341 .expect("the frame was numbered");
2342 assert_eq!(seq_of(&stream.next().await.expect("frame 1")), 1);
2343
2344 for _ in 0..2 {
2345 repos
2346 .publish_commit(&commit("3lqaaa", None))
2347 .expect("the frame was numbered");
2348 }
2349 assert_eq!(repos.oldest(), 2, "the byte bound evicted frame 1");
2350 assert_eq!(repos.ring().evicted, 1);
2351
2352 assert_eq!(seq_of(&stream.next().await.expect("frame 2")), 2);
2353 assert_eq!(seq_of(&stream.next().await.expect("frame 3")), 3);
2354 assert_eq!(stream.lagged(), 0);
2355 }
2356}