1use std::time::SystemTime;
31
32use anyhow::Context;
33use schemars::JsonSchema;
34use serde::Deserialize;
35use serde::Serialize;
36
37use super::ActiveHandler;
38use super::Execution;
39use super::FailureInfo;
40use super::InboundOrdering;
41use super::NodePayload;
42use super::NodeProperties;
43use super::NodeRef;
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
56#[schemars(title = "NodePayload")]
57pub struct NodePayloadDto {
58 pub identity: String,
60 pub properties: NodePropertiesDto,
62 pub children: Vec<String>,
65 pub parent: Option<String>,
67 pub as_of: String,
69}
70
71#[derive(
74 Debug,
75 Clone,
76 Copy,
77 PartialEq,
78 Eq,
79 Default,
80 Serialize,
81 Deserialize,
82 JsonSchema
83)]
84#[schemars(rename = "ProcessMemoryStats")]
85pub struct ProcessMemoryStatsDto {
86 pub process_rss_bytes: Option<u64>,
88 pub process_vm_size_bytes: Option<u64>,
90}
91
92#[derive(
95 Debug,
96 Clone,
97 Copy,
98 PartialEq,
99 Eq,
100 Default,
101 Serialize,
102 Deserialize,
103 JsonSchema
104)]
105#[schemars(rename = "ProcDebugStats")]
106pub struct ProcDebugStatsDto {
107 pub memory: ProcessMemoryStatsDto,
109 pub actor_work_queue_depth_total: u64,
111 pub actor_work_queue_depth_max: u64,
113 pub actor_work_queue_depth_high_water_mark: u64,
115 pub last_nonzero_queue_depth_age_ms: Option<u64>,
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
122#[schemars(rename = "NodeProperties")]
123pub enum NodePropertiesDto {
124 Root {
126 num_hosts: usize,
127 started_at: String,
128 started_by: String,
129 system_children: Vec<String>,
130 },
131 Host {
133 addr: String,
134 num_procs: usize,
135 system_children: Vec<String>,
136 memory: ProcessMemoryStatsDto,
138 },
139 Proc {
141 proc_name: String,
142 num_actors: usize,
143 system_children: Vec<String>,
144 stopped_children: Vec<String>,
145 stopped_retention_cap: usize,
146 is_poisoned: bool,
147 failed_actor_count: usize,
148 debug: ProcDebugStatsDto,
150 },
151 Actor {
153 actor_status: String,
154 actor_type: String,
155 instance_id: String,
158 messages_processed: u64,
159 created_at: Option<String>,
160 last_message_handler: Option<String>,
161 total_processing_time_us: u64,
162 queue_depth: u64,
167 flight_recorder: Option<String>,
168 is_system: bool,
169 inbound_ordering: Option<Box<InboundOrderingDto>>,
175 failure_info: Option<FailureInfoDto>,
176 execution: Option<Box<ExecutionDto>>,
180 },
181 Error { code: String, message: String },
183}
184
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
187#[schemars(rename = "FailureInfo")]
188pub struct FailureInfoDto {
189 pub error_message: String,
191 pub root_cause_actor: String,
193 pub root_cause_name: Option<String>,
195 pub occurred_at: String,
197 pub is_propagated: bool,
199}
200
201#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
203#[schemars(rename = "OrderingSessionSnapshot")]
204pub struct OrderingSessionSnapshotDto {
205 pub session_id: String,
207 pub sender: Option<String>,
210 pub last_released_seq: u64,
213 pub expected_next_seq: u64,
215 pub buffered_count: usize,
217 pub oldest_buffered_seq: Option<u64>,
219 pub newest_buffered_seq: Option<u64>,
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
228#[schemars(rename = "InboundOrdering")]
229pub struct InboundOrderingDto {
230 pub enabled: bool,
232 pub snapshot_complete: bool,
234 pub skipped_session_count: usize,
237 pub known_session_count: usize,
239 pub returned_buffered_session_count: usize,
242 pub returned_buffered_message_count: usize,
245 pub returned_max_buffered_count: usize,
247 pub sessions: Vec<OrderingSessionSnapshotDto>,
249}
250
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
253#[schemars(rename = "ActiveHandler")]
254pub struct ActiveHandlerDto {
255 pub name: String,
257 pub active_count: u64,
259 pub oldest_since: String,
261}
262
263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
270#[schemars(rename = "Execution")]
271pub struct ExecutionDto {
272 pub active_count: u64,
274 pub active_handlers: Vec<ActiveHandlerDto>,
277 pub complete: bool,
279 pub truncated: bool,
281}
282
283fn format_time(t: &SystemTime) -> String {
286 humantime::format_rfc3339_millis(*t).to_string()
287}
288
289fn refs_to_strings(refs: &[NodeRef]) -> Vec<String> {
290 refs.iter().map(|r| r.to_string()).collect()
291}
292
293fn parse_refs(field: &str, strings: &[String]) -> anyhow::Result<Vec<NodeRef>> {
294 strings
295 .iter()
296 .enumerate()
297 .map(|(i, s)| {
298 s.parse()
299 .with_context(|| format!("failed to parse {field}[{i}]: {s:?}"))
300 })
301 .collect()
302}
303
304impl From<NodePayload> for NodePayloadDto {
307 fn from(p: NodePayload) -> Self {
308 Self {
309 identity: p.identity.to_string(),
310 properties: p.properties.into(),
311 children: refs_to_strings(&p.children),
312 parent: p.parent.as_ref().map(|r| r.to_string()),
313 as_of: format_time(&p.as_of),
314 }
315 }
316}
317
318impl From<NodeProperties> for NodePropertiesDto {
319 fn from(p: NodeProperties) -> Self {
320 match p {
321 NodeProperties::Root {
322 num_hosts,
323 started_at,
324 started_by,
325 system_children,
326 } => Self::Root {
327 num_hosts,
328 started_at: format_time(&started_at),
329 started_by,
330 system_children: refs_to_strings(&system_children),
331 },
332 NodeProperties::Host {
333 addr,
334 num_procs,
335 system_children,
336 memory,
337 } => Self::Host {
338 addr,
339 num_procs,
340 system_children: refs_to_strings(&system_children),
341 memory: ProcessMemoryStatsDto {
342 process_rss_bytes: memory.process_rss_bytes,
343 process_vm_size_bytes: memory.process_vm_size_bytes,
344 },
345 },
346 NodeProperties::Proc {
347 proc_name,
348 num_actors,
349 system_children,
350 stopped_children,
351 stopped_retention_cap,
352 is_poisoned,
353 failed_actor_count,
354 debug,
355 } => Self::Proc {
356 proc_name,
357 num_actors,
358 system_children: refs_to_strings(&system_children),
359 stopped_children: refs_to_strings(&stopped_children),
360 stopped_retention_cap,
361 is_poisoned,
362 failed_actor_count,
363 debug: ProcDebugStatsDto {
364 memory: ProcessMemoryStatsDto {
365 process_rss_bytes: debug.memory.process_rss_bytes,
366 process_vm_size_bytes: debug.memory.process_vm_size_bytes,
367 },
368 actor_work_queue_depth_total: debug.actor_work_queue_depth_total,
369 actor_work_queue_depth_max: debug.actor_work_queue_depth_max,
370 actor_work_queue_depth_high_water_mark: debug
371 .actor_work_queue_depth_high_water_mark,
372 last_nonzero_queue_depth_age_ms: debug.last_nonzero_queue_depth_age_ms,
373 },
374 },
375 NodeProperties::Actor {
376 actor_status,
377 actor_type,
378 instance_id,
379 messages_processed,
380 created_at,
381 last_message_handler,
382 total_processing_time_us,
383 queue_depth,
384 flight_recorder,
385 is_system,
386 inbound_ordering,
387 failure_info,
388 execution,
389 } => Self::Actor {
390 actor_status,
391 actor_type,
392 instance_id,
393 messages_processed,
394 created_at: created_at.as_ref().map(format_time),
395 last_message_handler,
396 total_processing_time_us,
397 queue_depth,
398 flight_recorder,
399 is_system,
400 inbound_ordering: inbound_ordering
401 .map(|io| Box::new(InboundOrderingDto::from(*io))),
402 failure_info: failure_info.map(Into::into),
403 execution: execution.map(|e| Box::new(ExecutionDto::from(*e))),
404 },
405 NodeProperties::Error { code, message } => Self::Error { code, message },
406 }
407 }
408}
409
410impl From<FailureInfo> for FailureInfoDto {
411 fn from(f: FailureInfo) -> Self {
412 Self {
413 error_message: f.error_message,
414 root_cause_actor: f.root_cause_actor.to_string(),
415 root_cause_name: f.root_cause_name,
416 occurred_at: format_time(&f.occurred_at),
417 is_propagated: f.is_propagated,
418 }
419 }
420}
421
422impl From<hyperactor::ordering::OrderingSessionSnapshot> for OrderingSessionSnapshotDto {
423 fn from(s: hyperactor::ordering::OrderingSessionSnapshot) -> Self {
424 Self {
425 session_id: s.session_id.to_string(),
426 sender: s.sender.as_ref().map(|a| a.to_string()),
427 last_released_seq: s.last_released_seq,
428 expected_next_seq: s.expected_next_seq,
429 buffered_count: s.buffered_count,
430 oldest_buffered_seq: s.oldest_buffered_seq,
431 newest_buffered_seq: s.newest_buffered_seq,
432 }
433 }
434}
435
436impl From<InboundOrdering> for InboundOrderingDto {
437 fn from(o: InboundOrdering) -> Self {
438 Self {
439 enabled: o.enabled,
440 snapshot_complete: o.snapshot_complete,
441 skipped_session_count: o.skipped_session_count,
442 known_session_count: o.known_session_count,
443 returned_buffered_session_count: o.returned_buffered_session_count,
444 returned_buffered_message_count: o.returned_buffered_message_count,
445 returned_max_buffered_count: o.returned_max_buffered_count,
446 sessions: o.sessions.into_iter().map(Into::into).collect(),
447 }
448 }
449}
450
451impl From<Execution> for ExecutionDto {
452 fn from(e: Execution) -> Self {
453 Self {
454 active_count: e.active_count,
455 active_handlers: e.active_handlers.into_iter().map(Into::into).collect(),
456 complete: e.complete,
457 truncated: e.truncated,
458 }
459 }
460}
461
462impl From<ActiveHandler> for ActiveHandlerDto {
463 fn from(h: ActiveHandler) -> Self {
464 Self {
465 name: h.name,
466 active_count: h.active_count,
467 oldest_since: format_time(&h.oldest_since),
468 }
469 }
470}
471
472impl TryFrom<NodePayloadDto> for NodePayload {
475 type Error = anyhow::Error;
476
477 fn try_from(dto: NodePayloadDto) -> Result<Self, Self::Error> {
478 Ok(Self {
479 identity: dto
480 .identity
481 .parse()
482 .with_context(|| format!("failed to parse identity: {:?}", dto.identity))?,
483 properties: dto
484 .properties
485 .try_into()
486 .context("failed to parse properties")?,
487 children: parse_refs("children", &dto.children)?,
488 parent: dto
489 .parent
490 .map(|s| {
491 s.parse()
492 .with_context(|| format!("failed to parse parent: {s:?}"))
493 })
494 .transpose()?,
495 as_of: humantime::parse_rfc3339(&dto.as_of)
496 .with_context(|| format!("failed to parse as_of: {:?}", dto.as_of))?,
497 })
498 }
499}
500
501impl TryFrom<NodePropertiesDto> for NodeProperties {
502 type Error = anyhow::Error;
503
504 fn try_from(
505 dto: NodePropertiesDto,
506 ) -> Result<Self, <Self as TryFrom<NodePropertiesDto>>::Error> {
507 Ok(match dto {
508 NodePropertiesDto::Root {
509 num_hosts,
510 started_at,
511 started_by,
512 system_children,
513 } => Self::Root {
514 num_hosts,
515 started_at: humantime::parse_rfc3339(&started_at)
516 .context("failed to parse Root.started_at")?,
517 started_by,
518 system_children: parse_refs("Root.system_children", &system_children)?,
519 },
520 NodePropertiesDto::Host {
521 addr,
522 num_procs,
523 system_children,
524 memory,
525 } => Self::Host {
526 addr,
527 num_procs,
528 system_children: parse_refs("Host.system_children", &system_children)?,
529 memory: super::ProcessMemoryStats {
530 process_rss_bytes: memory.process_rss_bytes,
531 process_vm_size_bytes: memory.process_vm_size_bytes,
532 },
533 },
534 NodePropertiesDto::Proc {
535 proc_name,
536 num_actors,
537 system_children,
538 stopped_children,
539 stopped_retention_cap,
540 is_poisoned,
541 failed_actor_count,
542 debug,
543 } => Self::Proc {
544 proc_name,
545 num_actors,
546 system_children: parse_refs("Proc.system_children", &system_children)?,
547 stopped_children: parse_refs("Proc.stopped_children", &stopped_children)?,
548 stopped_retention_cap,
549 is_poisoned,
550 failed_actor_count,
551 debug: super::ProcDebugStats {
552 memory: super::ProcessMemoryStats {
553 process_rss_bytes: debug.memory.process_rss_bytes,
554 process_vm_size_bytes: debug.memory.process_vm_size_bytes,
555 },
556 actor_work_queue_depth_total: debug.actor_work_queue_depth_total,
557 actor_work_queue_depth_max: debug.actor_work_queue_depth_max,
558 actor_work_queue_depth_high_water_mark: debug
559 .actor_work_queue_depth_high_water_mark,
560 last_nonzero_queue_depth_age_ms: debug.last_nonzero_queue_depth_age_ms,
561 },
562 },
563 NodePropertiesDto::Actor {
564 actor_status,
565 actor_type,
566 instance_id,
567 messages_processed,
568 created_at,
569 last_message_handler,
570 total_processing_time_us,
571 queue_depth,
572 flight_recorder,
573 is_system,
574 inbound_ordering,
575 failure_info,
576 execution,
577 } => Self::Actor {
578 actor_status,
579 actor_type,
580 instance_id,
581 messages_processed,
582 created_at: created_at
583 .map(|s| {
584 humantime::parse_rfc3339(&s)
585 .with_context(|| format!("failed to parse Actor.created_at: {s:?}"))
586 })
587 .transpose()?,
588 last_message_handler,
589 total_processing_time_us,
590 queue_depth,
591 flight_recorder,
592 is_system,
593 inbound_ordering: inbound_ordering
594 .map(|dto| InboundOrdering::try_from(*dto).map(Box::new))
595 .transpose()
596 .context("failed to parse Actor.inbound_ordering")?,
597 failure_info: failure_info
598 .map(TryInto::try_into)
599 .transpose()
600 .context("failed to parse Actor.failure_info")?,
601 execution: execution
602 .map(|dto| Execution::try_from(*dto).map(Box::new))
603 .transpose()
604 .context("failed to parse Actor.execution")?,
605 },
606 NodePropertiesDto::Error { code, message } => Self::Error { code, message },
607 })
608 }
609}
610
611impl TryFrom<FailureInfoDto> for FailureInfo {
612 type Error = anyhow::Error;
613
614 fn try_from(dto: FailureInfoDto) -> Result<Self, Self::Error> {
615 Ok(Self {
616 error_message: dto.error_message,
617 root_cause_actor: dto.root_cause_actor.parse().with_context(|| {
618 format!(
619 "failed to parse FailureInfo.root_cause_actor: {:?}",
620 dto.root_cause_actor
621 )
622 })?,
623 root_cause_name: dto.root_cause_name,
624 occurred_at: humantime::parse_rfc3339(&dto.occurred_at).with_context(|| {
625 format!(
626 "failed to parse FailureInfo.occurred_at: {:?}",
627 dto.occurred_at
628 )
629 })?,
630 is_propagated: dto.is_propagated,
631 })
632 }
633}
634
635impl TryFrom<ExecutionDto> for Execution {
636 type Error = anyhow::Error;
637
638 fn try_from(dto: ExecutionDto) -> Result<Self, Self::Error> {
639 Ok(Self {
640 active_count: dto.active_count,
641 active_handlers: dto
642 .active_handlers
643 .into_iter()
644 .map(ActiveHandler::try_from)
645 .collect::<Result<Vec<_>, _>>()?,
646 complete: dto.complete,
647 truncated: dto.truncated,
648 })
649 }
650}
651
652impl TryFrom<ActiveHandlerDto> for ActiveHandler {
653 type Error = anyhow::Error;
654
655 fn try_from(dto: ActiveHandlerDto) -> Result<Self, Self::Error> {
656 Ok(Self {
657 name: dto.name,
658 active_count: dto.active_count,
659 oldest_since: humantime::parse_rfc3339(&dto.oldest_since).with_context(|| {
660 format!(
661 "failed to parse ActiveHandler.oldest_since: {:?}",
662 dto.oldest_since
663 )
664 })?,
665 })
666 }
667}
668
669impl TryFrom<OrderingSessionSnapshotDto> for hyperactor::ordering::OrderingSessionSnapshot {
670 type Error = anyhow::Error;
671
672 fn try_from(dto: OrderingSessionSnapshotDto) -> Result<Self, Self::Error> {
673 let session_id = dto.session_id.parse().with_context(|| {
674 format!(
675 "failed to parse OrderingSessionSnapshot.session_id: {:?}",
676 dto.session_id
677 )
678 })?;
679 let sender = dto
680 .sender
681 .as_ref()
682 .map(|s| {
683 s.parse().with_context(|| {
684 format!("failed to parse OrderingSessionSnapshot.sender: {s:?}")
685 })
686 })
687 .transpose()?;
688 Ok(Self {
689 session_id,
690 sender,
691 last_released_seq: dto.last_released_seq,
692 expected_next_seq: dto.expected_next_seq,
693 buffered_count: dto.buffered_count,
694 oldest_buffered_seq: dto.oldest_buffered_seq,
695 newest_buffered_seq: dto.newest_buffered_seq,
696 })
697 }
698}
699
700impl TryFrom<InboundOrderingDto> for InboundOrdering {
701 type Error = anyhow::Error;
702
703 fn try_from(dto: InboundOrderingDto) -> Result<Self, Self::Error> {
704 let sessions: Vec<hyperactor::ordering::OrderingSessionSnapshot> = dto
705 .sessions
706 .into_iter()
707 .enumerate()
708 .map(|(i, s)| {
709 s.try_into()
710 .with_context(|| format!("failed to parse InboundOrdering.sessions[{i}]"))
711 })
712 .collect::<Result<_, _>>()?;
713 Ok(Self {
714 enabled: dto.enabled,
715 snapshot_complete: dto.snapshot_complete,
716 skipped_session_count: dto.skipped_session_count,
717 known_session_count: dto.known_session_count,
718 returned_buffered_session_count: dto.returned_buffered_session_count,
719 returned_buffered_message_count: dto.returned_buffered_message_count,
720 returned_max_buffered_count: dto.returned_max_buffered_count,
721 sessions,
722 })
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use crate::mesh_id::ResourceId;
730
731 fn test_proc_id() -> hyperactor::ProcAddr {
734 ResourceId::proc_addr_from_name(hyperactor::channel::ChannelAddr::Local(0), "worker")
735 }
736
737 fn test_actor_id() -> hyperactor::ActorAddr {
738 test_proc_id().actor_addr("actor")
739 }
740
741 fn test_host_actor_id() -> hyperactor::ActorAddr {
742 test_proc_id().actor_addr("host_agent")
743 }
744
745 fn test_time() -> SystemTime {
746 humantime::parse_rfc3339("2025-01-15T10:30:00.123Z").unwrap()
747 }
748
749 fn test_time_2() -> SystemTime {
750 humantime::parse_rfc3339("2025-01-15T11:00:00.456Z").unwrap()
751 }
752
753 fn make_root_payload() -> NodePayload {
754 NodePayload {
755 identity: NodeRef::Root,
756 properties: NodeProperties::Root {
757 num_hosts: 2,
758 started_at: test_time(),
759 started_by: "test_user".to_string(),
760 system_children: vec![],
761 },
762 children: vec![NodeRef::Host(test_host_actor_id())],
763 parent: None,
764 as_of: test_time(),
765 }
766 }
767
768 fn make_host_payload() -> NodePayload {
769 NodePayload {
770 identity: NodeRef::Host(test_host_actor_id()),
771 properties: NodeProperties::Host {
772 addr: "127.0.0.1:8080".to_string(),
773 num_procs: 1,
774 system_children: vec![],
775 memory: Default::default(),
776 },
777 children: vec![NodeRef::Proc(test_proc_id())],
778 parent: Some(NodeRef::Root),
779 as_of: test_time(),
780 }
781 }
782
783 fn make_proc_payload() -> NodePayload {
784 NodePayload {
785 identity: NodeRef::Proc(test_proc_id()),
786 properties: NodeProperties::Proc {
787 proc_name: "worker".to_string(),
788 num_actors: 3,
789 system_children: vec![NodeRef::Actor(test_actor_id())],
790 stopped_children: vec![],
791 stopped_retention_cap: 100,
792 is_poisoned: false,
793 failed_actor_count: 0,
794 debug: Default::default(),
795 },
796 children: vec![NodeRef::Actor(test_actor_id())],
797 parent: Some(NodeRef::Host(test_host_actor_id())),
798 as_of: test_time(),
799 }
800 }
801
802 fn test_instance_id() -> String {
803 "01900000-0000-7000-8000-000000000001".to_string()
805 }
806
807 fn make_actor_payload_no_failure() -> NodePayload {
808 NodePayload {
809 identity: NodeRef::Actor(test_actor_id()),
810 properties: NodeProperties::Actor {
811 actor_status: "running".to_string(),
812 actor_type: "MyActor".to_string(),
813 instance_id: test_instance_id(),
814 messages_processed: 42,
815 created_at: Some(test_time()),
816 last_message_handler: Some("handle_msg".to_string()),
817 total_processing_time_us: 1500,
818 queue_depth: 0,
819 flight_recorder: None,
820 is_system: false,
821 inbound_ordering: None,
822 failure_info: None,
823 execution: None,
824 },
825 children: vec![],
826 parent: Some(NodeRef::Proc(test_proc_id())),
827 as_of: test_time(),
828 }
829 }
830
831 fn make_actor_payload_with_failure() -> NodePayload {
832 NodePayload {
833 identity: NodeRef::Actor(test_actor_id()),
834 properties: NodeProperties::Actor {
835 actor_status: "failed".to_string(),
836 actor_type: "MyActor".to_string(),
837 instance_id: test_instance_id(),
838 messages_processed: 10,
839 created_at: Some(test_time()),
840 last_message_handler: None,
841 total_processing_time_us: 500,
842 queue_depth: 0,
843 flight_recorder: Some("trace-abc".to_string()),
844 is_system: true,
845 inbound_ordering: None,
846 failure_info: Some(FailureInfo {
847 error_message: "boom".to_string(),
848 root_cause_actor: test_actor_id(),
849 root_cause_name: Some("root_actor".to_string()),
850 occurred_at: test_time_2(),
851 is_propagated: true,
852 }),
853 execution: None,
854 },
855 children: vec![],
856 parent: Some(NodeRef::Proc(test_proc_id())),
857 as_of: test_time(),
858 }
859 }
860
861 fn make_actor_payload_minimal() -> NodePayload {
862 NodePayload {
863 identity: NodeRef::Actor(test_actor_id()),
864 properties: NodeProperties::Actor {
865 actor_status: "idle".to_string(),
866 actor_type: "MinimalActor".to_string(),
867 instance_id: test_instance_id(),
868 messages_processed: 0,
869 created_at: None,
870 last_message_handler: None,
871 total_processing_time_us: 0,
872 queue_depth: 0,
873 flight_recorder: None,
874 is_system: false,
875 inbound_ordering: None,
876 failure_info: None,
877 execution: None,
878 },
879 children: vec![],
880 parent: Some(NodeRef::Proc(test_proc_id())),
881 as_of: test_time(),
882 }
883 }
884
885 fn make_ordering_session(
886 session_id: uuid::Uuid,
887 last_released_seq: u64,
888 buffered_count: usize,
889 ) -> hyperactor::ordering::OrderingSessionSnapshot {
890 let (oldest, newest) = if buffered_count > 0 {
891 (
892 Some(last_released_seq + 2),
893 Some(last_released_seq + 1 + buffered_count as u64),
894 )
895 } else {
896 (None, None)
897 };
898 hyperactor::ordering::OrderingSessionSnapshot {
899 session_id,
900 sender: Some(test_actor_id()),
901 last_released_seq,
902 expected_next_seq: last_released_seq.saturating_add(1),
903 buffered_count,
904 oldest_buffered_seq: oldest,
905 newest_buffered_seq: newest,
906 }
907 }
908
909 fn make_actor_payload_inbound_ordering_complete() -> NodePayload {
910 NodePayload {
911 identity: NodeRef::Actor(test_actor_id()),
912 properties: NodeProperties::Actor {
913 actor_status: "running".to_string(),
914 actor_type: "MyActor".to_string(),
915 instance_id: test_instance_id(),
916 messages_processed: 17,
917 created_at: Some(test_time()),
918 last_message_handler: Some("handle_msg".to_string()),
919 total_processing_time_us: 900,
920 queue_depth: 5,
921 flight_recorder: None,
922 is_system: false,
923 inbound_ordering: Some(Box::new(InboundOrdering {
924 enabled: true,
925 snapshot_complete: true,
926 skipped_session_count: 0,
927 known_session_count: 2,
928 returned_buffered_session_count: 1,
929 returned_buffered_message_count: 3,
930 returned_max_buffered_count: 3,
931 sessions: vec![
932 make_ordering_session(uuid::Uuid::from_u128(1), 7, 0),
933 make_ordering_session(uuid::Uuid::from_u128(2), 1, 3),
934 ],
935 })),
936 failure_info: None,
937 execution: None,
938 },
939 children: vec![],
940 parent: Some(NodeRef::Proc(test_proc_id())),
941 as_of: test_time(),
942 }
943 }
944
945 fn make_actor_payload_inbound_ordering_unavailable() -> NodePayload {
946 NodePayload {
947 identity: NodeRef::Actor(test_actor_id()),
948 properties: NodeProperties::Actor {
949 actor_status: "running".to_string(),
950 actor_type: "MyActor".to_string(),
951 instance_id: test_instance_id(),
952 messages_processed: 17,
953 created_at: Some(test_time()),
954 last_message_handler: Some("handle_msg".to_string()),
955 total_processing_time_us: 900,
956 queue_depth: 5,
957 flight_recorder: None,
958 is_system: false,
959 inbound_ordering: Some(Box::new(InboundOrdering {
960 enabled: true,
961 snapshot_complete: false,
962 skipped_session_count: 1,
963 known_session_count: 1,
964 returned_buffered_session_count: 0,
965 returned_buffered_message_count: 0,
966 returned_max_buffered_count: 0,
967 sessions: vec![],
968 })),
969 failure_info: None,
970 execution: None,
971 },
972 children: vec![],
973 parent: Some(NodeRef::Proc(test_proc_id())),
974 as_of: test_time(),
975 }
976 }
977
978 fn make_error_payload() -> NodePayload {
979 NodePayload {
980 identity: NodeRef::Actor(test_actor_id()),
981 properties: NodeProperties::Error {
982 code: "not_found".to_string(),
983 message: "actor not found".to_string(),
984 },
985 children: vec![],
986 parent: None,
987 as_of: test_time(),
988 }
989 }
990
991 fn assert_round_trip(payload: &NodePayload) {
995 let dto: NodePayloadDto = payload.clone().into();
996 let back: NodePayload = dto.try_into().expect("round-trip conversion");
997 assert_eq!(payload, &back);
998 }
999
1000 #[test]
1002 fn test_round_trip_root() {
1003 assert_round_trip(&make_root_payload());
1004 }
1005
1006 #[test]
1008 fn test_round_trip_host() {
1009 assert_round_trip(&make_host_payload());
1010 }
1011
1012 #[test]
1014 fn test_round_trip_proc() {
1015 assert_round_trip(&make_proc_payload());
1016 }
1017
1018 #[test]
1020 fn test_round_trip_actor_no_failure() {
1021 assert_round_trip(&make_actor_payload_no_failure());
1022 }
1023
1024 #[test]
1026 fn test_round_trip_actor_with_failure() {
1027 assert_round_trip(&make_actor_payload_with_failure());
1028 }
1029
1030 #[test]
1032 fn test_round_trip_actor_minimal() {
1033 assert_round_trip(&make_actor_payload_minimal());
1034 }
1035
1036 #[test]
1042 fn test_round_trip_actor_inbound_ordering_complete() {
1043 let payload = make_actor_payload_inbound_ordering_complete();
1044 if let NodeProperties::Actor {
1048 inbound_ordering: Some(io),
1049 ..
1050 } = &payload.properties
1051 {
1052 assert_eq!(io.snapshot_complete, io.skipped_session_count == 0); assert_eq!(io.known_session_count, io.sessions.len()); } else {
1055 panic!("fixture must be Actor with Some(inbound_ordering)");
1056 }
1057 assert_round_trip(&payload);
1058 }
1059
1060 #[test]
1063 fn test_round_trip_actor_inbound_ordering_unavailable() {
1064 let payload = make_actor_payload_inbound_ordering_unavailable();
1065 if let NodeProperties::Actor {
1066 inbound_ordering: Some(io),
1067 ..
1068 } = &payload.properties
1069 {
1070 assert!(!io.snapshot_complete);
1071 assert_eq!(io.snapshot_complete, io.skipped_session_count == 0);
1072 } else {
1073 panic!("fixture must be Actor with Some(inbound_ordering)");
1074 }
1075 assert_round_trip(&payload);
1076 }
1077
1078 #[test]
1080 fn test_round_trip_error() {
1081 assert_round_trip(&make_error_payload());
1082 }
1083
1084 #[test]
1090 fn test_json_shape_root() {
1091 let dto: NodePayloadDto = make_root_payload().into();
1092 let json = serde_json::to_value(&dto).unwrap();
1093
1094 assert_eq!(json["identity"], "root");
1095 assert!(json["parent"].is_null());
1096 assert_eq!(json["as_of"], "2025-01-15T10:30:00.123Z");
1097
1098 let children = json["children"].as_array().unwrap();
1099 assert_eq!(children.len(), 1);
1100 assert_eq!(children[0], format!("host:{}", test_host_actor_id()));
1101
1102 let root = &json["properties"]["Root"];
1103 assert_eq!(root["num_hosts"], 2);
1104 assert_eq!(root["started_at"], "2025-01-15T10:30:00.123Z");
1105 assert_eq!(root["started_by"], "test_user");
1106 assert!(root["system_children"].as_array().unwrap().is_empty());
1107 }
1108
1109 #[test]
1112 fn test_json_shape_actor_with_failure() {
1113 let dto: NodePayloadDto = make_actor_payload_with_failure().into();
1114 let json = serde_json::to_value(&dto).unwrap();
1115
1116 assert_eq!(json["identity"], test_actor_id().to_string());
1117 assert_eq!(json["parent"], test_proc_id().to_string());
1118
1119 let actor = &json["properties"]["Actor"];
1120 assert_eq!(actor["actor_status"], "failed");
1121 assert_eq!(actor["messages_processed"], 10);
1122 assert_eq!(actor["created_at"], "2025-01-15T10:30:00.123Z");
1123 assert!(actor["last_message_handler"].is_null());
1124 assert_eq!(actor["flight_recorder"], "trace-abc");
1125 assert_eq!(actor["is_system"], true);
1126
1127 let fi = &actor["failure_info"];
1128 assert_eq!(fi["error_message"], "boom");
1129 assert_eq!(fi["root_cause_actor"], test_actor_id().to_string());
1130 assert_eq!(fi["root_cause_name"], "root_actor");
1131 assert_eq!(fi["occurred_at"], "2025-01-15T11:00:00.456Z");
1132 assert_eq!(fi["is_propagated"], true);
1133 }
1134
1135 #[test]
1137 fn test_json_shape_optional_none_fields() {
1138 let dto: NodePayloadDto = make_actor_payload_minimal().into();
1139 let json = serde_json::to_value(&dto).unwrap();
1140
1141 let actor = &json["properties"]["Actor"];
1142 assert!(actor["created_at"].is_null());
1143 assert!(actor["last_message_handler"].is_null());
1144 assert!(actor["flight_recorder"].is_null());
1145 assert!(actor["failure_info"].is_null());
1146 }
1147
1148 #[test]
1150 fn test_json_shape_error() {
1151 let dto: NodePayloadDto = make_error_payload().into();
1152 let json = serde_json::to_value(&dto).unwrap();
1153
1154 let err = &json["properties"]["Error"];
1155 assert_eq!(err["code"], "not_found");
1156 assert_eq!(err["message"], "actor not found");
1157 }
1158
1159 #[test]
1161 fn test_json_shape_empty_children() {
1162 let dto: NodePayloadDto = make_actor_payload_no_failure().into();
1163 let json = serde_json::to_value(&dto).unwrap();
1164 assert!(json["children"].as_array().unwrap().is_empty());
1165 }
1166
1167 #[test]
1174 fn test_schema_defs_keys() {
1175 let schema = schemars::schema_for!(NodePayloadDto);
1176 let json = serde_json::to_value(&schema).unwrap();
1177 let defs = json["$defs"].as_object().unwrap();
1178 assert!(
1179 defs.contains_key("NodeProperties"),
1180 "$defs must contain 'NodeProperties', got: {:?}",
1181 defs.keys().collect::<Vec<_>>()
1182 );
1183 assert!(
1184 defs.contains_key("FailureInfo"),
1185 "$defs must contain 'FailureInfo', got: {:?}",
1186 defs.keys().collect::<Vec<_>>()
1187 );
1188 }
1189
1190 #[test]
1193 fn test_schema_title() {
1194 let schema = schemars::schema_for!(NodePayloadDto);
1195 let json = serde_json::to_value(&schema).unwrap();
1196 assert_eq!(json["title"], "NodePayload");
1197 }
1198}