1mod conversion;
16
17use conversion::*;
18
19use std::sync::atomic::AtomicU64;
20use std::sync::atomic::Ordering;
21use std::sync::Arc;
22use std::time::Duration;
23
24use chrono::DateTime;
25use chrono::Utc;
26use serde::Deserialize;
27use serde::Serialize;
28
29use crate::blocking::BlockingExecutorAggregate;
30use crate::blocking::BlockingExecutorSnapshot;
31use crate::blocking::BlockingKind;
32use crate::blocking::BlockingLane;
33use crate::metadata_io::MetadataIoSnapshot;
34use crate::scheduled::ScheduledTaskSnapshot;
35use crate::shutdown_report::ShutdownReport;
36use crate::task_group::TaskDetailScope;
37use crate::task_group::TaskGroup;
38use crate::task_group::TaskGroupEventCounts;
39use crate::task_group::TaskGroupId;
40use crate::task_group::TaskGroupLifecycleState;
41use crate::task_group::TaskKind;
42
43static NEXT_RUNTIME_DIAGNOSTICS_ID: AtomicU64 = AtomicU64::new(1);
44
45#[derive(Debug, Clone)]
47pub struct RuntimeDiagnostics {
48 runtime_id: Arc<str>,
49}
50
51#[derive(Debug, Clone, Serialize)]
53pub struct RuntimeDiagnosticsSnapshot {
54 pub runtime_id: String,
56 pub root_name: String,
58 pub group_id: TaskGroupId,
60 pub parent_group_id: Option<TaskGroupId>,
62 pub lifecycle_state: TaskGroupLifecycleState,
64 pub task_count: usize,
66 pub child_count: usize,
68 pub active_tasks: usize,
70 pub registry_slots: usize,
72 pub events: TaskGroupEventCounts,
74 pub blocking_lanes: Vec<BlockingExecutorSnapshot>,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum RuntimeComponent {
85 Broker,
87 NameServer,
89 Controller,
91 Proxy,
93 Mcp,
95 SreControlPlane,
97 SreConnector,
99 Other,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum RuntimeLifecycleStateV1 {
107 Open,
109 Closing,
111 Closed,
113 ShutdownCompleted,
115 Poisoned,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum RuntimeTaskKindV1 {
123 Service,
125 Worker,
127 ScheduledDriver,
129 ScheduledRun,
131 BlockingReaper,
133 Shutdown,
135 Other,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case")]
142pub enum RuntimeBlockingLaneV1 {
143 StorageIo,
145 MetadataIo,
147 CpuCrypto,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub enum RuntimeBlockingKindV1 {
155 ShortIo,
157 CpuBound,
159 LongRunning,
161}
162
163#[derive(Debug, Clone, Copy)]
165pub struct RuntimeDiagnosticsViewOptions {
166 pub long_running_threshold: Duration,
168 pub max_task_kind_summaries: usize,
170 pub max_blocking_lane_summaries: usize,
172}
173
174impl Default for RuntimeDiagnosticsViewOptions {
175 fn default() -> Self {
176 Self {
177 long_running_threshold: Duration::from_secs(30),
178 max_task_kind_summaries: TaskKind::COUNT,
179 max_blocking_lane_summaries: BlockingLane::ALL.len(),
180 }
181 }
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct RuntimeDiagnosticsViewV1 {
192 pub schema_version: String,
194 pub observed_at: DateTime<Utc>,
196 pub component: RuntimeComponent,
198 pub lifecycle_state: RuntimeLifecycleStateV1,
200 pub task_group_count: usize,
202 pub task_count: usize,
204 pub task_kinds: Vec<RuntimeTaskKindSummaryV1>,
206 pub blocking_lanes: Vec<RuntimeBlockingLaneSummaryV1>,
208 pub truncated: bool,
210}
211
212impl RuntimeDiagnosticsViewV1 {
213 pub const SCHEMA_VERSION: &'static str = "rocketmq.runtime-diagnostics.v1";
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219pub struct RuntimeTaskKindSummaryV1 {
220 pub kind: RuntimeTaskKindV1,
222 pub active: usize,
224 pub long_running: usize,
226 pub max_elapsed_millis: u64,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct RuntimeBlockingLaneSummaryV1 {
233 pub lane: RuntimeBlockingLaneV1,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub max_concurrency: Option<usize>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub max_queue_depth: Option<usize>,
241 pub queued: usize,
243 pub running: usize,
245 pub timed_out_still_running: usize,
247 pub blocking_still_running: usize,
249 pub task_kinds: Vec<RuntimeBlockingKindSummaryV1>,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct RuntimeBlockingKindSummaryV1 {
256 pub kind: RuntimeBlockingKindV1,
258 pub active: usize,
260 pub max_elapsed_millis: u64,
262}
263
264impl RuntimeDiagnostics {
265 pub(crate) fn new() -> Self {
266 let runtime_id = NEXT_RUNTIME_DIAGNOSTICS_ID.fetch_add(1, Ordering::Relaxed);
267 Self {
268 runtime_id: Arc::from(format!("rocketmq-runtime-{runtime_id}")),
269 }
270 }
271
272 pub fn runtime_id(&self) -> &str {
274 &self.runtime_id
275 }
276
277 pub fn snapshot(
279 &self,
280 root: &TaskGroup,
281 blocking_lanes: Vec<BlockingExecutorSnapshot>,
282 ) -> RuntimeDiagnosticsSnapshot {
283 RuntimeDiagnosticsSnapshot {
284 runtime_id: self.runtime_id.to_string(),
285 root_name: root.name().to_string(),
286 group_id: root.id(),
287 parent_group_id: root.parent_id(),
288 lifecycle_state: root.lifecycle_state(),
289 task_count: root.task_count(),
290 child_count: root.component_count(),
291 active_tasks: root.task_count(),
292 registry_slots: root.component_count(),
293 events: root.event_counts(),
294 blocking_lanes,
295 }
296 }
297
298 pub fn view_v1(
300 &self,
301 component: RuntimeComponent,
302 root: &TaskGroup,
303 blocking_lanes: Vec<BlockingExecutorSnapshot>,
304 ) -> RuntimeDiagnosticsViewV1 {
305 self.view_v1_with_options(
306 component,
307 root,
308 blocking_lanes,
309 RuntimeDiagnosticsViewOptions::default(),
310 )
311 }
312
313 pub fn view_v1_with_options(
315 &self,
316 component: RuntimeComponent,
317 root: &TaskGroup,
318 blocking_lanes: Vec<BlockingExecutorSnapshot>,
319 options: RuntimeDiagnosticsViewOptions,
320 ) -> RuntimeDiagnosticsViewV1 {
321 self.view_v1_with_aggregates(
322 component,
323 root,
324 blocking_lanes.into_iter().map(Into::into).collect(),
325 options,
326 )
327 }
328
329 pub(crate) fn view_v1_with_aggregates(
330 &self,
331 component: RuntimeComponent,
332 root: &TaskGroup,
333 blocking_lanes: Vec<BlockingExecutorAggregate>,
334 options: RuntimeDiagnosticsViewOptions,
335 ) -> RuntimeDiagnosticsViewV1 {
336 let task_diagnostics = root.diagnostics(options.long_running_threshold);
337 let task_kind_count = task_diagnostics.task_kinds.len();
338 let task_kinds = task_diagnostics
339 .task_kinds
340 .into_iter()
341 .take(options.max_task_kind_summaries)
342 .map(|summary| RuntimeTaskKindSummaryV1 {
343 kind: runtime_task_kind(summary.kind),
344 active: summary.active,
345 long_running: summary.long_running,
346 max_elapsed_millis: duration_millis(summary.max_elapsed),
347 })
348 .collect();
349
350 let blocking_lane_count = blocking_lanes.len();
351 let blocking_lanes = blocking_lanes
352 .into_iter()
353 .take(options.max_blocking_lane_summaries.min(BlockingLane::ALL.len()))
354 .map(sanitize_blocking_lane)
355 .collect();
356
357 RuntimeDiagnosticsViewV1 {
358 schema_version: RuntimeDiagnosticsViewV1::SCHEMA_VERSION.to_string(),
359 observed_at: Utc::now(),
360 component,
361 lifecycle_state: runtime_lifecycle_state(root.lifecycle_state()),
362 task_group_count: task_diagnostics.group_count,
363 task_count: task_diagnostics.task_count,
364 task_kinds,
365 blocking_lanes,
366 truncated: task_kind_count > options.max_task_kind_summaries
367 || blocking_lane_count > options.max_blocking_lane_summaries.min(3),
368 }
369 }
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(rename_all = "snake_case")]
378pub enum RuntimeDiagnosticsScope {
379 Local,
381 Subtree,
383 ProcessShared,
385}
386
387#[derive(Debug, Clone, Copy)]
395pub struct RuntimeDiagnosticsViewOptionsV2 {
396 pub long_running_threshold: Duration,
398 pub max_task_kind_summaries: usize,
400 pub max_blocking_lane_summaries: usize,
402 pub max_schedule_tasks: usize,
404 pub max_metadata_resources: usize,
406 pub max_detail_entries: usize,
408 pub detail_scan_budget: usize,
411}
412
413impl Default for RuntimeDiagnosticsViewOptionsV2 {
414 fn default() -> Self {
415 Self {
416 long_running_threshold: Duration::from_secs(30),
417 max_task_kind_summaries: TaskKind::COUNT,
418 max_blocking_lane_summaries: BlockingLane::ALL.len(),
419 max_schedule_tasks: 16,
420 max_metadata_resources: 32,
421 max_detail_entries: 0,
422 detail_scan_budget: 0,
423 }
424 }
425}
426
427#[derive(Debug, Clone, Default)]
433pub struct RuntimeDiagnosticsInputs {
434 pub schedule: Vec<ScheduledTaskSnapshot>,
436 pub metadata: Option<MetadataIoSnapshot>,
438 pub shutdown: Option<ShutdownReport>,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
444pub struct RuntimeTaskSectionV2 {
445 pub scope: RuntimeDiagnosticsScope,
447 pub task_group_count: usize,
449 pub task_count: usize,
451 pub local_task_count: usize,
457 pub long_running: usize,
459 pub max_elapsed_millis: u64,
461 pub task_kinds: Vec<RuntimeTaskKindSummaryV1>,
463 pub truncated: bool,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct RuntimeBlockingSectionV2 {
470 pub scope: RuntimeDiagnosticsScope,
472 pub queued: usize,
474 pub running: usize,
476 pub timed_out_still_running: usize,
478 pub blocking_still_running: usize,
480 pub lanes: Vec<RuntimeBlockingLaneSummaryV1>,
482 pub truncated: bool,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
488pub struct RuntimeScheduleSectionV2 {
489 pub scope: RuntimeDiagnosticsScope,
491 pub tasks_scanned: usize,
493 pub tasks_emitted: usize,
495 pub active_runs: u64,
497 pub runs: u64,
499 pub skips: u64,
501 pub overlaps: u64,
503 pub failures: u64,
505 pub max_elapsed_millis: u64,
507 pub truncated: bool,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
513pub struct RuntimeMetadataSectionV2 {
514 pub scope: RuntimeDiagnosticsScope,
516 pub accepting: bool,
518 pub retained_operations: usize,
520 pub retained_bytes: usize,
522 pub waiters: usize,
524 pub resources_scanned: usize,
526 pub resources_emitted: usize,
528 pub truncated: bool,
530}
531
532#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534pub struct RuntimeShutdownSectionV2 {
535 pub scope: RuntimeDiagnosticsScope,
537 pub elapsed_millis: u64,
539 pub cancelled: usize,
541 pub failed: usize,
543 pub timed_out: usize,
545 pub leaked: usize,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
555pub struct RuntimeTaskDetailV2 {
556 pub kind: RuntimeTaskKindV1,
558 pub scope: RuntimeDiagnosticsScope,
560 pub elapsed_millis: u64,
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571pub struct RuntimeDiagnosticsViewV2 {
572 pub schema_version: String,
574 pub observed_at: DateTime<Utc>,
576 pub component: RuntimeComponent,
578 pub lifecycle_state: RuntimeLifecycleStateV1,
580 pub tasks: RuntimeTaskSectionV2,
582 pub blocking: RuntimeBlockingSectionV2,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub schedule: Option<RuntimeScheduleSectionV2>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub metadata: Option<RuntimeMetadataSectionV2>,
590 #[serde(default, skip_serializing_if = "Option::is_none")]
592 pub shutdown: Option<RuntimeShutdownSectionV2>,
593 pub details: Vec<RuntimeTaskDetailV2>,
595 pub detail_scan_budget: usize,
597 pub details_scanned: usize,
599 pub truncated: bool,
601}
602
603#[derive(Debug, Clone, Serialize)]
608pub struct RuntimeTaskDetails {
609 pub details: Vec<RuntimeTaskDetailV2>,
611 pub tasks_scanned: usize,
613 pub group_entries_scanned: usize,
615 pub truncated: bool,
617}
618
619impl From<crate::task_group::TaskDetailScan> for RuntimeTaskDetails {
620 fn from(scan: crate::task_group::TaskDetailScan) -> Self {
621 Self {
622 details: scan
623 .details
624 .into_iter()
625 .map(|detail| RuntimeTaskDetailV2 {
626 kind: runtime_task_kind(detail.kind),
627 scope: runtime_detail_scope(detail.scope),
628 elapsed_millis: duration_millis(detail.elapsed),
629 })
630 .collect(),
631 tasks_scanned: scan.scanned,
632 group_entries_scanned: scan.group_entries_scanned,
633 truncated: scan.truncated,
634 }
635 }
636}
637
638impl RuntimeDiagnosticsViewV2 {
639 pub const SCHEMA_VERSION: &'static str = "rocketmq.runtime-diagnostics.v2";
641}
642
643impl RuntimeDiagnostics {
644 pub fn view_v2(
646 &self,
647 component: RuntimeComponent,
648 root: &TaskGroup,
649 blocking_lanes: Vec<BlockingExecutorSnapshot>,
650 inputs: RuntimeDiagnosticsInputs,
651 ) -> RuntimeDiagnosticsViewV2 {
652 self.view_v2_with_options(
653 component,
654 root,
655 blocking_lanes,
656 inputs,
657 RuntimeDiagnosticsViewOptionsV2::default(),
658 )
659 }
660
661 pub fn view_v2_with_options(
664 &self,
665 component: RuntimeComponent,
666 root: &TaskGroup,
667 blocking_lanes: Vec<BlockingExecutorSnapshot>,
668 inputs: RuntimeDiagnosticsInputs,
669 options: RuntimeDiagnosticsViewOptionsV2,
670 ) -> RuntimeDiagnosticsViewV2 {
671 self.view_v2_with_aggregates(
672 component,
673 root,
674 blocking_lanes.into_iter().map(Into::into).collect(),
675 inputs,
676 options,
677 )
678 }
679
680 pub(crate) fn view_v2_with_aggregates(
681 &self,
682 component: RuntimeComponent,
683 root: &TaskGroup,
684 blocking_lanes: Vec<BlockingExecutorAggregate>,
685 inputs: RuntimeDiagnosticsInputs,
686 options: RuntimeDiagnosticsViewOptionsV2,
687 ) -> RuntimeDiagnosticsViewV2 {
688 let (tasks, tasks_truncated) = task_section_v2(root, options);
689 let (blocking, blocking_truncated) = blocking_section_v2(blocking_lanes, options);
690 let schedule = (!inputs.schedule.is_empty()).then(|| schedule_section_v2(&inputs.schedule, options));
691 let metadata = inputs
692 .metadata
693 .as_ref()
694 .map(|snapshot| metadata_section_v2(snapshot, options));
695 let shutdown = inputs.shutdown.as_ref().map(shutdown_section_v2);
696 let details = if options.max_detail_entries == 0 && options.detail_scan_budget == 0 {
700 crate::task_group::TaskDetailScan::default()
701 } else {
702 root.bounded_task_details(options.detail_scan_budget, options.max_detail_entries)
703 };
704 let details_truncated = details.truncated;
705
706 let schedule_truncated = schedule.as_ref().is_some_and(|section| section.truncated);
707 let metadata_truncated = metadata.as_ref().is_some_and(|section| section.truncated);
708
709 RuntimeDiagnosticsViewV2 {
710 schema_version: RuntimeDiagnosticsViewV2::SCHEMA_VERSION.to_string(),
711 observed_at: Utc::now(),
712 component,
713 lifecycle_state: runtime_lifecycle_state(root.lifecycle_state()),
714 tasks,
715 blocking,
716 schedule,
717 metadata,
718 shutdown,
719 details: details
720 .details
721 .into_iter()
722 .map(|detail| RuntimeTaskDetailV2 {
723 kind: runtime_task_kind(detail.kind),
724 scope: runtime_detail_scope(detail.scope),
725 elapsed_millis: duration_millis(detail.elapsed),
726 })
727 .collect(),
728 detail_scan_budget: options.detail_scan_budget,
729 details_scanned: details.scanned,
730 truncated: tasks_truncated
731 || blocking_truncated
732 || schedule_truncated
733 || metadata_truncated
734 || details_truncated,
735 }
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use crate::RuntimeContext;
743
744 #[tokio::test]
745 async fn lane_identity_survives_partial_and_reordered_inputs_in_both_schemas() {
746 let context = RuntimeContext::from_current("lane-identity");
747 let diagnostics = RuntimeDiagnostics::new();
748 let snapshot = |lane: BlockingLane| BlockingExecutorSnapshot {
749 name: "private-lane".to_owned(),
750 lane,
751 max_concurrency: 8,
752 max_queue_depth: 32,
753 global_capacity: 8,
754 global_running: 0,
755 global_available: 8,
756 lane_reserved: 1,
757 lane_running: 0,
758 lane_borrowed: 0,
759 queued: lane.index() + 10,
760 running: 0,
761 timed_out_still_running: 0,
762 blocking_still_running: 0,
763 rejected: 0,
764 oldest_queue_wait: Duration::ZERO,
765 tasks: Vec::new(),
766 };
767 for lanes in [
768 vec![BlockingLane::MetadataIo],
769 vec![
770 BlockingLane::CpuCrypto,
771 BlockingLane::StorageIo,
772 BlockingLane::MetadataIo,
773 ],
774 vec![BlockingLane::MetadataIo, BlockingLane::CpuCrypto],
775 vec![],
776 ] {
777 let snapshots: Vec<_> = lanes.iter().copied().map(snapshot).collect();
778 let v1 = diagnostics.view_v1(RuntimeComponent::Broker, context.root_group(), snapshots.clone());
779 let v2 = diagnostics.view_v2(
780 RuntimeComponent::Broker,
781 context.root_group(),
782 snapshots,
783 RuntimeDiagnosticsInputs::default(),
784 );
785 assert_eq!(v1.blocking_lanes, v2.blocking.lanes);
786 assert_eq!(v1.blocking_lanes.len(), lanes.len());
787 for (summary, lane) in v1.blocking_lanes.iter().zip(lanes) {
788 let expected = match lane {
789 BlockingLane::StorageIo => "storage_io",
790 BlockingLane::MetadataIo => "metadata_io",
791 BlockingLane::CpuCrypto => "cpu_crypto",
792 };
793 assert_eq!(serde_json::to_value(summary.lane).unwrap(), expected);
794 assert_eq!(summary.queued, lane.index() + 10);
795 }
796 }
797 }
798
799 #[tokio::test]
800 async fn every_task_kind_keeps_its_own_population_and_wire_label() {
801 let context = RuntimeContext::from_current("kind-population");
802 let root = context.root_group();
803 let labels = [
804 "service",
805 "worker",
806 "scheduled_driver",
807 "scheduled_run",
808 "blocking_reaper",
809 "shutdown",
810 "other",
811 ];
812 for (kind, count) in TaskKind::ALL.into_iter().zip(1..) {
813 for _ in 0..count {
814 let cancellation = root.cancellation_token();
815 root.spawn("pending", kind, async move { cancellation.cancelled().await })
816 .unwrap();
817 }
818 }
819 let view = RuntimeDiagnostics::new().view_v1(RuntimeComponent::Broker, root, Vec::new());
820 assert_eq!(view.task_kinds.len(), labels.len());
821 for ((summary, label), count) in view.task_kinds.iter().zip(labels).zip(1..) {
822 assert_eq!(serde_json::to_value(summary.kind).unwrap(), label);
823 assert_eq!(summary.active, count);
824 }
825 assert!(root.shutdown(Duration::from_secs(1)).await.is_healthy());
826 }
827
828 #[tokio::test]
829 async fn sanitized_view_does_not_expose_runtime_or_task_names() {
830 let context = RuntimeContext::from_current("sensitive-root-name");
831 let child = context.service_context("sensitive-child-name");
832 child
833 .spawn("sensitive-task-name", TaskKind::Worker, std::future::pending())
834 .expect("task should spawn");
835 let diagnostics = RuntimeDiagnostics::new();
836
837 let view = diagnostics.view_v1(RuntimeComponent::Mcp, context.root_group(), Vec::new());
838 let json = serde_json::to_string(&view).expect("view should serialize");
839
840 assert!(!json.contains("sensitive-root-name"));
841 assert!(!json.contains("sensitive-child-name"));
842 assert!(!json.contains("sensitive-task-name"));
843 assert!(!json.contains("rocketmq-runtime-"));
844 assert!(json.contains("\"lifecycle_state\":\"open\""));
845 assert!(json.contains("\"kind\":\"worker\""));
846 assert!(view.task_group_count >= 2);
847 assert_eq!(view.task_count, 1);
848 }
849
850 #[tokio::test]
851 async fn zero_bounds_produce_an_explicit_truncated_view() {
852 let context = RuntimeContext::from_current("runtime-view-bounds");
853 context
854 .root_group()
855 .spawn("worker", TaskKind::Worker, std::future::pending())
856 .expect("task should spawn");
857 let diagnostics = RuntimeDiagnostics::new();
858 let view = diagnostics.view_v1_with_options(
859 RuntimeComponent::Other,
860 context.root_group(),
861 Vec::new(),
862 RuntimeDiagnosticsViewOptions {
863 max_task_kind_summaries: 0,
864 max_blocking_lane_summaries: 0,
865 ..RuntimeDiagnosticsViewOptions::default()
866 },
867 );
868
869 assert!(view.truncated);
870 assert!(view.task_kinds.is_empty());
871 assert!(view.blocking_lanes.is_empty());
872 }
873
874 #[tokio::test]
875 async fn sanitized_view_exposes_only_bounded_blocking_capacity_and_counts() {
876 let context = RuntimeContext::from_current("runtime-blocking-capacity");
877 let diagnostics = RuntimeDiagnostics::new();
878 let view = diagnostics.view_v1(
879 RuntimeComponent::Broker,
880 context.root_group(),
881 vec![BlockingExecutorSnapshot {
882 name: "sensitive-lane-name".to_owned(),
883 lane: crate::BlockingLane::StorageIo,
884 max_concurrency: 8,
885 max_queue_depth: 32,
886 global_capacity: 8,
887 global_running: 2,
888 global_available: 6,
889 lane_reserved: 8,
890 lane_running: 2,
891 lane_borrowed: 0,
892 queued: 3,
893 running: 2,
894 timed_out_still_running: 1,
895 blocking_still_running: 0,
896 rejected: 0,
897 oldest_queue_wait: Duration::ZERO,
898 tasks: Vec::new(),
899 }],
900 );
901
902 assert_eq!(view.blocking_lanes.len(), 1);
903 assert_eq!(view.blocking_lanes[0].max_concurrency, Some(8));
904 assert_eq!(view.blocking_lanes[0].max_queue_depth, Some(32));
905 let encoded = serde_json::to_string(&view).expect("view should serialize");
906 assert!(!encoded.contains("sensitive-lane-name"));
907 let decoded: RuntimeDiagnosticsViewV1 =
908 serde_json::from_str(&encoded).expect("versioned view should deserialize");
909 assert_eq!(decoded, view);
910
911 let mut legacy = serde_json::to_value(&view).expect("view should encode");
912 let lane = legacy
913 .get_mut("blocking_lanes")
914 .and_then(serde_json::Value::as_array_mut)
915 .and_then(|lanes| lanes.first_mut())
916 .and_then(serde_json::Value::as_object_mut)
917 .expect("blocking lane");
918 lane.remove("max_concurrency");
919 lane.remove("max_queue_depth");
920 let legacy: RuntimeDiagnosticsViewV1 =
921 serde_json::from_value(legacy).expect("older v1 view should remain readable");
922 assert_eq!(legacy.blocking_lanes[0].max_concurrency, None);
923 assert_eq!(legacy.blocking_lanes[0].max_queue_depth, None);
924 }
925
926 fn scheduled_snapshot(name: &str, runs: u64, skips: u64, max_elapsed_ms: u64) -> ScheduledTaskSnapshot {
927 ScheduledTaskSnapshot {
928 name: name.to_string(),
929 mode: crate::scheduled::ScheduleMode::FixedDelay,
930 running: true,
931 active_runs: 1,
932 runs,
933 skips,
934 overlaps: 0,
935 failures: 0,
936 last_drift_ms: 0,
937 last_elapsed_ms: max_elapsed_ms,
938 max_elapsed_ms,
939 }
940 }
941
942 fn metadata_snapshot(waiter_count: usize) -> MetadataIoSnapshot {
943 MetadataIoSnapshot {
944 accepting: true,
945 pending_operations: 2,
946 pending_bytes: 4_096,
947 max_pending_operations: 1_024,
948 max_pending_bytes: 65_536,
949 resources: vec![crate::metadata_io::MetadataIoResourceSnapshot {
950 resource: "sensitive-resource-name".into(),
951 target: None,
952 durable_generation: None,
953 in_flight_generation: None,
954 queued_generation: None,
955 waiter_count,
956 }],
957 }
958 }
959
960 #[tokio::test]
961 async fn v2_sections_state_their_scope_and_absent_inputs_stay_absent() {
962 let context = RuntimeContext::from_current("runtime-v2-scope");
963 context
964 .root_group()
965 .spawn("local-worker", TaskKind::Worker, std::future::pending())
966 .expect("task should spawn");
967 context
968 .service_context("runtime-v2-child")
969 .spawn("child-worker", TaskKind::Worker, std::future::pending())
970 .expect("task should spawn");
971 let diagnostics = RuntimeDiagnostics::new();
972
973 let view = diagnostics.view_v2(
974 RuntimeComponent::Broker,
975 context.root_group(),
976 Vec::new(),
977 RuntimeDiagnosticsInputs::default(),
978 );
979
980 assert_eq!(view.schema_version, "rocketmq.runtime-diagnostics.v2");
981 assert_eq!(view.tasks.scope, RuntimeDiagnosticsScope::Subtree);
982 assert_eq!(view.tasks.task_count, 2);
983 assert_eq!(view.tasks.local_task_count, 1);
984 assert_eq!(view.blocking.scope, RuntimeDiagnosticsScope::ProcessShared);
985 assert!(view.schedule.is_none());
986 assert!(view.metadata.is_none());
987 assert!(view.shutdown.is_none());
988 assert!(view.details.is_empty());
989 assert!(!view.truncated);
991 }
992
993 #[tokio::test]
994 async fn v2_reports_component_owned_sections_with_their_scope() {
995 let context = RuntimeContext::from_current("runtime-v2-sections");
996 let diagnostics = RuntimeDiagnostics::new();
997
998 let view = diagnostics.view_v2(
999 RuntimeComponent::Broker,
1000 context.root_group(),
1001 Vec::new(),
1002 RuntimeDiagnosticsInputs {
1003 schedule: vec![scheduled_snapshot("sensitive-schedule", 4, 3, 250)],
1004 metadata: Some(metadata_snapshot(2)),
1005 shutdown: Some(ShutdownReport {
1006 cancelled: 1,
1007 failed: 1,
1008 timed_out: 1,
1009 leaked: 1,
1010 ..ShutdownReport::new("sensitive-group", Duration::from_millis(120))
1011 }),
1012 },
1013 );
1014
1015 let schedule = view.schedule.clone().expect("a supplied schedule section is present");
1016 assert_eq!(schedule.scope, RuntimeDiagnosticsScope::Local);
1017 assert_eq!(schedule.tasks_scanned, 1);
1018 assert_eq!(schedule.tasks_emitted, 1);
1019 assert_eq!(schedule.runs, 4);
1020 assert_eq!(schedule.skips, 3);
1021 assert_eq!(schedule.max_elapsed_millis, 250);
1022 assert!(!schedule.truncated);
1023
1024 let metadata = view.metadata.clone().expect("a supplied metadata section is present");
1025 assert_eq!(metadata.scope, RuntimeDiagnosticsScope::Local);
1026 assert_eq!(metadata.retained_operations, 2);
1027 assert_eq!(metadata.retained_bytes, 4_096);
1028 assert_eq!(metadata.waiters, 2);
1029 assert!(metadata.accepting);
1030
1031 let shutdown = view.shutdown.clone().expect("a supplied shutdown section is present");
1032 assert_eq!(shutdown.scope, RuntimeDiagnosticsScope::Local);
1033 assert_eq!(shutdown.elapsed_millis, 120);
1034 assert_eq!(shutdown.failed, 1);
1035 assert_eq!(shutdown.timed_out, 1);
1036 assert_eq!(shutdown.leaked, 1);
1037
1038 let json = serde_json::to_string(&view).expect("view should serialize");
1039 assert!(!json.contains("sensitive"));
1040 }
1041
1042 #[tokio::test]
1043 async fn v2_truncation_reports_what_was_scanned_and_emitted() {
1044 let context = RuntimeContext::from_current("runtime-v2-truncation");
1045 let diagnostics = RuntimeDiagnostics::new();
1046 let schedule = vec![
1047 scheduled_snapshot("first", 1, 0, 10),
1048 scheduled_snapshot("second", 1, 0, 20),
1049 scheduled_snapshot("third", 1, 0, 30),
1050 ];
1051
1052 let view = diagnostics.view_v2_with_options(
1053 RuntimeComponent::Other,
1054 context.root_group(),
1055 Vec::new(),
1056 RuntimeDiagnosticsInputs {
1057 schedule,
1058 ..RuntimeDiagnosticsInputs::default()
1059 },
1060 RuntimeDiagnosticsViewOptionsV2 {
1061 max_schedule_tasks: 1,
1062 ..RuntimeDiagnosticsViewOptionsV2::default()
1063 },
1064 );
1065
1066 let section = view.schedule.expect("a supplied schedule section is present");
1067 assert!(section.truncated);
1068 assert_eq!(section.tasks_scanned, 3);
1069 assert_eq!(section.tasks_emitted, 1);
1070 assert_eq!(section.runs, 1);
1071 assert!(view.truncated);
1072 }
1073
1074 #[tokio::test]
1075 async fn v2_detail_lists_stay_bounded_and_redacted() {
1076 let context = RuntimeContext::from_current("sensitive-runtime-name");
1077 let child = context.service_context("sensitive-child-name");
1078 for index in 0..4 {
1079 child
1080 .spawn(
1081 format!("sensitive-task-{index}"),
1082 TaskKind::Worker,
1083 std::future::pending(),
1084 )
1085 .expect("task should spawn");
1086 }
1087 let diagnostics = RuntimeDiagnostics::new();
1088
1089 let view = diagnostics.view_v2_with_options(
1090 RuntimeComponent::Mcp,
1091 context.root_group(),
1092 Vec::new(),
1093 RuntimeDiagnosticsInputs::default(),
1094 RuntimeDiagnosticsViewOptionsV2 {
1095 max_detail_entries: 2,
1096 detail_scan_budget: 4,
1097 ..RuntimeDiagnosticsViewOptionsV2::default()
1098 },
1099 );
1100
1101 assert_eq!(view.details.len(), 2);
1102 assert_eq!(view.details_scanned, 4);
1103 assert_eq!(view.detail_scan_budget, 4);
1104 assert!(view.truncated);
1105 assert!(
1106 view.details
1107 .iter()
1108 .all(|detail| detail.scope == RuntimeDiagnosticsScope::Subtree),
1109 "the tasks belong to a descendant group"
1110 );
1111
1112 let json = serde_json::to_string(&view).expect("view should serialize");
1113 assert!(!json.contains("sensitive"));
1114 assert!(!json.contains("rocketmq-runtime-"));
1115 let decoded: RuntimeDiagnosticsViewV2 = serde_json::from_str(&json).expect("versioned view should deserialize");
1116 assert_eq!(decoded, view);
1117 }
1118
1119 #[tokio::test]
1120 async fn v2_detail_scan_budget_bounds_the_scan_and_not_only_the_output() {
1121 let context = RuntimeContext::from_current("runtime-v2-scan-budget");
1122 for index in 0..4 {
1123 context
1124 .root_group()
1125 .spawn(format!("worker-{index}"), TaskKind::Worker, std::future::pending())
1126 .expect("task should spawn");
1127 }
1128 let diagnostics = RuntimeDiagnostics::new();
1129
1130 let view = diagnostics.view_v2_with_options(
1131 RuntimeComponent::Other,
1132 context.root_group(),
1133 Vec::new(),
1134 RuntimeDiagnosticsInputs::default(),
1135 RuntimeDiagnosticsViewOptionsV2 {
1136 max_detail_entries: 8,
1137 detail_scan_budget: 2,
1138 ..RuntimeDiagnosticsViewOptionsV2::default()
1139 },
1140 );
1141
1142 assert_eq!(view.details_scanned, 2);
1143 assert_eq!(view.details.len(), 2);
1144 assert!(view.truncated, "the scan stopped before every task was examined");
1145 assert!(view
1146 .details
1147 .iter()
1148 .all(|detail| detail.scope == RuntimeDiagnosticsScope::Local));
1149 }
1150
1151 #[tokio::test]
1152 async fn v2_detail_scan_budget_also_bounds_the_groups_it_visits() {
1153 let context = RuntimeContext::from_current("runtime-v2-group-budget");
1154 let groups = (0..8)
1156 .map(|index| context.service_context(crate::ScopeId::try_new(format!("empty-{index}")).unwrap()))
1157 .collect::<Vec<_>>();
1158 let diagnostics = RuntimeDiagnostics::new();
1159 let view_with_budget = |detail_scan_budget| {
1160 diagnostics.view_v2_with_options(
1161 RuntimeComponent::Other,
1162 context.root_group(),
1163 Vec::new(),
1164 RuntimeDiagnosticsInputs::default(),
1165 RuntimeDiagnosticsViewOptionsV2 {
1166 max_detail_entries: 8,
1167 detail_scan_budget,
1168 ..RuntimeDiagnosticsViewOptionsV2::default()
1169 },
1170 )
1171 };
1172
1173 let bounded = view_with_budget(3);
1174 assert_eq!(bounded.details_scanned, 0);
1175 assert!(bounded.details.is_empty());
1176 assert!(bounded.truncated, "the scan stopped before every group was visited");
1177 assert_eq!(
1178 bounded.tasks.task_group_count, 9,
1179 "the aggregate section still covers every group"
1180 );
1181
1182 let complete = view_with_budget(8);
1183 assert_eq!(complete.details_scanned, 0);
1184 assert!(!complete.truncated, "a budget covering every group completes the scan");
1185 drop(groups);
1186 }
1187
1188 #[tokio::test]
1189 async fn v2_view_is_a_snapshot_that_late_completions_cannot_rewrite() {
1190 let context = RuntimeContext::from_current("runtime-v2-frozen");
1191 let (release, wait) = tokio::sync::oneshot::channel::<()>();
1192 context
1193 .root_group()
1194 .spawn("finishes-later", TaskKind::Worker, async move {
1195 let _ = wait.await;
1196 })
1197 .expect("task should spawn");
1198 let diagnostics = RuntimeDiagnostics::new();
1199
1200 let view = diagnostics.view_v2(
1201 RuntimeComponent::Other,
1202 context.root_group(),
1203 Vec::new(),
1204 RuntimeDiagnosticsInputs::default(),
1205 );
1206 assert_eq!(view.tasks.task_count, 1);
1207
1208 release.send(()).expect("the task should still be waiting");
1209 for _ in 0..10_000 {
1210 if context.root_group().task_count() == 0 {
1211 break;
1212 }
1213 tokio::task::yield_now().await;
1214 }
1215
1216 assert_eq!(context.root_group().task_count(), 0, "the task really finished");
1217 assert_eq!(
1218 view.tasks.task_count, 1,
1219 "the earlier view keeps what it observed instead of following late completions"
1220 );
1221 }
1222}