/home/noah/src/ruchy/src/runtime/observatory.rs
Line | Count | Source |
1 | | //! Actor observatory for live system introspection (RUCHY-0817) |
2 | | //! |
3 | | //! Provides comprehensive monitoring and debugging capabilities for the actor system, |
4 | | //! including message tracing, deadlock detection, and performance analysis. |
5 | | |
6 | | #[cfg(test)] |
7 | | mod tests { |
8 | | use super::*; |
9 | | use crate::runtime::actor::{ActorSystem, Message}; |
10 | | use std::sync::{Arc, Mutex}; |
11 | | use std::time::Duration; |
12 | | |
13 | | // Helper functions for consistent test setup |
14 | 17 | fn create_test_actor_system() -> Arc<Mutex<ActorSystem>> { |
15 | 17 | ActorSystem::new() |
16 | 17 | } |
17 | | |
18 | 17 | fn create_test_config() -> ObservatoryConfig { |
19 | 17 | ObservatoryConfig { |
20 | 17 | max_traces: 100, |
21 | 17 | trace_retention_seconds: 3600, |
22 | 17 | enable_deadlock_detection: true, |
23 | 17 | deadlock_check_interval_ms: 1000, |
24 | 17 | enable_metrics: true, |
25 | 17 | metrics_interval_ms: 5000, |
26 | 17 | max_snapshots: 50, |
27 | 17 | } |
28 | 17 | } |
29 | | |
30 | 13 | fn create_test_observatory() -> ActorObservatory { |
31 | 13 | let system = create_test_actor_system(); |
32 | 13 | let config = create_test_config(); |
33 | 13 | ActorObservatory::new(system, config) |
34 | 13 | } |
35 | | |
36 | 25 | fn create_test_message_trace() -> MessageTrace { |
37 | 25 | MessageTrace { |
38 | 25 | trace_id: 12345, |
39 | 25 | timestamp: current_timestamp(), |
40 | 25 | source: Some(ActorId(1)), |
41 | 25 | destination: ActorId(2), |
42 | 25 | message: Message::User("test_message".to_string(), vec![]), |
43 | 25 | status: MessageStatus::Queued, |
44 | 25 | processing_duration_us: None, |
45 | 25 | error: None, |
46 | 25 | stack_depth: 1, |
47 | 25 | correlation_id: Some("corr-123".to_string()), |
48 | 25 | } |
49 | 25 | } |
50 | | |
51 | 3 | fn create_test_actor_snapshot() -> ActorSnapshot { |
52 | 3 | ActorSnapshot { |
53 | 3 | actor_id: ActorId(1), |
54 | 3 | name: "test_actor".to_string(), |
55 | 3 | timestamp: current_timestamp(), |
56 | 3 | state: ActorState::Running, |
57 | 3 | mailbox_size: 5, |
58 | 3 | parent: Some(ActorId(0)), |
59 | 3 | children: vec![ActorId(2), ActorId(3)], |
60 | 3 | message_stats: MessageStats::default(), |
61 | 3 | memory_usage: Some(1024), |
62 | 3 | } |
63 | 3 | } |
64 | | |
65 | 3 | fn create_test_message_filter() -> MessageFilter { |
66 | 3 | MessageFilter { |
67 | 3 | name: "test_filter".to_string(), |
68 | 3 | actor_id: Some(ActorId(1)), |
69 | 3 | actor_name_pattern: Some("test_actor".to_string()), |
70 | 3 | message_type_pattern: Some(".*message.*".to_string()), |
71 | 3 | min_processing_time_us: None, |
72 | 3 | failed_only: false, |
73 | 3 | max_stack_depth: Some(10), |
74 | 3 | } |
75 | 3 | } |
76 | | |
77 | | // ========== Observatory Configuration Tests ========== |
78 | | |
79 | | #[test] |
80 | 1 | fn test_observatory_config_default() { |
81 | 1 | let config = ObservatoryConfig::default(); |
82 | 1 | assert_eq!(config.max_traces, 10000); |
83 | 1 | assert_eq!(config.trace_retention_seconds, 3600); |
84 | 1 | assert!(config.enable_deadlock_detection); |
85 | 1 | assert_eq!(config.deadlock_check_interval_ms, 1000); |
86 | 1 | assert!(config.enable_metrics); |
87 | 1 | assert_eq!(config.metrics_interval_ms, 5000); |
88 | 1 | assert_eq!(config.max_snapshots, 1000); |
89 | 1 | } |
90 | | |
91 | | #[test] |
92 | 1 | fn test_observatory_config_clone() { |
93 | 1 | let config1 = create_test_config(); |
94 | 1 | let config2 = config1.clone(); |
95 | 1 | assert_eq!(config1.max_traces, config2.max_traces); |
96 | 1 | assert_eq!(config1.enable_deadlock_detection, config2.enable_deadlock_detection); |
97 | 1 | } |
98 | | |
99 | | #[test] |
100 | 1 | fn test_observatory_config_debug() { |
101 | 1 | let config = create_test_config(); |
102 | 1 | let debug_str = format!("{:?}", config); |
103 | 1 | assert!(debug_str.contains("ObservatoryConfig")); |
104 | 1 | assert!(debug_str.contains("max_traces")); |
105 | 1 | assert!(debug_str.contains("enable_deadlock_detection")); |
106 | 1 | } |
107 | | |
108 | | #[test] |
109 | 1 | fn test_observatory_config_serialization() { |
110 | 1 | let config = create_test_config(); |
111 | 1 | let json = serde_json::to_string(&config).unwrap(); |
112 | 1 | let deserialized: ObservatoryConfig = serde_json::from_str(&json).unwrap(); |
113 | 1 | assert_eq!(config.max_traces, deserialized.max_traces); |
114 | 1 | assert_eq!(config.enable_metrics, deserialized.enable_metrics); |
115 | 1 | } |
116 | | |
117 | | // ========== Observatory Creation and Setup Tests ========== |
118 | | |
119 | | #[test] |
120 | 1 | fn test_observatory_creation() { |
121 | 1 | let system = create_test_actor_system(); |
122 | 1 | let config = create_test_config(); |
123 | 1 | let observatory = ActorObservatory::new(system, config.clone()); |
124 | | |
125 | 1 | assert_eq!(observatory.config.max_traces, config.max_traces); |
126 | 1 | assert!(observatory.filters.is_empty()); |
127 | 1 | assert!(observatory.start_time.elapsed() < Duration::from_secs(1)); |
128 | 1 | } |
129 | | |
130 | | #[test] |
131 | 1 | fn test_observatory_with_default_config() { |
132 | 1 | let system = create_test_actor_system(); |
133 | 1 | let config = ObservatoryConfig::default(); |
134 | 1 | let observatory = ActorObservatory::new(system, config); |
135 | | |
136 | 1 | assert_eq!(observatory.config.max_traces, 10000); |
137 | 1 | assert!(observatory.config.enable_deadlock_detection); |
138 | 1 | } |
139 | | |
140 | | #[test] |
141 | 1 | fn test_observatory_initialization_state() { |
142 | 1 | let observatory = create_test_observatory(); |
143 | | |
144 | | // Should start with empty state |
145 | 1 | assert!(observatory.get_filters().is_empty()); |
146 | 1 | let metrics = observatory.metrics.lock().unwrap(); |
147 | 1 | assert_eq!(metrics.active_actors, 0); |
148 | 1 | assert_eq!(metrics.total_messages_processed, 0); |
149 | 1 | } |
150 | | |
151 | | // ========== Message Filter Management Tests ========== |
152 | | |
153 | | #[test] |
154 | 1 | fn test_add_message_filter() { |
155 | 1 | let mut observatory = create_test_observatory(); |
156 | 1 | let filter = create_test_message_filter(); |
157 | | |
158 | 1 | observatory.add_filter(filter.clone()); |
159 | | |
160 | 1 | assert_eq!(observatory.get_filters().len(), 1); |
161 | 1 | assert_eq!(observatory.get_filters()[0].name, filter.name); |
162 | 1 | } |
163 | | |
164 | | #[test] |
165 | 1 | fn test_add_multiple_filters() { |
166 | 1 | let mut observatory = create_test_observatory(); |
167 | | |
168 | 1 | let filter1 = MessageFilter { |
169 | 1 | name: "filter1".to_string(), |
170 | 1 | actor_id: None, |
171 | 1 | actor_name_pattern: None, |
172 | 1 | message_type_pattern: Some("type1".to_string()), |
173 | 1 | min_processing_time_us: None, |
174 | 1 | failed_only: false, |
175 | 1 | max_stack_depth: None, |
176 | 1 | }; |
177 | | |
178 | 1 | let filter2 = MessageFilter { |
179 | 1 | name: "filter2".to_string(), |
180 | 1 | actor_id: Some(ActorId(2)), |
181 | 1 | actor_name_pattern: Some("actor2".to_string()), |
182 | 1 | message_type_pattern: None, |
183 | 1 | min_processing_time_us: Some(1000), |
184 | 1 | failed_only: true, |
185 | 1 | max_stack_depth: Some(5), |
186 | 1 | }; |
187 | | |
188 | 1 | observatory.add_filter(filter1); |
189 | 1 | observatory.add_filter(filter2); |
190 | | |
191 | 1 | assert_eq!(observatory.get_filters().len(), 2); |
192 | 1 | assert_eq!(observatory.get_filters()[0].name, "filter1"); |
193 | 1 | assert_eq!(observatory.get_filters()[1].name, "filter2"); |
194 | 1 | } |
195 | | |
196 | | #[test] |
197 | 1 | fn test_remove_message_filter() { |
198 | 1 | let mut observatory = create_test_observatory(); |
199 | 1 | let filter = create_test_message_filter(); |
200 | | |
201 | 1 | observatory.add_filter(filter); |
202 | 1 | assert_eq!(observatory.get_filters().len(), 1); |
203 | | |
204 | 1 | let removed = observatory.remove_filter("test_filter"); |
205 | 1 | assert!(removed); |
206 | 1 | assert!(observatory.get_filters().is_empty()); |
207 | 1 | } |
208 | | |
209 | | #[test] |
210 | 1 | fn test_remove_nonexistent_filter() { |
211 | 1 | let mut observatory = create_test_observatory(); |
212 | 1 | let removed = observatory.remove_filter("nonexistent"); |
213 | 1 | assert!(!removed); |
214 | 1 | } |
215 | | |
216 | | #[test] |
217 | 1 | fn test_get_filters_empty() { |
218 | 1 | let observatory = create_test_observatory(); |
219 | 1 | assert!(observatory.get_filters().is_empty()); |
220 | 1 | } |
221 | | |
222 | | // ========== Message Tracing Tests ========== |
223 | | |
224 | | #[test] |
225 | 1 | fn test_trace_message() { |
226 | 1 | let observatory = create_test_observatory(); |
227 | 1 | let trace = create_test_message_trace(); |
228 | | |
229 | 1 | let result = observatory.trace_message(trace.clone()); |
230 | 1 | assert!(result.is_ok()); |
231 | | |
232 | 1 | let traces = observatory.get_traces(None, None).unwrap(); |
233 | 1 | assert_eq!(traces.len(), 1); |
234 | 1 | assert_eq!(traces[0].trace_id, trace.trace_id); |
235 | 1 | } |
236 | | |
237 | | #[test] |
238 | 1 | fn test_trace_message_with_limit() { |
239 | 1 | let mut observatory = create_test_observatory(); |
240 | 1 | observatory.config.max_traces = 2; |
241 | | |
242 | | // Add 3 traces, should only keep 2 most recent |
243 | 4 | for i3 in 0..3 { |
244 | 3 | let mut trace = create_test_message_trace(); |
245 | 3 | trace.trace_id = i as u64; |
246 | 3 | observatory.trace_message(trace).unwrap(); |
247 | 3 | } |
248 | | |
249 | 1 | let traces = observatory.get_traces(None, None).unwrap(); |
250 | 1 | assert_eq!(traces.len(), 2); |
251 | 1 | assert_eq!(traces[0].trace_id, 1); // First trace should be evicted |
252 | 1 | assert_eq!(traces[1].trace_id, 2); |
253 | 1 | } |
254 | | |
255 | | #[test] |
256 | 1 | fn test_trace_message_age_retention() { |
257 | 1 | let mut observatory = create_test_observatory(); |
258 | 1 | observatory.config.trace_retention_seconds = 1; // 1 second retention |
259 | | |
260 | | // Add an old trace |
261 | 1 | let mut old_trace = create_test_message_trace(); |
262 | 1 | old_trace.timestamp = current_timestamp() - 3600; // 1 hour ago |
263 | 1 | observatory.trace_message(old_trace).unwrap(); |
264 | | |
265 | | // Add a recent trace |
266 | 1 | let recent_trace = create_test_message_trace(); |
267 | 1 | observatory.trace_message(recent_trace).unwrap(); |
268 | | |
269 | 1 | let traces = observatory.get_traces(None, None).unwrap(); |
270 | | // Only recent trace should remain |
271 | 1 | assert_eq!(traces.len(), 1); |
272 | 1 | } |
273 | | |
274 | | #[test] |
275 | 1 | fn test_get_traces_with_limit() { |
276 | 1 | let observatory = create_test_observatory(); |
277 | | |
278 | | // Add 5 traces |
279 | 6 | for i5 in 0..5 { |
280 | 5 | let mut trace = create_test_message_trace(); |
281 | 5 | trace.trace_id = i as u64; |
282 | 5 | observatory.trace_message(trace).unwrap(); |
283 | 5 | } |
284 | | |
285 | 1 | let traces = observatory.get_traces(Some(3), None).unwrap(); |
286 | 1 | assert_eq!(traces.len(), 3); |
287 | 1 | } |
288 | | |
289 | | // ========== Message Status Tests ========== |
290 | | |
291 | | #[test] |
292 | 1 | fn test_message_status_variants() { |
293 | 1 | let statuses = vec![ |
294 | 1 | MessageStatus::Queued, |
295 | 1 | MessageStatus::Processing, |
296 | 1 | MessageStatus::Completed, |
297 | 1 | MessageStatus::Failed, |
298 | 1 | MessageStatus::Dropped, |
299 | | ]; |
300 | | |
301 | 1 | assert_eq!(statuses.len(), 5); |
302 | 1 | assert_eq!(statuses[0], MessageStatus::Queued); |
303 | 1 | assert_ne!(statuses[0], MessageStatus::Processing); |
304 | 1 | } |
305 | | |
306 | | #[test] |
307 | 1 | fn test_message_status_serialization() { |
308 | 1 | let status = MessageStatus::Processing; |
309 | 1 | let json = serde_json::to_string(&status).unwrap(); |
310 | 1 | let deserialized: MessageStatus = serde_json::from_str(&json).unwrap(); |
311 | 1 | assert_eq!(status, deserialized); |
312 | 1 | } |
313 | | |
314 | | #[test] |
315 | 1 | fn test_message_status_debug() { |
316 | 1 | let status = MessageStatus::Failed; |
317 | 1 | let debug_str = format!("{:?}", status); |
318 | 1 | assert!(debug_str.contains("Failed")); |
319 | 1 | } |
320 | | |
321 | | // ========== Actor State Tests ========== |
322 | | |
323 | | #[test] |
324 | 1 | fn test_actor_state_variants() { |
325 | 1 | let states = vec![ |
326 | 1 | ActorState::Starting, |
327 | 1 | ActorState::Running, |
328 | 1 | ActorState::Processing("test_message".to_string()), |
329 | 1 | ActorState::Restarting, |
330 | 1 | ActorState::Stopping, |
331 | 1 | ActorState::Stopped, |
332 | 1 | ActorState::Failed("test_error".to_string()), |
333 | | ]; |
334 | | |
335 | 1 | assert_eq!(states.len(), 7); |
336 | 1 | assert_eq!(states[0], ActorState::Starting); |
337 | 1 | assert_ne!(states[0], ActorState::Running); |
338 | 1 | } |
339 | | |
340 | | #[test] |
341 | 1 | fn test_actor_state_processing() { |
342 | 1 | let state = ActorState::Processing("handle_request".to_string()); |
343 | 1 | if let ActorState::Processing(message_type) = state { |
344 | 1 | assert_eq!(message_type, "handle_request"); |
345 | | } else { |
346 | 0 | panic!("Expected Processing state"); |
347 | | } |
348 | 1 | } |
349 | | |
350 | | #[test] |
351 | 1 | fn test_actor_state_failed() { |
352 | 1 | let state = ActorState::Failed("connection_timeout".to_string()); |
353 | 1 | if let ActorState::Failed(reason) = state { |
354 | 1 | assert_eq!(reason, "connection_timeout"); |
355 | | } else { |
356 | 0 | panic!("Expected Failed state"); |
357 | | } |
358 | 1 | } |
359 | | |
360 | | #[test] |
361 | 1 | fn test_actor_state_serialization() { |
362 | 1 | let state = ActorState::Processing("test".to_string()); |
363 | 1 | let json = serde_json::to_string(&state).unwrap(); |
364 | 1 | let deserialized: ActorState = serde_json::from_str(&json).unwrap(); |
365 | 1 | assert_eq!(state, deserialized); |
366 | 1 | } |
367 | | |
368 | | // ========== Message Statistics Tests ========== |
369 | | |
370 | | #[test] |
371 | 1 | fn test_message_stats_default() { |
372 | 1 | let stats = MessageStats::default(); |
373 | 1 | assert_eq!(stats.total_processed, 0); |
374 | 1 | assert_eq!(stats.messages_per_second, 0.0); |
375 | 1 | assert_eq!(stats.avg_processing_time_us, 0.0); |
376 | 1 | assert_eq!(stats.max_processing_time_us, 0); |
377 | 1 | assert_eq!(stats.failed_messages, 0); |
378 | 1 | assert!(stats.last_processed.is_none()); |
379 | 1 | } |
380 | | |
381 | | #[test] |
382 | 1 | fn test_message_stats_clone() { |
383 | 1 | let mut stats1 = MessageStats::default(); |
384 | 1 | stats1.total_processed = 100; |
385 | 1 | stats1.messages_per_second = 10.5; |
386 | | |
387 | 1 | let stats2 = stats1.clone(); |
388 | 1 | assert_eq!(stats1.total_processed, stats2.total_processed); |
389 | 1 | assert_eq!(stats1.messages_per_second, stats2.messages_per_second); |
390 | 1 | } |
391 | | |
392 | | #[test] |
393 | 1 | fn test_message_stats_serialization() { |
394 | 1 | let mut stats = MessageStats::default(); |
395 | 1 | stats.total_processed = 500; |
396 | 1 | stats.avg_processing_time_us = 1500.0; |
397 | | |
398 | 1 | let json = serde_json::to_string(&stats).unwrap(); |
399 | 1 | let deserialized: MessageStats = serde_json::from_str(&json).unwrap(); |
400 | 1 | assert_eq!(stats.total_processed, deserialized.total_processed); |
401 | 1 | } |
402 | | |
403 | | // ========== System Metrics Tests ========== |
404 | | |
405 | | #[test] |
406 | 1 | fn test_system_metrics_default() { |
407 | 1 | let metrics = SystemMetrics::default(); |
408 | 1 | assert_eq!(metrics.active_actors, 0); |
409 | 1 | assert_eq!(metrics.total_messages_processed, 0); |
410 | 1 | assert_eq!(metrics.system_messages_per_second, 0.0); |
411 | 1 | assert_eq!(metrics.total_memory_usage, 0); |
412 | 1 | assert_eq!(metrics.total_queued_messages, 0); |
413 | 1 | assert_eq!(metrics.avg_mailbox_size, 0.0); |
414 | 1 | assert_eq!(metrics.recent_restarts, 0); |
415 | 1 | assert!(metrics.last_updated > 0); // Uses current timestamp |
416 | 1 | } |
417 | | |
418 | | #[test] |
419 | 1 | fn test_system_metrics_update() { |
420 | 1 | let mut metrics = SystemMetrics::default(); |
421 | 1 | metrics.active_actors = 10; |
422 | 1 | metrics.total_messages_processed = 1000; |
423 | 1 | metrics.system_messages_per_second = 50.5; |
424 | 1 | metrics.last_updated = current_timestamp(); |
425 | | |
426 | 1 | assert_eq!(metrics.active_actors, 10); |
427 | 1 | assert_eq!(metrics.total_messages_processed, 1000); |
428 | 1 | assert!(metrics.last_updated > 0); |
429 | 1 | } |
430 | | |
431 | | #[test] |
432 | 1 | fn test_system_metrics_serialization() { |
433 | 1 | let mut metrics = SystemMetrics::default(); |
434 | 1 | metrics.active_actors = 5; |
435 | 1 | metrics.total_memory_usage = 1024000; |
436 | | |
437 | 1 | let json = serde_json::to_string(&metrics).unwrap(); |
438 | 1 | let deserialized: SystemMetrics = serde_json::from_str(&json).unwrap(); |
439 | 1 | assert_eq!(metrics.active_actors, deserialized.active_actors); |
440 | 1 | assert_eq!(metrics.total_memory_usage, deserialized.total_memory_usage); |
441 | 1 | } |
442 | | |
443 | | // ========== Actor Snapshot Tests ========== |
444 | | |
445 | | #[test] |
446 | 1 | fn test_actor_snapshot_creation() { |
447 | 1 | let snapshot = create_test_actor_snapshot(); |
448 | | |
449 | 1 | assert_eq!(snapshot.actor_id, ActorId(1)); |
450 | 1 | assert_eq!(snapshot.name, "test_actor"); |
451 | 1 | assert_eq!(snapshot.state, ActorState::Running); |
452 | 1 | assert_eq!(snapshot.mailbox_size, 5); |
453 | 1 | assert_eq!(snapshot.children.len(), 2); |
454 | 1 | assert!(snapshot.memory_usage.is_some()); |
455 | 1 | } |
456 | | |
457 | | #[test] |
458 | 1 | fn test_actor_snapshot_with_no_parent() { |
459 | 1 | let mut snapshot = create_test_actor_snapshot(); |
460 | 1 | snapshot.parent = None; |
461 | | |
462 | 1 | assert!(snapshot.parent.is_none()); |
463 | 1 | assert_eq!(snapshot.children.len(), 2); |
464 | 1 | } |
465 | | |
466 | | #[test] |
467 | 1 | fn test_actor_snapshot_serialization() { |
468 | 1 | let snapshot = create_test_actor_snapshot(); |
469 | | |
470 | 1 | let json = serde_json::to_string(&snapshot).unwrap(); |
471 | 1 | let deserialized: ActorSnapshot = serde_json::from_str(&json).unwrap(); |
472 | | |
473 | 1 | assert_eq!(snapshot.actor_id, deserialized.actor_id); |
474 | 1 | assert_eq!(snapshot.name, deserialized.name); |
475 | 1 | assert_eq!(snapshot.state, deserialized.state); |
476 | 1 | } |
477 | | |
478 | | // ========== Message Filter Tests ========== |
479 | | |
480 | | #[test] |
481 | 1 | fn test_message_filter_creation() { |
482 | 1 | let filter = create_test_message_filter(); |
483 | | |
484 | 1 | assert_eq!(filter.name, "test_filter"); |
485 | 1 | assert!(filter.actor_id.is_some()); |
486 | 1 | assert!(filter.actor_name_pattern.is_some()); |
487 | 1 | assert!(filter.message_type_pattern.is_some()); |
488 | 1 | assert!(!filter.failed_only); |
489 | 1 | } |
490 | | |
491 | | #[test] |
492 | 1 | fn test_message_filter_with_duration_limits() { |
493 | 1 | let filter = MessageFilter { |
494 | 1 | name: "duration_filter".to_string(), |
495 | 1 | actor_id: None, |
496 | 1 | actor_name_pattern: None, |
497 | 1 | message_type_pattern: None, |
498 | 1 | min_processing_time_us: Some(1000), |
499 | 1 | failed_only: false, |
500 | 1 | max_stack_depth: None, |
501 | 1 | }; |
502 | | |
503 | 1 | assert_eq!(filter.min_processing_time_us, Some(1000)); |
504 | 1 | } |
505 | | |
506 | | #[test] |
507 | 1 | fn test_message_filter_errors_only() { |
508 | 1 | let filter = MessageFilter { |
509 | 1 | name: "error_filter".to_string(), |
510 | 1 | actor_id: None, |
511 | 1 | actor_name_pattern: None, |
512 | 1 | message_type_pattern: None, |
513 | 1 | min_processing_time_us: None, |
514 | 1 | failed_only: true, |
515 | 1 | max_stack_depth: None, |
516 | 1 | }; |
517 | | |
518 | 1 | assert!(filter.failed_only); |
519 | 1 | } |
520 | | |
521 | | // ========== Message Trace Tests ========== |
522 | | |
523 | | #[test] |
524 | 1 | fn test_message_trace_creation() { |
525 | 1 | let trace = create_test_message_trace(); |
526 | | |
527 | 1 | assert_eq!(trace.trace_id, 12345); |
528 | 1 | assert!(trace.source.is_some()); |
529 | 1 | assert_eq!(trace.destination, ActorId(2)); |
530 | 1 | assert_eq!(trace.status, MessageStatus::Queued); |
531 | 1 | assert_eq!(trace.stack_depth, 1); |
532 | 1 | assert!(trace.correlation_id.is_some()); |
533 | 1 | } |
534 | | |
535 | | #[test] |
536 | 1 | fn test_message_trace_with_processing() { |
537 | 1 | let mut trace = create_test_message_trace(); |
538 | 1 | trace.status = MessageStatus::Processing; |
539 | 1 | trace.processing_duration_us = Some(1500); |
540 | | |
541 | 1 | assert_eq!(trace.status, MessageStatus::Processing); |
542 | 1 | assert_eq!(trace.processing_duration_us, Some(1500)); |
543 | 1 | } |
544 | | |
545 | | #[test] |
546 | 1 | fn test_message_trace_with_error() { |
547 | 1 | let mut trace = create_test_message_trace(); |
548 | 1 | trace.status = MessageStatus::Failed; |
549 | 1 | trace.error = Some("timeout_error".to_string()); |
550 | | |
551 | 1 | assert_eq!(trace.status, MessageStatus::Failed); |
552 | 1 | assert_eq!(trace.error, Some("timeout_error".to_string())); |
553 | 1 | } |
554 | | |
555 | | #[test] |
556 | 1 | fn test_message_trace_serialization() { |
557 | 1 | let trace = create_test_message_trace(); |
558 | | |
559 | 1 | let json = serde_json::to_string(&trace).unwrap(); |
560 | 1 | let deserialized: MessageTrace = serde_json::from_str(&json).unwrap(); |
561 | | |
562 | 1 | assert_eq!(trace.trace_id, deserialized.trace_id); |
563 | 1 | assert_eq!(trace.status, deserialized.status); |
564 | 1 | } |
565 | | |
566 | | // ========== Utility Function Tests ========== |
567 | | |
568 | | #[test] |
569 | 1 | fn test_current_timestamp() { |
570 | 1 | let ts1 = current_timestamp(); |
571 | 1 | std::thread::sleep(Duration::from_millis(10)); // Increase sleep time for more reliable test |
572 | 1 | let ts2 = current_timestamp(); |
573 | | |
574 | 1 | assert!(ts2 >= ts1); // May be equal due to precision |
575 | 1 | } |
576 | | |
577 | | // ========== Integration Tests ========== |
578 | | |
579 | | #[test] |
580 | 1 | fn test_observatory_full_workflow() { |
581 | 1 | let mut observatory = create_test_observatory(); |
582 | | |
583 | | // Add filter that matches our test message |
584 | 1 | let filter = MessageFilter { |
585 | 1 | name: "permissive_filter".to_string(), |
586 | 1 | actor_id: None, // Allow all actors |
587 | 1 | actor_name_pattern: None, |
588 | 1 | message_type_pattern: None, // Allow all message types |
589 | 1 | min_processing_time_us: None, |
590 | 1 | failed_only: false, |
591 | 1 | max_stack_depth: None, |
592 | 1 | }; |
593 | 1 | observatory.add_filter(filter); |
594 | 1 | assert_eq!(observatory.get_filters().len(), 1); |
595 | | |
596 | | // Add trace |
597 | 1 | let trace = create_test_message_trace(); |
598 | 1 | observatory.trace_message(trace.clone()).unwrap(); |
599 | | |
600 | | // Verify trace was recorded |
601 | 1 | let traces = observatory.get_traces(None, None).unwrap(); |
602 | 1 | assert_eq!(traces.len(), 1); |
603 | 1 | assert_eq!(traces[0].trace_id, trace.trace_id); |
604 | | |
605 | | // Remove filter |
606 | 1 | let removed = observatory.remove_filter("permissive_filter"); |
607 | 1 | assert!(removed); |
608 | 1 | assert!(observatory.get_filters().is_empty()); |
609 | 1 | } |
610 | | |
611 | | #[test] |
612 | 1 | fn test_observatory_multiple_traces() { |
613 | 1 | let observatory = create_test_observatory(); |
614 | | |
615 | | // Add multiple traces with different statuses |
616 | 1 | let statuses = vec![ |
617 | 1 | MessageStatus::Queued, |
618 | 1 | MessageStatus::Processing, |
619 | 1 | MessageStatus::Completed, |
620 | 1 | MessageStatus::Failed, |
621 | | ]; |
622 | | |
623 | 4 | for (i, status) in statuses.iter()1 .enumerate1 () { |
624 | 4 | let mut trace = create_test_message_trace(); |
625 | 4 | trace.trace_id = i as u64; |
626 | 4 | trace.status = status.clone(); |
627 | 4 | observatory.trace_message(trace).unwrap(); |
628 | 4 | } |
629 | | |
630 | 1 | let traces = observatory.get_traces(None, None).unwrap(); |
631 | 1 | assert_eq!(traces.len(), 4); |
632 | | |
633 | | // Verify different statuses were recorded - count unique statuses differently |
634 | 1 | let mut status_counts = std::collections::HashMap::new(); |
635 | 5 | for trace4 in &traces { |
636 | 4 | *status_counts.entry(&trace.status).or_insert(0) += 1; |
637 | 4 | } |
638 | 1 | assert_eq!(status_counts.len(), 4); |
639 | 1 | } |
640 | | |
641 | | #[test] |
642 | 1 | fn test_observatory_config_variations() { |
643 | 1 | let configs = vec![ |
644 | 1 | ObservatoryConfig { |
645 | 1 | max_traces: 50, |
646 | 1 | enable_deadlock_detection: false, |
647 | 1 | enable_metrics: false, |
648 | 1 | ..ObservatoryConfig::default() |
649 | 1 | }, |
650 | 1 | ObservatoryConfig { |
651 | 1 | trace_retention_seconds: 7200, |
652 | 1 | deadlock_check_interval_ms: 500, |
653 | 1 | metrics_interval_ms: 2000, |
654 | 1 | ..ObservatoryConfig::default() |
655 | 1 | }, |
656 | | ]; |
657 | | |
658 | 3 | for config2 in configs { |
659 | 2 | let system = create_test_actor_system(); |
660 | 2 | let observatory = ActorObservatory::new(system, config.clone()); |
661 | 2 | assert_eq!(observatory.config.max_traces, config.max_traces); |
662 | 2 | assert_eq!(observatory.config.enable_deadlock_detection, config.enable_deadlock_detection); |
663 | | } |
664 | 1 | } |
665 | | |
666 | | #[test] |
667 | 1 | fn test_observatory_concurrent_access() { |
668 | | use std::thread; |
669 | | |
670 | 1 | let observatory = Arc::new(create_test_observatory()); |
671 | 1 | let mut handles = vec![]; |
672 | | |
673 | | // Spawn multiple threads to add traces concurrently |
674 | 6 | for i5 in 0..5 { |
675 | 5 | let obs = observatory.clone(); |
676 | 5 | let handle = thread::spawn(move || { |
677 | 5 | let mut trace = create_test_message_trace(); |
678 | 5 | trace.trace_id = i; |
679 | 5 | obs.trace_message(trace).unwrap(); |
680 | 5 | }); |
681 | 5 | handles.push(handle); |
682 | | } |
683 | | |
684 | | // Wait for all threads to complete |
685 | 6 | for handle5 in handles { |
686 | 5 | handle.join().unwrap(); |
687 | 5 | } |
688 | | |
689 | | // Verify all traces were recorded |
690 | 1 | let traces = observatory.get_traces(None, None).unwrap(); |
691 | 1 | assert_eq!(traces.len(), 5); |
692 | 1 | } |
693 | | } |
694 | | |
695 | | use crate::runtime::actor::{ActorId, ActorSystem, Message}; |
696 | | use anyhow::Result; |
697 | | use serde::{Deserialize, Serialize}; |
698 | | use std::collections::{HashMap, HashSet, VecDeque}; |
699 | | use std::sync::{Arc, Mutex}; |
700 | | use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; |
701 | | |
702 | | /// Actor system observatory for live introspection and monitoring |
703 | | pub struct ActorObservatory { |
704 | | /// Reference to the actor system being observed |
705 | | actor_system: Arc<Mutex<ActorSystem>>, |
706 | | |
707 | | /// Message trace storage |
708 | | message_traces: Arc<Mutex<VecDeque<MessageTrace>>>, |
709 | | |
710 | | /// Actor state snapshots |
711 | | actor_snapshots: Arc<Mutex<HashMap<ActorId, ActorSnapshot>>>, |
712 | | |
713 | | /// Deadlock detection state |
714 | | deadlock_detector: Arc<Mutex<DeadlockDetector>>, |
715 | | |
716 | | /// Observatory configuration |
717 | | config: ObservatoryConfig, |
718 | | |
719 | | /// Active filters for message tracing |
720 | | filters: Vec<MessageFilter>, |
721 | | |
722 | | /// Performance metrics |
723 | | metrics: Arc<Mutex<SystemMetrics>>, |
724 | | |
725 | | /// Observatory start time |
726 | | start_time: Instant, |
727 | | } |
728 | | |
729 | | /// Configuration for the actor observatory |
730 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
731 | | pub struct ObservatoryConfig { |
732 | | /// Maximum number of message traces to keep |
733 | | pub max_traces: usize, |
734 | | |
735 | | /// Maximum age for message traces (in seconds) |
736 | | pub trace_retention_seconds: u64, |
737 | | |
738 | | /// Enable deadlock detection |
739 | | pub enable_deadlock_detection: bool, |
740 | | |
741 | | /// Deadlock detection interval (in milliseconds) |
742 | | pub deadlock_check_interval_ms: u64, |
743 | | |
744 | | /// Enable performance metrics collection |
745 | | pub enable_metrics: bool, |
746 | | |
747 | | /// Metrics collection interval (in milliseconds) |
748 | | pub metrics_interval_ms: u64, |
749 | | |
750 | | /// Maximum number of actor snapshots to keep |
751 | | pub max_snapshots: usize, |
752 | | } |
753 | | |
754 | | impl Default for ObservatoryConfig { |
755 | 36 | fn default() -> Self { |
756 | 36 | Self { |
757 | 36 | max_traces: 10000, |
758 | 36 | trace_retention_seconds: 3600, // 1 hour |
759 | 36 | enable_deadlock_detection: true, |
760 | 36 | deadlock_check_interval_ms: 1000, // 1 second |
761 | 36 | enable_metrics: true, |
762 | 36 | metrics_interval_ms: 5000, // 5 seconds |
763 | 36 | max_snapshots: 1000, |
764 | 36 | } |
765 | 36 | } |
766 | | } |
767 | | |
768 | | /// Message trace entry for debugging and analysis |
769 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
770 | | pub struct MessageTrace { |
771 | | /// Unique trace ID |
772 | | pub trace_id: u64, |
773 | | |
774 | | /// Timestamp when the message was traced |
775 | | pub timestamp: u64, |
776 | | |
777 | | /// Source actor ID (None for external messages) |
778 | | pub source: Option<ActorId>, |
779 | | |
780 | | /// Destination actor ID |
781 | | pub destination: ActorId, |
782 | | |
783 | | /// The traced message |
784 | | pub message: Message, |
785 | | |
786 | | /// Message processing status |
787 | | pub status: MessageStatus, |
788 | | |
789 | | /// Processing duration in microseconds |
790 | | pub processing_duration_us: Option<u64>, |
791 | | |
792 | | /// Error information if message processing failed |
793 | | pub error: Option<String>, |
794 | | |
795 | | /// Stack depth for nested message calls |
796 | | pub stack_depth: usize, |
797 | | |
798 | | /// Correlation ID for tracking message chains |
799 | | pub correlation_id: Option<String>, |
800 | | } |
801 | | |
802 | | /// Status of a traced message |
803 | | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
804 | | pub enum MessageStatus { |
805 | | /// Message is queued for processing |
806 | | Queued, |
807 | | /// Message is currently being processed |
808 | | Processing, |
809 | | /// Message was processed successfully |
810 | | Completed, |
811 | | /// Message processing failed |
812 | | Failed, |
813 | | /// Message was dropped due to actor failure |
814 | | Dropped, |
815 | | } |
816 | | |
817 | | /// Snapshot of an actor's state at a point in time |
818 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
819 | | pub struct ActorSnapshot { |
820 | | /// Actor ID |
821 | | pub actor_id: ActorId, |
822 | | |
823 | | /// Actor name |
824 | | pub name: String, |
825 | | |
826 | | /// Snapshot timestamp |
827 | | pub timestamp: u64, |
828 | | |
829 | | /// Current state of the actor |
830 | | pub state: ActorState, |
831 | | |
832 | | /// Number of messages in the actor's mailbox |
833 | | pub mailbox_size: usize, |
834 | | |
835 | | /// Actor's supervision parent (if any) |
836 | | pub parent: Option<ActorId>, |
837 | | |
838 | | /// Actor's supervised children |
839 | | pub children: Vec<ActorId>, |
840 | | |
841 | | /// Recent message processing statistics |
842 | | pub message_stats: MessageStats, |
843 | | |
844 | | /// Memory usage estimate (in bytes) |
845 | | pub memory_usage: Option<usize>, |
846 | | } |
847 | | |
848 | | /// Current state of an actor |
849 | | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] |
850 | | pub enum ActorState { |
851 | | /// Actor is starting up |
852 | | Starting, |
853 | | /// Actor is running normally |
854 | | Running, |
855 | | /// Actor is processing a message |
856 | | Processing(String), // Message type being processed |
857 | | /// Actor is restarting due to failure |
858 | | Restarting, |
859 | | /// Actor is stopping |
860 | | Stopping, |
861 | | /// Actor has stopped |
862 | | Stopped, |
863 | | /// Actor has failed |
864 | | Failed(String), // Failure reason |
865 | | } |
866 | | |
867 | | /// Message processing statistics for an actor |
868 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
869 | | pub struct MessageStats { |
870 | | /// Total messages processed |
871 | | pub total_processed: u64, |
872 | | |
873 | | /// Messages processed per second (recent average) |
874 | | pub messages_per_second: f64, |
875 | | |
876 | | /// Average message processing time in microseconds |
877 | | pub avg_processing_time_us: f64, |
878 | | |
879 | | /// Maximum message processing time in microseconds |
880 | | pub max_processing_time_us: u64, |
881 | | |
882 | | /// Number of failed message processings |
883 | | pub failed_messages: u64, |
884 | | |
885 | | /// Last processing timestamp |
886 | | pub last_processed: Option<u64>, |
887 | | } |
888 | | |
889 | | impl Default for MessageStats { |
890 | 7 | fn default() -> Self { |
891 | 7 | Self { |
892 | 7 | total_processed: 0, |
893 | 7 | messages_per_second: 0.0, |
894 | 7 | avg_processing_time_us: 0.0, |
895 | 7 | max_processing_time_us: 0, |
896 | 7 | failed_messages: 0, |
897 | 7 | last_processed: None, |
898 | 7 | } |
899 | 7 | } |
900 | | } |
901 | | |
902 | | /// System-wide performance metrics |
903 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
904 | | pub struct SystemMetrics { |
905 | | /// Total number of active actors |
906 | | pub active_actors: usize, |
907 | | |
908 | | /// Total messages processed across all actors |
909 | | pub total_messages_processed: u64, |
910 | | |
911 | | /// System-wide messages per second |
912 | | pub system_messages_per_second: f64, |
913 | | |
914 | | /// Total memory usage estimate (in bytes) |
915 | | pub total_memory_usage: usize, |
916 | | |
917 | | /// Number of currently queued messages across all actors |
918 | | pub total_queued_messages: usize, |
919 | | |
920 | | /// Average actor mailbox size |
921 | | pub avg_mailbox_size: f64, |
922 | | |
923 | | /// Number of actor restarts in the last period |
924 | | pub recent_restarts: u64, |
925 | | |
926 | | /// Last metrics update timestamp |
927 | | pub last_updated: u64, |
928 | | } |
929 | | |
930 | | impl Default for SystemMetrics { |
931 | 52 | fn default() -> Self { |
932 | 52 | Self { |
933 | 52 | active_actors: 0, |
934 | 52 | total_messages_processed: 0, |
935 | 52 | system_messages_per_second: 0.0, |
936 | 52 | total_memory_usage: 0, |
937 | 52 | total_queued_messages: 0, |
938 | 52 | avg_mailbox_size: 0.0, |
939 | 52 | recent_restarts: 0, |
940 | 52 | last_updated: current_timestamp(), |
941 | 52 | } |
942 | 52 | } |
943 | | } |
944 | | |
945 | | /// Filter for message tracing |
946 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
947 | | pub struct MessageFilter { |
948 | | /// Filter name for identification |
949 | | pub name: String, |
950 | | |
951 | | /// Actor ID to filter by (None for all actors) |
952 | | pub actor_id: Option<ActorId>, |
953 | | |
954 | | /// Actor name pattern to filter by |
955 | | pub actor_name_pattern: Option<String>, |
956 | | |
957 | | /// Message type pattern to filter by |
958 | | pub message_type_pattern: Option<String>, |
959 | | |
960 | | /// Minimum message processing time to include (microseconds) |
961 | | pub min_processing_time_us: Option<u64>, |
962 | | |
963 | | /// Only include failed messages |
964 | | pub failed_only: bool, |
965 | | |
966 | | /// Maximum stack depth to include |
967 | | pub max_stack_depth: Option<usize>, |
968 | | } |
969 | | |
970 | | /// Deadlock detection system |
971 | | #[derive(Debug)] |
972 | | pub struct DeadlockDetector { |
973 | | /// Graph of actor message dependencies |
974 | | dependency_graph: HashMap<ActorId, HashSet<ActorId>>, |
975 | | |
976 | | /// Currently blocked actors waiting for responses |
977 | | blocked_actors: HashMap<ActorId, Vec<BlockedRequest>>, |
978 | | |
979 | | /// Last deadlock check timestamp |
980 | | last_check: Instant, |
981 | | |
982 | | /// Detected deadlocks |
983 | | detected_deadlocks: Vec<DeadlockCycle>, |
984 | | } |
985 | | |
986 | | /// Information about a blocked request |
987 | | #[derive(Debug, Clone)] |
988 | | pub struct BlockedRequest { |
989 | | /// Actor making the request |
990 | | pub requester: ActorId, |
991 | | |
992 | | /// Actor being requested from |
993 | | pub target: ActorId, |
994 | | |
995 | | /// When the request was made |
996 | | pub timestamp: Instant, |
997 | | |
998 | | /// Timeout for the request |
999 | | pub timeout: Duration, |
1000 | | |
1001 | | /// Message correlation ID |
1002 | | pub correlation_id: Option<String>, |
1003 | | } |
1004 | | |
1005 | | /// A detected deadlock cycle |
1006 | | #[derive(Debug, Clone, Serialize, Deserialize)] |
1007 | | pub struct DeadlockCycle { |
1008 | | /// Actors involved in the deadlock |
1009 | | pub actors: Vec<ActorId>, |
1010 | | |
1011 | | /// When the deadlock was detected |
1012 | | pub detected_at: u64, |
1013 | | |
1014 | | /// Estimated duration of the deadlock |
1015 | | pub duration_estimate_ms: u64, |
1016 | | |
1017 | | /// Suggested resolution strategy |
1018 | | pub resolution_suggestion: String, |
1019 | | } |
1020 | | |
1021 | | impl ActorObservatory { |
1022 | | /// Create a new actor observatory |
1023 | 49 | pub fn new(actor_system: Arc<Mutex<ActorSystem>>, config: ObservatoryConfig) -> Self { |
1024 | 49 | Self { |
1025 | 49 | actor_system, |
1026 | 49 | message_traces: Arc::new(Mutex::new(VecDeque::new())), |
1027 | 49 | actor_snapshots: Arc::new(Mutex::new(HashMap::new())), |
1028 | 49 | deadlock_detector: Arc::new(Mutex::new(DeadlockDetector::new())), |
1029 | 49 | config, |
1030 | 49 | filters: Vec::new(), |
1031 | 49 | metrics: Arc::new(Mutex::new(SystemMetrics::default())), |
1032 | 49 | start_time: Instant::now(), |
1033 | 49 | } |
1034 | 49 | } |
1035 | | |
1036 | | /// Add a message filter for tracing |
1037 | 5 | pub fn add_filter(&mut self, filter: MessageFilter) { |
1038 | 5 | self.filters.push(filter); |
1039 | 5 | } |
1040 | | |
1041 | | /// Remove a message filter by name |
1042 | 3 | pub fn remove_filter(&mut self, name: &str) -> bool { |
1043 | 3 | let initial_len = self.filters.len(); |
1044 | 3 | self.filters.retain(|f| f.name2 != name2 ); |
1045 | 3 | self.filters.len() != initial_len |
1046 | 3 | } |
1047 | | |
1048 | | /// Get current list of filters |
1049 | 11 | pub fn get_filters(&self) -> &[MessageFilter] { |
1050 | 11 | &self.filters |
1051 | 11 | } |
1052 | | |
1053 | | /// Record a message trace |
1054 | 22 | pub fn trace_message(&self, trace: MessageTrace) -> Result<()> { |
1055 | 22 | let mut traces = self.message_traces |
1056 | 22 | .lock() |
1057 | 22 | .map_err(|_| anyhow::anyhow!("Failed to acquire message traces lock"0 ))?0 ; |
1058 | | |
1059 | | // Apply filters |
1060 | 22 | if !self.message_matches_filters(&trace) { |
1061 | 0 | return Ok(()); |
1062 | 22 | } |
1063 | | |
1064 | 22 | traces.push_back(trace); |
1065 | | |
1066 | | // Enforce retention limits |
1067 | 23 | while traces.len() > self.config.max_traces { |
1068 | 1 | traces.pop_front(); |
1069 | 1 | } |
1070 | | |
1071 | | // Remove old traces based on age |
1072 | 22 | let retention_threshold = current_timestamp() - self.config.trace_retention_seconds; |
1073 | 24 | while let Some(front22 ) = traces.front() { |
1074 | 22 | if front.timestamp < retention_threshold { |
1075 | 2 | traces.pop_front(); |
1076 | 2 | } else { |
1077 | 20 | break; |
1078 | | } |
1079 | | } |
1080 | | |
1081 | 22 | Ok(()) |
1082 | 22 | } |
1083 | | |
1084 | | /// Get recent message traces with optional filtering |
1085 | 8 | pub fn get_traces(&self, limit: Option<usize>, filter_name: Option<&str>) -> Result<Vec<MessageTrace>> { |
1086 | 8 | let traces = self.message_traces |
1087 | 8 | .lock() |
1088 | 8 | .map_err(|_| anyhow::anyhow!("Failed to acquire message traces lock"0 ))?0 ; |
1089 | | |
1090 | 8 | let mut result: Vec<MessageTrace> = if let Some(filter_name0 ) = filter_name { |
1091 | 0 | traces.iter() |
1092 | 0 | .filter(|trace| self.trace_matches_filter(trace, filter_name)) |
1093 | 0 | .cloned() |
1094 | 0 | .collect() |
1095 | | } else { |
1096 | 8 | traces.iter().cloned().collect() |
1097 | | }; |
1098 | | |
1099 | 8 | if let Some(limit2 ) = limit { |
1100 | 2 | result.truncate(limit); |
1101 | 6 | } |
1102 | | |
1103 | 8 | Ok(result) |
1104 | 8 | } |
1105 | | |
1106 | | /// Update actor snapshot |
1107 | 1 | pub fn update_actor_snapshot(&self, snapshot: ActorSnapshot) -> Result<()> { |
1108 | 1 | let mut snapshots = self.actor_snapshots |
1109 | 1 | .lock() |
1110 | 1 | .map_err(|_| anyhow::anyhow!("Failed to acquire actor snapshots lock"0 ))?0 ; |
1111 | | |
1112 | 1 | snapshots.insert(snapshot.actor_id, snapshot); |
1113 | | |
1114 | | // Enforce snapshot limits |
1115 | 1 | if snapshots.len() > self.config.max_snapshots { |
1116 | | // Remove oldest snapshots |
1117 | 0 | let mut oldest_actors: Vec<_> = snapshots.iter() |
1118 | 0 | .map(|(&id, snapshot)| (id, snapshot.timestamp)) |
1119 | 0 | .collect(); |
1120 | 0 | oldest_actors.sort_by_key(|(_, timestamp)| *timestamp); |
1121 | | |
1122 | 0 | let to_remove = snapshots.len() - self.config.max_snapshots; |
1123 | 0 | for i in 0..to_remove { |
1124 | 0 | if let Some((actor_id, _)) = oldest_actors.get(i) { |
1125 | 0 | snapshots.remove(actor_id); |
1126 | 0 | } |
1127 | | } |
1128 | 1 | } |
1129 | | |
1130 | 1 | Ok(()) |
1131 | 1 | } |
1132 | | |
1133 | | /// Get current actor snapshots |
1134 | 0 | pub fn get_actor_snapshots(&self) -> Result<HashMap<ActorId, ActorSnapshot>> { |
1135 | 0 | Ok(self.actor_snapshots |
1136 | 0 | .lock() |
1137 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire actor snapshots lock"))? |
1138 | 0 | .clone()) |
1139 | 0 | } |
1140 | | |
1141 | | /// Get specific actor snapshot |
1142 | 0 | pub fn get_actor_snapshot(&self, actor_id: ActorId) -> Result<Option<ActorSnapshot>> { |
1143 | 0 | Ok(self.actor_snapshots |
1144 | 0 | .lock() |
1145 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire actor snapshots lock"))? |
1146 | 0 | .get(&actor_id) |
1147 | 0 | .cloned()) |
1148 | 0 | } |
1149 | | |
1150 | | /// Perform deadlock detection |
1151 | 0 | pub fn detect_deadlocks(&self) -> Result<Vec<DeadlockCycle>> { |
1152 | 0 | if !self.config.enable_deadlock_detection { |
1153 | 0 | return Ok(Vec::new()); |
1154 | 0 | } |
1155 | | |
1156 | 0 | let mut detector = self.deadlock_detector |
1157 | 0 | .lock() |
1158 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire deadlock detector lock"))?; |
1159 | | |
1160 | 0 | detector.detect_cycles() |
1161 | 0 | } |
1162 | | |
1163 | | /// Update system metrics |
1164 | 0 | pub fn update_metrics(&self) -> Result<()> { |
1165 | 0 | if !self.config.enable_metrics { |
1166 | 0 | return Ok(()); |
1167 | 0 | } |
1168 | | |
1169 | 0 | let _system = self.actor_system |
1170 | 0 | .lock() |
1171 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire actor system lock"))?; |
1172 | | |
1173 | 0 | let mut metrics = self.metrics |
1174 | 0 | .lock() |
1175 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))?; |
1176 | | |
1177 | 0 | let snapshots = self.actor_snapshots |
1178 | 0 | .lock() |
1179 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire actor snapshots lock"))?; |
1180 | | |
1181 | | // Update metrics based on current system state |
1182 | 0 | metrics.active_actors = snapshots.len(); |
1183 | 0 | metrics.total_messages_processed = snapshots.values() |
1184 | 0 | .map(|s| s.message_stats.total_processed) |
1185 | 0 | .sum(); |
1186 | | |
1187 | 0 | metrics.total_queued_messages = snapshots.values() |
1188 | 0 | .map(|s| s.mailbox_size) |
1189 | 0 | .sum(); |
1190 | | |
1191 | 0 | metrics.avg_mailbox_size = if snapshots.is_empty() { |
1192 | 0 | 0.0 |
1193 | | } else { |
1194 | 0 | metrics.total_queued_messages as f64 / snapshots.len() as f64 |
1195 | | }; |
1196 | | |
1197 | 0 | metrics.total_memory_usage = snapshots.values() |
1198 | 0 | .filter_map(|s| s.memory_usage) |
1199 | 0 | .sum(); |
1200 | | |
1201 | 0 | metrics.last_updated = current_timestamp(); |
1202 | | |
1203 | 0 | Ok(()) |
1204 | 0 | } |
1205 | | |
1206 | | /// Get current system metrics |
1207 | 0 | pub fn get_metrics(&self) -> Result<SystemMetrics> { |
1208 | 0 | Ok(self.metrics |
1209 | 0 | .lock() |
1210 | 0 | .map_err(|_| anyhow::anyhow!("Failed to acquire metrics lock"))? |
1211 | 0 | .clone()) |
1212 | 0 | } |
1213 | | |
1214 | | /// Get observatory uptime |
1215 | 0 | pub fn uptime(&self) -> Duration { |
1216 | 0 | self.start_time.elapsed() |
1217 | 0 | } |
1218 | | |
1219 | | /// Check if a message matches the configured filters |
1220 | 22 | fn message_matches_filters(&self, trace: &MessageTrace) -> bool { |
1221 | 22 | if self.filters.is_empty() { |
1222 | 21 | return true; // No filters means include all messages |
1223 | 1 | } |
1224 | | |
1225 | 1 | self.filters.iter().any(|filter| self.message_matches_filter(trace, filter)) |
1226 | 22 | } |
1227 | | |
1228 | | /// Check if a message matches a specific filter |
1229 | 1 | fn message_matches_filter(&self, trace: &MessageTrace, filter: &MessageFilter) -> bool { |
1230 | | // Filter by actor ID |
1231 | 1 | if let Some(filter_actor_id0 ) = filter.actor_id { |
1232 | 0 | if trace.destination != filter_actor_id { |
1233 | 0 | return false; |
1234 | 0 | } |
1235 | 1 | } |
1236 | | |
1237 | | // Filter by processing time |
1238 | 1 | if let Some(min_time0 ) = filter.min_processing_time_us { |
1239 | 0 | if let Some(duration) = trace.processing_duration_us { |
1240 | 0 | if duration < min_time { |
1241 | 0 | return false; |
1242 | 0 | } |
1243 | | } else { |
1244 | 0 | return false; |
1245 | | } |
1246 | 1 | } |
1247 | | |
1248 | | // Filter by failed messages only |
1249 | 1 | if filter.failed_only && trace.status != MessageStatus::Failed0 { |
1250 | 0 | return false; |
1251 | 1 | } |
1252 | | |
1253 | | // Filter by stack depth |
1254 | 1 | if let Some(max_depth0 ) = filter.max_stack_depth { |
1255 | 0 | if trace.stack_depth > max_depth { |
1256 | 0 | return false; |
1257 | 0 | } |
1258 | 1 | } |
1259 | | |
1260 | 1 | true |
1261 | 1 | } |
1262 | | |
1263 | | /// Check if a trace matches a filter by name |
1264 | 0 | fn trace_matches_filter(&self, trace: &MessageTrace, filter_name: &str) -> bool { |
1265 | 0 | self.filters.iter() |
1266 | 0 | .find(|f| f.name == filter_name) |
1267 | 0 | .is_some_and(|filter| self.message_matches_filter(trace, filter)) |
1268 | 0 | } |
1269 | | } |
1270 | | |
1271 | | impl Default for DeadlockDetector { |
1272 | 0 | fn default() -> Self { |
1273 | 0 | Self::new() |
1274 | 0 | } |
1275 | | } |
1276 | | |
1277 | | impl DeadlockDetector { |
1278 | | /// Create a new deadlock detector |
1279 | 49 | pub fn new() -> Self { |
1280 | 49 | Self { |
1281 | 49 | dependency_graph: HashMap::new(), |
1282 | 49 | blocked_actors: HashMap::new(), |
1283 | 49 | last_check: Instant::now(), |
1284 | 49 | detected_deadlocks: Vec::new(), |
1285 | 49 | } |
1286 | 49 | } |
1287 | | |
1288 | | /// Add a blocked request to track |
1289 | 0 | pub fn add_blocked_request(&mut self, request: BlockedRequest) { |
1290 | 0 | self.blocked_actors |
1291 | 0 | .entry(request.requester) |
1292 | 0 | .or_default() |
1293 | 0 | .push(request.clone()); |
1294 | | |
1295 | | // Update dependency graph |
1296 | 0 | self.dependency_graph |
1297 | 0 | .entry(request.requester) |
1298 | 0 | .or_default() |
1299 | 0 | .insert(request.target); |
1300 | 0 | } |
1301 | | |
1302 | | /// Remove a blocked request (when resolved) |
1303 | 0 | pub fn remove_blocked_request(&mut self, requester: ActorId, target: ActorId) { |
1304 | 0 | if let Some(requests) = self.blocked_actors.get_mut(&requester) { |
1305 | 0 | requests.retain(|r| r.target != target); |
1306 | 0 | if requests.is_empty() { |
1307 | 0 | self.blocked_actors.remove(&requester); |
1308 | 0 | } |
1309 | 0 | } |
1310 | | |
1311 | | // Update dependency graph |
1312 | 0 | if let Some(dependencies) = self.dependency_graph.get_mut(&requester) { |
1313 | 0 | dependencies.remove(&target); |
1314 | 0 | if dependencies.is_empty() { |
1315 | 0 | self.dependency_graph.remove(&requester); |
1316 | 0 | } |
1317 | 0 | } |
1318 | 0 | } |
1319 | | |
1320 | | /// Detect cycles in the dependency graph (potential deadlocks) |
1321 | 0 | pub fn detect_cycles(&mut self) -> Result<Vec<DeadlockCycle>> { |
1322 | 0 | let mut cycles = Vec::new(); |
1323 | 0 | let mut visited = HashSet::new(); |
1324 | 0 | let mut path = Vec::new(); |
1325 | | |
1326 | 0 | for &actor in self.dependency_graph.keys() { |
1327 | 0 | if !visited.contains(&actor) { |
1328 | 0 | self.dfs_detect_cycle(actor, &mut visited, &mut path, &mut cycles)?; |
1329 | 0 | } |
1330 | | } |
1331 | | |
1332 | 0 | self.detected_deadlocks.extend(cycles.clone()); |
1333 | 0 | self.last_check = Instant::now(); |
1334 | | |
1335 | 0 | Ok(cycles) |
1336 | 0 | } |
1337 | | |
1338 | | /// Depth-first search to detect cycles |
1339 | 0 | fn dfs_detect_cycle( |
1340 | 0 | &self, |
1341 | 0 | actor: ActorId, |
1342 | 0 | visited: &mut HashSet<ActorId>, |
1343 | 0 | path: &mut Vec<ActorId>, |
1344 | 0 | cycles: &mut Vec<DeadlockCycle>, |
1345 | 0 | ) -> Result<()> { |
1346 | 0 | visited.insert(actor); |
1347 | 0 | path.push(actor); |
1348 | | |
1349 | 0 | if let Some(dependencies) = self.dependency_graph.get(&actor) { |
1350 | 0 | for &dependent_actor in dependencies { |
1351 | 0 | if let Some(cycle_start_index) = path.iter().position(|&a| a == dependent_actor) { |
1352 | 0 | // Found a cycle |
1353 | 0 | let cycle_actors = path[cycle_start_index..].to_vec(); |
1354 | 0 | let duration_estimate = self.estimate_cycle_duration(&cycle_actors); |
1355 | 0 | |
1356 | 0 | cycles.push(DeadlockCycle { |
1357 | 0 | actors: cycle_actors.clone(), |
1358 | 0 | detected_at: current_timestamp(), |
1359 | 0 | duration_estimate_ms: duration_estimate, |
1360 | 0 | resolution_suggestion: self.suggest_resolution(&cycle_actors), |
1361 | 0 | }); |
1362 | 0 | } else if !visited.contains(&dependent_actor) { |
1363 | 0 | self.dfs_detect_cycle(dependent_actor, visited, path, cycles)?; |
1364 | 0 | } |
1365 | | } |
1366 | 0 | } |
1367 | | |
1368 | 0 | path.pop(); |
1369 | 0 | Ok(()) |
1370 | 0 | } |
1371 | | |
1372 | | /// Estimate how long a deadlock cycle has been active |
1373 | 0 | fn estimate_cycle_duration(&self, actors: &[ActorId]) -> u64 { |
1374 | 0 | let now = Instant::now(); |
1375 | 0 | actors.iter() |
1376 | 0 | .filter_map(|&actor| self.blocked_actors.get(&actor)) |
1377 | 0 | .flatten() |
1378 | 0 | .map(|request| now.duration_since(request.timestamp).as_millis() as u64) |
1379 | 0 | .max() |
1380 | 0 | .unwrap_or(0) |
1381 | 0 | } |
1382 | | |
1383 | | /// Suggest a resolution strategy for a deadlock cycle |
1384 | 0 | fn suggest_resolution(&self, actors: &[ActorId]) -> String { |
1385 | 0 | match actors.len() { |
1386 | 0 | 1 => "Self-deadlock: Check for recursive message sending".to_string(), |
1387 | 0 | 2 => "Binary deadlock: Consider using ask with timeout or redesign interaction pattern".to_string(), |
1388 | 0 | 3..=5 => "Multi-actor deadlock: Implement hierarchical message ordering or use supervision".to_string(), |
1389 | 0 | _ => "Complex deadlock: Consider breaking into smaller subsystems or using event sourcing".to_string(), |
1390 | | } |
1391 | 0 | } |
1392 | | } |
1393 | | |
1394 | | /// Get current timestamp in seconds since Unix epoch |
1395 | 106 | fn current_timestamp() -> u64 { |
1396 | 106 | SystemTime::now() |
1397 | 106 | .duration_since(UNIX_EPOCH) |
1398 | 106 | .unwrap_or_default() |
1399 | 106 | .as_secs() |
1400 | 106 | } |
1401 | | |
1402 | | /// Create a simple message filter for testing |
1403 | | impl MessageFilter { |
1404 | 0 | pub fn new(name: &str) -> Self { |
1405 | 0 | Self { |
1406 | 0 | name: name.to_string(), |
1407 | 0 | actor_id: None, |
1408 | 0 | actor_name_pattern: None, |
1409 | 0 | message_type_pattern: None, |
1410 | 0 | min_processing_time_us: None, |
1411 | 0 | failed_only: false, |
1412 | 0 | max_stack_depth: None, |
1413 | 0 | } |
1414 | 0 | } |
1415 | | |
1416 | | /// Create a filter for a specific actor |
1417 | 0 | pub fn for_actor(name: &str, actor_id: ActorId) -> Self { |
1418 | 0 | Self { |
1419 | 0 | name: name.to_string(), |
1420 | 0 | actor_id: Some(actor_id), |
1421 | 0 | actor_name_pattern: None, |
1422 | 0 | message_type_pattern: None, |
1423 | 0 | min_processing_time_us: None, |
1424 | 0 | failed_only: false, |
1425 | 0 | max_stack_depth: None, |
1426 | 0 | } |
1427 | 0 | } |
1428 | | |
1429 | | /// Create a filter for failed messages only |
1430 | 0 | pub fn failed_messages_only(name: &str) -> Self { |
1431 | 0 | Self { |
1432 | 0 | name: name.to_string(), |
1433 | 0 | actor_id: None, |
1434 | 0 | actor_name_pattern: None, |
1435 | 0 | message_type_pattern: None, |
1436 | 0 | min_processing_time_us: None, |
1437 | 0 | failed_only: true, |
1438 | 0 | max_stack_depth: None, |
1439 | 0 | } |
1440 | 0 | } |
1441 | | |
1442 | | /// Create a filter for delayed messages |
1443 | 0 | pub fn slow_messages(name: &str, min_time_us: u64) -> Self { |
1444 | 0 | Self { |
1445 | 0 | name: name.to_string(), |
1446 | 0 | actor_id: None, |
1447 | 0 | actor_name_pattern: None, |
1448 | 0 | message_type_pattern: None, |
1449 | 0 | min_processing_time_us: Some(min_time_us), |
1450 | 0 | failed_only: false, |
1451 | 0 | max_stack_depth: None, |
1452 | 0 | } |
1453 | 0 | } |
1454 | | } |