Skip to main content

rust_ethernet_ip/client/
diagnostics.rs

1use super::EipClient;
2use tokio::time::Duration;
3
4impl EipClient {
5    /// Quick connection health check (no I/O).
6    ///
7    /// Returns `true` if the session handle is valid and there has been activity
8    /// within the last 150 seconds. Use this for cheap periodic checks; for a
9    /// definitive check that the PLC is still responding, use [`check_health_detailed`](Self::check_health_detailed).
10    pub async fn check_health(&self) -> bool {
11        self.session_handle() != 0
12            && self.last_activity.lock().await.elapsed() < Duration::from_secs(150)
13    }
14
15    /// Builds a lightweight diagnostics snapshot from the current client state.
16    pub async fn get_diagnostics_snapshot(&self) -> crate::DiagnosticsSnapshot {
17        self.build_diagnostics_snapshot(crate::HealthCheckMode::Passive, self.check_health().await)
18            .await
19    }
20
21    /// Verifies the connection by sending a keep-alive (and re-registering if needed).
22    ///
23    /// Use this when you need to confirm the PLC is still reachable (e.g. after
24    /// a long idle or before a critical operation). On failure, consider
25    /// reconnecting; check [`EtherNetIpError::is_retriable`](crate::error::EtherNetIpError::is_retriable) on errors.
26    pub async fn check_health_detailed(&mut self) -> crate::error::Result<bool> {
27        if self.session_handle() == 0 {
28            return Ok(false);
29        }
30
31        // Try sending a lightweight keep-alive command
32        match self.send_keep_alive().await {
33            Ok(()) => Ok(true),
34            Err(_) => {
35                // If keep-alive fails, try re-registering the shared session.
36                // The new handle is atomically visible to all clones using this stream.
37                match self.register_session().await {
38                    Ok(()) => Ok(true),
39                    Err(_) => Ok(false),
40                }
41            }
42        }
43    }
44
45    /// Builds a verified diagnostics snapshot by actively checking PLC connectivity.
46    pub async fn get_diagnostics_snapshot_detailed(
47        &mut self,
48    ) -> crate::error::Result<crate::DiagnosticsSnapshot> {
49        let is_healthy = self.check_health_detailed().await?;
50        Ok(self
51            .build_diagnostics_snapshot(crate::HealthCheckMode::Verified, is_healthy)
52            .await)
53    }
54
55    /// Returns controller-schema cache and bounded-recovery counters.
56    pub fn schema_cache_metrics(&self) -> crate::SchemaCacheMetrics {
57        crate::SchemaCacheMetrics {
58            generation: self.schema_generation(),
59            refreshes: self
60                .diagnostic_counters
61                .schema_refreshes
62                .load(std::sync::atomic::Ordering::Relaxed),
63            array_classification_hits: self
64                .diagnostic_counters
65                .array_cache_hits
66                .load(std::sync::atomic::Ordering::Relaxed),
67            array_classification_misses: self
68                .diagnostic_counters
69                .array_cache_misses
70                .load(std::sync::atomic::Ordering::Relaxed),
71            array_classification_evictions: self
72                .diagnostic_counters
73                .array_cache_evictions
74                .load(std::sync::atomic::Ordering::Relaxed),
75            datatype_contradictions: self
76                .diagnostic_counters
77                .schema_type_contradictions
78                .load(std::sync::atomic::Ordering::Relaxed),
79            successful_read_recoveries: self
80                .diagnostic_counters
81                .schema_read_recoveries_succeeded
82                .load(std::sync::atomic::Ordering::Relaxed),
83            failed_read_recoveries: self
84                .diagnostic_counters
85                .schema_read_recoveries_failed
86                .load(std::sync::atomic::Ordering::Relaxed),
87        }
88    }
89
90    async fn build_diagnostics_snapshot(
91        &self,
92        health_mode: crate::HealthCheckMode,
93        is_healthy: bool,
94    ) -> crate::DiagnosticsSnapshot {
95        let now = std::time::SystemTime::now();
96        let session_active = self.session_handle() != 0;
97        let last_activity_elapsed = self.last_activity.lock().await.elapsed();
98        let last_success_time = if is_healthy || session_active {
99            Some(
100                now.checked_sub(last_activity_elapsed)
101                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH),
102            )
103        } else {
104            None
105        };
106
107        let error_category = if session_active && !is_healthy {
108            Some(crate::ErrorCategory::Session)
109        } else {
110            None
111        };
112
113        let operations = self.diagnostic_counters.operation_metrics();
114        let mut errors = self.diagnostic_counters.error_metrics();
115        if error_category == Some(crate::ErrorCategory::Session) {
116            errors.session_errors += 1;
117            errors.retriable_errors += 1;
118            if errors.last_error_time.is_none() {
119                errors.last_error_time = Some(now);
120                errors.last_error_message = Some(
121                    "Detailed health check reported session-level connectivity failure".to_string(),
122                );
123                errors.last_error_category = error_category;
124                errors.last_retriable_error_time = Some(now);
125            }
126        }
127
128        crate::DiagnosticsSnapshot {
129            captured_at: now,
130            connections: crate::ConnectionMetrics {
131                active_connections: if session_active { 1 } else { 0 },
132                total_connections: if session_active { 1 } else { 0 },
133                failed_connections: 0,
134                connection_uptime_avg: Duration::ZERO,
135                last_connection_time: last_success_time,
136            },
137            operations,
138            performance: crate::PerformanceMetrics {
139                avg_read_latency_ms: 0.0,
140                avg_write_latency_ms: 0.0,
141                max_read_latency_ms: 0.0,
142                max_write_latency_ms: 0.0,
143                reads_per_second: 0.0,
144                writes_per_second: 0.0,
145                memory_usage_mb: 0.0,
146                cpu_usage_percent: 0.0,
147            },
148            errors,
149            health: crate::HealthMetrics {
150                overall_health: if is_healthy {
151                    crate::HealthStatus::Healthy
152                } else if session_active {
153                    crate::HealthStatus::Critical
154                } else {
155                    crate::HealthStatus::Unknown
156                },
157                last_health_check: now,
158                health_mode,
159                last_verified_health_check: if health_mode == crate::HealthCheckMode::Verified {
160                    Some(now)
161                } else {
162                    None
163                },
164                consecutive_failures: if is_healthy { 0 } else { 1 },
165                recovery_attempts: 0,
166                system_uptime: Duration::ZERO,
167                last_success_time,
168                last_failure_time: if session_active && !is_healthy {
169                    Some(now)
170                } else {
171                    None
172                },
173            },
174            system_metrics_are_placeholders: true,
175        }
176    }
177}