1use std::sync::Arc;
11
12use fdf::{AsAsyncDispatcherRef, DriverDispatcherRef};
13use fidl_next::Server;
14use fidl_next_fuchsia_driver_framework::{
15 Driver as DriverProtocol, HealthAlert, HealthCheckError, HealthCheckRequest,
16 HealthCheckResponse, HealthDiagnosticItem, HealthDiagnosticValue, HealthStatus,
17};
18use fuchsia_sync::Mutex;
19use futures::channel::{mpsc, oneshot};
20use log::{debug, warn};
21
22use crate::Driver;
23use crate::server::Transport;
24
25pub type HealthCheckResult = Result<HealthCheckResponse, HealthCheckError>;
27
28const PING_HEALTH_CHECK_DEADLINE: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(250);
33const FAST_HEALTH_CHECK_DEADLINE: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(2);
34const DEEP_HEALTH_CHECK_DEADLINE: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(120);
35const HEALTH_CHECK_TIMEOUT_MARGIN: zx::MonotonicDuration = zx::MonotonicDuration::from_millis(100);
36
37fn health_check_timeout(level: HealthCheckRequest) -> Option<zx::MonotonicDuration> {
40 let deadline = match level {
41 HealthCheckRequest::Ping => PING_HEALTH_CHECK_DEADLINE,
42 HealthCheckRequest::Fast => FAST_HEALTH_CHECK_DEADLINE,
43 HealthCheckRequest::Deep => DEEP_HEALTH_CHECK_DEADLINE,
44 _ => return None,
45 };
46 Some(deadline + HEALTH_CHECK_TIMEOUT_MARGIN)
47}
48
49async fn ping_root_dispatcher(
56 root_dispatcher: DriverDispatcherRef<'static>,
57) -> Result<(), HealthCheckError> {
58 let (tx, rx) = oneshot::channel();
59 root_dispatcher
60 .post_task_sync(move |status| {
61 if status.is_ok() {
62 let _ = tx.send(());
63 }
64 })
65 .map_err(|_| HealthCheckError::InvalidState)?;
66 rx.await.map_err(|_| HealthCheckError::InvalidState)
68}
69
70pub(crate) async fn run_health_check<D: Driver>(
73 driver: &D,
74 root_dispatcher: DriverDispatcherRef<'static>,
75 request: HealthCheckRequest,
76) -> HealthCheckResult {
77 let Some(timeout) = health_check_timeout(request) else {
78 return Err(HealthCheckError::NotSupported);
79 };
80 let check = async move {
81 ping_root_dispatcher(root_dispatcher).await?;
82 driver.health_check(request).await
83 };
84 fuchsia_async::TimeoutExt::on_timeout(check, timeout, || Err(HealthCheckError::Timeout)).await
85}
86
87const HEALTH_ALERT_QUEUE_CAPACITY: usize = 64;
90
91enum HealthAlertSink {
92 Server(Server<DriverProtocol, Transport>),
94 TestQueue { tx: mpsc::Sender<HealthAlert>, dropped: u64 },
96 Closed,
98}
99
100#[derive(Clone)]
107pub struct HealthAlertReporter {
108 sink: Arc<Mutex<HealthAlertSink>>,
109}
110
111impl HealthAlertReporter {
112 pub(crate) fn new(server: Server<DriverProtocol, Transport>) -> Self {
114 Self { sink: Arc::new(Mutex::new(HealthAlertSink::Server(server))) }
115 }
116
117 pub(crate) fn close(&self) {
119 *self.sink.lock() = HealthAlertSink::Closed;
120 }
121
122 pub fn new_for_testing() -> (Self, mpsc::Receiver<HealthAlert>) {
125 let (tx, rx) = mpsc::channel(HEALTH_ALERT_QUEUE_CAPACITY - 1);
128 (Self { sink: Arc::new(Mutex::new(HealthAlertSink::TestQueue { tx, dropped: 0 })) }, rx)
129 }
130
131 pub fn report_health_alert(&self, alert: HealthAlert) {
140 let mut sink = self.sink.lock();
141 match &mut *sink {
142 HealthAlertSink::Server(server) => {
143 let server = server.clone();
144 drop(sink);
145 if let Err(e) = server.on_health_alert(alert).send_immediately() {
146 debug!("Dropping health alert on closed or failed driver channel: {e}");
147 }
148 }
149 HealthAlertSink::TestQueue { tx, dropped } => {
150 let Err(e) = tx.try_send(alert) else {
151 return;
152 };
153 if e.is_full() {
154 *dropped += 1;
155 let total_dropped = *dropped;
156 drop(sink);
157 if total_dropped.is_power_of_two() {
158 warn!("Health alert queue is full; dropped {total_dropped} alerts so far");
159 }
160 } else {
161 drop(sink);
162 debug!("Dropping health alert because the receiver was dropped");
163 }
164 }
165 HealthAlertSink::Closed => {
166 drop(sink);
167 debug!("Dropping health alert because the driver server has shut down");
168 }
169 }
170 }
171}
172
173impl Default for HealthAlertReporter {
174 fn default() -> Self {
179 Self { sink: Arc::new(Mutex::new(HealthAlertSink::Closed)) }
180 }
181}
182
183pub const HEALTH_INSPECT_NODE_NAME: &str = "driver_health_check";
186
187pub struct HealthCheckInspect {
209 parent: fuchsia_inspect::Node,
210 current: Mutex<Option<CurrentRecord>>,
212}
213
214struct CurrentRecord {
215 _node: fuchsia_inspect::Node,
216 level: Option<HealthCheckRequest>,
217 is_informative: bool,
218}
219
220impl HealthCheckInspect {
221 pub fn new(parent: &fuchsia_inspect::Node) -> Self {
226 Self { parent: parent.clone_weak(), current: Mutex::new(None) }
227 }
228
229 pub fn record(&self, level: HealthCheckRequest, result: &HealthCheckResult) {
237 self.replace(Some(level), result.as_ref().map_err(|e| *e));
238 }
239
240 pub fn record_alert(&self, alert: &HealthAlert) {
245 if let HealthAlert::SelfFault(details) = alert {
246 self.replace(None, Ok(details));
247 }
248 }
249
250 fn replace(
251 &self,
252 level: Option<HealthCheckRequest>,
253 result: Result<&HealthCheckResponse, HealthCheckError>,
254 ) {
255 let is_no_info_error = matches!(
256 result,
257 Err(HealthCheckError::NotSupported
258 | HealthCheckError::Unavailable
259 | HealthCheckError::InvalidState)
260 );
261 let is_healthy_ping = level == Some(HealthCheckRequest::Ping)
262 && matches!(result, Ok(resp) if derive_health_status(resp) == HealthStatus::Ok);
263
264 let mut current = self.current.lock();
265 if let Some(prev) = current.as_ref()
266 && prev.is_informative
267 {
268 if is_no_info_error {
269 return;
270 }
271 if is_healthy_ping && prev.level != Some(HealthCheckRequest::Ping) {
272 return;
273 }
274 }
275 *current = None;
279 *current = Some(CurrentRecord {
280 _node: record_health_check_to_inspect(&self.parent, level, result),
281 level,
282 is_informative: !is_no_info_error,
283 });
284 }
285}
286
287fn derive_health_status(response: &HealthCheckResponse) -> HealthStatus {
291 let mut overall = HealthStatus::Ok;
292 if let Some(conditions) = &response.conditions {
293 for condition in conditions {
294 let status = condition.status.unwrap_or(HealthStatus::Unhealthy);
295 if status == HealthStatus::Degraded {
296 overall = HealthStatus::Degraded;
297 } else if status != HealthStatus::Ok {
298 return HealthStatus::Unhealthy;
299 }
300 }
301 }
302 overall
303}
304
305fn health_status_str(status: HealthStatus) -> &'static str {
306 match status {
307 HealthStatus::Ok => "OK",
308 HealthStatus::Degraded => "DEGRADED",
309 HealthStatus::Unhealthy => "UNHEALTHY",
310 _ => "UNKNOWN",
311 }
312}
313
314fn health_level_str(level: HealthCheckRequest) -> &'static str {
315 match level {
316 HealthCheckRequest::Ping => "PING",
317 HealthCheckRequest::Fast => "FAST",
318 HealthCheckRequest::Deep => "DEEP",
319 _ => "UNKNOWN",
320 }
321}
322
323fn health_error_str(error: HealthCheckError) -> &'static str {
324 match error {
325 HealthCheckError::Timeout => "TIMEOUT",
326 HealthCheckError::NotSupported => "NOT_SUPPORTED",
327 HealthCheckError::InternalError => "INTERNAL_ERROR",
328 HealthCheckError::InvalidState => "INVALID_STATE",
329 HealthCheckError::Unavailable => "UNAVAILABLE",
330 HealthCheckError::CommunicationError => "COMMUNICATION_ERROR",
331 HealthCheckError::NotFound => "NOT_FOUND",
332 _ => "UNKNOWN",
333 }
334}
335
336fn record_health_diagnostics(parent: &fuchsia_inspect::Node, diagnostics: &[HealthDiagnosticItem]) {
338 parent.record_child("diagnostics", |diag_node| {
339 for item in diagnostics {
340 match &item.value {
341 HealthDiagnosticValue::StringValue(s) => diag_node.record_string(&item.key, s),
342 HealthDiagnosticValue::IntValue(i) => diag_node.record_int(&item.key, *i),
343 HealthDiagnosticValue::UintValue(u) => diag_node.record_uint(&item.key, *u),
344 HealthDiagnosticValue::BoolValue(b) => diag_node.record_bool(&item.key, *b),
345 HealthDiagnosticValue::Timestamp(t) => diag_node.record_int(&item.key, *t),
346 _ => {}
347 }
348 }
349 });
350}
351
352pub fn record_health_check_to_inspect(
364 parent: &fuchsia_inspect::Node,
365 level: Option<HealthCheckRequest>,
366 result: Result<&HealthCheckResponse, HealthCheckError>,
367) -> fuchsia_inspect::Node {
368 let node = parent.create_child(HEALTH_INSPECT_NODE_NAME);
369 if let Some(level) = level {
370 node.record_string("level", health_level_str(level));
371 }
372 let response = match result {
373 Ok(response) => response,
374 Err(error) => {
375 node.record_string("status", "ERROR");
376 node.record_string("error", health_error_str(error));
377 return node;
378 }
379 };
380 node.record_string("status", health_status_str(derive_health_status(response)));
381 if let Some(diagnostics) = response.diagnostics.as_deref().filter(|d| !d.is_empty()) {
382 record_health_diagnostics(&node, diagnostics);
383 }
384 if let Some(conditions) = response.conditions.as_deref().filter(|c| !c.is_empty()) {
385 node.record_child("conditions", |conditions_node| {
386 for condition in conditions {
387 let reason_code = condition.reason_code.as_deref().unwrap_or("UNKNOWN");
388 conditions_node.record_child(reason_code, |condition_node| {
389 let status = condition.status.unwrap_or(HealthStatus::Unhealthy);
390 condition_node.record_string("status", health_status_str(status));
391 if let Some(message) = &condition.message {
392 condition_node.record_string("message", message);
393 }
394 if let Some(first_observed) = condition.first_observed {
395 condition_node.record_int("first_observed", first_observed);
396 }
397 if let Some(last_observed) = condition.last_observed {
398 condition_node.record_int("last_observed", last_observed);
399 }
400 if let Some(occurrence_count) = condition.occurrence_count {
401 condition_node.record_uint("occurrence_count", occurrence_count);
402 }
403 if let Some(diagnostics) =
404 condition.diagnostics.as_deref().filter(|d| !d.is_empty())
405 {
406 record_health_diagnostics(condition_node, diagnostics);
407 }
408 });
409 }
410 });
411 }
412 node
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use diagnostics_assertions::assert_data_tree;
419 use fidl_next_fuchsia_driver_framework::HealthCondition;
420 use futures::StreamExt;
421
422 #[fuchsia::test]
423 async fn record_health_check_to_inspect_mirrors_full_response() {
424 let inspector = fuchsia_inspect::Inspector::default();
425 let response = HealthCheckResponse {
426 diagnostics: Some(vec![
427 HealthDiagnosticItem {
428 key: "state".to_string(),
429 value: HealthDiagnosticValue::StringValue("active".to_string()),
430 },
431 HealthDiagnosticItem {
432 key: "underruns".to_string(),
433 value: HealthDiagnosticValue::UintValue(2),
434 },
435 HealthDiagnosticItem {
436 key: "temp".to_string(),
437 value: HealthDiagnosticValue::IntValue(-3),
438 },
439 HealthDiagnosticItem {
440 key: "powered".to_string(),
441 value: HealthDiagnosticValue::BoolValue(true),
442 },
443 HealthDiagnosticItem {
444 key: "last_irq".to_string(),
445 value: HealthDiagnosticValue::Timestamp(99),
446 },
447 ]),
448 conditions: Some(vec![
449 HealthCondition {
450 status: Some(HealthStatus::Unhealthy),
451 reason_code: Some("DSI_UNLOCKED".to_string()),
452 message: Some("DSI PHY lost lock".to_string()),
453 first_observed: Some(10),
454 last_observed: Some(20),
455 occurrence_count: Some(3),
456 diagnostics: Some(vec![HealthDiagnosticItem {
457 key: "phy_status".to_string(),
458 value: HealthDiagnosticValue::UintValue(7),
459 }]),
460 },
461 HealthCondition {
462 status: Some(HealthStatus::Degraded),
463 reason_code: Some("TE_JITTER".to_string()),
464 ..Default::default()
465 },
466 ]),
467 };
468 let _node = record_health_check_to_inspect(
469 inspector.root(),
470 Some(HealthCheckRequest::Deep),
471 Ok(&response),
472 );
473 assert_data_tree!(inspector, root: {
474 driver_health_check: {
475 level: "DEEP",
476 status: "UNHEALTHY",
477 diagnostics: {
478 state: "active",
479 underruns: 2u64,
480 temp: -3i64,
481 powered: true,
482 last_irq: 99i64,
483 },
484 conditions: {
485 DSI_UNLOCKED: {
486 status: "UNHEALTHY",
487 message: "DSI PHY lost lock",
488 first_observed: 10i64,
489 last_observed: 20i64,
490 occurrence_count: 3u64,
491 diagnostics: { phy_status: 7u64 },
492 },
493 TE_JITTER: { status: "DEGRADED" },
494 },
495 },
496 });
497 }
498
499 #[fuchsia::test]
500 async fn record_health_check_to_inspect_mirrors_error() {
501 let inspector = fuchsia_inspect::Inspector::default();
502 let node = record_health_check_to_inspect(
503 inspector.root(),
504 Some(HealthCheckRequest::Ping),
505 Err(HealthCheckError::NotSupported),
506 );
507 assert_data_tree!(inspector, root: {
508 driver_health_check: { level: "PING", status: "ERROR", error: "NOT_SUPPORTED" },
509 });
510
511 drop(node);
514 let _node =
515 record_health_check_to_inspect(inspector.root(), None, Err(HealthCheckError::Timeout));
516 assert_data_tree!(inspector, root: {
517 driver_health_check: { status: "ERROR", error: "TIMEOUT" },
518 });
519 }
520
521 #[fuchsia::test]
522 async fn derive_status_prefers_unhealthy_over_degraded() {
523 let response = HealthCheckResponse {
524 conditions: Some(vec![
525 HealthCondition { status: Some(HealthStatus::Degraded), ..Default::default() },
526 HealthCondition { status: Some(HealthStatus::Unhealthy), ..Default::default() },
527 ]),
528 ..Default::default()
529 };
530 assert_eq!(derive_health_status(&response), HealthStatus::Unhealthy);
531 assert_eq!(derive_health_status(&HealthCheckResponse::default()), HealthStatus::Ok);
532 }
533
534 #[fuchsia::test]
535 async fn health_check_inspect_replaces_and_retains_records() {
536 let inspector = fuchsia_inspect::Inspector::default();
537 let health_inspect = HealthCheckInspect::new(inspector.root());
538 assert_data_tree!(inspector, root: {});
539
540 health_inspect.record(HealthCheckRequest::Deep, &Err(HealthCheckError::NotSupported));
542 assert_data_tree!(inspector, root: {
543 driver_health_check: { level: "DEEP", status: "ERROR", error: "NOT_SUPPORTED" },
544 });
545
546 health_inspect.record(HealthCheckRequest::Ping, &Ok(HealthCheckResponse::default()));
548 assert_data_tree!(inspector, root: {
549 driver_health_check: { level: "PING", status: "OK" },
550 });
551
552 health_inspect.record(
554 HealthCheckRequest::Fast,
555 &Ok(HealthCheckResponse {
556 conditions: Some(vec![HealthCondition {
557 status: Some(HealthStatus::Degraded),
558 reason_code: Some("FIFO_WARN".to_string()),
559 ..Default::default()
560 }]),
561 ..Default::default()
562 }),
563 );
564 assert_data_tree!(inspector, root: {
565 driver_health_check: {
566 level: "FAST",
567 status: "DEGRADED",
568 conditions: {
569 FIFO_WARN: { status: "DEGRADED" },
570 },
571 },
572 });
573
574 health_inspect.record(HealthCheckRequest::Deep, &Err(HealthCheckError::NotSupported));
577 health_inspect.record(HealthCheckRequest::Fast, &Err(HealthCheckError::Unavailable));
578 health_inspect.record(HealthCheckRequest::Fast, &Err(HealthCheckError::InvalidState));
579 health_inspect.record(HealthCheckRequest::Ping, &Ok(HealthCheckResponse::default()));
580 assert_data_tree!(inspector, root: {
581 driver_health_check: {
582 level: "FAST",
583 status: "DEGRADED",
584 conditions: {
585 FIFO_WARN: { status: "DEGRADED" },
586 },
587 },
588 });
589
590 health_inspect.record(HealthCheckRequest::Fast, &Err(HealthCheckError::CommunicationError));
592 assert_data_tree!(inspector, root: {
593 driver_health_check: {
594 level: "FAST",
595 status: "ERROR",
596 error: "COMMUNICATION_ERROR",
597 },
598 });
599
600 drop(health_inspect);
602 assert_data_tree!(inspector, root: {});
603 }
604
605 #[fuchsia::test]
606 async fn self_fault_alert_replaces_inspect_node() {
607 let inspector = fuchsia_inspect::Inspector::default();
608 let health_inspect = HealthCheckInspect::new(inspector.root());
609
610 health_inspect.record_alert(&HealthAlert::SelfFault(HealthCheckResponse {
611 conditions: Some(vec![HealthCondition {
612 status: Some(HealthStatus::Unhealthy),
613 reason_code: Some("DMA_STALLED".to_string()),
614 ..Default::default()
615 }]),
616 ..Default::default()
617 }));
618 assert_data_tree!(inspector, root: {
619 driver_health_check: {
620 status: "UNHEALTHY",
621 conditions: { DMA_STALLED: { status: "UNHEALTHY" } },
622 },
623 });
624
625 health_inspect
627 .record_alert(&HealthAlert::UpstreamNeighborFault(HealthCheckResponse::default()));
628 assert_data_tree!(inspector, root: {
629 driver_health_check: {
630 status: "UNHEALTHY",
631 conditions: { DMA_STALLED: { status: "UNHEALTHY" } },
632 },
633 });
634
635 health_inspect.record_alert(&HealthAlert::SelfFault(HealthCheckResponse::default()));
637 assert_data_tree!(inspector, root: { driver_health_check: { status: "OK" } });
638 }
639
640 #[fuchsia::test]
641 async fn alerts_are_dropped_when_queue_is_full() {
642 let (reporter, alerts) = HealthAlertReporter::new_for_testing();
643 for _ in 0..(HEALTH_ALERT_QUEUE_CAPACITY + 5) {
645 reporter.clone().report_health_alert(HealthAlert::SelfFault(Default::default()));
646 }
647 match &*reporter.sink.lock() {
648 HealthAlertSink::TestQueue { dropped, .. } => assert_eq!(*dropped, 5),
649 _ => panic!("expected TestQueue"),
650 }
651 drop(reporter);
653 let received: Vec<_> = alerts.collect().await;
654 assert_eq!(received.len(), HEALTH_ALERT_QUEUE_CAPACITY);
655 }
656}