Skip to main content

alloy_transport/layers/
fallback.rs

1use crate::time::Instant;
2use alloy_json_rpc::{RequestPacket, ResponsePacket};
3use core::time::Duration;
4use derive_more::{Deref, DerefMut};
5use futures::{stream::FuturesUnordered, StreamExt};
6use parking_lot::RwLock;
7use std::{
8    collections::{HashSet, VecDeque},
9    num::NonZeroUsize,
10    sync::Arc,
11    task::{Context, Poll},
12};
13use tower::{Layer, Service};
14use tracing::trace;
15
16use crate::{TransportError, TransportErrorKind, TransportFut};
17
18// Constants for the transport ranking algorithm
19const STABILITY_WEIGHT: f64 = 0.7;
20const LATENCY_WEIGHT: f64 = 0.3;
21const DEFAULT_SAMPLE_COUNT: usize = 10;
22const DEFAULT_ACTIVE_TRANSPORT_COUNT: usize = 3;
23
24/// The [`FallbackService`] consumes multiple transports and is able to
25/// query them in parallel, returning the first successful response.
26///
27/// The service ranks transports based on latency and stability metrics,
28/// and will attempt to always use the best available transports.
29#[derive(Debug, Clone)]
30pub struct FallbackService<S> {
31    /// The list of transports to use
32    transports: Arc<Vec<ScoredTransport<S>>>,
33    /// The maximum number of transports to use in parallel
34    active_transport_count: usize,
35    /// Set of RPC methods that require sequential execution (non-deterministic results in
36    /// parallel)
37    sequential_methods: Arc<HashSet<String>>,
38}
39
40impl<S: Clone> FallbackService<S> {
41    /// Create a new fallback service from a list of transports.
42    ///
43    /// The `active_transport_count` parameter controls how many transports are used for requests
44    /// at any one time.
45    ///
46    /// Uses the default set of sequential methods (eth_sendRawTransactionSync,
47    /// eth_sendTransactionSync).
48    pub fn new(transports: Vec<S>, active_transport_count: usize) -> Self {
49        Self::new_with_sequential_methods(
50            transports,
51            active_transport_count,
52            default_sequential_methods(),
53        )
54    }
55
56    /// Create a new fallback service from a list of transports.
57    ///
58    /// The `active_transport_count` parameter controls how many transports are used for requests
59    /// at any one time.
60    ///
61    /// Uses the given set of sequential methods (eth_sendRawTransactionSync,
62    /// eth_sendTransactionSync).
63    pub fn new_with_sequential_methods(
64        transports: Vec<S>,
65        active_transport_count: usize,
66        sequential_methods: HashSet<String>,
67    ) -> Self {
68        let scored_transports = transports
69            .into_iter()
70            .enumerate()
71            .map(|(id, transport)| ScoredTransport::new(id, transport))
72            .collect::<Vec<_>>();
73
74        Self {
75            transports: Arc::new(scored_transports),
76            active_transport_count,
77            sequential_methods: Arc::new(sequential_methods),
78        }
79    }
80
81    /// Inserts the sequential method into the set.
82    pub fn append_sequential_method(mut self, sequential_method: impl Into<String>) -> Self {
83        let mut methods = Arc::unwrap_or_clone(self.sequential_methods);
84        methods.insert(sequential_method.into());
85        self.sequential_methods = Arc::new(methods);
86        self
87    }
88
89    /// Configures the `sequential_methods` parameter specifies which RPC methods require sequential
90    /// execution due to non-deterministic results in parallel execution.
91    pub fn with_sequential_methods(mut self, sequential_methods: HashSet<String>) -> Self {
92        self.sequential_methods = Arc::new(sequential_methods);
93        self
94    }
95
96    /// Log the current ranking of transports
97    fn log_transport_rankings(&self) {
98        if !tracing::enabled!(tracing::Level::TRACE) {
99            return;
100        }
101
102        // Prepare lightweight ranking data without cloning transports
103        let mut ranked: Vec<(usize, f64, String)> =
104            self.transports.iter().map(|t| (t.id, t.score(), t.metrics_summary())).collect();
105
106        ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
107
108        trace!("Current transport rankings:");
109        for (idx, (id, _score, summary)) in ranked.iter().enumerate() {
110            trace!("  #{}: Transport[{}] - {}", idx + 1, id, summary);
111        }
112    }
113
114    /// Returns the top transports sorted by score (best first), limited by
115    /// `active_transport_count`.
116    fn top_transports(&self) -> Vec<ScoredTransport<S>> {
117        // Clone the vec, sort it, and keep only the top `self.active_transport_count`.
118        let mut transports_clone = (*self.transports).clone();
119        transports_clone.sort_by(|a, b| b.cmp(a));
120        transports_clone.truncate(self.active_transport_count);
121        transports_clone
122    }
123}
124
125impl<S> FallbackService<S>
126where
127    S: Service<RequestPacket, Future = TransportFut<'static>, Error = TransportError>
128        + Send
129        + Clone
130        + 'static,
131{
132    /// Make a request to the fallback service middleware.
133    ///
134    /// Here is a high-level overview of how requests are handled:
135    ///
136    /// **For methods with non-deterministic results** (e.g., `eth_sendRawTransactionSync`):
137    /// - Methods are tried sequentially on each transport
138    /// - Returns the first successful response
139    /// - Prevents returning wrong results (e.g., "already known" instead of receipt)
140    ///
141    /// **For methods with deterministic results** (default - most methods):
142    /// - At the start of each request, we sort transports by score
143    /// - We take the top `self.active_transport_count` and call them in parallel
144    /// - If any of them succeeds, we update the transport scores and return the response
145    /// - If all transports fail, we update the scores and return the last error that occurred
146    ///
147    /// This strategy allows us to always make requests to the best available transports
148    /// while ensuring correctness for methods that return different results in parallel.
149    async fn make_request(&self, req: RequestPacket) -> Result<ResponsePacket, TransportError> {
150        // Check if any method in the request requires sequential execution
151        // For batch requests: if ANY method needs sequential execution, the entire batch must be
152        // sequential
153        if req.method_names().any(|name| self.sequential_methods.contains(name)) {
154            return self.make_request_sequential(req).await;
155        }
156
157        // Default: parallel execution for methods with deterministic results
158        // Get the top transports to use for this request
159        let top_transports = self.top_transports();
160
161        if top_transports.is_empty() {
162            return Err(TransportErrorKind::custom_str(
163                "No transports available for fallback service",
164            ));
165        }
166
167        // Create a collection of future requests
168        let mut futures = FuturesUnordered::new();
169
170        // Launch requests to all active transports in parallel
171        for mut transport in top_transports {
172            let req_clone = req.clone();
173
174            let future = async move {
175                let start = Instant::now();
176                let result = transport.call(req_clone).await;
177                trace!(
178                    "Transport[{}] completed: latency={:?}, status={}",
179                    transport.id,
180                    start.elapsed(),
181                    if result.is_ok() { "success" } else { "fail" }
182                );
183
184                (result, transport, start.elapsed())
185            };
186
187            futures.push(future);
188        }
189
190        // Wait for the first successful response or until all fail
191        let mut last_error = None;
192
193        while let Some((result, transport, duration)) = futures.next().await {
194            match result {
195                Ok(response) => {
196                    // Record success
197                    transport.track_success(duration);
198
199                    self.log_transport_rankings();
200
201                    return Ok(response);
202                }
203                Err(error) => {
204                    // Record failure
205                    transport.track_failure();
206
207                    last_error = Some(error);
208                }
209            }
210        }
211
212        Err(last_error.unwrap_or_else(|| {
213            TransportErrorKind::custom_str("All transport futures failed to complete")
214        }))
215    }
216
217    /// Make a sequential request for methods with non-deterministic results.
218    ///
219    /// This method tries each transport one at a time, in order of their score.
220    /// It returns the first successful response, or an error if all transports fail.
221    ///
222    /// This approach ensures methods like `eth_sendRawTransactionSync` return the correct
223    /// receipt instead of "already known" errors from parallel execution.
224    async fn make_request_sequential(
225        &self,
226        req: RequestPacket,
227    ) -> Result<ResponsePacket, TransportError> {
228        trace!("Using sequential fallback for method with non-deterministic results");
229
230        // Get transports sorted by score (best first)
231        let top_transports = self.top_transports();
232
233        if top_transports.is_empty() {
234            return Err(TransportErrorKind::custom_str(
235                "No transports available for fallback service",
236            ));
237        }
238
239        let mut last_error = None;
240
241        // Try each transport sequentially
242        for mut transport in top_transports {
243            let req_clone = req.clone();
244            let start = Instant::now();
245
246            trace!("Trying transport[{}] sequentially", transport.id);
247
248            match transport.call(req_clone).await {
249                Ok(response) => {
250                    // Record success and return immediately
251                    transport.track_success(start.elapsed());
252                    trace!("Transport[{}] succeeded in {:?}", transport.id, start.elapsed());
253                    self.log_transport_rankings();
254                    return Ok(response);
255                }
256                Err(error) => {
257                    // Record failure and try next transport
258                    transport.track_failure();
259                    trace!("Transport[{}] failed: {:?}, trying next", transport.id, error);
260                    last_error = Some(error);
261                }
262            }
263        }
264
265        // All transports failed
266        Err(last_error.unwrap_or_else(|| {
267            TransportErrorKind::custom_str("All transports failed for sequential request")
268        }))
269    }
270}
271
272impl<S> Service<RequestPacket> for FallbackService<S>
273where
274    S: Service<RequestPacket, Future = TransportFut<'static>, Error = TransportError>
275        + Send
276        + Sync
277        + Clone
278        + 'static,
279{
280    type Response = ResponsePacket;
281    type Error = TransportError;
282    type Future = TransportFut<'static>;
283
284    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
285        // Service is always ready
286        Poll::Ready(Ok(()))
287    }
288
289    fn call(&mut self, req: RequestPacket) -> Self::Future {
290        let this = self.clone();
291        Box::pin(async move { this.make_request(req).await })
292    }
293}
294
295/// Fallback layer for transparent transport failover. This layer will
296/// consume a list of transports to provide better availability and
297/// reliability.
298///
299/// The [`FallbackService`] will attempt to make requests to multiple
300/// transports in parallel, and return the first successful response.
301///
302/// If all transports fail, the fallback service will return an error.
303///
304/// # Automatic Transport Ranking
305///
306/// Each transport is automatically ranked based on latency & stability
307/// using a weighted algorithm. By default:
308///
309/// - Stability (success rate) is weighted at 70%
310/// - Latency (response time) is weighted at 30%
311/// - The `active_transport_count` parameter controls how many transports are queried at any one
312///   time.
313#[derive(Debug, Clone)]
314pub struct FallbackLayer {
315    /// The maximum number of transports to use in parallel
316    active_transport_count: usize,
317    /// Set of RPC methods that require sequential execution (non-deterministic results in
318    /// parallel)
319    sequential_methods: HashSet<String>,
320}
321
322impl FallbackLayer {
323    /// Set the number of active transports to use (must be greater than 0)
324    pub const fn with_active_transport_count(mut self, count: NonZeroUsize) -> Self {
325        self.active_transport_count = count.get();
326        self
327    }
328
329    /// Add an RPC method that requires sequential execution.
330    ///
331    /// Sequential execution is needed for methods that return non-deterministic results
332    /// when executed in parallel across multiple nodes (e.g., methods that wait for confirmations).
333    pub fn with_sequential_method(mut self, method: impl Into<String>) -> Self {
334        self.sequential_methods.insert(method.into());
335        self
336    }
337
338    /// Set the complete list of RPC methods that require sequential execution.
339    ///
340    /// This replaces the default set. Use this if you want full control over which methods
341    /// use sequential execution.
342    pub fn with_sequential_methods(mut self, methods: HashSet<String>) -> Self {
343        self.sequential_methods = methods;
344        self
345    }
346
347    /// Clear all sequential methods (all requests will use parallel execution).
348    ///
349    /// **Warning**: Only use this if you're certain none of your RPC methods have
350    /// non-deterministic results in parallel execution.
351    pub fn without_sequential_methods(mut self) -> Self {
352        self.sequential_methods.clear();
353        self
354    }
355}
356
357impl<S> Layer<Vec<S>> for FallbackLayer
358where
359    S: Service<RequestPacket, Future = TransportFut<'static>, Error = TransportError>
360        + Send
361        + Clone
362        + 'static,
363{
364    type Service = FallbackService<S>;
365
366    fn layer(&self, inner: Vec<S>) -> Self::Service {
367        FallbackService::new_with_sequential_methods(
368            inner,
369            self.active_transport_count,
370            self.sequential_methods.clone(),
371        )
372    }
373}
374
375impl Default for FallbackLayer {
376    fn default() -> Self {
377        Self {
378            active_transport_count: DEFAULT_ACTIVE_TRANSPORT_COUNT,
379            sequential_methods: default_sequential_methods(),
380        }
381    }
382}
383
384/// A scored transport that can be ordered in a heap.
385///
386/// The transport is scored every time it is used according to
387/// a simple weighted algorithm that favors latency and stability.
388///
389/// The score is calculated as follows (by default):
390///
391/// - Stability (success rate) is weighted at 70%
392/// - Latency (response time) is weighted at 30%
393///
394/// The score is then used to determine which transport to use next in
395/// the [`FallbackService`].
396#[derive(Debug, Clone, Deref, DerefMut)]
397struct ScoredTransport<S> {
398    /// The transport itself
399    #[deref]
400    #[deref_mut]
401    transport: S,
402    /// Unique identifier for the transport
403    id: usize,
404    /// Metrics for the transport
405    metrics: Arc<RwLock<TransportMetrics>>,
406}
407
408impl<S> ScoredTransport<S> {
409    /// Create a new scored transport
410    fn new(id: usize, transport: S) -> Self {
411        Self { id, transport, metrics: Arc::new(Default::default()) }
412    }
413
414    /// Returns the current score of the transport based on the weighted algorithm.
415    fn score(&self) -> f64 {
416        let metrics = self.metrics.read();
417        metrics.calculate_score()
418    }
419
420    /// Get metrics summary for debugging
421    fn metrics_summary(&self) -> String {
422        let metrics = self.metrics.read();
423        metrics.get_summary()
424    }
425
426    /// Track a successful request and its latency.
427    fn track_success(&self, duration: Duration) {
428        let mut metrics = self.metrics.write();
429        metrics.track_success(duration);
430    }
431
432    /// Track a failed request.
433    fn track_failure(&self) {
434        let mut metrics = self.metrics.write();
435        metrics.track_failure();
436    }
437}
438
439impl<S> PartialEq for ScoredTransport<S> {
440    fn eq(&self, other: &Self) -> bool {
441        self.score().eq(&other.score())
442    }
443}
444
445impl<S> Eq for ScoredTransport<S> {}
446
447#[expect(clippy::non_canonical_partial_ord_impl)]
448impl<S> PartialOrd for ScoredTransport<S> {
449    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
450        self.score().partial_cmp(&other.score())
451    }
452}
453
454impl<S> Ord for ScoredTransport<S> {
455    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
456        self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
457    }
458}
459
460/// Represents performance metrics for a transport.
461#[derive(Debug)]
462struct TransportMetrics {
463    // Latency history - tracks last N responses
464    latencies: VecDeque<Duration>,
465    // Success history - tracks last N successes (true) or failures (false)
466    successes: VecDeque<bool>,
467    // Last time this transport was checked/used
468    last_update: Instant,
469    // Total number of requests made to this transport
470    total_requests: u64,
471    // Total number of successful requests
472    successful_requests: u64,
473}
474
475impl TransportMetrics {
476    /// Track a successful request and its latency.
477    fn track_success(&mut self, duration: Duration) {
478        self.total_requests += 1;
479        self.successful_requests += 1;
480        self.last_update = Instant::now();
481
482        // Add to sample windows
483        self.latencies.push_back(duration);
484        self.successes.push_back(true);
485
486        // Limit to sample count
487        while self.latencies.len() > DEFAULT_SAMPLE_COUNT {
488            self.latencies.pop_front();
489        }
490        while self.successes.len() > DEFAULT_SAMPLE_COUNT {
491            self.successes.pop_front();
492        }
493    }
494
495    /// Track a failed request.
496    fn track_failure(&mut self) {
497        self.total_requests += 1;
498        self.last_update = Instant::now();
499
500        // Add to sample windows (no latency for failures)
501        self.successes.push_back(false);
502
503        // Limit to sample count
504        while self.successes.len() > DEFAULT_SAMPLE_COUNT {
505            self.successes.pop_front();
506        }
507    }
508
509    /// Calculate weighted score based on stability and latency
510    fn calculate_score(&self) -> f64 {
511        // If no data yet, return initial neutral score
512        if self.successes.is_empty() {
513            return 0.0;
514        }
515
516        // Calculate stability score (percentage of successful requests)
517        let success_count = self.successes.iter().filter(|&&s| s).count();
518        let stability_score = success_count as f64 / self.successes.len() as f64;
519
520        // Calculate latency score (lower is better)
521        let latency_score = if !self.latencies.is_empty() {
522            let avg_latency = self.latencies.iter().map(|d| d.as_secs_f64()).sum::<f64>()
523                / self.latencies.len() as f64;
524
525            // Normalize latency score (1.0 for 0ms, approaches 0.0 as latency increases)
526            1.0 / (1.0 + avg_latency)
527        } else {
528            0.0
529        };
530
531        // Apply weights to calculate final score
532        (stability_score * STABILITY_WEIGHT) + (latency_score * LATENCY_WEIGHT)
533    }
534
535    /// Get a summary of metrics for debugging
536    fn get_summary(&self) -> String {
537        let success_rate = if !self.successes.is_empty() {
538            let success_count = self.successes.iter().filter(|&&s| s).count();
539            success_count as f64 / self.successes.len() as f64
540        } else {
541            0.0
542        };
543
544        let avg_latency = if !self.latencies.is_empty() {
545            self.latencies.iter().map(|d| d.as_secs_f64()).sum::<f64>()
546                / self.latencies.len() as f64
547        } else {
548            0.0
549        };
550
551        format!(
552            "success_rate: {:.2}%, avg_latency: {:.2}ms, samples: {}, score: {:.4}",
553            success_rate * 100.0,
554            avg_latency * 1000.0,
555            self.successes.len(),
556            self.calculate_score()
557        )
558    }
559}
560
561impl Default for TransportMetrics {
562    fn default() -> Self {
563        Self {
564            latencies: VecDeque::new(),
565            successes: VecDeque::new(),
566            last_update: Instant::now(),
567            total_requests: 0,
568            successful_requests: 0,
569        }
570    }
571}
572
573/// Returns the default set of RPC methods that require sequential execution.
574///
575/// These methods return different valid results when the same request is sent to multiple
576/// nodes in parallel, requiring sequential execution to ensure correct results.
577///
578/// Methods in this list share a common pattern:
579/// - They wait for transaction confirmation before returning
580/// - First node: submits tx → waits → returns receipt
581/// - Other nodes: tx already in mempool → return "already known" error
582/// - Result: parallel execution returns error instead of receipt
583///
584/// Sequential execution tries transports one at a time, in order of their score.
585/// Only moves to the next transport if the previous one fails. This ensures we
586/// always get the correct result while maintaining fallback capability.
587///
588/// # Default Methods:
589/// - `eth_sendRawTransactionSync` (EIP-7966): waits for receipt
590/// - `eth_sendTransactionSync`: same as above but for unsigned transactions
591fn default_sequential_methods() -> HashSet<String> {
592    ["eth_sendRawTransactionSync".to_string(), "eth_sendTransactionSync".to_string()]
593        .into_iter()
594        .collect()
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use alloy_json_rpc::{Id, Request, Response, ResponsePayload};
601    use std::sync::atomic::{AtomicUsize, Ordering};
602    use tokio::time::{sleep, Duration};
603    use tower::Service;
604
605    /// A mock transport that can be configured to return responses with delays
606    #[derive(Clone)]
607    struct DelayedMockTransport {
608        delay: Duration,
609        response: Arc<RwLock<Option<ResponsePayload>>>,
610        call_count: Arc<AtomicUsize>,
611    }
612
613    impl DelayedMockTransport {
614        fn new(delay: Duration, response: ResponsePayload) -> Self {
615            Self {
616                delay,
617                response: Arc::new(RwLock::new(Some(response))),
618                call_count: Arc::new(AtomicUsize::new(0)),
619            }
620        }
621
622        fn call_count(&self) -> usize {
623            self.call_count.load(Ordering::SeqCst)
624        }
625    }
626
627    impl Service<RequestPacket> for DelayedMockTransport {
628        type Response = ResponsePacket;
629        type Error = TransportError;
630        type Future = TransportFut<'static>;
631
632        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
633            Poll::Ready(Ok(()))
634        }
635
636        fn call(&mut self, req: RequestPacket) -> Self::Future {
637            self.call_count.fetch_add(1, Ordering::SeqCst);
638            let delay = self.delay;
639            let response = self.response.clone();
640
641            Box::pin(async move {
642                sleep(delay).await;
643
644                match req {
645                    RequestPacket::Single(single) => {
646                        let resp = response.read().clone().ok_or_else(|| {
647                            TransportErrorKind::custom_str("No response configured")
648                        })?;
649
650                        Ok(ResponsePacket::Single(Response {
651                            id: single.id().clone(),
652                            payload: resp,
653                        }))
654                    }
655                    RequestPacket::Batch(batch) => {
656                        let resp = response.read().clone().ok_or_else(|| {
657                            TransportErrorKind::custom_str("No response configured")
658                        })?;
659
660                        // Return the same response for each request in the batch
661                        let responses = batch
662                            .iter()
663                            .map(|req| Response { id: req.id().clone(), payload: resp.clone() })
664                            .collect();
665
666                        Ok(ResponsePacket::Batch(responses))
667                    }
668                }
669            })
670        }
671    }
672
673    /// Helper to create a successful response with given data
674    fn success_response(data: &str) -> ResponsePayload {
675        let raw = serde_json::value::RawValue::from_string(format!("\"{}\"", data)).unwrap();
676        ResponsePayload::Success(raw)
677    }
678
679    #[tokio::test]
680    async fn test_non_deterministic_method_uses_sequential_fallback() {
681        // Test that eth_sendRawTransactionSync (which returns non-deterministic results
682        // in parallel) uses sequential fallback and returns the correct receipt, not "already
683        // known"
684
685        let transport_a = DelayedMockTransport::new(
686            Duration::from_millis(50),
687            success_response("0x1234567890abcdef"), // Actual receipt
688        );
689
690        let transport_b = DelayedMockTransport::new(
691            Duration::from_millis(10),
692            success_response("already_known"), // Fast but wrong
693        );
694
695        let transports = vec![transport_a.clone(), transport_b.clone()];
696        let mut fallback_service = FallbackService::new(transports, 2);
697
698        let request = Request::new(
699            "eth_sendRawTransactionSync",
700            Id::Number(1),
701            [serde_json::Value::String("0xabcdef".to_string())],
702        );
703        let serialized = request.serialize().unwrap();
704        let request_packet = RequestPacket::Single(serialized);
705
706        let start = std::time::Instant::now();
707        let response = fallback_service.call(request_packet).await.unwrap();
708        let elapsed = start.elapsed();
709
710        let result = match response {
711            ResponsePacket::Single(resp) => match resp.payload {
712                ResponsePayload::Success(data) => data.get().to_string(),
713                ResponsePayload::Failure(err) => panic!("Unexpected error: {:?}", err),
714            },
715            ResponsePacket::Batch(_) => panic!("Unexpected batch response"),
716        };
717
718        // Should only call the first transport sequentially (succeeds immediately)
719        assert_eq!(transport_a.call_count(), 1, "First transport should be called");
720        // Should NOT call second transport since first succeeded
721        assert_eq!(transport_b.call_count(), 0, "Second transport should NOT be called");
722
723        // Should return the actual receipt, not "already_known"
724        assert_eq!(result, "\"0x1234567890abcdef\"");
725
726        // Should take ~50ms (first transport only), not ~10ms (second transport)
727        assert!(
728            elapsed >= Duration::from_millis(40),
729            "Should wait for first transport: {:?}",
730            elapsed
731        );
732    }
733
734    #[tokio::test]
735    async fn test_deterministic_method_uses_parallel_execution() {
736        // Test that eth_sendRawTransaction (which returns deterministic results)
737        // uses parallel execution because the tx hash is the same from all nodes
738
739        let tx_hash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
740
741        let transport_a = DelayedMockTransport::new(
742            Duration::from_millis(100),
743            success_response(tx_hash), // Same hash
744        );
745
746        let transport_b = DelayedMockTransport::new(
747            Duration::from_millis(20),
748            success_response(tx_hash), // Same hash, faster
749        );
750
751        let transports = vec![transport_a.clone(), transport_b.clone()];
752        let mut fallback_service = FallbackService::new(transports, 2);
753
754        let request = Request::new(
755            "eth_sendRawTransaction",
756            Id::Number(1),
757            [serde_json::Value::String("0xabcdef".to_string())],
758        );
759        let serialized = request.serialize().unwrap();
760        let request_packet = RequestPacket::Single(serialized);
761
762        let start = std::time::Instant::now();
763        let response = fallback_service.call(request_packet).await.unwrap();
764        let elapsed = start.elapsed();
765
766        let result = match response {
767            ResponsePacket::Single(resp) => match resp.payload {
768                ResponsePayload::Success(data) => data.get().to_string(),
769                ResponsePayload::Failure(err) => panic!("Unexpected error: {:?}", err),
770            },
771            ResponsePacket::Batch(_) => panic!("Unexpected batch response"),
772        };
773
774        // Both transports should be called in parallel
775        assert_eq!(transport_a.call_count(), 1, "Transport A should be called");
776        assert_eq!(transport_b.call_count(), 1, "Transport B should be called");
777
778        // Should return the tx hash (same from both)
779        assert_eq!(result, format!("\"{}\"", tx_hash));
780
781        // Should complete in ~20ms (fast transport), not ~100ms (slow transport)
782        assert!(
783            elapsed < Duration::from_millis(50),
784            "Should use parallel execution and return fast: {:?}",
785            elapsed
786        );
787    }
788
789    #[tokio::test]
790    async fn test_batch_with_any_sequential_method_uses_sequential_execution() {
791        // Test that if ANY method in a batch requires sequential execution,
792        // the entire batch is executed sequentially
793
794        let tx_hash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
795
796        // Transport A: Fast, returns success for both methods
797        let transport_a =
798            DelayedMockTransport::new(Duration::from_millis(10), success_response(tx_hash));
799
800        // Transport B: Also fast, but would return error (but shouldn't be called in sequential
801        // mode)
802        let transport_b = DelayedMockTransport::new(
803            Duration::from_millis(10),
804            success_response("should_not_be_called"),
805        );
806
807        let transports = vec![transport_a.clone(), transport_b.clone()];
808        let mut fallback_service = FallbackService::new(transports, 2);
809
810        // Create a batch with:
811        // 1. eth_blockNumber (deterministic, normally parallel)
812        // 2. eth_sendRawTransactionSync (non-deterministic, requires sequential)
813        let request1 = Request::new("eth_blockNumber", Id::Number(1), ());
814        let request2 = Request::new(
815            "eth_sendRawTransactionSync",
816            Id::Number(2),
817            [serde_json::Value::String("0xabcdef".to_string())],
818        );
819
820        let batch = vec![request1.serialize().unwrap(), request2.serialize().unwrap()];
821        let request_packet = RequestPacket::Batch(batch);
822
823        let start = std::time::Instant::now();
824        let response = fallback_service.call(request_packet).await.unwrap();
825        let elapsed = start.elapsed();
826
827        // In sequential mode: only transport_a should be called (it succeeds)
828        // transport_b should NOT be called because transport_a already succeeded
829        assert_eq!(
830            transport_a.call_count(),
831            1,
832            "Transport A should be called once (first in sequence)"
833        );
834        assert_eq!(
835            transport_b.call_count(),
836            0,
837            "Transport B should NOT be called (transport A succeeded)"
838        );
839
840        // Verify we got the correct response
841        match response {
842            ResponsePacket::Batch(responses) => {
843                assert_eq!(responses.len(), 2, "Should get 2 responses in batch");
844                // Both should be successful responses from transport A
845                for resp in responses {
846                    match resp.payload {
847                        ResponsePayload::Success(_) => {} // Expected
848                        ResponsePayload::Failure(err) => panic!("Unexpected error: {:?}", err),
849                    }
850                }
851            }
852            ResponsePacket::Single(_) => panic!("Expected batch response"),
853        }
854
855        // Should complete quickly since transport A is fast (10ms)
856        assert!(
857            elapsed < Duration::from_millis(50),
858            "Sequential execution with fast first transport should be quick: {:?}",
859            elapsed
860        );
861    }
862
863    #[tokio::test]
864    async fn test_custom_sequential_method() {
865        // Test that users can add custom methods to the sequential execution list
866
867        // Transport A: Fast, always succeeds
868        let transport_a =
869            DelayedMockTransport::new(Duration::from_millis(10), success_response("result_a"));
870
871        // Transport B: Also fast, returns different result
872        let transport_b =
873            DelayedMockTransport::new(Duration::from_millis(10), success_response("result_b"));
874
875        let transports = vec![transport_a.clone(), transport_b.clone()];
876
877        // Create FallbackService with custom sequential method "my_custom_method"
878        let custom_methods = ["my_custom_method".to_string()].into_iter().collect();
879        let mut fallback_service =
880            FallbackService::new(transports, 2).with_sequential_methods(custom_methods);
881
882        let request = Request::new("my_custom_method", Id::Number(1), ());
883        let serialized = request.serialize().unwrap();
884        let request_packet = RequestPacket::Single(serialized);
885
886        let start = std::time::Instant::now();
887        let _response = fallback_service.call(request_packet).await.unwrap();
888        let elapsed = start.elapsed();
889
890        // Should use sequential execution:
891        // - Only transport_a called (first in list, succeeds)
892        // - transport_b NOT called (sequential mode stops after first success)
893        assert_eq!(
894            transport_a.call_count(),
895            1,
896            "Transport A should be called once (sequential, first transport)"
897        );
898        assert_eq!(
899            transport_b.call_count(),
900            0,
901            "Transport B should NOT be called (sequential mode, A succeeded)"
902        );
903
904        // Should complete in ~10ms (only transport A called)
905        assert!(
906            elapsed < Duration::from_millis(50),
907            "Sequential execution with fast first transport: {:?}",
908            elapsed
909        );
910    }
911}