Skip to main content

linera_core/client/requests_scheduler/
scheduler.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{collections::BTreeMap, future::Future, sync::Arc};
5
6use custom_debug_derive::Debug;
7use futures::stream::{FuturesUnordered, StreamExt};
8use linera_base::{
9    crypto::ValidatorPublicKey,
10    data_types::{Blob, BlobContent, BlockHeight},
11    identifiers::{BlobId, ChainId},
12    time::Duration,
13};
14use linera_chain::types::ConfirmedBlockCertificate;
15use linera_storage::Clock as _;
16use rand::distributions::{Distribution, WeightedIndex};
17use tracing::{instrument, warn};
18
19use super::{
20    cache::{RequestsCache, SubsumingKey},
21    in_flight_tracker::{InFlightMatch, InFlightTracker},
22    node_info::NodeInfo,
23    request::{RequestKey, RequestResult},
24    scoring::ScoringWeights,
25};
26use crate::{
27    client::{
28        communicate_concurrently,
29        requests_scheduler::{in_flight_tracker::Subscribed, request::Cacheable},
30        ClockOf, RequestsSchedulerConfig,
31    },
32    environment::Environment,
33    node::{NodeError, ValidatorNode},
34    remote_node::RemoteNode,
35};
36
37#[cfg(with_metrics)]
38pub(crate) mod metrics {
39    use linera_base::prometheus_util::{
40        exponential_bucket_latencies, register_histogram_vec, register_int_counter,
41        register_int_counter_vec,
42    };
43    use prometheus::{HistogramVec, IntCounter, IntCounterVec};
44
45    linera_base::declare_metrics! {
46        /// Histogram of response times per validator (in milliseconds)
47        pub(super) static VALIDATOR_RESPONSE_TIME: HistogramVec =
48            register_histogram_vec(
49                "requests_scheduler_response_time_ms",
50                "Response time for requests to validators in milliseconds",
51                &["validator", "address"],
52                exponential_bucket_latencies(10000.0), // up to 10 seconds
53            );
54
55        /// Counter of total requests made to each validator
56        pub(super) static VALIDATOR_REQUEST_TOTAL: IntCounterVec =
57            register_int_counter_vec(
58                "requests_scheduler_request_total",
59                "Total number of requests made to each validator",
60                &["validator", "address"],
61            );
62
63        /// Counter of successful requests per validator
64        pub(super) static VALIDATOR_REQUEST_SUCCESS: IntCounterVec =
65            register_int_counter_vec(
66                "requests_scheduler_request_success",
67                "Number of successful requests to each validator",
68                &["validator", "address"],
69            );
70
71        /// Counter for requests that were resolved from the response cache.
72        pub(super) static REQUEST_CACHE_DEDUPLICATION: IntCounter =
73            register_int_counter(
74                "requests_scheduler_request_deduplication_total",
75                "Number of requests that were deduplicated by finding the result in the cache.",
76            );
77
78        /// Counter for requests that were served from cache
79        pub static REQUEST_CACHE_HIT: IntCounter =
80            register_int_counter(
81                "requests_scheduler_request_cache_hit_total",
82                "Number of requests that were served from cache",
83            );
84    }
85}
86
87/// Manages a pool of validator nodes with intelligent load balancing and performance tracking.
88///
89/// The `RequestsScheduler` maintains performance metrics for each validator node using
90/// Exponential Moving Averages (EMA) and uses these metrics to make intelligent routing
91/// decisions. It prevents node overload through request capacity limits and automatically
92/// retries failed requests on alternative nodes.
93///
94/// # Examples
95///
96/// ```ignore
97/// // Create with default configuration (balanced scoring)
98/// let manager = RequestsScheduler::new(validator_nodes);
99///
100/// // Create with custom configuration prioritizing low latency
101/// let latency_weights = ScoringWeights {
102///     latency: 0.6,
103///     success: 0.3,
104///     load: 0.1,
105/// };
106/// let manager = RequestsScheduler::with_config(
107///     validator_nodes,
108///     latency_weights,               // custom scoring weights
109///     0.2,                           // higher alpha for faster adaptation
110///     3000.0,                        // max expected latency (3 seconds)
111///     Duration::from_secs(60),       // 60 second cache TTL
112///     200,                           // cache up to 200 entries
113///     Duration::from_millis(200),    // max request TTL
114///     Duration::from_millis(150),    // retry delay
115/// );
116/// ```
117#[derive(Debug, Clone)]
118pub struct RequestsScheduler<Env: Environment> {
119    /// Thread-safe map of validator nodes indexed by their public keys.
120    /// Each node is wrapped with EMA-based performance tracking information.
121    nodes: Arc<tokio::sync::RwLock<BTreeMap<ValidatorPublicKey, NodeInfo<Env>>>>,
122    /// Default scoring weights applied to new nodes.
123    weights: ScoringWeights,
124    /// Default EMA smoothing factor for new nodes.
125    alpha: f64,
126    /// Default maximum expected latency in milliseconds for score normalization.
127    max_expected_latency: f64,
128    /// Delay between starting requests to alternative peers.
129    retry_delay: Duration,
130    /// Tracks in-flight requests to deduplicate concurrent requests for the same data.
131    in_flight_tracker: InFlightTracker<RemoteNode<Env::ValidatorNode>>,
132    /// Cache of recently completed requests with their results and timestamps.
133    cache: RequestsCache<RequestKey, RequestResult>,
134    /// The node clock, used to time retries and request TTLs in (possibly simulated) time.
135    clock: ClockOf<Env>,
136}
137
138impl<Env: Environment> RequestsScheduler<Env> {
139    /// Creates a new `RequestsScheduler` with the provided configuration.
140    pub fn new(
141        nodes: impl IntoIterator<Item = RemoteNode<Env::ValidatorNode>>,
142        config: &RequestsSchedulerConfig,
143        clock: ClockOf<Env>,
144    ) -> Self {
145        Self::with_config(
146            nodes,
147            ScoringWeights::default(),
148            config.alpha,
149            config.max_accepted_latency_ms,
150            Duration::from_millis(config.cache_ttl_ms),
151            config.cache_max_size,
152            Duration::from_millis(config.max_request_ttl_ms),
153            Duration::from_millis(config.retry_delay_ms),
154            clock,
155        )
156    }
157
158    /// Creates a new `RequestsScheduler` with custom configuration.
159    ///
160    /// # Arguments
161    /// - `nodes`: Initial set of validator nodes
162    /// - `max_requests_per_node`: Maximum concurrent requests per node
163    /// - `weights`: Scoring weights for performance metrics
164    /// - `alpha`: EMA smoothing factor (0 < alpha < 1)
165    /// - `max_expected_latency_ms`: Maximum expected latency for score normalization
166    /// - `cache_ttl`: Time-to-live for cached responses
167    /// - `max_cache_size`: Maximum number of entries in the cache
168    /// - `max_request_ttl`: Maximum latency for an in-flight request before we stop deduplicating it
169    /// - `retry_delay_ms`: Delay in milliseconds between starting requests to different peers.
170    #[expect(clippy::too_many_arguments)]
171    pub fn with_config(
172        nodes: impl IntoIterator<Item = RemoteNode<Env::ValidatorNode>>,
173        weights: ScoringWeights,
174        alpha: f64,
175        max_expected_latency_ms: f64,
176        cache_ttl: Duration,
177        max_cache_size: usize,
178        max_request_ttl: Duration,
179        retry_delay: Duration,
180        clock: ClockOf<Env>,
181    ) -> Self {
182        assert!(alpha > 0.0 && alpha < 1.0, "Alpha must be in (0, 1) range");
183        Self {
184            nodes: Arc::new(tokio::sync::RwLock::new(
185                nodes
186                    .into_iter()
187                    .map(|node| {
188                        (
189                            node.public_key,
190                            NodeInfo::with_config(node, weights, alpha, max_expected_latency_ms),
191                        )
192                    })
193                    .collect(),
194            )),
195            weights,
196            alpha,
197            max_expected_latency: max_expected_latency_ms,
198            retry_delay,
199            in_flight_tracker: InFlightTracker::new(max_request_ttl),
200            cache: RequestsCache::new(cache_ttl, max_cache_size),
201            clock,
202        }
203    }
204
205    /// Executes an operation with an automatically selected peer, handling deduplication,
206    /// tracking, and peer selection.
207    ///
208    /// This method provides a high-level API for executing operations against remote nodes
209    /// while leveraging the [`RequestsScheduler`]'s intelligent peer selection, performance tracking,
210    /// and request deduplication capabilities.
211    ///
212    /// # Type Parameters
213    /// - `R`: The inner result type (what the operation returns on success)
214    /// - `F`: The async closure type that takes a `RemoteNode` and returns a future
215    /// - `Fut`: The future type returned by the closure
216    ///
217    /// # Arguments
218    /// - `key`: Unique identifier for request deduplication
219    /// - `operation`: Async closure that takes a selected peer and performs the operation
220    ///
221    /// # Returns
222    /// The result from the operation, potentially from cache or a deduplicated in-flight request
223    ///
224    /// # Example
225    /// ```ignore
226    /// let result: Result<Vec<ConfirmedBlockCertificate>, NodeError> = requests_scheduler
227    ///     .with_best(
228    ///         RequestKey::Certificates { chain_id, start, limit },
229    ///         |peer| async move {
230    ///             peer.download_certificates_from(chain_id, start, limit).await
231    ///         }
232    ///     )
233    ///     .await;
234    /// ```
235    #[allow(unused)]
236    async fn with_best<R, F, Fut>(&self, key: RequestKey, operation: F) -> Result<R, NodeError>
237    where
238        R: Cacheable + Clone + Send + 'static,
239        F: Fn(RemoteNode<Env::ValidatorNode>) -> Fut,
240        Fut: Future<Output = Result<R, NodeError>> + 'static,
241    {
242        // Select the best available peer
243        let peer = self
244            .select_best_peer()
245            .await
246            .ok_or_else(|| NodeError::WorkerError {
247                error: "No validators available".to_string(),
248            })?;
249        self.with_peer(key, peer, operation).await
250    }
251
252    /// Executes an operation with a specific peer.
253    ///
254    /// Similar to [`with_best`](Self::with_best), but uses the provided peer directly
255    /// instead of selecting the best available peer. This is useful when you need to
256    /// query a specific validator node.
257    ///
258    /// # Type Parameters
259    /// - `R`: The inner result type (what the operation returns on success)
260    /// - `F`: The async closure type that takes a `RemoteNode` and returns a future
261    /// - `Fut`: The future type returned by the closure
262    ///
263    /// # Arguments
264    /// - `key`: Unique identifier for request deduplication
265    /// - `peer`: The specific peer to use for the operation
266    /// - `operation`: Async closure that takes the peer and performs the operation
267    ///
268    /// # Returns
269    /// The result from the operation, potentially from cache or a deduplicated in-flight request
270    async fn with_peer<R, F, Fut>(
271        &self,
272        key: RequestKey,
273        peer: RemoteNode<Env::ValidatorNode>,
274        operation: F,
275    ) -> Result<R, NodeError>
276    where
277        R: Cacheable + Clone + Send + 'static,
278        F: Fn(RemoteNode<Env::ValidatorNode>) -> Fut,
279        Fut: Future<Output = Result<R, NodeError>> + 'static,
280    {
281        self.add_peer(peer.clone()).await;
282        self.in_flight_tracker
283            .add_alternative_peer(&key, peer.clone())
284            .await;
285
286        // Clone the nodes Arc so we can move it into the closure
287        let nodes = self.nodes.clone();
288        let clock = self.clock.clone();
289        self.deduplicated_request(key, peer, move |peer| {
290            let fut = operation(peer.clone());
291            let nodes = nodes.clone();
292            let clock = clock.clone();
293            async move { Self::track_request(nodes, peer, fut, &clock).await }
294        })
295        .await
296    }
297
298    #[instrument(level = "trace", skip_all)]
299    async fn download_blob(
300        &self,
301        peers: &[RemoteNode<Env::ValidatorNode>],
302        blob_id: BlobId,
303        hedge_delay: Duration,
304    ) -> Result<Option<Blob>, NodeError> {
305        let key = RequestKey::Blob(blob_id);
306        communicate_concurrently(
307            peers,
308            async move |peer| {
309                self.with_peer(key, peer, move |peer| async move {
310                    peer.download_blob(blob_id).await
311                })
312                .await
313            },
314            hedge_delay,
315            &self.clock,
316        )
317        .await
318        .map_err(|errors| {
319            for (validator, error) in &errors {
320                warn!(
321                    %validator,
322                    %blob_id,
323                    %error,
324                    "failed to download blob from validator",
325                );
326            }
327            errors
328                .into_iter()
329                .last()
330                .map_or(NodeError::NoValidators, |(_, error)| error)
331        })
332    }
333
334    /// Downloads the blobs with the given IDs. This is done in one concurrent task per blob.
335    /// Uses intelligent peer selection based on scores and load balancing.
336    /// Returns `None` if it couldn't find all blobs.
337    #[instrument(level = "trace", skip_all)]
338    pub async fn download_blobs(
339        &self,
340        peers: &[RemoteNode<Env::ValidatorNode>],
341        blob_ids: &[BlobId],
342        hedge_delay: Duration,
343    ) -> Result<Option<Vec<Blob>>, NodeError> {
344        let mut stream = blob_ids
345            .iter()
346            .map(|blob_id| self.download_blob(peers, *blob_id, hedge_delay))
347            .collect::<FuturesUnordered<_>>();
348
349        let mut blobs = Vec::new();
350        while let Some(maybe_blob) = stream.next().await {
351            blobs.push(maybe_blob?);
352        }
353        Ok(blobs.into_iter().collect::<Option<Vec<_>>>())
354    }
355
356    /// Downloads a range of certificates, starting at the given height, from the given validator.
357    pub async fn download_certificates(
358        &self,
359        peer: &RemoteNode<Env::ValidatorNode>,
360        chain_id: ChainId,
361        start: BlockHeight,
362        limit: u64,
363    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
364        let heights = (start.0..start.0 + limit)
365            .map(BlockHeight)
366            .collect::<Vec<_>>();
367        self.with_peer(
368            RequestKey::Certificates {
369                chain_id,
370                heights: heights.clone(),
371            },
372            peer.clone(),
373            move |peer| {
374                let heights = heights.clone();
375                async move {
376                    Box::pin(peer.download_certificates_by_heights(chain_id, heights)).await
377                }
378            },
379        )
380        .await
381    }
382
383    /// Downloads certificates from any of the given validators, using staggered
384    /// concurrent requests so that slow validators are quickly bypassed.
385    pub async fn download_certificates_from_validators(
386        &self,
387        peers: &[RemoteNode<Env::ValidatorNode>],
388        chain_id: ChainId,
389        start: BlockHeight,
390        limit: u64,
391        hedge_delay: Duration,
392    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
393        if peers.is_empty() {
394            return Err(NodeError::NoValidators);
395        }
396        let heights = (start.0..start.0 + limit)
397            .map(BlockHeight)
398            .collect::<Vec<_>>();
399        let key = RequestKey::Certificates {
400            chain_id,
401            heights: heights.clone(),
402        };
403        communicate_concurrently(
404            peers,
405            async move |peer| {
406                self.with_peer(key, peer, move |peer| {
407                    let heights = heights.clone();
408                    async move {
409                        Box::pin(peer.download_certificates_by_heights(chain_id, heights)).await
410                    }
411                })
412                .await
413            },
414            hedge_delay,
415            &self.clock,
416        )
417        .await
418        .map_err(|errors| {
419            for (validator, error) in &errors {
420                warn!(
421                    %validator,
422                    %chain_id,
423                    %error,
424                    "failed to download certificates from validator",
425                );
426            }
427            errors
428                .into_iter()
429                .last()
430                .map_or(NodeError::NoValidators, |(_, error)| error)
431        })
432    }
433
434    /// Downloads the certificates at the given heights from the given validator.
435    pub async fn download_certificates_by_heights(
436        &self,
437        peer: &RemoteNode<Env::ValidatorNode>,
438        chain_id: ChainId,
439        heights: Vec<BlockHeight>,
440    ) -> Result<Vec<ConfirmedBlockCertificate>, NodeError> {
441        self.with_peer(
442            RequestKey::Certificates {
443                chain_id,
444                heights: heights.clone(),
445            },
446            peer.clone(),
447            move |peer| {
448                let heights = heights.clone();
449                async move {
450                    peer.download_certificates_by_heights(chain_id, heights)
451                        .await
452                }
453            },
454        )
455        .await
456    }
457
458    /// Downloads the certificate that published the given blob, from the given validator.
459    pub async fn download_certificate_for_blob(
460        &self,
461        peer: &RemoteNode<Env::ValidatorNode>,
462        blob_id: BlobId,
463    ) -> Result<ConfirmedBlockCertificate, NodeError> {
464        self.with_peer(
465            RequestKey::CertificateForBlob(blob_id),
466            peer.clone(),
467            move |peer| async move { peer.download_certificate_for_blob(blob_id).await },
468        )
469        .await
470    }
471
472    /// Downloads a pending blob from the given validator.
473    pub async fn download_pending_blob(
474        &self,
475        peer: &RemoteNode<Env::ValidatorNode>,
476        chain_id: ChainId,
477        blob_id: BlobId,
478    ) -> Result<BlobContent, NodeError> {
479        self.with_peer(
480            RequestKey::PendingBlob { chain_id, blob_id },
481            peer.clone(),
482            move |peer| async move { peer.node.download_pending_blob(chain_id, blob_id).await },
483        )
484        .await
485    }
486
487    /// Returns the alternative peers registered for an in-flight request, if any.
488    ///
489    /// This can be used to retry a failed request with alternative data sources
490    /// that were registered during request deduplication.
491    pub async fn get_alternative_peers(
492        &self,
493        key: &RequestKey,
494    ) -> Option<Vec<RemoteNode<Env::ValidatorNode>>> {
495        self.in_flight_tracker.get_alternative_peers(key).await
496    }
497
498    /// Wraps a request operation with performance tracking and capacity management.
499    ///
500    /// This method:
501    /// 1. Measures response time
502    /// 2. Updates node metrics based on success/failure
503    ///
504    /// # Arguments
505    /// - `nodes`: Arc to the nodes map for updating metrics
506    /// - `peer`: The remote node to track metrics for
507    /// - `operation`: Future that performs the actual request
508    ///
509    /// # Behavior
510    /// Executes the provided future and tracks metrics for the given peer.
511    async fn track_request<T, Fut>(
512        nodes: Arc<tokio::sync::RwLock<BTreeMap<ValidatorPublicKey, NodeInfo<Env>>>>,
513        peer: RemoteNode<Env::ValidatorNode>,
514        operation: Fut,
515        clock: &ClockOf<Env>,
516    ) -> Result<T, NodeError>
517    where
518        Fut: Future<Output = Result<T, NodeError>> + 'static,
519    {
520        let start_time = clock.current_time();
521        let public_key = peer.public_key;
522
523        // Execute the operation
524        let result = operation.await;
525
526        // Update metrics and release slot
527        let response_time_ms = clock.current_time().delta_since(start_time).as_micros() / 1000;
528        let is_success = result.is_ok();
529        {
530            let mut nodes_guard = nodes.write().await;
531            if let Some(info) = nodes_guard.get_mut(&public_key) {
532                info.update_metrics(is_success, response_time_ms);
533                let score = info.calculate_score().await;
534                tracing::trace!(
535                    node = %public_key,
536                    address = %info.node.node.address(),
537                    success = %is_success,
538                    response_time_ms = %response_time_ms,
539                    score = %score,
540                    total_requests = %info.total_requests(),
541                    "Request completed"
542                );
543            }
544        }
545
546        // Record Prometheus metrics
547        #[cfg(with_metrics)]
548        {
549            let validator_name = public_key.to_string();
550            let address = peer.address();
551            metrics::VALIDATOR_RESPONSE_TIME
552                .with_label_values(&[&validator_name, &address])
553                .observe(response_time_ms as f64);
554            metrics::VALIDATOR_REQUEST_TOTAL
555                .with_label_values(&[&validator_name, &address])
556                .inc();
557            if is_success {
558                metrics::VALIDATOR_REQUEST_SUCCESS
559                    .with_label_values(&[&validator_name, &address])
560                    .inc();
561            }
562        }
563
564        result
565    }
566
567    /// Deduplicates concurrent requests for the same data.
568    ///
569    /// If a request for the same key is already in flight, this method waits for
570    /// the existing request to complete and returns its result. Otherwise, it
571    /// executes the operation and broadcasts the result to all waiting callers.
572    ///
573    /// This method also performs **subsumption-based deduplication**: if a larger
574    /// request that contains all the data needed by this request is already cached
575    /// or in flight, we can extract the subset result instead of making a new request.
576    ///
577    /// # Arguments
578    /// - `key`: Unique identifier for the request
579    /// - `operation`: Async closure that performs the actual request
580    ///
581    /// # Returns
582    /// The result from either the in-flight request or the newly executed operation
583    async fn deduplicated_request<T, F, Fut>(
584        &self,
585        key: RequestKey,
586        peer: RemoteNode<Env::ValidatorNode>,
587        operation: F,
588    ) -> Result<T, NodeError>
589    where
590        T: Cacheable + Clone + Send + 'static,
591        F: Fn(RemoteNode<Env::ValidatorNode>) -> Fut,
592        Fut: Future<Output = Result<T, NodeError>> + 'static,
593    {
594        // Check cache for exact or subsuming match
595        if let Some(result) = self.cache.get(&key).await {
596            return Ok(result);
597        }
598
599        // Check if there's an in-flight request (exact or subsuming)
600        if let Some(in_flight_match) = self
601            .in_flight_tracker
602            .try_subscribe(&key, self.clock.current_time())
603        {
604            match in_flight_match {
605                InFlightMatch::Exact(Subscribed(mut receiver)) => {
606                    tracing::trace!(
607                        ?key,
608                        "deduplicating request (exact match) - joining existing in-flight request"
609                    );
610                    #[cfg(with_metrics)]
611                    metrics::REQUEST_CACHE_DEDUPLICATION.inc();
612                    // Wait for result from existing request
613                    match receiver.recv().await {
614                        Ok(result) => match result.as_ref().clone() {
615                            Ok(res) => match T::try_from(res) {
616                                Ok(converted) => {
617                                    tracing::trace!(
618                                        ?key,
619                                        "received result from deduplicated in-flight request"
620                                    );
621                                    return Ok(converted);
622                                }
623                                Err(_) => {
624                                    tracing::warn!(
625                                        ?key,
626                                        "failed to convert result from deduplicated in-flight request, will execute independently"
627                                    );
628                                }
629                            },
630                            Err(error) => {
631                                tracing::trace!(
632                                    ?key,
633                                    %error,
634                                    "in-flight request failed",
635                                );
636                                // Fall through to execute a new request
637                            }
638                        },
639                        Err(_) => {
640                            tracing::trace!(?key, "in-flight request sender dropped");
641                            // Fall through to execute a new request
642                        }
643                    }
644                }
645                InFlightMatch::Subsuming {
646                    key: subsuming_key,
647                    outcome: Subscribed(mut receiver),
648                } => {
649                    tracing::trace!(
650                    ?key,
651                    subsumed_by = ?subsuming_key,
652                        "deduplicating request (subsumption) - joining larger in-flight request"
653                    );
654                    #[cfg(with_metrics)]
655                    metrics::REQUEST_CACHE_DEDUPLICATION.inc();
656                    // Wait for result from the subsuming request
657                    match receiver.recv().await {
658                        Ok(result) => {
659                            match result.as_ref() {
660                                Ok(res) => {
661                                    if let Some(extracted) =
662                                        key.try_extract_result(&subsuming_key, res)
663                                    {
664                                        tracing::trace!(
665                                            ?key,
666                                            "extracted subset result from larger in-flight request"
667                                        );
668                                        match T::try_from(extracted) {
669                                            Ok(converted) => return Ok(converted),
670                                            Err(_) => {
671                                                tracing::trace!(
672                                                    ?key,
673                                                    "failed to convert extracted result, will execute independently"
674                                                );
675                                            }
676                                        }
677                                    } else {
678                                        // Extraction failed, fall through to execute our own request
679                                        tracing::trace!(
680                                            ?key,
681                                            "failed to extract from subsuming request, will execute independently"
682                                        );
683                                    }
684                                }
685                                Err(error) => {
686                                    tracing::trace!(
687                                        ?key,
688                                        ?error,
689                                        "subsuming in-flight request failed",
690                                    );
691                                    // Fall through to execute our own request
692                                }
693                            }
694                        }
695                        Err(_) => {
696                            tracing::trace!(?key, "subsuming in-flight request sender dropped");
697                        }
698                    }
699                }
700            }
701        };
702
703        // Create a new in-flight entry for this request. The returned guard owns the
704        // entry: if this task is cancelled before completing (e.g. a losing branch of
705        // `communicate_with_quorum`), dropping it removes the entry so subscribers wake
706        // up and execute the request themselves instead of waiting forever.
707        let in_flight_guard = self
708            .in_flight_tracker
709            .insert_new(key.clone(), self.clock.current_time());
710
711        // Remove the peer we're about to use from alternatives (it shouldn't retry with itself)
712        self.in_flight_tracker
713            .remove_alternative_peer(&key, &peer)
714            .await;
715
716        // Execute request with staggered parallel - first peer starts immediately,
717        // alternatives are tried after stagger delays (even if first peer is slow but not failing)
718        tracing::trace!(?key, ?peer, "executing staggered parallel request");
719        let result = self
720            .try_staggered_parallel(&key, peer, &operation, self.retry_delay)
721            .await;
722
723        let result_for_broadcast: Result<RequestResult, NodeError> = result.clone().map(Into::into);
724        let shared_result = Arc::new(result_for_broadcast);
725
726        // Broadcast result and clean up.
727        in_flight_guard.complete_and_broadcast(shared_result.clone());
728
729        if let Ok(success) = shared_result.as_ref() {
730            self.cache
731                .store(
732                    key.clone(),
733                    Arc::new(success.clone()),
734                    self.clock.current_time(),
735                )
736                .await;
737        }
738        result
739    }
740
741    /// Tries alternative peers in staggered parallel fashion.
742    ///
743    /// Launches requests starting with the first peer, then dynamically pops alternative peers
744    /// with a stagger delay between each. Returns the first successful result. This provides
745    /// a balance between sequential (slow) and fully parallel (wasteful) approaches.
746    ///
747    /// # Arguments
748    /// - `key`: The request key (for logging and popping alternatives)
749    /// - `first_peer`: The initial peer to try first (at time 0)
750    /// - `operation`: The operation to execute on each peer
751    /// - `staggered_delay_ms`: Delay in milliseconds between starting each subsequent peer
752    ///
753    /// # Returns
754    /// The first successful result, or the last error if all fail
755    async fn try_staggered_parallel<T, F, Fut>(
756        &self,
757        key: &RequestKey,
758        first_peer: RemoteNode<Env::ValidatorNode>,
759        operation: &F,
760        staggered_delay: Duration,
761    ) -> Result<T, NodeError>
762    where
763        T: 'static,
764        F: Fn(RemoteNode<Env::ValidatorNode>) -> Fut,
765        Fut: Future<Output = Result<T, NodeError>> + 'static,
766    {
767        // Source additional peers from the in-flight tracker's alternative queue (populated by
768        // concurrent deduplicated requests), staggering with a linearly-growing delay.
769        crate::client::hedged_fan_out(
770            first_peer,
771            || self.in_flight_tracker.pop_alternative_peer(key),
772            operation,
773            |started| {
774                let n = u32::try_from(started).unwrap_or(u32::MAX);
775                staggered_delay.saturating_mul(n)
776            },
777            &self.clock,
778        )
779        .await
780        .map_err(|errors| {
781            errors
782                .into_iter()
783                .next_back()
784                .unwrap_or(NodeError::UnexpectedMessage)
785        })
786    }
787
788    /// Returns all peers ordered by their score (highest first).
789    ///
790    /// Only includes peers that can currently accept requests. Each peer is paired
791    /// with its calculated score based on latency, success rate, and availability.
792    ///
793    /// # Returns
794    /// A vector of `(score, peer)` tuples sorted by score in descending order.
795    /// Returns an empty vector if no peers can accept requests.
796    async fn peers_by_score(&self) -> Vec<(f64, RemoteNode<Env::ValidatorNode>)> {
797        let nodes = self.nodes.read().await;
798
799        // Filter nodes that can accept requests and calculate their scores
800        let mut scored_nodes = Vec::new();
801        for info in nodes.values() {
802            let score = info.calculate_score().await;
803            scored_nodes.push((score, info.node.clone()));
804        }
805
806        // Sort by score (highest first)
807        scored_nodes.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
808
809        scored_nodes
810    }
811
812    /// Selects the best available peer using weighted random selection from top performers.
813    ///
814    /// This method:
815    /// 1. Sorts nodes by performance score
816    /// 2. Performs weighted random selection from the top 3 performers
817    ///
818    /// This approach balances between choosing high-performing nodes and distributing
819    /// load across multiple validators to avoid creating hotspots.
820    ///
821    /// Returns `None` if no nodes are available.
822    async fn select_best_peer(&self) -> Option<RemoteNode<Env::ValidatorNode>> {
823        let scored_nodes = self.peers_by_score().await;
824
825        if scored_nodes.is_empty() {
826            return None;
827        }
828
829        // Use weighted random selection from top performers (top 3 or all if less)
830        let top_count = scored_nodes.len().min(3);
831        let top_nodes = &scored_nodes[..top_count];
832
833        // Create weights based on normalized scores
834        // Add small epsilon to prevent zero weights
835        let weights: Vec<f64> = top_nodes.iter().map(|(score, _)| score.max(0.01)).collect();
836
837        if let Ok(dist) = WeightedIndex::new(&weights) {
838            let mut rng = rand::thread_rng();
839            let index = dist.sample(&mut rng);
840            Some(top_nodes[index].1.clone())
841        } else {
842            // Fallback to the best node if weights are invalid
843            tracing::warn!("failed to create weighted distribution, defaulting to best node");
844            Some(scored_nodes[0].1.clone())
845        }
846    }
847
848    /// Adds a new peer to the manager if it doesn't already exist.
849    async fn add_peer(&self, node: RemoteNode<Env::ValidatorNode>) {
850        let mut nodes = self.nodes.write().await;
851        let public_key = node.public_key;
852        nodes.entry(public_key).or_insert_with(|| {
853            NodeInfo::with_config(node, self.weights, self.alpha, self.max_expected_latency)
854        });
855    }
856}
857
858#[cfg(test)]
859mod tests {
860    use std::sync::{
861        atomic::{AtomicUsize, Ordering},
862        Arc,
863    };
864
865    use linera_base::{
866        crypto::{CryptoHash, InMemorySigner},
867        data_types::{BlockHeight, TimeDelta},
868        identifiers::ChainId,
869        time::Duration,
870    };
871    use linera_chain::types::ConfirmedBlockCertificate;
872    use linera_storage::TestClock;
873    use tokio::sync::oneshot;
874
875    use super::{super::request::RequestKey, *};
876    use crate::{
877        client::requests_scheduler::{MAX_REQUEST_TTL_MS, STAGGERED_DELAY_MS},
878        node::NodeError,
879    };
880
881    type TestEnvironment = crate::environment::Test;
882
883    /// Helper function to create a test RequestsScheduler with custom configuration.
884    ///
885    /// The scheduler is driven by a [`TestClock`] starting at the epoch, so tests control all
886    /// time deterministically via `manager.clock` (e.g. `add`/`set`) instead of sleeping.
887    fn create_test_manager(
888        in_flight_timeout: Duration,
889        cache_ttl: Duration,
890    ) -> Arc<RequestsScheduler<TestEnvironment>> {
891        let mut manager = RequestsScheduler::with_config(
892            vec![], // No actual nodes needed for these tests
893            ScoringWeights::default(),
894            0.1,
895            1000.0,
896            cache_ttl,
897            100,
898            in_flight_timeout,
899            Duration::from_millis(STAGGERED_DELAY_MS),
900            TestClock::new(),
901        );
902        // Replace the tracker with one using the custom timeout
903        manager.in_flight_tracker = InFlightTracker::new(in_flight_timeout);
904        Arc::new(manager)
905    }
906
907    /// Helper function to create a test request key
908    fn test_key() -> RequestKey {
909        RequestKey::Certificates {
910            chain_id: ChainId(CryptoHash::test_hash("test")),
911            heights: vec![BlockHeight(0), BlockHeight(1)],
912        }
913    }
914
915    /// Helper function to create a dummy peer for testing
916    fn dummy_peer() -> RemoteNode<<TestEnvironment as Environment>::ValidatorNode> {
917        use crate::test_utils::{MemoryStorageBuilder, TestBuilder};
918
919        // Create a minimal test builder to get a validator node
920        let mut builder = futures::executor::block_on(async {
921            TestBuilder::new(
922                MemoryStorageBuilder::default(),
923                1,
924                0,
925                linera_base::crypto::InMemorySigner::new(None),
926            )
927            .await
928            .unwrap()
929        });
930
931        let node = builder.node(0);
932        let public_key = node.name();
933        RemoteNode { public_key, node }
934    }
935
936    #[tokio::test]
937    async fn test_cache_hit_returns_cached_result() {
938        // Create a manager with standard cache TTL
939        let manager = create_test_manager(Duration::from_secs(60), Duration::from_secs(60));
940        let key = test_key();
941        let peer = dummy_peer();
942
943        // Track how many times the operation is executed
944        let execution_count = Arc::new(AtomicUsize::new(0));
945        let execution_count_clone = execution_count.clone();
946
947        // First call - should execute the operation and cache the result
948        let result1: Result<Vec<ConfirmedBlockCertificate>, NodeError> = manager
949            .deduplicated_request(key.clone(), peer.clone(), |_| {
950                let count = execution_count_clone.clone();
951                async move {
952                    count.fetch_add(1, Ordering::SeqCst);
953                    Ok(vec![])
954                }
955            })
956            .await;
957
958        assert!(result1.is_ok());
959        assert_eq!(execution_count.load(Ordering::SeqCst), 1);
960
961        // Second call - should return cached result without executing the operation
962        let execution_count_clone2 = execution_count.clone();
963        let result2: Result<Vec<ConfirmedBlockCertificate>, NodeError> = manager
964            .deduplicated_request(key.clone(), peer.clone(), |_| {
965                let count = execution_count_clone2.clone();
966                async move {
967                    count.fetch_add(1, Ordering::SeqCst);
968                    Ok(vec![])
969                }
970            })
971            .await;
972
973        assert_eq!(result1, result2);
974        // Operation should still only have been executed once (cache hit)
975        assert_eq!(execution_count.load(Ordering::SeqCst), 1);
976    }
977
978    #[tokio::test]
979    async fn test_in_flight_request_deduplication() {
980        let manager = create_test_manager(Duration::from_secs(60), Duration::from_secs(60));
981        let key = test_key();
982        let peer = dummy_peer();
983
984        // Track how many times the operation is executed
985        let execution_count = Arc::new(AtomicUsize::new(0));
986
987        // Create a channel to control when the first operation completes
988        let (tx, rx) = oneshot::channel();
989        let rx = Arc::new(tokio::sync::Mutex::new(Some(rx)));
990
991        // Start first request (will be slow - waits for signal)
992        let manager_clone = Arc::clone(&manager);
993        let key_clone = key.clone();
994        let execution_count_clone = execution_count.clone();
995        let rx_clone = Arc::clone(&rx);
996        let peer_clone = peer.clone();
997        let first_request = tokio::spawn(async move {
998            manager_clone
999                .deduplicated_request(key_clone, peer_clone, |_| {
1000                    let count = execution_count_clone.clone();
1001                    let rx = Arc::clone(&rx_clone);
1002                    async move {
1003                        count.fetch_add(1, Ordering::SeqCst);
1004                        // Wait for signal before completing
1005                        if let Some(receiver) = rx.lock().await.take() {
1006                            receiver.await.unwrap();
1007                        }
1008                        Ok(vec![])
1009                    }
1010                })
1011                .await
1012        });
1013
1014        // Start second request - should deduplicate and wait for the first
1015        let execution_count_clone2 = execution_count.clone();
1016        let second_request = tokio::spawn(async move {
1017            manager
1018                .deduplicated_request(key, peer, |_| {
1019                    let count = execution_count_clone2.clone();
1020                    async move {
1021                        count.fetch_add(1, Ordering::SeqCst);
1022                        Ok(vec![])
1023                    }
1024                })
1025                .await
1026        });
1027
1028        // Signal the first request to complete
1029        tx.send(()).unwrap();
1030
1031        // Both requests should complete successfully
1032        let result1: Result<Vec<ConfirmedBlockCertificate>, NodeError> =
1033            first_request.await.unwrap();
1034        let result2: Result<Vec<ConfirmedBlockCertificate>, NodeError> =
1035            second_request.await.unwrap();
1036
1037        assert!(result1.is_ok());
1038        assert_eq!(result1, result2);
1039
1040        // Operation should only have been executed once (deduplication worked)
1041        assert_eq!(execution_count.load(Ordering::SeqCst), 1);
1042    }
1043
1044    #[tokio::test]
1045    async fn test_multiple_subscribers_all_notified() {
1046        let manager = create_test_manager(Duration::from_secs(60), Duration::from_secs(60));
1047        let key = test_key();
1048        let peer = dummy_peer();
1049
1050        // Track how many times the operation is executed
1051        let execution_count = Arc::new(AtomicUsize::new(0));
1052
1053        // Create a channel to control when the operation completes
1054        let (tx, rx) = oneshot::channel();
1055        let rx = Arc::new(tokio::sync::Mutex::new(Some(rx)));
1056
1057        // Start first request (will be slow - waits for signal)
1058        let manager_clone1 = Arc::clone(&manager);
1059        let key_clone1 = key.clone();
1060        let execution_count_clone = execution_count.clone();
1061        let rx_clone = Arc::clone(&rx);
1062        let peer_clone = peer.clone();
1063        let first_request = tokio::spawn(async move {
1064            manager_clone1
1065                .deduplicated_request(key_clone1, peer_clone, |_| {
1066                    let count = execution_count_clone.clone();
1067                    let rx = Arc::clone(&rx_clone);
1068                    async move {
1069                        count.fetch_add(1, Ordering::SeqCst);
1070                        if let Some(receiver) = rx.lock().await.take() {
1071                            receiver.await.unwrap();
1072                        }
1073                        Ok(vec![])
1074                    }
1075                })
1076                .await
1077        });
1078
1079        // Start multiple additional requests - all should deduplicate
1080        let mut handles = vec![];
1081        for _ in 0..5 {
1082            let manager_clone = Arc::clone(&manager);
1083            let key_clone = key.clone();
1084            let execution_count_clone = execution_count.clone();
1085            let peer_clone = peer.clone();
1086            let handle = tokio::spawn(async move {
1087                manager_clone
1088                    .deduplicated_request(key_clone, peer_clone, |_| {
1089                        let count = execution_count_clone.clone();
1090                        async move {
1091                            count.fetch_add(1, Ordering::SeqCst);
1092                            Ok(vec![])
1093                        }
1094                    })
1095                    .await
1096            });
1097            handles.push(handle);
1098        }
1099
1100        // Signal the first request to complete
1101        tx.send(()).unwrap();
1102
1103        // First request should complete successfully
1104        let result: Result<Vec<ConfirmedBlockCertificate>, NodeError> =
1105            first_request.await.unwrap();
1106        assert!(result.is_ok());
1107
1108        // All subscriber requests should also complete successfully
1109        for handle in handles {
1110            assert_eq!(handle.await.unwrap(), result);
1111        }
1112
1113        // Operation should only have been executed once (all requests were deduplicated)
1114        assert_eq!(execution_count.load(Ordering::SeqCst), 1);
1115    }
1116
1117    #[tokio::test]
1118    async fn test_timeout_triggers_new_request() {
1119        // Create a manager with a very short in-flight timeout
1120        let manager = create_test_manager(Duration::from_millis(50), Duration::from_secs(60));
1121
1122        let key = test_key();
1123        let peer = dummy_peer();
1124
1125        // Track how many times the operation is executed
1126        let execution_count = Arc::new(AtomicUsize::new(0));
1127        // Signaled once the first request's operation starts, i.e. its in-flight entry exists.
1128        let started = Arc::new(tokio::sync::Notify::new());
1129
1130        // Create a channel to control when the first operation completes
1131        let (tx, rx) = oneshot::channel();
1132        let rx = Arc::new(tokio::sync::Mutex::new(Some(rx)));
1133
1134        // Start first request (will be slow - waits for signal)
1135        let manager_clone = Arc::clone(&manager);
1136        let key_clone = key.clone();
1137        let execution_count_clone = execution_count.clone();
1138        let started_clone = started.clone();
1139        let rx_clone = Arc::clone(&rx);
1140        let peer_clone = peer.clone();
1141        let first_request = tokio::spawn(async move {
1142            manager_clone
1143                .deduplicated_request(key_clone, peer_clone, |_| {
1144                    let count = execution_count_clone.clone();
1145                    let started = started_clone.clone();
1146                    let rx = Arc::clone(&rx_clone);
1147                    async move {
1148                        count.fetch_add(1, Ordering::SeqCst);
1149                        started.notify_one();
1150                        if let Some(receiver) = rx.lock().await.take() {
1151                            receiver.await.unwrap();
1152                        }
1153                        Ok(vec![])
1154                    }
1155                })
1156                .await
1157        });
1158
1159        // Wait for the first request's operation to start (its in-flight entry now exists), then
1160        // advance virtual time past the in-flight timeout so deduplication is skipped.
1161        started.notified().await;
1162        manager
1163            .clock
1164            .add(TimeDelta::from_millis(MAX_REQUEST_TTL_MS + 1));
1165
1166        // Start second request - should NOT deduplicate because first request exceeded timeout
1167        let execution_count_clone2 = execution_count.clone();
1168        let second_request = tokio::spawn(async move {
1169            manager
1170                .deduplicated_request(key, peer, |_| {
1171                    let count = execution_count_clone2.clone();
1172                    async move {
1173                        count.fetch_add(1, Ordering::SeqCst);
1174                        Ok(vec![])
1175                    }
1176                })
1177                .await
1178        });
1179
1180        // Wait for second request to complete
1181        let result2: Result<Vec<ConfirmedBlockCertificate>, NodeError> =
1182            second_request.await.unwrap();
1183        assert!(result2.is_ok());
1184
1185        // Complete the first request
1186        tx.send(()).unwrap();
1187        let result1: Result<Vec<ConfirmedBlockCertificate>, NodeError> =
1188            first_request.await.unwrap();
1189        assert!(result1.is_ok());
1190
1191        // Operation should have been executed twice (timeout triggered new request)
1192        assert_eq!(execution_count.load(Ordering::SeqCst), 2);
1193    }
1194
1195    #[tokio::test]
1196    async fn test_alternative_peers_registered_and_cleared() {
1197        use linera_base::identifiers::BlobType;
1198
1199        use crate::test_utils::{MemoryStorageBuilder, TestBuilder};
1200
1201        // Three validators, to register as distinct alternative sources.
1202        let mut builder = TestBuilder::new(
1203            MemoryStorageBuilder::default(),
1204            3,
1205            0,
1206            InMemorySigner::new(None),
1207        )
1208        .await
1209        .unwrap();
1210        let nodes: Vec<_> = (0..3)
1211            .map(|i| {
1212                let node = builder.node(i);
1213                let public_key = node.name();
1214                RemoteNode { public_key, node }
1215            })
1216            .collect();
1217
1218        let manager = create_test_manager(Duration::from_secs(60), Duration::from_secs(60));
1219        let key = RequestKey::Blob(BlobId::new(
1220            CryptoHash::test_hash("test_blob"),
1221            BlobType::Data,
1222        ));
1223        let now = manager.clock.current_time();
1224
1225        // A request is in flight, and two other peers register as alternative sources for it.
1226        let guard = manager.in_flight_tracker.insert_new(key.clone(), now);
1227        manager
1228            .in_flight_tracker
1229            .add_alternative_peer(&key, nodes[1].clone())
1230            .await;
1231        manager
1232            .in_flight_tracker
1233            .add_alternative_peer(&key, nodes[2].clone())
1234            .await;
1235        assert_eq!(
1236            manager
1237                .get_alternative_peers(&key)
1238                .await
1239                .map(|peers| peers.len()),
1240            Some(2),
1241        );
1242
1243        // Completing the request removes the in-flight entry along with its alternatives.
1244        guard.complete_and_broadcast(Arc::new(Ok(RequestResult::Blob(None))));
1245        assert!(
1246            manager.get_alternative_peers(&key).await.is_none(),
1247            "Expected the in-flight entry to be removed after completion",
1248        );
1249    }
1250
1251    /// Dropping the owner guard without completing must wake subscribers — they fall back to
1252    /// executing the request themselves — rather than leaving them blocked forever. Regression
1253    /// test for the cold-sync hang where a cancelled `communicate_with_quorum` branch leaked the
1254    /// in-flight entry, so its subscribers' `recv().await` never returned.
1255    #[tokio::test]
1256    async fn test_owner_drop_wakes_subscribers() {
1257        use linera_base::identifiers::BlobType;
1258
1259        let manager = create_test_manager(Duration::from_secs(60), Duration::from_secs(60));
1260        let key = RequestKey::Blob(BlobId::new(
1261            CryptoHash::test_hash("test_blob"),
1262            BlobType::Data,
1263        ));
1264        let now = manager.clock.current_time();
1265
1266        // An owner registers an in-flight request; a second caller subscribes to it.
1267        let guard = manager.in_flight_tracker.insert_new(key.clone(), now);
1268        let Some(InFlightMatch::Exact(Subscribed(mut receiver))) =
1269            manager.in_flight_tracker.try_subscribe(&key, now)
1270        else {
1271            panic!("expected to subscribe to the in-flight request");
1272        };
1273
1274        // The owner is cancelled before completing the request.
1275        drop(guard);
1276
1277        // The subscriber observes the channel closing instead of hanging, and the entry is gone
1278        // so a later caller starts its own request.
1279        assert!(matches!(
1280            receiver.recv().await,
1281            Err(tokio::sync::broadcast::error::RecvError::Closed),
1282        ));
1283        assert!(manager.in_flight_tracker.try_subscribe(&key, now).is_none());
1284    }
1285
1286    /// A new owner for a key adopts the previous entry's broadcast sender, so waiters
1287    /// subscribed to the replaced entry receive the new owner's result instead of being
1288    /// disconnected — and the stale owner can neither broadcast nor remove the new entry.
1289    #[tokio::test]
1290    async fn test_new_owner_adopts_existing_waiters() {
1291        use linera_base::identifiers::BlobType;
1292
1293        let manager = create_test_manager(Duration::from_secs(60), Duration::from_secs(60));
1294        let key = RequestKey::Blob(BlobId::new(
1295            CryptoHash::test_hash("test_blob"),
1296            BlobType::Data,
1297        ));
1298        let now = manager.clock.current_time();
1299
1300        let first_owner = manager.in_flight_tracker.insert_new(key.clone(), now);
1301        let Some(InFlightMatch::Exact(Subscribed(mut receiver))) =
1302            manager.in_flight_tracker.try_subscribe(&key, now)
1303        else {
1304            panic!("expected to subscribe to the in-flight request");
1305        };
1306
1307        // A second owner takes over the same key (e.g. the first went stale).
1308        let second_owner = manager.in_flight_tracker.insert_new(key.clone(), now);
1309
1310        // The stale owner completing late neither broadcasts nor removes the new entry.
1311        assert_eq!(
1312            first_owner.complete_and_broadcast(Arc::new(Ok(RequestResult::Blob(None)))),
1313            0
1314        );
1315
1316        // The waiter subscribed before the takeover receives the new owner's result.
1317        assert_eq!(
1318            second_owner.complete_and_broadcast(Arc::new(Ok(RequestResult::Blob(None)))),
1319            1
1320        );
1321        assert!(receiver.recv().await.is_ok());
1322    }
1323
1324    #[tokio::test]
1325    async fn test_staggered_parallel_retry_on_failure() {
1326        use crate::test_utils::{MemoryStorageBuilder, TestBuilder};
1327
1328        // Create a test environment with four validators
1329        let mut builder = TestBuilder::new(
1330            MemoryStorageBuilder::default(),
1331            4,
1332            0,
1333            InMemorySigner::new(None),
1334        )
1335        .await
1336        .unwrap();
1337
1338        // Get validator nodes
1339        let nodes: Vec<_> = (0..4)
1340            .map(|i| {
1341                let node = builder.node(i);
1342                let public_key = node.name();
1343                RemoteNode { public_key, node }
1344            })
1345            .collect();
1346
1347        let staggered_delay = Duration::from_millis(100);
1348
1349        // Store public keys for comparison
1350        let node0_key = nodes[0].public_key;
1351        let node2_key = nodes[2].public_key;
1352
1353        // Auto-advance the clock on every sleep, so the scheduler's staggered delays resolve in
1354        // virtual time; the test is then deterministic and never blocks on real time, and asserts
1355        // on the order peers are tried rather than on wall-clock durations.
1356        let clock = TestClock::new();
1357        clock.set_sleep_callback(|_| true);
1358
1359        // Create a RequestsScheduler
1360        let manager: Arc<RequestsScheduler<TestEnvironment>> =
1361            Arc::new(RequestsScheduler::with_config(
1362                nodes.clone(),
1363                ScoringWeights::default(),
1364                0.1,
1365                1000.0,
1366                Duration::from_secs(60),
1367                100,
1368                Duration::from_millis(MAX_REQUEST_TTL_MS),
1369                staggered_delay,
1370                clock,
1371            ));
1372
1373        let key = test_key();
1374
1375        // Record the order in which peers are tried.
1376        let call_order = Arc::new(tokio::sync::Mutex::new(Vec::new()));
1377        let call_order_clone = Arc::clone(&call_order);
1378
1379        // Node 0 (first peer) and node 1 fail immediately; node 2 succeeds. The staggered retry
1380        // must walk past the two failing peers to the working one.
1381        let operation = |peer: RemoteNode<<TestEnvironment as Environment>::ValidatorNode>| {
1382            let order = Arc::clone(&call_order_clone);
1383            async move {
1384                order.lock().await.push(peer.public_key);
1385                if peer.public_key == node2_key {
1386                    Ok(vec![])
1387                } else {
1388                    Err(NodeError::UnexpectedMessage)
1389                }
1390            }
1391        };
1392
1393        // Setup: Insert in-flight entry and register alternative peers. The guard is
1394        // held for the rest of the test so the entry is not dropped before the request runs.
1395        let _guard = manager
1396            .in_flight_tracker
1397            .insert_new(key.clone(), manager.clock.current_time());
1398        // Register nodes 3, 2, 1 as alternatives (will be popped in reverse: 1, 2, 3)
1399        for node in nodes.iter().skip(1).rev() {
1400            manager
1401                .in_flight_tracker
1402                .add_alternative_peer(&key, node.clone())
1403                .await;
1404        }
1405
1406        // Use node 0 as first peer, alternatives will be popped: node 1, then 2, then 3
1407        let result: Result<Vec<ConfirmedBlockCertificate>, NodeError> = manager
1408            .try_staggered_parallel(&key, nodes[0].clone(), &operation, staggered_delay)
1409            .await;
1410
1411        // Should succeed with the result from node 2, after walking past the failing peers.
1412        assert!(
1413            result.is_ok(),
1414            "Expected request to succeed with alternative peer"
1415        );
1416
1417        let order = call_order.lock().await;
1418        assert_eq!(
1419            order.first(),
1420            Some(&node0_key),
1421            "First peer tried should be node 0"
1422        );
1423        assert!(
1424            order.contains(&node2_key),
1425            "Retry should have reached the working peer (node 2)"
1426        );
1427    }
1428}