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