openzeppelin_monitor/services/blockwatcher/
error.rs

1//! Block watcher error types and handling.
2//!
3//! Provides a comprehensive error handling system for block watching operations,
4//! including scheduling, network connectivity, and storage operations.
5
6use crate::utils::logging::error::{ErrorContext, TraceableError};
7use std::collections::HashMap;
8use thiserror::Error as ThisError;
9use uuid::Uuid;
10
11/// Represents possible errors that can occur during block watching operations
12#[derive(ThisError, Debug)]
13pub enum BlockWatcherError {
14	/// Errors related to network connectivity issues
15	#[error("Scheduler error: {0}")]
16	SchedulerError(ErrorContext),
17
18	/// Errors related to malformed requests or invalid responses
19	#[error("Network error: {0}")]
20	NetworkError(ErrorContext),
21
22	/// When a requested block cannot be found on the blockchain
23	#[error("Processing error: {0}")]
24	ProcessingError(ErrorContext),
25
26	/// Errors related to transaction processing
27	#[error("Storage error: {0}")]
28	StorageError(ErrorContext),
29
30	/// Internal errors within the blockchain client
31	#[error("Block tracker error: {0}")]
32	BlockTrackerError(ErrorContext),
33
34	/// Errors related to missed block recovery operations
35	#[error("Recovery error: {0}")]
36	RecoveryError(ErrorContext),
37
38	/// Other errors that don't fit into the categories above
39	#[error(transparent)]
40	Other(#[from] anyhow::Error),
41}
42
43impl BlockWatcherError {
44	// Scheduler error
45	pub fn scheduler_error(
46		msg: impl Into<String>,
47		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
48		metadata: Option<HashMap<String, String>>,
49	) -> Self {
50		Self::SchedulerError(ErrorContext::new_with_log(msg, source, metadata))
51	}
52
53	// Network error
54	pub fn network_error(
55		msg: impl Into<String>,
56		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
57		metadata: Option<HashMap<String, String>>,
58	) -> Self {
59		Self::NetworkError(ErrorContext::new_with_log(msg, source, metadata))
60	}
61
62	// Processing error
63	pub fn processing_error(
64		msg: impl Into<String>,
65		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
66		metadata: Option<HashMap<String, String>>,
67	) -> Self {
68		Self::ProcessingError(ErrorContext::new_with_log(msg, source, metadata))
69	}
70
71	// Storage error
72	pub fn storage_error(
73		msg: impl Into<String>,
74		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
75		metadata: Option<HashMap<String, String>>,
76	) -> Self {
77		Self::StorageError(ErrorContext::new_with_log(msg, source, metadata))
78	}
79
80	// Block tracker error
81	pub fn block_tracker_error(
82		msg: impl Into<String>,
83		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
84		metadata: Option<HashMap<String, String>>,
85	) -> Self {
86		Self::BlockTrackerError(ErrorContext::new_with_log(msg, source, metadata))
87	}
88
89	// Recovery error
90	pub fn recovery_error(
91		msg: impl Into<String>,
92		source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
93		metadata: Option<HashMap<String, String>>,
94	) -> Self {
95		Self::RecoveryError(ErrorContext::new_with_log(msg, source, metadata))
96	}
97}
98
99impl TraceableError for BlockWatcherError {
100	fn trace_id(&self) -> String {
101		match self {
102			Self::SchedulerError(ctx) => ctx.trace_id.clone(),
103			Self::NetworkError(ctx) => ctx.trace_id.clone(),
104			Self::ProcessingError(ctx) => ctx.trace_id.clone(),
105			Self::StorageError(ctx) => ctx.trace_id.clone(),
106			Self::BlockTrackerError(ctx) => ctx.trace_id.clone(),
107			Self::RecoveryError(ctx) => ctx.trace_id.clone(),
108			Self::Other(_) => Uuid::new_v4().to_string(),
109		}
110	}
111}
112
113#[cfg(test)]
114mod tests {
115	use super::*;
116	use std::io::{Error as IoError, ErrorKind};
117
118	#[test]
119	fn test_scheduler_error_formatting() {
120		let error = BlockWatcherError::scheduler_error("test error", None, None);
121		assert_eq!(error.to_string(), "Scheduler error: test error");
122
123		let source_error = IoError::new(ErrorKind::NotFound, "test source");
124		let error = BlockWatcherError::scheduler_error(
125			"test error",
126			Some(Box::new(source_error)),
127			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
128		);
129		assert_eq!(
130			error.to_string(),
131			"Scheduler error: test error [key1=value1]"
132		);
133	}
134
135	#[test]
136	fn test_network_error_formatting() {
137		let error = BlockWatcherError::network_error("test error", None, None);
138		assert_eq!(error.to_string(), "Network error: test error");
139
140		let source_error = IoError::new(ErrorKind::NotFound, "test source");
141		let error = BlockWatcherError::network_error(
142			"test error",
143			Some(Box::new(source_error)),
144			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
145		);
146		assert_eq!(error.to_string(), "Network error: test error [key1=value1]");
147	}
148
149	#[test]
150	fn test_processing_error_formatting() {
151		let error = BlockWatcherError::processing_error("test error", None, None);
152		assert_eq!(error.to_string(), "Processing error: test error");
153
154		let source_error = IoError::new(ErrorKind::NotFound, "test source");
155		let error = BlockWatcherError::processing_error(
156			"test error",
157			Some(Box::new(source_error)),
158			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
159		);
160		assert_eq!(
161			error.to_string(),
162			"Processing error: test error [key1=value1]"
163		);
164	}
165
166	#[test]
167	fn test_storage_error_formatting() {
168		let error = BlockWatcherError::storage_error("test error", None, None);
169		assert_eq!(error.to_string(), "Storage error: test error");
170
171		let source_error = IoError::new(ErrorKind::NotFound, "test source");
172		let error = BlockWatcherError::storage_error(
173			"test error",
174			Some(Box::new(source_error)),
175			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
176		);
177		assert_eq!(error.to_string(), "Storage error: test error [key1=value1]");
178	}
179
180	#[test]
181	fn test_block_tracker_error_formatting() {
182		let error = BlockWatcherError::block_tracker_error("test error", None, None);
183		assert_eq!(error.to_string(), "Block tracker error: test error");
184
185		let source_error = IoError::new(ErrorKind::NotFound, "test source");
186		let error = BlockWatcherError::block_tracker_error(
187			"test error",
188			Some(Box::new(source_error)),
189			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
190		);
191		assert_eq!(
192			error.to_string(),
193			"Block tracker error: test error [key1=value1]"
194		);
195	}
196
197	#[test]
198	fn test_recovery_error_formatting() {
199		let error = BlockWatcherError::recovery_error("test error", None, None);
200		assert_eq!(error.to_string(), "Recovery error: test error");
201
202		let source_error = IoError::new(ErrorKind::NotFound, "test source");
203		let error = BlockWatcherError::recovery_error(
204			"test error",
205			Some(Box::new(source_error)),
206			Some(HashMap::from([("key1".to_string(), "value1".to_string())])),
207		);
208		assert_eq!(
209			error.to_string(),
210			"Recovery error: test error [key1=value1]"
211		);
212
213		// Test trace_id for RecoveryError variant
214		let error = BlockWatcherError::recovery_error("trace test", None, None);
215		assert!(!error.trace_id().is_empty());
216	}
217
218	#[test]
219	fn test_from_anyhow_error() {
220		let anyhow_error = anyhow::anyhow!("test anyhow error");
221		let block_watcher_error: BlockWatcherError = anyhow_error.into();
222		assert!(matches!(block_watcher_error, BlockWatcherError::Other(_)));
223		assert_eq!(block_watcher_error.to_string(), "test anyhow error");
224	}
225
226	#[test]
227	fn test_error_source_chain() {
228		let io_error = std::io::Error::other("while reading config");
229
230		let outer_error = BlockWatcherError::scheduler_error(
231			"Failed to initialize",
232			Some(Box::new(io_error)),
233			None,
234		);
235
236		// Just test the string representation instead of the source chain
237		assert!(outer_error.to_string().contains("Failed to initialize"));
238
239		// For BlockWatcherError::SchedulerError, we know the implementation details
240		if let BlockWatcherError::SchedulerError(ctx) = &outer_error {
241			// Check that the context has the right message
242			assert_eq!(ctx.message, "Failed to initialize");
243
244			// Check that the context has the source error
245			assert!(ctx.source.is_some());
246
247			if let Some(src) = &ctx.source {
248				assert_eq!(src.to_string(), "while reading config");
249			}
250		} else {
251			panic!("Expected SchedulerError variant");
252		}
253	}
254
255	#[test]
256	fn test_trace_id_propagation() {
257		// Create an error context with a known trace ID
258		let error_context = ErrorContext::new("Inner error", None, None);
259		let original_trace_id = error_context.trace_id.clone();
260
261		// Wrap it in a BlockWatcherError
262		let block_watcher_error = BlockWatcherError::SchedulerError(error_context);
263
264		// Verify the trace ID is preserved
265		assert_eq!(block_watcher_error.trace_id(), original_trace_id);
266
267		// Test trace ID propagation through error chain
268		let source_error = IoError::other("Source error");
269		let error_context = ErrorContext::new("Middle error", Some(Box::new(source_error)), None);
270		let original_trace_id = error_context.trace_id.clone();
271
272		let block_watcher_error = BlockWatcherError::SchedulerError(error_context);
273		assert_eq!(block_watcher_error.trace_id(), original_trace_id);
274
275		// Test Other variant
276		let anyhow_error = anyhow::anyhow!("Test anyhow error");
277		let block_watcher_error: BlockWatcherError = anyhow_error.into();
278
279		// Other variant should generate a new UUID
280		assert!(!block_watcher_error.trace_id().is_empty());
281	}
282}