Skip to main content

consensus_core/
subscriber.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    sync::{Arc, Weak},
6    time::Duration,
7};
8
9use consensus_config::AuthorityIndex;
10use consensus_types::block::Round;
11use futures::StreamExt;
12use mysten_metrics::spawn_monitored_task;
13use parking_lot::{Mutex, RwLock};
14use tokio::{
15    task::JoinHandle,
16    time::{sleep, timeout},
17};
18use tracing::{debug, error, info};
19
20use crate::{
21    block::BlockAPI as _,
22    context::Context,
23    dag_state::DagState,
24    error::ConsensusError,
25    network::{ValidatorNetworkClient, ValidatorNetworkService},
26    task::{join_and_propagate_panic, reap_finished_task},
27};
28
29/// Bounds both establishing a subscription and waiting for the next block on it, so the
30/// subscription is abandoned and retried when either makes no progress for this long. A healthy
31/// peer proposes blocks multiple times per second, and (re)subscribing to a peer that has proposed
32/// before immediately yields at least its last proposed block, so timeouts and reconnections stay
33/// rare unless the peer is not proposing. This primarily guards against subscriptions that stop
34/// making progress without surfacing a transport error, e.g. a peer whose runtime stalls while its
35/// connections stay open. Kept well above the expected gap between proposals, because a peer that
36/// is reachable but not proposing gets resubscribed to on every timeout.
37const SUBSCRIPTION_TIMEOUT: Duration = Duration::from_secs(30);
38
39/// Subscriber manages the block stream subscriptions to other peers, taking care of retrying
40/// when subscription streams break. Blocks returned from the peer are sent to the authority
41/// service for processing.
42/// Currently subscription management for individual peer is not exposed, but it could become
43/// useful in future.
44pub(crate) struct Subscriber<C: ValidatorNetworkClient, S: ValidatorNetworkService> {
45    context: Arc<Context>,
46    network_client: Arc<C>,
47    authority_service: Arc<S>,
48    dag_state: Arc<RwLock<DagState>>,
49    subscriptions: Arc<Mutex<Box<[Option<JoinHandle<()>>]>>>,
50    // Retain replaced subscription tasks so stop() can await them and propagate panics.
51    retired_subscriptions: Arc<Mutex<Vec<JoinHandle<()>>>>,
52}
53
54impl<C: ValidatorNetworkClient, S: ValidatorNetworkService> Subscriber<C, S> {
55    pub(crate) fn new(
56        context: Arc<Context>,
57        network_client: Arc<C>,
58        authority_service: Arc<S>,
59        dag_state: Arc<RwLock<DagState>>,
60    ) -> Self {
61        let subscriptions = (0..context.committee.size())
62            .map(|_| None)
63            .collect::<Vec<_>>();
64        Self {
65            context,
66            network_client,
67            authority_service,
68            dag_state,
69            subscriptions: Arc::new(Mutex::new(subscriptions.into_boxed_slice())),
70            retired_subscriptions: Arc::new(Mutex::new(Vec::new())),
71        }
72    }
73
74    pub(crate) fn subscribe(&self, peer: AuthorityIndex) {
75        if peer == self.context.own_index {
76            error!("Attempt to subscribe to own validator {peer} is ignored!");
77            return;
78        }
79        let context = self.context.clone();
80        let network_client = self.network_client.clone();
81        // Subscriber already holds these resources strongly. Give subscription tasks weak
82        // references so they do not become additional owners during shutdown.
83        let authority_service = Arc::downgrade(&self.authority_service);
84        let dag_state = Arc::downgrade(&self.dag_state);
85
86        let mut subscriptions = self.subscriptions.lock();
87        self.unsubscribe_locked(peer, &mut subscriptions[peer.value()]);
88        subscriptions[peer.value()] = Some(spawn_monitored_task!(Self::subscription_loop(
89            context,
90            network_client,
91            authority_service,
92            dag_state,
93            peer,
94        )));
95    }
96
97    pub(crate) async fn stop(&self) {
98        {
99            let mut subscriptions = self.subscriptions.lock();
100            for (peer, _) in self.context.committee.authorities() {
101                self.unsubscribe_locked(peer, &mut subscriptions[peer.value()]);
102            }
103        }
104
105        // All retired subscriptions have already been aborted by unsubscribe_locked().
106        let subscriptions = std::mem::take(&mut *self.retired_subscriptions.lock());
107        for subscription in subscriptions {
108            join_and_propagate_panic(subscription).await;
109        }
110    }
111
112    fn unsubscribe_locked(&self, peer: AuthorityIndex, subscription: &mut Option<JoinHandle<()>>) {
113        let peer_hostname = &self.context.committee.authority(peer).hostname;
114        if let Some(subscription) = subscription.take() {
115            subscription.abort();
116            let mut retired_subscriptions = self.retired_subscriptions.lock();
117            // Reap retired subscriptions that have finished, so the list stays bounded under
118            // repeated resubscriptions.
119            retired_subscriptions.retain_mut(|task| !reap_finished_task(task));
120            retired_subscriptions.push(subscription);
121        }
122        // There is a race between shutting down the subscription task and clearing the metric here.
123        // TODO: fix the race when unsubscribe_locked() gets called outside of stop().
124        self.context
125            .metrics
126            .node_metrics
127            .subscribed_to
128            .with_label_values(&[peer_hostname])
129            .set(0);
130    }
131
132    async fn subscription_loop(
133        context: Arc<Context>,
134        network_client: Arc<C>,
135        authority_service: Weak<S>,
136        dag_state: Weak<RwLock<DagState>>,
137        peer: AuthorityIndex,
138    ) {
139        const IMMEDIATE_RETRIES: i64 = 3;
140        const MIN_TIMEOUT: Duration = Duration::from_millis(500);
141        // When not immediately retrying, limit retry delay between 100ms and 10s.
142        let mut backoff = mysten_common::backoff::ExponentialBackoff::new(
143            Duration::from_millis(100),
144            Duration::from_secs(10),
145        );
146
147        let peer_hostname = &context.committee.authority(peer).hostname;
148        let mut retries: i64 = 0;
149        'subscription: loop {
150            context
151                .metrics
152                .node_metrics
153                .subscribed_to
154                .with_label_values(&[peer_hostname])
155                .set(0);
156
157            let mut delay = Duration::ZERO;
158            if retries > IMMEDIATE_RETRIES {
159                delay = backoff.next().unwrap();
160                debug!(
161                    "Delaying retry {} of peer {} subscription, in {} seconds",
162                    retries,
163                    peer_hostname,
164                    delay.as_secs_f32(),
165                );
166                sleep(delay).await;
167            } else if retries > 0 {
168                // Retry immediately, but still yield to avoid monopolizing the thread.
169                tokio::task::yield_now().await;
170            }
171            retries += 1;
172
173            let last_accepted: Round = {
174                let Some(dag_state) = dag_state.upgrade() else {
175                    return;
176                };
177                let dag_state = dag_state.read();
178                let gc_round = dag_state.gc_round();
179                dag_state
180                    .get_last_block_for_authority(peer)
181                    .round()
182                    .max(gc_round)
183            };
184
185            // Use longer timeout when retry delay is long, to adapt to slow network.
186            let request_timeout = MIN_TIMEOUT.max(delay);
187            // `request_timeout` only bounds acquiring the channel, and the channel is usually
188            // cached, so establishing the stream can otherwise block indefinitely waiting for the
189            // peer's response headers, e.g. when the peer accepts connections but its runtime is
190            // stalled. Bound it here rather than with a gRPC deadline on the request, which would
191            // cap the lifetime of the whole subscription.
192            let subscribe = timeout(
193                SUBSCRIPTION_TIMEOUT,
194                network_client.subscribe_blocks(peer, last_accepted, request_timeout),
195            )
196            .await;
197            let mut blocks = match subscribe {
198                Ok(Ok(blocks)) => {
199                    debug!(
200                        "Subscribed to peer {} {} after {} attempts",
201                        peer, peer_hostname, retries
202                    );
203                    context
204                        .metrics
205                        .node_metrics
206                        .subscriber_connection_attempts
207                        .with_label_values(&[peer_hostname.as_str(), "success"])
208                        .inc();
209                    blocks
210                }
211                Ok(Err(e)) => {
212                    debug!(
213                        "Failed to subscribe to blocks from peer {} {}: {}",
214                        peer, peer_hostname, e
215                    );
216                    context
217                        .metrics
218                        .node_metrics
219                        .subscriber_connection_attempts
220                        .with_label_values(&[peer_hostname.as_str(), "failure"])
221                        .inc();
222                    continue 'subscription;
223                }
224                Err(_) => {
225                    debug!(
226                        "Timed out subscribing to blocks from peer {} {} after {:?}",
227                        peer, peer_hostname, SUBSCRIPTION_TIMEOUT
228                    );
229                    context
230                        .metrics
231                        .node_metrics
232                        .subscriber_connection_attempts
233                        .with_label_values(&[peer_hostname.as_str(), "failure"])
234                        .inc();
235                    continue 'subscription;
236                }
237            };
238
239            // Now can consider the subscription successful
240            context
241                .metrics
242                .node_metrics
243                .subscribed_to
244                .with_label_values(&[peer_hostname])
245                .set(1);
246
247            'stream: loop {
248                match timeout(SUBSCRIPTION_TIMEOUT, blocks.next()).await {
249                    Ok(Some(block)) => {
250                        context
251                            .metrics
252                            .node_metrics
253                            .subscribed_blocks
254                            .with_label_values(&[peer_hostname])
255                            .inc();
256                        let Some(authority_service) = authority_service.upgrade() else {
257                            return;
258                        };
259                        let result = authority_service.handle_send_block(peer, block).await;
260                        if let Err(e) = result {
261                            match e {
262                                ConsensusError::BlockRejected { block_ref, reason } => {
263                                    debug!(
264                                        "Failed to process block from peer {} {} for block {:?}: {}",
265                                        peer, peer_hostname, block_ref, reason
266                                    );
267                                }
268                                _ => {
269                                    info!(
270                                        "Invalid block received from peer {} {}: {}",
271                                        peer, peer_hostname, e
272                                    );
273                                }
274                            }
275                        }
276                        // Reset the retry counter and backoff when a block is received, so a peer
277                        // that recovers after flapping reconnects promptly instead of inheriting
278                        // the previously escalated delay.
279                        retries = 0;
280                        backoff.reset();
281                    }
282                    Ok(None) => {
283                        debug!(
284                            "Subscription to blocks from peer {} {} ended",
285                            peer, peer_hostname
286                        );
287                        retries += 1;
288                        break 'stream;
289                    }
290                    Err(_) => {
291                        info!(
292                            "Subscription to blocks from peer {} {} made no progress for {:?}",
293                            peer, peer_hostname, SUBSCRIPTION_TIMEOUT
294                        );
295                        retries += 1;
296                        break 'stream;
297                    }
298                }
299            }
300        }
301    }
302}
303
304#[cfg(test)]
305mod test {
306    use async_trait::async_trait;
307    use bytes::Bytes;
308    use consensus_types::block::BlockRef;
309    use futures::stream;
310
311    use super::*;
312    use crate::{
313        VerifiedBlock,
314        commit::CommitRange,
315        error::ConsensusResult,
316        network::{BlockStream, ExtendedSerializedBlock, test_network::TestService},
317        storage::mem_store::MemStore,
318    };
319
320    struct SubscriberTestClient {
321        // Records the `last_received` round passed to each subscribe_blocks() call.
322        subscribe_calls: Mutex<Vec<Round>>,
323        // Interval between blocks on the returned stream. None keeps the stream open
324        // forever without yielding any block.
325        block_interval: Option<Duration>,
326        // When true, subscribe_blocks() itself never returns.
327        hang_on_subscribe: bool,
328    }
329
330    impl SubscriberTestClient {
331        fn new() -> Self {
332            Self::new_with_block_interval(Duration::from_millis(1))
333        }
334
335        fn new_pending() -> Self {
336            Self {
337                subscribe_calls: Mutex::new(Vec::new()),
338                block_interval: None,
339                hang_on_subscribe: false,
340            }
341        }
342
343        fn new_with_block_interval(interval: Duration) -> Self {
344            Self {
345                subscribe_calls: Mutex::new(Vec::new()),
346                block_interval: Some(interval),
347                hang_on_subscribe: false,
348            }
349        }
350
351        fn new_hanging_subscribe() -> Self {
352            Self {
353                subscribe_calls: Mutex::new(Vec::new()),
354                block_interval: None,
355                hang_on_subscribe: true,
356            }
357        }
358
359        fn subscribe_calls(&self) -> Vec<Round> {
360            self.subscribe_calls.lock().clone()
361        }
362    }
363
364    #[async_trait]
365    impl ValidatorNetworkClient for SubscriberTestClient {
366        async fn send_block(
367            &self,
368            _peer: AuthorityIndex,
369            _block: &VerifiedBlock,
370            _timeout: Duration,
371        ) -> ConsensusResult<()> {
372            unimplemented!("Unimplemented")
373        }
374
375        async fn subscribe_blocks(
376            &self,
377            _peer: AuthorityIndex,
378            last_received: Round,
379            _timeout: Duration,
380        ) -> ConsensusResult<BlockStream> {
381            self.subscribe_calls.lock().push(last_received);
382            if self.hang_on_subscribe {
383                std::future::pending::<()>().await;
384            }
385            let Some(interval) = self.block_interval else {
386                return Ok(Box::pin(stream::pending()));
387            };
388            let block_stream = stream::unfold((), move |_| async move {
389                sleep(interval).await;
390                let block = ExtendedSerializedBlock {
391                    block: Bytes::from(vec![1u8; 8]),
392                    excluded_ancestors: vec![],
393                };
394                Some((block, ()))
395            })
396            .take(10);
397            Ok(Box::pin(block_stream))
398        }
399
400        async fn fetch_blocks(
401            &self,
402            _peer: AuthorityIndex,
403            _block_refs: Vec<BlockRef>,
404            _fetch_after_rounds: Vec<Round>,
405            _fetch_missing_ancestors: bool,
406            _timeout: Duration,
407        ) -> ConsensusResult<Vec<Bytes>> {
408            unimplemented!("Unimplemented")
409        }
410
411        async fn fetch_commits(
412            &self,
413            _peer: AuthorityIndex,
414            _commit_range: CommitRange,
415            _timeout: Duration,
416        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
417            unimplemented!("Unimplemented")
418        }
419
420        async fn fetch_latest_blocks(
421            &self,
422            _peer: AuthorityIndex,
423            _authorities: Vec<AuthorityIndex>,
424            _timeout: Duration,
425        ) -> ConsensusResult<Vec<Bytes>> {
426            unimplemented!("Unimplemented")
427        }
428
429        async fn get_latest_rounds(
430            &self,
431            _peer: AuthorityIndex,
432            _timeout: Duration,
433        ) -> ConsensusResult<(Vec<Round>, Vec<Round>)> {
434            unimplemented!("Unimplemented")
435        }
436    }
437
438    #[tokio::test(flavor = "current_thread", start_paused = true)]
439    async fn subscriber_retries() {
440        let (context, _keys) = Context::new_for_test(4);
441        let context = Arc::new(context);
442        let authority_service = Arc::new(Mutex::new(TestService::new()));
443        let network_client = Arc::new(SubscriberTestClient::new());
444        let store = Arc::new(MemStore::new());
445        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
446        let subscriber = Subscriber::new(
447            context.clone(),
448            network_client,
449            authority_service.clone(),
450            dag_state,
451        );
452
453        let peer = context.committee.to_authority_index(2).unwrap();
454        subscriber.subscribe(peer);
455
456        // Wait for enough blocks received.
457        for _ in 0..10 {
458            tokio::time::sleep(Duration::from_secs(1)).await;
459            let service = authority_service.lock();
460            if service.handle_send_block.len() >= 100 {
461                break;
462            }
463        }
464
465        // Even if the stream ends after 10 blocks, the subscriber should retry and get enough
466        // blocks eventually.
467        let service = authority_service.lock();
468        assert!(service.handle_send_block.len() >= 100);
469        for (p, block) in service.handle_send_block.iter() {
470            assert_eq!(*p, peer);
471            assert_eq!(
472                *block,
473                ExtendedSerializedBlock {
474                    block: Bytes::from(vec![1u8; 8]),
475                    excluded_ancestors: vec![]
476                }
477            );
478        }
479    }
480
481    #[tokio::test(flavor = "current_thread", start_paused = true)]
482    async fn subscriber_reconnects_when_stream_makes_no_progress() {
483        let (context, _keys) = Context::new_for_test(4);
484        let context = Arc::new(context);
485        let authority_service = Arc::new(Mutex::new(TestService::new()));
486        let network_client = Arc::new(SubscriberTestClient::new_pending());
487        let store = Arc::new(MemStore::new());
488        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
489        let subscriber = Subscriber::new(
490            context.clone(),
491            network_client.clone(),
492            authority_service,
493            dag_state,
494        );
495
496        let peer = context.committee.to_authority_index(2).unwrap();
497        subscriber.subscribe(peer);
498
499        tokio::time::sleep(SUBSCRIPTION_TIMEOUT + Duration::from_millis(1)).await;
500
501        assert!(
502            network_client.subscribe_calls().len() >= 2,
503            "an idle subscription should be re-established"
504        );
505    }
506
507    #[tokio::test(flavor = "current_thread", start_paused = true)]
508    async fn subscriber_retries_when_subscribing_makes_no_progress() {
509        let (context, _keys) = Context::new_for_test(4);
510        let context = Arc::new(context);
511        let authority_service = Arc::new(Mutex::new(TestService::new()));
512        // The peer accepts the subscription but never responds, so the request to establish the
513        // stream never completes.
514        let network_client = Arc::new(SubscriberTestClient::new_hanging_subscribe());
515        let store = Arc::new(MemStore::new());
516        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
517        let subscriber = Subscriber::new(
518            context.clone(),
519            network_client.clone(),
520            authority_service,
521            dag_state,
522        );
523
524        let peer = context.committee.to_authority_index(2).unwrap();
525        subscriber.subscribe(peer);
526
527        tokio::time::sleep(SUBSCRIPTION_TIMEOUT + Duration::from_millis(1)).await;
528
529        assert!(
530            network_client.subscribe_calls().len() >= 2,
531            "subscribing should be abandoned and retried when the peer never responds"
532        );
533    }
534
535    #[tokio::test(flavor = "current_thread", start_paused = true)]
536    async fn subscriber_stays_subscribed_when_stream_progresses_within_idle_timeout() {
537        let (context, _keys) = Context::new_for_test(4);
538        let context = Arc::new(context);
539        let authority_service = Arc::new(Mutex::new(TestService::new()));
540        // Blocks arrive slower than from a healthy peer but within the idle timeout, so the
541        // timeout must reset on every received block and never tear down the subscription.
542        let network_client = Arc::new(SubscriberTestClient::new_with_block_interval(
543            SUBSCRIPTION_TIMEOUT - Duration::from_secs(1),
544        ));
545        let store = Arc::new(MemStore::new());
546        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
547        let subscriber = Subscriber::new(
548            context.clone(),
549            network_client.clone(),
550            authority_service.clone(),
551            dag_state,
552        );
553
554        let peer = context.committee.to_authority_index(2).unwrap();
555        subscriber.subscribe(peer);
556
557        tokio::time::sleep(SUBSCRIPTION_TIMEOUT * 4).await;
558
559        assert_eq!(
560            network_client.subscribe_calls().len(),
561            1,
562            "a stream that keeps delivering blocks within the idle timeout should not be re-established"
563        );
564        assert!(
565            !authority_service.lock().handle_send_block.is_empty(),
566            "blocks from the slow stream should have been processed"
567        );
568    }
569
570    // Regression test: `last_received` must be recomputed from DagState before each connection
571    // attempt. Previously it was captured once at subscribe() time and reused for every reconnect,
572    // causing already-accepted blocks to be re-streamed and re-verified.
573    #[tokio::test(flavor = "current_thread", start_paused = true)]
574    async fn subscriber_recomputes_resume_round_on_reconnect() {
575        use crate::block::TestBlock;
576
577        let (context, _keys) = Context::new_for_test(4);
578        let context = Arc::new(context);
579        let store = Arc::new(MemStore::new());
580        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
581        let network_client = Arc::new(SubscriberTestClient::new());
582        let authority_service = Arc::new(Mutex::new(TestService::new()));
583        let subscriber = Subscriber::new(
584            context.clone(),
585            network_client.clone(),
586            authority_service,
587            dag_state.clone(),
588        );
589
590        let peer = context.committee.to_authority_index(2).unwrap();
591        subscriber.subscribe(peer);
592
593        // Before any block from the peer is accepted, every reconnect resumes from genesis (0).
594        tokio::time::sleep(Duration::from_secs(3)).await;
595        {
596            let recorded = network_client.subscribe_calls();
597            assert!(
598                !recorded.is_empty() && recorded.iter().all(|&r| r == 0),
599                "before a block is accepted, every reconnect should resume from round 0: {recorded:?}"
600            );
601        }
602
603        // Advance the locally accepted round for the peer.
604        const RESUME_ROUND: Round = 10;
605        dag_state.write().accept_block(VerifiedBlock::new_for_test(
606            TestBlock::new(RESUME_ROUND, peer.value() as u32).build(),
607        ));
608
609        // After the block is accepted, reconnects must resume from the advanced round. With the
610        // bug, `last_received` would stay at 0 forever.
611        let mut observed_resume = false;
612        for _ in 0..10 {
613            tokio::time::sleep(Duration::from_secs(1)).await;
614            if network_client.subscribe_calls().last() == Some(&RESUME_ROUND) {
615                observed_resume = true;
616                break;
617            }
618        }
619        assert!(
620            observed_resume,
621            "after accepting a block at round {RESUME_ROUND}, the subscriber should resume from it; \
622             recorded resume rounds: {:?}",
623            network_client.subscribe_calls()
624        );
625    }
626}