openzeppelin_relayer/jobs/
status_check_context.rs

1//! Status check context for circuit breaker decisions.
2//!
3//! This module provides the `StatusCheckContext` struct which carries failure tracking
4//! information to network handlers, enabling them to make intelligent decisions about
5//! when to force-finalize transactions that have exceeded retry limits.
6//!
7//! Two thresholds are used for circuit breaker decisions:
8//! - **Consecutive failures**: Triggers when RPC is completely down
9//! - **Total failures**: Safety net for flaky RPC that succeeds occasionally but keeps failing
10
11use crate::constants::{EVM_MAX_CONSECUTIVE_STATUS_FAILURES, EVM_MAX_TOTAL_STATUS_FAILURES};
12use crate::models::NetworkType;
13
14/// Context for status check circuit breaker decisions.
15///
16/// This struct is passed to network handlers during status checks to provide
17/// failure tracking information. Handlers can use this to decide whether to
18/// force-finalize a transaction that has exceeded the maximum retry attempts.
19///
20/// The circuit breaker triggers when EITHER threshold is exceeded:
21/// - `consecutive_failures >= max_consecutive_failures` (RPC completely down)
22/// - `total_failures >= max_total_failures` (flaky RPC, safety net)
23///
24/// # Example
25///
26/// ```ignore
27/// let context = StatusCheckContext::new(
28///     consecutive_failures,
29///     total_failures,
30///     total_retries,
31///     max_consecutive_failures,
32///     max_total_failures,
33///     NetworkType::Stellar,
34/// );
35///
36/// if context.should_force_finalize() {
37///     // Mark transaction as Failed with appropriate reason
38/// }
39/// ```
40#[derive(Debug, Clone)]
41pub struct StatusCheckContext {
42    /// Number of consecutive failures since last successful status check.
43    /// Resets to 0 when a status check succeeds (even if transaction not final).
44    pub consecutive_failures: u32,
45
46    /// Total number of failures across all status check attempts.
47    /// Never resets - serves as safety net for flaky RPC connections.
48    pub total_failures: u32,
49
50    /// Total number of retries (from Apalis attempt counter).
51    /// Includes both successful and failed attempts.
52    pub total_retries: u32,
53
54    /// Maximum consecutive failures allowed before forcing finalization.
55    /// Network-specific value from constants.
56    pub max_consecutive_failures: u32,
57
58    /// Maximum total failures allowed before forcing finalization.
59    /// Safety net for flaky RPC that occasionally succeeds (resetting consecutive counter).
60    pub max_total_failures: u32,
61
62    /// The network type for this transaction.
63    pub network_type: NetworkType,
64}
65
66impl Default for StatusCheckContext {
67    fn default() -> Self {
68        Self {
69            consecutive_failures: 0,
70            total_failures: 0,
71            total_retries: 0,
72            max_consecutive_failures: EVM_MAX_CONSECUTIVE_STATUS_FAILURES,
73            max_total_failures: EVM_MAX_TOTAL_STATUS_FAILURES,
74            network_type: NetworkType::Evm,
75        }
76    }
77}
78
79impl StatusCheckContext {
80    /// Creates a new `StatusCheckContext` with the specified failure counts and limits.
81    ///
82    /// # Arguments
83    ///
84    /// * `consecutive_failures` - Current count of consecutive failures
85    /// * `total_failures` - Total count of all failures
86    /// * `total_retries` - Total Apalis retry attempts (includes successes)
87    /// * `max_consecutive_failures` - Network-specific consecutive max before force-finalization
88    /// * `max_total_failures` - Network-specific total max (safety net)
89    /// * `network_type` - The blockchain network type
90    pub fn new(
91        consecutive_failures: u32,
92        total_failures: u32,
93        total_retries: u32,
94        max_consecutive_failures: u32,
95        max_total_failures: u32,
96        network_type: NetworkType,
97    ) -> Self {
98        Self {
99            consecutive_failures,
100            total_failures,
101            total_retries,
102            max_consecutive_failures,
103            max_total_failures,
104            network_type,
105        }
106    }
107
108    /// Determines if the circuit breaker should force-finalize the transaction.
109    ///
110    /// Returns `true` if EITHER threshold is exceeded:
111    /// - Consecutive failures reached the network-specific maximum (RPC completely down)
112    /// - Total failures reached the network-specific maximum (flaky RPC safety net)
113    pub fn should_force_finalize(&self) -> bool {
114        self.consecutive_failures >= self.max_consecutive_failures
115            || self.total_failures >= self.max_total_failures
116    }
117
118    /// Returns true if triggered by consecutive failures threshold.
119    pub fn triggered_by_consecutive(&self) -> bool {
120        self.consecutive_failures >= self.max_consecutive_failures
121    }
122
123    /// Returns true if triggered by total failures threshold (safety net).
124    pub fn triggered_by_total(&self) -> bool {
125        self.total_failures >= self.max_total_failures
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_status_check_context_default() {
135        let ctx = StatusCheckContext::default();
136        assert_eq!(ctx.consecutive_failures, 0);
137        assert_eq!(ctx.total_failures, 0);
138        assert_eq!(ctx.total_retries, 0);
139        assert_eq!(
140            ctx.max_consecutive_failures,
141            EVM_MAX_CONSECUTIVE_STATUS_FAILURES
142        );
143        assert_eq!(ctx.max_total_failures, EVM_MAX_TOTAL_STATUS_FAILURES);
144        assert_eq!(ctx.network_type, NetworkType::Evm);
145    }
146
147    #[test]
148    fn test_status_check_context_new() {
149        let ctx = StatusCheckContext::new(5, 10, 20, 15, 45, NetworkType::Stellar);
150        assert_eq!(ctx.consecutive_failures, 5);
151        assert_eq!(ctx.total_failures, 10);
152        assert_eq!(ctx.total_retries, 20);
153        assert_eq!(ctx.max_consecutive_failures, 15);
154        assert_eq!(ctx.max_total_failures, 45);
155        assert_eq!(ctx.network_type, NetworkType::Stellar);
156    }
157
158    #[test]
159    fn test_should_force_finalize_below_both_thresholds() {
160        // consecutive: 5 < 15, total: 10 < 45
161        let ctx = StatusCheckContext::new(5, 10, 20, 15, 45, NetworkType::Evm);
162        assert!(!ctx.should_force_finalize());
163    }
164
165    #[test]
166    fn test_should_force_finalize_consecutive_at_threshold() {
167        // consecutive: 15 >= 15 (triggers), total: 20 < 45
168        let ctx = StatusCheckContext::new(15, 20, 30, 15, 45, NetworkType::Evm);
169        assert!(ctx.should_force_finalize());
170        assert!(ctx.triggered_by_consecutive());
171        assert!(!ctx.triggered_by_total());
172    }
173
174    #[test]
175    fn test_should_force_finalize_total_at_threshold() {
176        // consecutive: 5 < 15, total: 45 >= 45 (triggers - safety net)
177        let ctx = StatusCheckContext::new(5, 45, 50, 15, 45, NetworkType::Evm);
178        assert!(ctx.should_force_finalize());
179        assert!(!ctx.triggered_by_consecutive());
180        assert!(ctx.triggered_by_total());
181    }
182
183    #[test]
184    fn test_should_force_finalize_both_exceeded() {
185        // Both thresholds exceeded
186        let ctx = StatusCheckContext::new(20, 50, 60, 15, 45, NetworkType::Evm);
187        assert!(ctx.should_force_finalize());
188        assert!(ctx.triggered_by_consecutive());
189        assert!(ctx.triggered_by_total());
190    }
191
192    #[test]
193    fn test_flaky_rpc_scenario() {
194        // Simulates flaky RPC: consecutive keeps resetting but total grows
195        // consecutive: 3 < 15, total: 100 >= 45 (triggers safety net)
196        let ctx = StatusCheckContext::new(3, 100, 150, 15, 45, NetworkType::Evm);
197        assert!(ctx.should_force_finalize());
198        assert!(!ctx.triggered_by_consecutive());
199        assert!(ctx.triggered_by_total());
200    }
201}