Skip to main content

p2panda_sync/protocols/
topic_log_sync.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Two-party sync protocol over a topic associated with a collection of append-only logs.
4use 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/// Protocol for synchronizing logs which are associated with a generic T topic.
31///
32/// The mapping of T to a set of logs is handled on the application layer using an implementation
33/// of the `TopicStore` trait.
34///
35/// After sync is complete peers optionally enter "live-mode" where concurrently received and
36/// future messages will be sent directly to the application layer and forwarded to any
37/// concurrently running sync sessions. As we may receive messages from many sync sessions
38/// concurrently, messages forwarded to a sync session in live-mode are de-duplicated in order to
39/// avoid flooding the network with redundant data.
40///
41/// It is assumed that the T topic has been negotiated between parties prior to initiating this
42/// sync protocol.
43#[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    /// Returns a new sync protocol instance, configured with a store and `TopicStore` implementation
65    /// which associates the to-be-synced logs with a given topic.
66    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    /// Instantiates a sync protocol with custom buffer capacity.
82    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        // TODO: check there is overlap between the local and remote topic filters and end the
121        // session now if not.
122        debug!(
123            live_mode = self.live_mode_rx.is_some(),
124            "start sync session"
125        );
126
127        // Get the log ids which are associated with this topic query.
128        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        // Run the log sync protocol passing in our local topic logs.
147        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            // If the log sync session ended with an error, then send a "failed" event and return
158            // here with the error itself.
159            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                // Enter live-mode.
192                //
193                // In live-mode we process messages sent from the remote peer and received locally from a
194                // subscription or other concurrent sync sessions. In both cases we should deduplicate
195                // messages and also check they are part of our topic sub-set selection before forwarding
196                // them on the event stream, or to the remote peer.
197                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                                    // We send the close and wait for the remote to close the
239                                    // connection.
240
241                                    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                                        // We received the remotes close message and should close the
266                                        // connection ourselves.
267                                        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                                    // TODO: check that this message is a part of our topic T set.
278
279                                    // Insert operation hash into deduplication buffer and if it was
280                                    // previously present do not forward the operation to the application
281                                    // layer.
282                                    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/// Map raw message sink and stream into log sync protocol specific channels.
351#[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/// Error type occurring in topic log sync channels.
377#[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/// Error type occurring in topic log sync protocol.
390#[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/// Events emitted from topic log sync sessions.
462#[derive(Debug, Clone, PartialEq)]
463pub enum TopicLogSyncEvent<E = ()> {
464    /// A session has been initiated locally.
465    ///
466    /// This event is always sent and will be followed by `SyncStarted` or `Failed` events.
467    SessionStarted,
468
469    /// We have exchanged initial session metrics with the remote and the sync phase of this
470    /// session has started.
471    ///
472    /// This event will be followed by any number of `OperationReceived` events, or a `SyncFinished` or `Failed`.
473    SyncStarted { metrics: Metrics },
474
475    /// All past state has been replicated and we will now enter live mode `LiveModeStarted` (if configured) or the
476    /// session will end `SessionFinished`.
477    ///
478    /// This event will be followed by a `LiveModeStarted` event or a `SyncFinished` or `Failed` event.
479    SyncFinished { metrics: Metrics },
480
481    /// The session has entered live mode, we will send and receive operations in realtime.
482    ///
483    /// This event will be followed by any number of `OperationReceived` events or a `SyncFinished` or `Failed` event.
484    LiveModeStarted,
485
486    /// An operation has been received, this can be in the "sync" or "live mode" phase of a session.
487    OperationReceived {
488        operation: Box<Operation<E>>,
489        metrics: Metrics,
490    },
491
492    /// The session has finished.
493    ///
494    /// When no error occurs this event will always be sent at the end of a session.
495    SessionFinished { metrics: Metrics },
496
497    /// The session failed.
498    ///
499    /// This event will always be the final event sent when an error occurred in any phase of the
500    /// session.
501    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/// Protocol message types.
521#[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    /// Sink wrapper which converts messages and errors into the expected types.
551    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        // Assert peer a events.
805        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        // Assert peer b events.
819        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        // Sending subscription message twice.
1016        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                // Duplicate of message sent during sync.
1043                TestTopicSyncMessage::Live(header_0.clone(), Some(body.clone())),
1044                // Duplicate of message sent earlier in live-mode.
1045                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}