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}