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