1use std::collections::BTreeMap;
5use std::fmt::Debug;
6use std::hash::Hash as StdHash;
7use std::marker::PhantomData;
8use std::pin::Pin;
9use std::task::{Context, Poll};
10
11use futures_channel::mpsc;
12use futures_util::{Sink, SinkExt, Stream, StreamExt};
13use p2panda_core::{Body, Extensions, Hash, Header, LogId, Operation, SeqNum, VerifyingKey};
14use p2panda_store::logs::LogStore;
15use p2panda_store::topics::TopicStore;
16use pin_project_lite::pin_project;
17use serde::{Deserialize, Serialize};
18use thiserror::Error;
19use tokio::sync::broadcast;
20use tracing::{Level, debug, enabled, trace, warn};
21
22use crate::ToSync;
23use crate::dedup::DEFAULT_BUFFER_CAPACITY;
24use crate::protocols::ShortFormat;
25use crate::protocols::log_sync::{
26 LogSync, LogSyncError, LogSyncEvent, LogSyncMessage, LogSyncMetrics,
27};
28use crate::traits::Protocol;
29
30#[derive(Debug)]
44pub struct TopicLogSync<T, S, L, E> {
45 pub topic: T,
46 pub store: S,
47 pub event_tx: broadcast::Sender<TopicLogSyncEvent<E>>,
48 pub live_mode_rx: Option<mpsc::Receiver<ToSync<Operation<E>>>>,
49 pub buffer_capacity: usize,
50 pub _phantom: PhantomData<L>,
51}
52
53impl<T, S, L, E> TopicLogSync<T, S, L, E>
54where
55 T: Eq + StdHash + Serialize + for<'a> Deserialize<'a>,
56 S: LogStore<Operation<E>, VerifyingKey, L, SeqNum, Hash>
57 + TopicStore<T, VerifyingKey, L>
58 + Clone
59 + Send
60 + 'static,
61 L: LogId,
62 E: Extensions,
63{
64 pub fn new(
67 topic: T,
68 store: S,
69 live_mode_rx: Option<mpsc::Receiver<ToSync<Operation<E>>>>,
70 event_tx: broadcast::Sender<TopicLogSyncEvent<E>>,
71 ) -> Self {
72 Self::new_with_capacity(
73 topic,
74 store,
75 live_mode_rx,
76 event_tx,
77 DEFAULT_BUFFER_CAPACITY,
78 )
79 }
80
81 pub fn new_with_capacity(
83 topic: T,
84 store: S,
85 live_mode_rx: Option<mpsc::Receiver<ToSync<Operation<E>>>>,
86 event_tx: broadcast::Sender<TopicLogSyncEvent<E>>,
87 buffer_capacity: usize,
88 ) -> Self {
89 Self {
90 topic,
91 store,
92 event_tx,
93 live_mode_rx,
94 buffer_capacity,
95 _phantom: PhantomData,
96 }
97 }
98}
99
100impl<T, S, L, E> Protocol for TopicLogSync<T, S, L, E>
101where
102 T: Debug + Eq + StdHash + Serialize + for<'a> Deserialize<'a> + Send + 'static,
103 S: LogStore<Operation<E>, VerifyingKey, L, SeqNum, Hash>
104 + TopicStore<T, VerifyingKey, L>
105 + Clone
106 + Send
107 + 'static,
108 L: LogId + Debug + Send + 'static,
109 E: Extensions + Send + 'static,
110{
111 type Error = TopicLogSyncError;
112 type Message = TopicLogSyncMessage<L, E>;
113 type Output = ();
114
115 async fn run(
116 self,
117 mut sink: &mut (impl Sink<Self::Message, Error = impl Debug> + Unpin),
118 mut stream: &mut (impl Stream<Item = Result<Self::Message, impl Debug>> + Unpin),
119 ) -> Result<Self::Output, Self::Error> {
120 debug!(
123 live_mode = self.live_mode_rx.is_some(),
124 "start sync session"
125 );
126
127 let logs = self
129 .store
130 .resolve(&self.topic)
131 .await
132 .map_err(|err| TopicLogSyncError::TopicStore(err.to_string()))?;
133
134 if enabled!(Level::DEBUG) {
135 let display_logs: BTreeMap<String, usize> =
136 logs.iter().map(|(k, v)| (k.fmt_short(), v.len())).collect();
137 debug!(logs = ?display_logs, "local topic logs retrieved");
138 }
139
140 if enabled!(Level::TRACE) {
141 let trace_logs: Vec<(String, &Vec<L>)> =
142 logs.iter().map(|(k, v)| (k.fmt_short(), v)).collect();
143 trace!(logs = ?trace_logs, "local topic logs retrieved");
144 }
145
146 let (mut dedup, sync_metrics) = {
148 let (mut log_sync_sink, mut log_sync_stream) = sync_channels(&mut sink, &mut stream);
149 let protocol = LogSync::new_with_capacity(
150 self.store.clone(),
151 logs,
152 self.event_tx.clone(),
153 self.buffer_capacity,
154 );
155 let result = protocol.run(&mut log_sync_sink, &mut log_sync_stream).await;
156
157 match result {
160 Ok((dedup, metrics)) => {
161 self.event_tx
162 .send(TopicLogSyncEvent::SyncFinished {
163 metrics: metrics.clone().into(),
164 })
165 .map_err(|_| TopicLogSyncChannelError::EventSend)?;
166
167 (dedup, metrics)
168 }
169 Err(err) => {
170 self.event_tx
171 .send(TopicLogSyncEvent::Failed {
172 error: err.to_string(),
173 })
174 .map_err(|_| TopicLogSyncChannelError::EventSend)?;
175
176 log_sync_sink
177 .close()
178 .await
179 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")))?;
180
181 return Err(err.into());
182 }
183 }
184 };
185
186 let mut metrics: Metrics = sync_metrics.into();
187
188 let result = match self.live_mode_rx {
189 None => Ok(()),
190 Some(mut live_mode_rx) => {
191 let mut close_sent = false;
198 self.event_tx
199 .send(TopicLogSyncEvent::LiveModeStarted)
200 .map_err(|_| TopicLogSyncChannelError::EventSend)?;
201
202 loop {
203 tokio::select! {
204 biased;
205 Some(message) = live_mode_rx.next() => {
206 match message {
207 ToSync::Payload(operation) => {
208 if !dedup.insert(operation.hash) {
209 trace!(id = ?operation.hash.fmt_short(), "ignore duplicate operation sent on live-mode channel");
210 continue;
211 }
212
213 metrics.sent_live_bytes +=
214 operation.header.to_bytes().len() as u32 + operation.header.payload_size;
215 metrics.sent_live_operations += 1;
216
217 trace!(
218 phase = "live",
219 id = ?operation.hash.fmt_short(),
220 sent_ops = ?metrics.sent_live_operations,
221 sent_bytes = ?metrics.sent_live_bytes,
222 "sent operation"
223 );
224
225 let result = sink
226 .send(TopicLogSyncMessage::Live(
227 operation.header,
228 operation.body,
229 ))
230 .await
231 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")).into());
232
233 if result.is_err() {
234 break result;
235 };
236 }
237 ToSync::Close => {
238 debug!("closing sync session");
242
243 let result = sink
244 .send(TopicLogSyncMessage::Close)
245 .await
246 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")).into());
247 if result.is_err() {
248 break result;
249 };
250 close_sent = true;
251 }
252 };
253 }
254 message = stream.next() => {
255 let Some(message) = message else {
256 if close_sent {
257 break Ok(());
258 }
259 break Err(TopicLogSyncError::UnexpectedStreamClosure);
260 };
261
262 match message {
263 Ok(message) => {
264 if let TopicLogSyncMessage::Close = message {
265 debug!("received close message from remote");
268 break Ok(());
269 };
270
271 let TopicLogSyncMessage::Live(header, body) = message else {
272 break Err(TopicLogSyncError::UnexpectedProtocolMessage(
273 message.to_string(),
274 ));
275 };
276
277 if !dedup.insert(header.hash()) {
283 trace!(phase = "live", operation_id = ?header.hash().fmt_short(), "ignore duplicate operation sent from remote");
284 continue;
285 }
286
287 metrics.received_live_bytes += header.to_bytes().len() as u32 + header.payload_size;
288 metrics.received_live_operations += 1;
289
290 trace!(
291 phase = "live",
292 operation_id = ?header.hash().fmt_short(),
293 received_ops = %metrics.received_live_operations,
294 received_bytes = %metrics.received_live_bytes,
295 "received operation"
296 );
297
298 self.event_tx
299 .send(TopicLogSyncEvent::OperationReceived{operation: Box::new(Operation {
300 hash: header.hash(),
301 header,
302 body,
303 }), metrics: metrics.clone()})
304 .map_err(|_| TopicLogSyncChannelError::EventSend)?;
305 }
306 Err(err) => {
307 if close_sent {
308 break Ok(());
309 }
310 break Err(TopicLogSyncError::DecodeMessage(format!("{err:?}")));
311 }
312 }
313 }
314 }
315 }
316 }
317 };
318
319 sink.close()
320 .await
321 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")))?;
322
323 let final_event = match result.as_ref() {
324 Ok(_) => {
325 debug!(
326 sent_ops = ?metrics.sent_operations(),
327 sent_bytes = ?metrics.sent_bytes(),
328 received_ops = %metrics.received_operations(),
329 received_bytes = %metrics.received_bytes(),
330 "sync session closed"
331 );
332 TopicLogSyncEvent::SessionFinished { metrics }
333 }
334 Err(err) => {
335 warn!(error = ?err, "sync session closed with error");
336 TopicLogSyncEvent::Failed {
337 error: err.to_string(),
338 }
339 }
340 };
341
342 self.event_tx
343 .send(final_event)
344 .map_err(|_| TopicLogSyncChannelError::EventSend)?;
345
346 result
347 }
348}
349
350#[allow(clippy::complexity)]
352fn sync_channels<'a, L, E>(
353 sink: &mut (impl Sink<TopicLogSyncMessage<L, E>, Error = impl Debug> + Unpin),
354 stream: &mut (impl Stream<Item = Result<TopicLogSyncMessage<L, E>, impl Debug>> + Unpin),
355) -> (
356 impl Sink<LogSyncMessage<L>, Error = TopicLogSyncChannelError> + Unpin,
357 impl Stream<Item = Result<LogSyncMessage<L>, TopicLogSyncChannelError>> + Unpin,
358)
359where
360 L: LogId,
361 E: Extensions,
362{
363 let log_sync_sink = LogSyncSink::new(sink);
364
365 let log_sync_stream = stream.by_ref().map(|message| match message {
366 Ok(TopicLogSyncMessage::Sync(message)) => Ok(message),
367 Ok(TopicLogSyncMessage::Live { .. }) | Ok(TopicLogSyncMessage::Close) => Err(
368 TopicLogSyncChannelError::MessageStream("non-protocol message received".to_string()),
369 ),
370 Err(err) => Err(TopicLogSyncChannelError::MessageStream(format!("{err:?}"))),
371 });
372
373 (log_sync_sink, log_sync_stream)
374}
375
376#[derive(Debug, Error)]
378pub enum TopicLogSyncChannelError {
379 #[error("error sending on message sink: {0}")]
380 MessageSink(String),
381
382 #[error("error receiving from message stream: {0}")]
383 MessageStream(String),
384
385 #[error("no active receivers for broadcast")]
386 EventSend,
387}
388
389#[derive(Debug, Error)]
391pub enum TopicLogSyncError {
392 #[error(transparent)]
393 Sync(#[from] LogSyncError),
394
395 #[error("topic store error: {0}")]
396 TopicStore(String),
397
398 #[error("unexpected protocol message: {0}")]
399 UnexpectedProtocolMessage(String),
400
401 #[error(transparent)]
402 Channel(#[from] TopicLogSyncChannelError),
403
404 #[error("remote unexpectedly closed stream in live-mode")]
405 UnexpectedStreamClosure,
406
407 #[error("{0}")]
408 DecodeMessage(String),
409}
410
411#[derive(Clone, Debug, Default, PartialEq)]
412pub struct Metrics {
413 pub outbound_sync_bytes: u32,
414 pub outbound_sync_operations: u32,
415 pub inbound_sync_bytes: u32,
416 pub inbound_sync_operations: u32,
417 pub sent_sync_bytes: u32,
418 pub sent_sync_operations: u32,
419 pub received_sync_bytes: u32,
420 pub received_sync_operations: u32,
421 pub sent_live_bytes: u32,
422 pub sent_live_operations: u32,
423 pub received_live_bytes: u32,
424 pub received_live_operations: u32,
425}
426
427impl Metrics {
428 pub fn sent_bytes(&self) -> u32 {
429 self.sent_sync_bytes + self.sent_live_bytes
430 }
431
432 pub fn received_bytes(&self) -> u32 {
433 self.received_sync_bytes + self.received_live_bytes
434 }
435
436 pub fn sent_operations(&self) -> u32 {
437 self.sent_sync_operations + self.sent_live_operations
438 }
439
440 pub fn received_operations(&self) -> u32 {
441 self.received_sync_operations + self.received_live_operations
442 }
443}
444
445impl From<LogSyncMetrics> for Metrics {
446 fn from(value: LogSyncMetrics) -> Metrics {
447 Metrics {
448 inbound_sync_bytes: value.inbound_bytes,
449 outbound_sync_bytes: value.outbound_bytes,
450 sent_sync_bytes: value.sent_bytes,
451 received_sync_bytes: value.received_bytes,
452 inbound_sync_operations: value.inbound_operations,
453 outbound_sync_operations: value.outbound_operations,
454 sent_sync_operations: value.sent_operations,
455 received_sync_operations: value.received_operations,
456 ..Default::default()
457 }
458 }
459}
460
461#[derive(Debug, Clone, PartialEq)]
463pub enum TopicLogSyncEvent<E = ()> {
464 SessionStarted,
468
469 SyncStarted { metrics: Metrics },
474
475 SyncFinished { metrics: Metrics },
480
481 LiveModeStarted,
485
486 OperationReceived {
488 operation: Box<Operation<E>>,
489 metrics: Metrics,
490 },
491
492 SessionFinished { metrics: Metrics },
496
497 Failed { error: String },
502}
503
504impl<E> From<LogSyncEvent<E>> for TopicLogSyncEvent<E> {
505 fn from(event: LogSyncEvent<E>) -> Self {
506 match event {
507 LogSyncEvent::MetricsExchanged { metrics } => TopicLogSyncEvent::SyncStarted {
508 metrics: metrics.into(),
509 },
510 LogSyncEvent::OperationReceived { operation, metrics } => {
511 TopicLogSyncEvent::OperationReceived {
512 operation,
513 metrics: metrics.into(),
514 }
515 }
516 }
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
522#[serde(bound(deserialize = "L: LogId"))]
523#[allow(clippy::large_enum_variant)]
524pub enum TopicLogSyncMessage<L, E>
525where
526 L: LogId,
527 E: Extensions,
528{
529 Sync(LogSyncMessage<L>),
530 Live(Header<E>, Option<Body>),
531 Close,
532}
533
534impl<L, E> std::fmt::Display for TopicLogSyncMessage<L, E>
535where
536 L: LogId,
537 E: Extensions,
538{
539 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540 let value = match self {
541 TopicLogSyncMessage::Sync(_) => "sync",
542 TopicLogSyncMessage::Live(_, _) => "live",
543 TopicLogSyncMessage::Close => "close",
544 };
545 write!(f, "{value}")
546 }
547}
548
549pin_project! {
550 pub struct LogSyncSink<S, L, E> {
552 #[pin]
553 inner: S,
554 _phantom: std::marker::PhantomData<(L, E)>,
555 }
556}
557
558impl<S, L, E> LogSyncSink<S, L, E> {
559 pub fn new(inner: S) -> Self {
560 Self {
561 inner,
562 _phantom: std::marker::PhantomData,
563 }
564 }
565}
566
567impl<S, L, E> Sink<LogSyncMessage<L>> for LogSyncSink<S, L, E>
568where
569 L: LogId,
570 S: Sink<TopicLogSyncMessage<L, E>>,
571 S::Error: Debug,
572 E: Extensions,
573{
574 type Error = TopicLogSyncChannelError;
575
576 fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
577 let this = self.project();
578 this.inner
579 .poll_ready(cx)
580 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")))
581 }
582
583 fn start_send(self: Pin<&mut Self>, item: LogSyncMessage<L>) -> Result<(), Self::Error> {
584 let this = self.project();
585 let msg = TopicLogSyncMessage::Sync(item);
586 this.inner
587 .start_send(msg)
588 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")))
589 }
590
591 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
592 let this = self.project();
593 this.inner
594 .poll_flush(cx)
595 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")))
596 }
597
598 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
599 let this = self.project();
600 this.inner
601 .poll_close(cx)
602 .map_err(|err| TopicLogSyncChannelError::MessageSink(format!("{err:?}")))
603 }
604}
605#[cfg(test)]
606pub mod tests {
607 use std::collections::BTreeMap;
608
609 use futures_channel::mpsc;
610 use futures_util::{SinkExt, StreamExt};
611 use p2panda_core::test_utils::setup_logging;
612 use p2panda_core::{Body, Operation, Topic};
613
614 use crate::ToSync;
615 use crate::protocols::{LogSyncError, LogSyncMessage};
616 use crate::test_utils::{Peer, TestTopicSyncMessage, run_protocol, run_protocol_uni};
617 use crate::traits::Protocol;
618
619 use super::{TopicLogSyncError, TopicLogSyncEvent};
620
621 #[tokio::test]
622 async fn sync_session_no_operations() {
623 let topic = Topic::random();
624 let mut peer = Peer::new(0).await;
625 peer.associate(&topic, &BTreeMap::default()).await;
626
627 let (session, mut events_rx, _) = peer.topic_sync_protocol(topic.clone(), false);
628
629 let (_, remote_rx) = run_protocol_uni(
630 session,
631 &[
632 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::default())),
633 TestTopicSyncMessage::Sync(LogSyncMessage::Done),
634 ],
635 )
636 .await
637 .unwrap();
638
639 std::assert_matches!(
640 events_rx.recv().await.unwrap(),
641 TopicLogSyncEvent::SyncStarted { .. }
642 );
643 std::assert_matches!(
644 events_rx.recv().await.unwrap(),
645 TopicLogSyncEvent::SyncFinished { .. }
646 );
647 std::assert_matches!(
648 events_rx.recv().await.unwrap(),
649 TopicLogSyncEvent::SessionFinished { .. }
650 );
651
652 let messages = remote_rx.collect::<Vec<_>>().await;
653 assert_eq!(messages.len(), 2);
654 for (index, message) in messages.into_iter().enumerate() {
655 match index {
656 0 => assert_eq!(
657 message,
658 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::default()))
659 ),
660 1 => {
661 assert_eq!(message, TestTopicSyncMessage::Sync(LogSyncMessage::Done));
662 break;
663 }
664 _ => panic!(),
665 };
666 }
667 }
668
669 #[tokio::test]
670 async fn sync_operations_accept() {
671 setup_logging();
672
673 let log_id = 0;
674 let topic = Topic::random();
675 let mut peer = Peer::new(0).await;
676
677 let body = Body::new("Hello, Sloth!".as_bytes());
678 let (header_0, header_bytes_0) = peer.create_operation(&body, log_id).await;
679 let (header_1, header_bytes_1) = peer.create_operation(&body, log_id).await;
680 let (header_2, header_bytes_2) = peer.create_operation(&body, log_id).await;
681
682 let logs = BTreeMap::from([(peer.id(), vec![log_id])]);
683 peer.associate(&topic, &logs).await;
684
685 let (session, mut events_rx, _) = peer.topic_sync_protocol(topic.clone(), false);
686
687 let (_, remote_rx) = run_protocol_uni(
688 session,
689 &[
690 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::default())),
691 TestTopicSyncMessage::Sync(LogSyncMessage::Done),
692 ],
693 )
694 .await
695 .unwrap();
696
697 std::assert_matches!(
698 events_rx.recv().await.unwrap(),
699 TopicLogSyncEvent::SyncStarted { .. }
700 );
701 std::assert_matches!(
702 events_rx.recv().await.unwrap(),
703 TopicLogSyncEvent::SyncFinished { .. }
704 );
705 std::assert_matches!(
706 events_rx.recv().await.unwrap(),
707 TopicLogSyncEvent::SessionFinished { .. }
708 );
709
710 let messages = remote_rx.collect::<Vec<_>>().await;
711 assert_eq!(messages.len(), 6);
712 for (index, message) in messages.into_iter().enumerate() {
713 match index {
714 0 => assert_eq!(
715 message,
716 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::from([(
717 peer.id(),
718 BTreeMap::from([(0, 2)])
719 )])))
720 ),
721 1 => {
722 let expected_bytes = header_0.payload_size
723 + header_bytes_0.len() as u32
724 + header_1.payload_size
725 + header_bytes_1.len() as u32
726 + header_2.payload_size
727 + header_bytes_2.len() as u32;
728
729 assert_eq!(
730 message,
731 TestTopicSyncMessage::Sync(LogSyncMessage::PreSync {
732 total_operations: 3,
733 total_bytes: expected_bytes
734 })
735 )
736 }
737 2 => {
738 let TestTopicSyncMessage::Sync(LogSyncMessage::Operation(
739 header,
740 Some(body_inner),
741 )) = message
742 else {
743 panic!("Not a TestTopicSyncMessage::Sync: {message:?}");
744 };
745 assert_eq!(header, header_bytes_0);
746 assert_eq!(Body::new(&body_inner), body)
747 }
748 3 => {
749 let TestTopicSyncMessage::Sync(LogSyncMessage::Operation(
750 header,
751 Some(body_inner),
752 )) = message
753 else {
754 panic!("Not a TestTopicSyncMessage::Sync: {message:?}");
755 };
756 assert_eq!(header, header_bytes_1);
757 assert_eq!(Body::new(&body_inner), body)
758 }
759 4 => {
760 let TestTopicSyncMessage::Sync(LogSyncMessage::Operation(
761 header,
762 Some(body_inner),
763 )) = message
764 else {
765 panic!("Not a TestTopicSyncMessage::Sync: {message:?}");
766 };
767 assert_eq!(header, header_bytes_2);
768 assert_eq!(Body::new(&body_inner), body)
769 }
770 5 => {
771 assert_eq!(message, TestTopicSyncMessage::Sync(LogSyncMessage::Done));
772 break;
773 }
774 _ => panic!(),
775 };
776 }
777 }
778
779 #[tokio::test]
780 async fn topic_log_sync_full_duplex() {
781 setup_logging();
782 let topic = Topic::random();
783 let log_id = 0;
784
785 let mut peer_a = Peer::new(0).await;
786 let mut peer_b = Peer::new(1).await;
787
788 let body = Body::new("Hello, Sloth!".as_bytes());
789 let (header_0, _) = peer_a.create_operation(&body, 0).await;
790 let (header_1, _) = peer_a.create_operation(&body, 0).await;
791 let (header_2, _) = peer_a.create_operation(&body, 0).await;
792
793 let logs = BTreeMap::from([(peer_a.id(), vec![log_id])]);
794 peer_a.associate(&topic, &logs).await;
795
796 let (peer_a_session, mut peer_a_events_rx, _) =
797 peer_a.topic_sync_protocol(topic.clone(), false);
798
799 let (peer_b_session, mut peer_b_events_rx, _) =
800 peer_b.topic_sync_protocol(topic.clone(), false);
801
802 run_protocol(peer_a_session, peer_b_session).await.unwrap();
803
804 std::assert_matches!(
806 peer_a_events_rx.recv().await.unwrap(),
807 TopicLogSyncEvent::SyncStarted { .. }
808 );
809 std::assert_matches!(
810 peer_a_events_rx.recv().await.unwrap(),
811 TopicLogSyncEvent::SyncFinished { .. }
812 );
813 std::assert_matches!(
814 peer_a_events_rx.recv().await.unwrap(),
815 TopicLogSyncEvent::SessionFinished { .. }
816 );
817
818 std::assert_matches!(
820 peer_b_events_rx.recv().await.unwrap(),
821 TopicLogSyncEvent::SyncStarted { .. }
822 );
823 let recv = peer_b_events_rx.recv().await.unwrap();
824 let TopicLogSyncEvent::OperationReceived { operation, .. } = recv else {
825 panic!("Not a TopicLogSyncEvent::OperationReceived: {recv:?}");
826 };
827 let Operation {
828 header,
829 body: body_inner,
830 ..
831 } = *operation;
832 assert_eq!(header, header_0);
833 assert_eq!(body_inner.unwrap(), body);
834 let recv = peer_b_events_rx.recv().await.unwrap();
835 let TopicLogSyncEvent::OperationReceived { operation, .. } = recv else {
836 panic!("Not a TopicLogSyncEvent::OperationReceived: {recv:?}");
837 };
838 let Operation {
839 header,
840 body: body_inner,
841 ..
842 } = *operation;
843 assert_eq!(header, header_1);
844 assert_eq!(body_inner.unwrap(), body);
845 let recv = peer_b_events_rx.recv().await.unwrap();
846 let TopicLogSyncEvent::OperationReceived { operation, .. } = recv else {
847 panic!("Not a TopicLogSyncEvent::OperationReceived: {recv:?}");
848 };
849 let Operation {
850 header,
851 body: body_inner,
852 ..
853 } = *operation;
854 assert_eq!(header, header_2);
855 assert_eq!(body_inner.unwrap(), body);
856 std::assert_matches!(
857 peer_b_events_rx.recv().await.unwrap(),
858 TopicLogSyncEvent::SyncFinished { .. }
859 );
860 std::assert_matches!(
861 peer_b_events_rx.recv().await.unwrap(),
862 TopicLogSyncEvent::SessionFinished { .. }
863 );
864 }
865
866 #[tokio::test]
867 async fn live_mode() {
868 let log_id = 0;
869 let topic = Topic::random();
870 let mut peer_a = Peer::new(0).await;
871 let mut peer_b = Peer::new(1).await;
872
873 let body = Body::new("Hello, Sloth!".as_bytes());
874 let (header_0, header_bytes_0) = peer_b.create_operation(&body, log_id).await;
875
876 let logs = BTreeMap::from([(peer_a.id(), vec![log_id])]);
877 peer_a.associate(&topic, &logs).await;
878
879 let logs = BTreeMap::default();
880 peer_a.associate(&topic, &logs).await;
881
882 let (header_1, _) = peer_b.create_operation_no_insert(&body, log_id).await;
883 let expected_bytes_received = header_0.payload_size
884 + header_0.to_bytes().len() as u32
885 + header_1.payload_size
886 + header_1.to_bytes().len() as u32;
887 let (header_2, _) = peer_a.create_operation_no_insert(&body, log_id).await;
888 let expected_bytes_sent = header_2.payload_size + header_2.to_bytes().len() as u32;
889
890 let (protocol, mut events_rx, mut live_mode_tx) =
891 peer_a.topic_sync_protocol(topic.clone(), true);
892
893 live_mode_tx
894 .send(ToSync::Payload(Operation {
895 hash: header_2.hash(),
896 header: header_2.clone(),
897 body: Some(body.clone()),
898 }))
899 .await
900 .unwrap();
901 live_mode_tx.send(ToSync::Close).await.unwrap();
902
903 let total_bytes = header_bytes_0.len() + body.to_bytes().len();
904 let (_, remote_rx) = run_protocol_uni(
905 protocol,
906 &[
907 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::default())),
908 TestTopicSyncMessage::Sync(LogSyncMessage::PreSync {
909 total_operations: 1,
910 total_bytes: total_bytes as u32,
911 }),
912 TestTopicSyncMessage::Sync(LogSyncMessage::Operation(
913 header_bytes_0,
914 Some(body.to_bytes()),
915 )),
916 TestTopicSyncMessage::Sync(LogSyncMessage::Done),
917 TestTopicSyncMessage::Live(header_1.clone(), Some(body.clone())),
918 TestTopicSyncMessage::Close,
919 ],
920 )
921 .await
922 .unwrap();
923
924 std::assert_matches!(
925 events_rx.recv().await.unwrap(),
926 TopicLogSyncEvent::SyncStarted { .. }
927 );
928 std::assert_matches!(
929 events_rx.recv().await.unwrap(),
930 TopicLogSyncEvent::OperationReceived { .. }
931 );
932 std::assert_matches!(
933 events_rx.recv().await.unwrap(),
934 TopicLogSyncEvent::SyncFinished { .. }
935 );
936 std::assert_matches!(
937 events_rx.recv().await.unwrap(),
938 TopicLogSyncEvent::LiveModeStarted
939 );
940 let TopicLogSyncEvent::OperationReceived { metrics, .. } = events_rx.recv().await.unwrap()
941 else {
942 panic!("Not a TopicLogSyncEvent::OperationReceived");
943 };
944 assert_eq!(metrics.received_operations(), 2);
945 assert_eq!(metrics.sent_operations(), 1);
946 assert_eq!(metrics.received_bytes(), expected_bytes_received);
947 assert_eq!(metrics.sent_bytes(), expected_bytes_sent);
948 std::assert_matches!(
949 events_rx.recv().await.unwrap(),
950 TopicLogSyncEvent::SessionFinished { .. }
951 );
952
953 let messages = remote_rx.collect::<Vec<_>>().await;
954 assert_eq!(messages.len(), 4);
955 for (index, message) in messages.into_iter().enumerate() {
956 match index {
957 0 => std::assert_matches!(
958 message,
959 TestTopicSyncMessage::Sync(LogSyncMessage::Have(_))
960 ),
961 1 => {
962 std::assert_matches!(message, TestTopicSyncMessage::Sync(LogSyncMessage::Done))
963 }
964 2 => {
965 let TestTopicSyncMessage::Live(header, Some(body_inner)) = message else {
966 panic!("Not a TestTopicSyncMessage::Live");
967 };
968 assert_eq!(header, header_2);
969 assert_eq!(body_inner, body);
970 }
971 3 => {
972 std::assert_matches!(message, TestTopicSyncMessage::Close)
973 }
974 _ => panic!(),
975 };
976 }
977 }
978
979 #[tokio::test]
980 async fn dedup_live_mode_messages() {
981 let log_id = 0;
982 let topic = Topic::random();
983 let mut peer_a = Peer::new(0).await;
984 let mut peer_b = Peer::new(1).await;
985
986 let body = Body::new("Hello, Sloth!".as_bytes());
987 let (header_0, header_bytes_0) = peer_b.create_operation(&body, log_id).await;
988
989 let logs = BTreeMap::from([(peer_a.id(), vec![log_id])]);
990 peer_a.associate(&topic, &logs).await;
991
992 let logs = BTreeMap::default();
993 peer_a.associate(&topic, &logs).await;
994
995 let (header_1, _) = peer_b.create_operation_no_insert(&body, log_id).await;
996 let expected_bytes_received = header_0.payload_size
997 + header_0.to_bytes().len() as u32
998 + header_1.payload_size
999 + header_1.to_bytes().len() as u32;
1000 let (header_2, _) = peer_a.create_operation_no_insert(&body, log_id).await;
1001 let expected_bytes_sent = header_2.payload_size + header_2.to_bytes().len() as u32;
1002
1003 let (protocol, mut events_rx, mut live_mode_tx) =
1004 peer_a.topic_sync_protocol(topic.clone(), true);
1005
1006 live_mode_tx
1007 .send(ToSync::Payload(Operation {
1008 hash: header_2.hash(),
1009 header: header_2.clone(),
1010 body: Some(body.clone()),
1011 }))
1012 .await
1013 .unwrap();
1014
1015 live_mode_tx
1017 .send(ToSync::Payload(Operation {
1018 hash: header_2.hash(),
1019 header: header_2.clone(),
1020 body: Some(body.clone()),
1021 }))
1022 .await
1023 .unwrap();
1024
1025 live_mode_tx.send(ToSync::Close).await.unwrap();
1026
1027 let total_bytes = header_bytes_0.len() + body.to_bytes().len();
1028 let (_, remote_rx) = run_protocol_uni(
1029 protocol,
1030 &[
1031 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::default())),
1032 TestTopicSyncMessage::Sync(LogSyncMessage::PreSync {
1033 total_operations: 1,
1034 total_bytes: total_bytes as u32,
1035 }),
1036 TestTopicSyncMessage::Sync(LogSyncMessage::Operation(
1037 header_bytes_0,
1038 Some(body.to_bytes()),
1039 )),
1040 TestTopicSyncMessage::Sync(LogSyncMessage::Done),
1041 TestTopicSyncMessage::Live(header_1.clone(), Some(body.clone())),
1042 TestTopicSyncMessage::Live(header_0.clone(), Some(body.clone())),
1044 TestTopicSyncMessage::Live(header_1.clone(), Some(body.clone())),
1046 TestTopicSyncMessage::Close,
1047 ],
1048 )
1049 .await
1050 .unwrap();
1051
1052 std::assert_matches!(
1053 events_rx.recv().await.unwrap(),
1054 TopicLogSyncEvent::SyncStarted { .. }
1055 );
1056 std::assert_matches!(
1057 events_rx.recv().await.unwrap(),
1058 TopicLogSyncEvent::OperationReceived { .. }
1059 );
1060 std::assert_matches!(
1061 events_rx.recv().await.unwrap(),
1062 TopicLogSyncEvent::SyncFinished { .. }
1063 );
1064 std::assert_matches!(
1065 events_rx.recv().await.unwrap(),
1066 TopicLogSyncEvent::LiveModeStarted
1067 );
1068 let TopicLogSyncEvent::OperationReceived { metrics, .. } = events_rx.recv().await.unwrap()
1069 else {
1070 panic!("Not a TopicLogSyncEvent::OperationReceived");
1071 };
1072 assert_eq!(metrics.received_operations(), 2);
1073 assert_eq!(metrics.sent_operations(), 1);
1074 assert_eq!(metrics.received_bytes(), expected_bytes_received);
1075 assert_eq!(metrics.sent_bytes(), expected_bytes_sent);
1076 std::assert_matches!(
1077 events_rx.recv().await.unwrap(),
1078 TopicLogSyncEvent::SessionFinished { .. }
1079 );
1080
1081 let messages = remote_rx.collect::<Vec<_>>().await;
1082 assert_eq!(messages.len(), 4);
1083 for (index, message) in messages.into_iter().enumerate() {
1084 match index {
1085 0 => std::assert_matches!(
1086 message,
1087 TestTopicSyncMessage::Sync(LogSyncMessage::Have(_))
1088 ),
1089 1 => {
1090 std::assert_matches!(message, TestTopicSyncMessage::Sync(LogSyncMessage::Done))
1091 }
1092 2 => {
1093 std::assert_matches!(message, TestTopicSyncMessage::Live(_, Some(_)));
1094 let TestTopicSyncMessage::Live(header, Some(body_inner)) = message else {
1095 unreachable!();
1096 };
1097 assert_eq!(header, header_2);
1098 assert_eq!(body_inner, body);
1099 }
1100 3 => {
1101 std::assert_matches!(message, TestTopicSyncMessage::Close)
1102 }
1103 _ => panic!(),
1104 };
1105 }
1106 }
1107
1108 #[tokio::test]
1109 async fn unexpected_stream_closure_sync() {
1110 let topic = Topic::random();
1111 let mut peer = Peer::new(0).await;
1112 peer.associate(&topic, &Default::default()).await;
1113
1114 let (session, mut events_rx, _live_tx) = peer.topic_sync_protocol(topic.clone(), true);
1115
1116 let messages = [TestTopicSyncMessage::Sync(LogSyncMessage::Have(
1117 BTreeMap::default(),
1118 ))];
1119
1120 let (mut local_message_tx, _remote_message_rx) = mpsc::channel(128);
1121 let (mut remote_message_tx, local_message_rx) = mpsc::channel(128);
1122 let mut local_message_rx = local_message_rx.map(|message| Ok::<_, ()>(message));
1123
1124 for message in messages {
1125 remote_message_tx.send(message.to_owned()).await.unwrap();
1126 }
1127
1128 let handle = tokio::spawn(async move {
1129 session
1130 .run(&mut local_message_tx, &mut local_message_rx)
1131 .await
1132 .expect_err("expected unexpected stream closure")
1133 });
1134
1135 drop(remote_message_tx);
1136
1137 let err = handle.await.unwrap();
1138 std::assert_matches!(
1139 err,
1140 TopicLogSyncError::Sync(LogSyncError::UnexpectedStreamClosure)
1141 );
1142
1143 while let Ok(event) = events_rx.recv().await {
1144 if let TopicLogSyncEvent::Failed { error } = event {
1145 assert_eq!(
1146 error,
1147 "remote unexpectedly closed stream during initial sync".to_string()
1148 );
1149 break;
1150 }
1151 }
1152 }
1153
1154 #[tokio::test]
1155 async fn unexpected_stream_closure_live_mode() {
1156 let topic = Topic::random();
1157 let mut peer = Peer::new(0).await;
1158 peer.associate(&topic, &Default::default()).await;
1159
1160 let (session, mut events_rx, _live_tx) = peer.topic_sync_protocol(topic.clone(), true);
1161
1162 let messages = [
1163 TestTopicSyncMessage::Sync(LogSyncMessage::Have(BTreeMap::default())),
1164 TestTopicSyncMessage::Sync(LogSyncMessage::Done),
1165 ];
1166
1167 let (mut local_message_tx, _remote_message_rx) = mpsc::channel(128);
1168 let (mut remote_message_tx, local_message_rx) = mpsc::channel(128);
1169 let mut local_message_rx = local_message_rx.map(|message| Ok::<_, ()>(message));
1170
1171 for message in messages {
1172 remote_message_tx.send(message.to_owned()).await.unwrap();
1173 }
1174
1175 let handle = tokio::spawn(async move {
1176 session
1177 .run(&mut local_message_tx, &mut local_message_rx)
1178 .await
1179 .expect_err("expected unexpected stream closure")
1180 });
1181
1182 drop(remote_message_tx);
1183
1184 let err = handle.await.unwrap();
1185 std::assert_matches!(err, TopicLogSyncError::UnexpectedStreamClosure);
1186
1187 while let Ok(event) = events_rx.recv().await {
1188 if let TopicLogSyncEvent::Failed { error } = event {
1189 assert_eq!(
1190 error,
1191 "remote unexpectedly closed stream in live-mode".to_string()
1192 );
1193 break;
1194 }
1195 }
1196 }
1197}