Skip to main content

consensus_core/
subscriber.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{sync::Arc, time::Duration};
5
6use consensus_config::AuthorityIndex;
7use consensus_types::block::Round;
8use futures::StreamExt;
9use mysten_metrics::spawn_monitored_task;
10use parking_lot::{Mutex, RwLock};
11use tokio::{task::JoinHandle, time::sleep};
12use tracing::{debug, error, info};
13
14use crate::{
15    block::BlockAPI as _,
16    context::Context,
17    dag_state::DagState,
18    error::ConsensusError,
19    network::{ValidatorNetworkClient, ValidatorNetworkService},
20};
21
22/// Subscriber manages the block stream subscriptions to other peers, taking care of retrying
23/// when subscription streams break. Blocks returned from the peer are sent to the authority
24/// service for processing.
25/// Currently subscription management for individual peer is not exposed, but it could become
26/// useful in future.
27pub(crate) struct Subscriber<C: ValidatorNetworkClient, S: ValidatorNetworkService> {
28    context: Arc<Context>,
29    network_client: Arc<C>,
30    authority_service: Arc<S>,
31    dag_state: Arc<RwLock<DagState>>,
32    subscriptions: Arc<Mutex<Box<[Option<JoinHandle<()>>]>>>,
33}
34
35impl<C: ValidatorNetworkClient, S: ValidatorNetworkService> Subscriber<C, S> {
36    pub(crate) fn new(
37        context: Arc<Context>,
38        network_client: Arc<C>,
39        authority_service: Arc<S>,
40        dag_state: Arc<RwLock<DagState>>,
41    ) -> Self {
42        let subscriptions = (0..context.committee.size())
43            .map(|_| None)
44            .collect::<Vec<_>>();
45        Self {
46            context,
47            network_client,
48            authority_service,
49            dag_state,
50            subscriptions: Arc::new(Mutex::new(subscriptions.into_boxed_slice())),
51        }
52    }
53
54    pub(crate) fn subscribe(&self, peer: AuthorityIndex) {
55        if peer == self.context.own_index {
56            error!("Attempt to subscribe to own validator {peer} is ignored!");
57            return;
58        }
59        let context = self.context.clone();
60        let network_client = self.network_client.clone();
61        let authority_service = self.authority_service.clone();
62        let dag_state = self.dag_state.clone();
63
64        let mut subscriptions = self.subscriptions.lock();
65        self.unsubscribe_locked(peer, &mut subscriptions[peer.value()]);
66        subscriptions[peer.value()] = Some(spawn_monitored_task!(Self::subscription_loop(
67            context,
68            network_client,
69            authority_service,
70            dag_state,
71            peer,
72        )));
73    }
74
75    pub(crate) fn stop(&self) {
76        let mut subscriptions = self.subscriptions.lock();
77        for (peer, _) in self.context.committee.authorities() {
78            self.unsubscribe_locked(peer, &mut subscriptions[peer.value()]);
79        }
80    }
81
82    fn unsubscribe_locked(&self, peer: AuthorityIndex, subscription: &mut Option<JoinHandle<()>>) {
83        let peer_hostname = &self.context.committee.authority(peer).hostname;
84        if let Some(subscription) = subscription.take() {
85            subscription.abort();
86        }
87        // There is a race between shutting down the subscription task and clearing the metric here.
88        // TODO: fix the race when unsubscribe_locked() gets called outside of stop().
89        self.context
90            .metrics
91            .node_metrics
92            .subscribed_to
93            .with_label_values(&[peer_hostname])
94            .set(0);
95    }
96
97    async fn subscription_loop(
98        context: Arc<Context>,
99        network_client: Arc<C>,
100        authority_service: Arc<S>,
101        dag_state: Arc<RwLock<DagState>>,
102        peer: AuthorityIndex,
103    ) {
104        const IMMEDIATE_RETRIES: i64 = 3;
105        const MIN_TIMEOUT: Duration = Duration::from_millis(500);
106        // When not immediately retrying, limit retry delay between 100ms and 10s.
107        let mut backoff = mysten_common::backoff::ExponentialBackoff::new(
108            Duration::from_millis(100),
109            Duration::from_secs(10),
110        );
111
112        let peer_hostname = &context.committee.authority(peer).hostname;
113        let mut retries: i64 = 0;
114        'subscription: loop {
115            context
116                .metrics
117                .node_metrics
118                .subscribed_to
119                .with_label_values(&[peer_hostname])
120                .set(0);
121
122            let mut delay = Duration::ZERO;
123            if retries > IMMEDIATE_RETRIES {
124                delay = backoff.next().unwrap();
125                debug!(
126                    "Delaying retry {} of peer {} subscription, in {} seconds",
127                    retries,
128                    peer_hostname,
129                    delay.as_secs_f32(),
130                );
131                sleep(delay).await;
132            } else if retries > 0 {
133                // Retry immediately, but still yield to avoid monopolizing the thread.
134                tokio::task::yield_now().await;
135            }
136            retries += 1;
137
138            // Recompute the resume round from DagState before each connection attempt, so a
139            // reconnection resumes from the latest accepted round rather than re-streaming and
140            // re-verifying blocks that have been accepted since this subscription started.
141            let last_received: Round = {
142                let dag_state = dag_state.read();
143                let gc_round = dag_state.gc_round();
144                dag_state
145                    .get_last_block_for_authority(peer)
146                    .round()
147                    .max(gc_round)
148            };
149
150            // Use longer timeout when retry delay is long, to adapt to slow network.
151            let request_timeout = MIN_TIMEOUT.max(delay);
152            let mut blocks = match network_client
153                .subscribe_blocks(peer, last_received, request_timeout)
154                .await
155            {
156                Ok(blocks) => {
157                    debug!(
158                        "Subscribed to peer {} {} after {} attempts",
159                        peer, peer_hostname, retries
160                    );
161                    context
162                        .metrics
163                        .node_metrics
164                        .subscriber_connection_attempts
165                        .with_label_values(&[peer_hostname.as_str(), "success"])
166                        .inc();
167                    blocks
168                }
169                Err(e) => {
170                    debug!(
171                        "Failed to subscribe to blocks from peer {} {}: {}",
172                        peer, peer_hostname, e
173                    );
174                    context
175                        .metrics
176                        .node_metrics
177                        .subscriber_connection_attempts
178                        .with_label_values(&[peer_hostname.as_str(), "failure"])
179                        .inc();
180                    continue 'subscription;
181                }
182            };
183
184            // Now can consider the subscription successful
185            context
186                .metrics
187                .node_metrics
188                .subscribed_to
189                .with_label_values(&[peer_hostname])
190                .set(1);
191
192            'stream: loop {
193                match blocks.next().await {
194                    Some(block) => {
195                        context
196                            .metrics
197                            .node_metrics
198                            .subscribed_blocks
199                            .with_label_values(&[peer_hostname])
200                            .inc();
201                        let result = authority_service.handle_send_block(peer, block).await;
202                        if let Err(e) = result {
203                            match e {
204                                ConsensusError::BlockRejected { block_ref, reason } => {
205                                    debug!(
206                                        "Failed to process block from peer {} {} for block {:?}: {}",
207                                        peer, peer_hostname, block_ref, reason
208                                    );
209                                }
210                                _ => {
211                                    info!(
212                                        "Invalid block received from peer {} {}: {}",
213                                        peer, peer_hostname, e
214                                    );
215                                }
216                            }
217                        }
218                        // Reset the retry counter and backoff when a block is received, so a peer
219                        // that recovers after flapping reconnects promptly instead of inheriting
220                        // the previously escalated delay.
221                        retries = 0;
222                        backoff.reset();
223                    }
224                    None => {
225                        debug!(
226                            "Subscription to blocks from peer {} {} ended",
227                            peer, peer_hostname
228                        );
229                        retries += 1;
230                        break 'stream;
231                    }
232                }
233            }
234        }
235    }
236}
237
238#[cfg(test)]
239mod test {
240    use async_trait::async_trait;
241    use bytes::Bytes;
242    use consensus_types::block::BlockRef;
243    use futures::stream;
244
245    use super::*;
246    use crate::{
247        VerifiedBlock,
248        commit::CommitRange,
249        error::ConsensusResult,
250        network::{BlockStream, ExtendedSerializedBlock, test_network::TestService},
251        storage::mem_store::MemStore,
252    };
253
254    struct SubscriberTestClient {
255        // Records the `last_received` round passed to each subscribe_blocks() call.
256        subscribe_calls: Mutex<Vec<Round>>,
257    }
258
259    impl SubscriberTestClient {
260        fn new() -> Self {
261            Self {
262                subscribe_calls: Mutex::new(Vec::new()),
263            }
264        }
265
266        fn subscribe_calls(&self) -> Vec<Round> {
267            self.subscribe_calls.lock().clone()
268        }
269    }
270
271    #[async_trait]
272    impl ValidatorNetworkClient for SubscriberTestClient {
273        async fn send_block(
274            &self,
275            _peer: AuthorityIndex,
276            _block: &VerifiedBlock,
277            _timeout: Duration,
278        ) -> ConsensusResult<()> {
279            unimplemented!("Unimplemented")
280        }
281
282        async fn subscribe_blocks(
283            &self,
284            _peer: AuthorityIndex,
285            last_received: Round,
286            _timeout: Duration,
287        ) -> ConsensusResult<BlockStream> {
288            self.subscribe_calls.lock().push(last_received);
289            let block_stream = stream::unfold((), |_| async {
290                sleep(Duration::from_millis(1)).await;
291                let block = ExtendedSerializedBlock {
292                    block: Bytes::from(vec![1u8; 8]),
293                    excluded_ancestors: vec![],
294                };
295                Some((block, ()))
296            })
297            .take(10);
298            Ok(Box::pin(block_stream))
299        }
300
301        async fn fetch_blocks(
302            &self,
303            _peer: AuthorityIndex,
304            _block_refs: Vec<BlockRef>,
305            _fetch_after_rounds: Vec<Round>,
306            _fetch_missing_ancestors: bool,
307            _timeout: Duration,
308        ) -> ConsensusResult<Vec<Bytes>> {
309            unimplemented!("Unimplemented")
310        }
311
312        async fn fetch_commits(
313            &self,
314            _peer: AuthorityIndex,
315            _commit_range: CommitRange,
316            _timeout: Duration,
317        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
318            unimplemented!("Unimplemented")
319        }
320
321        async fn fetch_latest_blocks(
322            &self,
323            _peer: AuthorityIndex,
324            _authorities: Vec<AuthorityIndex>,
325            _timeout: Duration,
326        ) -> ConsensusResult<Vec<Bytes>> {
327            unimplemented!("Unimplemented")
328        }
329
330        async fn get_latest_rounds(
331            &self,
332            _peer: AuthorityIndex,
333            _timeout: Duration,
334        ) -> ConsensusResult<(Vec<Round>, Vec<Round>)> {
335            unimplemented!("Unimplemented")
336        }
337    }
338
339    #[tokio::test(flavor = "current_thread", start_paused = true)]
340    async fn subscriber_retries() {
341        let (context, _keys) = Context::new_for_test(4);
342        let context = Arc::new(context);
343        let authority_service = Arc::new(Mutex::new(TestService::new()));
344        let network_client = Arc::new(SubscriberTestClient::new());
345        let store = Arc::new(MemStore::new());
346        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
347        let subscriber = Subscriber::new(
348            context.clone(),
349            network_client,
350            authority_service.clone(),
351            dag_state,
352        );
353
354        let peer = context.committee.to_authority_index(2).unwrap();
355        subscriber.subscribe(peer);
356
357        // Wait for enough blocks received.
358        for _ in 0..10 {
359            tokio::time::sleep(Duration::from_secs(1)).await;
360            let service = authority_service.lock();
361            if service.handle_send_block.len() >= 100 {
362                break;
363            }
364        }
365
366        // Even if the stream ends after 10 blocks, the subscriber should retry and get enough
367        // blocks eventually.
368        let service = authority_service.lock();
369        assert!(service.handle_send_block.len() >= 100);
370        for (p, block) in service.handle_send_block.iter() {
371            assert_eq!(*p, peer);
372            assert_eq!(
373                *block,
374                ExtendedSerializedBlock {
375                    block: Bytes::from(vec![1u8; 8]),
376                    excluded_ancestors: vec![]
377                }
378            );
379        }
380    }
381
382    // Regression test: `last_received` must be recomputed from DagState before each connection
383    // attempt. Previously it was captured once at subscribe() time and reused for every reconnect,
384    // causing already-accepted blocks to be re-streamed and re-verified.
385    #[tokio::test(flavor = "current_thread", start_paused = true)]
386    async fn subscriber_recomputes_resume_round_on_reconnect() {
387        use crate::block::TestBlock;
388
389        let (context, _keys) = Context::new_for_test(4);
390        let context = Arc::new(context);
391        let store = Arc::new(MemStore::new());
392        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
393        let network_client = Arc::new(SubscriberTestClient::new());
394        let authority_service = Arc::new(Mutex::new(TestService::new()));
395        let subscriber = Subscriber::new(
396            context.clone(),
397            network_client.clone(),
398            authority_service,
399            dag_state.clone(),
400        );
401
402        let peer = context.committee.to_authority_index(2).unwrap();
403        subscriber.subscribe(peer);
404
405        // Before any block from the peer is accepted, every reconnect resumes from genesis (0).
406        tokio::time::sleep(Duration::from_secs(3)).await;
407        {
408            let recorded = network_client.subscribe_calls();
409            assert!(
410                !recorded.is_empty() && recorded.iter().all(|&r| r == 0),
411                "before a block is accepted, every reconnect should resume from round 0: {recorded:?}"
412            );
413        }
414
415        // Advance the locally accepted round for the peer.
416        const RESUME_ROUND: Round = 10;
417        dag_state.write().accept_block(VerifiedBlock::new_for_test(
418            TestBlock::new(RESUME_ROUND, peer.value() as u32).build(),
419        ));
420
421        // After the block is accepted, reconnects must resume from the advanced round. With the
422        // bug, `last_received` would stay at 0 forever.
423        let mut observed_resume = false;
424        for _ in 0..10 {
425            tokio::time::sleep(Duration::from_secs(1)).await;
426            if network_client.subscribe_calls().last() == Some(&RESUME_ROUND) {
427                observed_resume = true;
428                break;
429            }
430        }
431        assert!(
432            observed_resume,
433            "after accepting a block at round {RESUME_ROUND}, the subscriber should resume from it; \
434             recorded resume rounds: {:?}",
435            network_client.subscribe_calls()
436        );
437    }
438}