nros_core/service_info.rs
1//! Per-request metadata handed to a service callback — rclrs's `ServiceInfo`.
2//!
3//! Issue 1942. rclrs 0.7 can pass a service callback a `ServiceInfo` beside the
4//! request: who sent it (the client's GID and its sequence number, together
5//! the upstream `rmw_request_id_t`) and when (source and received timestamps).
6//! This is the counterpart, registered through `Node::create_service_with_info`
7//! or `node.service(name).service_info()`.
8//!
9//! **Not to be confused with `nros_rmw::ServiceInfo`**, the RMW seam's service
10//! DESCRIPTOR (name, type, hash, domain). That type is not re-exported by the
11//! `nros` facade (RFC-0071 D12, phase-506 W1), which is what lets this one take
12//! rclrs's name there.
13//!
14//! # What a backend reports
15//!
16//! Every field is an `Option`, because whether a request carries it is a
17//! BACKEND property, and a backend that cannot say leaves the field `None`
18//! rather than inventing a value — the `MessageInfo` precedent, where a backend
19//! that reports no metadata hands the callback `None` instead of a zeroed
20//! struct. rclrs's fields are not optional; ours are, and that is the one
21//! shape divergence.
22//!
23//! | backend | `request_id` | `source_timestamp` | `received_timestamp` |
24//! | --- | --- | --- | --- |
25//! | zenoh | from the query attachment (`rmw_zenoh_cpp` layout) | from the attachment | the queryable callback's wall-clock stamp |
26//! | Cyclone DDS | request header guid + publication handle, header seq (stock `rmw_cyclonedds_cpp`'s layout) | the DDS sample's source timestamp | `None` (Cyclone 0.10 has none) |
27//! | XRCE-DDS | the Agent's `SampleIdentity` (client writer GUID + sequence) | `None` | the request callback's wall-clock stamp |
28//! | a C-ABI backend without `take_request_with_info` | `None` | `None` | `None` |
29//!
30//! `received_timestamp` is upstream's: the server's wall clock (ns since the
31//! Unix epoch) when the request arrived (issue 2135); `None` on a platform
32//! with no wall clock.
33//!
34//! A C-ABI backend reports through the vtable's `take_request_with_info` slot
35//! (issue 2081), whose `rmw_service_info_t` spells "not reported" as zero; the
36//! runtime turns each zero into `None`.
37
38use crate::Time;
39
40/// Width of [`RequestId::writer_guid`], in bytes.
41///
42/// 16, which is upstream's `rmw_request_id_t::writer_guid` (`int8_t[16]`) and
43/// rclrs's `RequestId::writer_guid: [u8; 16]`. It is NOT
44/// [`PUBLISHER_GID_SIZE`](crate::PUBLISHER_GID_SIZE) (24, `RMW_GID_STORAGE_SIZE`):
45/// upstream keeps the two widths apart, and so does this.
46pub const REQUEST_WRITER_GUID_SIZE: usize = 16;
47
48/// Identity of one service request — rclrs's `RequestId`, upstream's
49/// `rmw_request_id_t`.
50///
51/// `sequence_number` is the number the CLIENT assigned to the request, which
52/// is what makes `(writer_guid, sequence_number)` unique across clients. It is
53/// not necessarily the token a backend replies against: the zenoh backend
54/// replies against a local reply-slot index, and that index is never reported
55/// here.
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
57pub struct RequestId {
58 /// The requesting client's GID. On zenoh this is XXH3-128 of the client's
59 /// liveliness keyexpr — `rmw_zenoh_cpp`'s derivation, so it equals the gid
60 /// that client's `get_gid` reports, ours or a stock peer's.
61 pub writer_guid: [u8; REQUEST_WRITER_GUID_SIZE],
62 /// The client-assigned sequence number of this request.
63 pub sequence_number: i64,
64}
65
66/// Metadata about one received service request — rclrs's `ServiceInfo`.
67///
68/// See the [module docs](self) for which backend fills which field. A field a
69/// backend cannot report is `None`; it is never a zero standing in for one.
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub struct ServiceInfo {
72 /// When the client sent the request, on the CLIENT's clock.
73 pub source_timestamp: Option<Time>,
74 /// When this server received the request, on the server's clock.
75 pub received_timestamp: Option<Time>,
76 /// Who sent the request: client GID plus client sequence number.
77 pub request_id: Option<RequestId>,
78}
79
80impl ServiceInfo {
81 /// Nothing reported — what a backend with no per-request metadata hands
82 /// every callback.
83 pub const NOT_REPORTED: Self = Self {
84 source_timestamp: None,
85 received_timestamp: None,
86 request_id: None,
87 };
88
89 /// Whether the backend reported ANY field for this request.
90 ///
91 /// The Rust spelling of the C++ `MessageInfo::is_reported()`: `false` means
92 /// "this backend said nothing", which is a different statement from any
93 /// particular field value.
94 pub const fn is_reported(&self) -> bool {
95 self.source_timestamp.is_some()
96 || self.received_timestamp.is_some()
97 || self.request_id.is_some()
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn not_reported_is_the_default_and_reports_nothing() {
107 assert_eq!(ServiceInfo::default(), ServiceInfo::NOT_REPORTED);
108 assert!(!ServiceInfo::NOT_REPORTED.is_reported());
109 }
110
111 #[test]
112 fn any_one_field_makes_it_reported() {
113 let only_id = ServiceInfo {
114 request_id: Some(RequestId {
115 writer_guid: [7; REQUEST_WRITER_GUID_SIZE],
116 sequence_number: 3,
117 }),
118 ..ServiceInfo::NOT_REPORTED
119 };
120 assert!(only_id.is_reported());
121 let only_ts = ServiceInfo {
122 source_timestamp: Some(Time::new(1, 2)),
123 ..ServiceInfo::NOT_REPORTED
124 };
125 assert!(only_ts.is_reported());
126 }
127
128 /// Upstream `rmw_request_id_t::writer_guid` is `int8_t[16]`; rclrs keeps
129 /// `[u8; 16]`. Not the 24-byte gid storage size.
130 #[test]
131 fn writer_guid_is_upstreams_request_id_width() {
132 assert_eq!(REQUEST_WRITER_GUID_SIZE, 16);
133 }
134}