simple_someip/capacity.rs
1//! Names the fixed-capacity internal structures whose exhaustion is
2//! reportable through the `Capacity` variant of `client::Error` and
3//! `server::Error`.
4//!
5//! Those are deliberately not intra-doc links: both modules are behind
6//! features, and this one is compiled unconditionally, so linking them
7//! fails the `--no-default-features` and single-feature doc builds.
8//!
9//! This lives at the crate root rather than under `client` or `server`
10//! because both report through it, and a consumer handling capacity
11//! exhaustion should not have to learn two different vocabularies for the
12//! same condition. Not every kind is reachable from every error type —
13//! the server currently only produces [`CapacityKind::UdpBuffer`] — and
14//! that asymmetry is deliberate: one shared kind keeps the handling code
15//! uniform as the server grows others.
16
17use core::fmt;
18
19/// Which fixed-capacity internal structure overflowed.
20///
21/// Each variant names the compile-time constant that governs it, so the
22/// bound is reachable from the type rather than by grepping for a string
23/// tag.
24///
25/// `#[non_exhaustive]`: new kinds appear as internal structures gain
26/// bounds, and that should not break a downstream `match`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum CapacityKind {
30 /// Bound by `UNICAST_SOCKETS_CAP`. The client cannot bind a new
31 /// ephemeral / requested-port unicast socket because the per-client
32 /// cap is exhausted.
33 UnicastSockets,
34
35 /// Bound by [`crate::UDP_BUFFER_SIZE`]. An outgoing message was
36 /// rejected because the encoded form exceeds the application-level
37 /// UDP cap.
38 ///
39 /// With E2E protect configured for the destination key, the
40 /// post-protect payload may add up to the protect profile's overhead
41 /// bytes (Profile 1: 4, Profile 4: 16). The pre-encode check uses the
42 /// raw size; the post-protect re-check inside the spawned send loop
43 /// produces this kind if the protected datagram would overflow the
44 /// cap.
45 ///
46 /// The only kind the server currently produces, where it means a
47 /// stack send buffer smaller than the outgoing message.
48 UdpBuffer,
49
50 /// Bound by `PENDING_RESPONSES_CAP`. A request was enqueued but the
51 /// in-flight response table is full; the request was dropped.
52 PendingResponses,
53
54 /// Bound by `REQUEST_QUEUE_CAP`. The client's internal control-message
55 /// queue overflowed during a multi-pass `push_front` re-enqueue (e.g.
56 /// an auto-bind path).
57 ///
58 /// Public callers normally hit the bounded(4) control channel first
59 /// and either backpressure or fail with `Shutdown`; this kind fires
60 /// only in the narrow re-enqueue overflow window.
61 RequestQueue,
62
63 /// Bound by `SERVICE_REGISTRY_CAP`. A new service-instance endpoint
64 /// cannot be registered because the registry is full.
65 ServiceRegistry,
66}
67
68impl CapacityKind {
69 /// The `snake_case` tag for this kind.
70 ///
71 /// These are the exact strings the pre-0.12.0 `Capacity(&'static str)`
72 /// variant carried, so log output and anything scraping it are
73 /// unchanged by the move to a typed kind.
74 #[must_use]
75 pub const fn as_str(self) -> &'static str {
76 match self {
77 Self::UnicastSockets => "unicast_sockets",
78 Self::UdpBuffer => "udp_buffer",
79 Self::PendingResponses => "pending_responses",
80 Self::RequestQueue => "request_queue",
81 Self::ServiceRegistry => "service_registry",
82 }
83 }
84}
85
86impl fmt::Display for CapacityKind {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(self.as_str())
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 /// The whole point of `as_str` is that the `Display` output of a
97 /// `Capacity` error is byte-identical to what the `&'static str`
98 /// variant produced. Pin the exact strings — a rename here is a
99 /// silent behavior change for anyone matching on log text, which is
100 /// precisely the fragile pattern this type exists to let them stop
101 /// doing.
102 #[test]
103 fn tags_match_the_pre_0_11_string_literals() {
104 assert_eq!(CapacityKind::UnicastSockets.as_str(), "unicast_sockets");
105 assert_eq!(CapacityKind::UdpBuffer.as_str(), "udp_buffer");
106 assert_eq!(CapacityKind::PendingResponses.as_str(), "pending_responses");
107 assert_eq!(CapacityKind::RequestQueue.as_str(), "request_queue");
108 assert_eq!(CapacityKind::ServiceRegistry.as_str(), "service_registry");
109 }
110
111 /// `Display` must delegate to `as_str` verbatim: both error enums
112 /// format the variant as `"internal capacity exceeded: {0}"`, so any
113 /// divergence changes the rendered error.
114 ///
115 /// Formats through [`core::fmt::Write`] into a fixed buffer rather
116 /// than `to_string()`, so this test runs in the `--no-default-features`
117 /// configuration too — where there is no allocator.
118 #[test]
119 fn display_delegates_to_as_str() {
120 use core::fmt::Write as _;
121
122 /// Writes into a fixed buffer; the longest tag is
123 /// `"pending_responses"` at 17 bytes.
124 struct Buf {
125 bytes: [u8; 32],
126 len: usize,
127 }
128
129 impl core::fmt::Write for Buf {
130 fn write_str(&mut self, s: &str) -> core::fmt::Result {
131 let end = self.len + s.len();
132 if end > self.bytes.len() {
133 return Err(core::fmt::Error);
134 }
135 self.bytes[self.len..end].copy_from_slice(s.as_bytes());
136 self.len = end;
137 Ok(())
138 }
139 }
140
141 for kind in [
142 CapacityKind::UnicastSockets,
143 CapacityKind::UdpBuffer,
144 CapacityKind::PendingResponses,
145 CapacityKind::RequestQueue,
146 CapacityKind::ServiceRegistry,
147 ] {
148 let mut buf = Buf {
149 bytes: [0; 32],
150 len: 0,
151 };
152 write!(buf, "{kind}").expect("tag must fit the buffer");
153 let rendered = core::str::from_utf8(&buf.bytes[..buf.len]).expect("tags are ASCII");
154 assert_eq!(rendered, kind.as_str());
155 }
156 }
157}