monocoque_core/options.rs
1//! Socket configuration options
2//!
3//! This module provides configuration options for `ZeroMQ` sockets, similar to
4//! libzmq's socket options (`zmq_setsockopt/zmq_getsockopt`).
5
6use std::{fmt, time::Duration};
7
8/// Socket configuration options.
9///
10/// These options control socket behavior including timeouts, buffer sizes,
11/// and reliability features. This struct consolidates all socket configuration
12/// in one place, following the `MongoDB` Rust driver pattern.
13///
14/// # Examples
15///
16/// ```
17/// use monocoque_core::options::SocketOptions;
18/// use std::time::Duration;
19///
20/// // Simple case: use defaults
21/// let opts = SocketOptions::default();
22///
23/// // Customize timeouts and buffers
24/// let opts = SocketOptions::default()
25/// .with_recv_timeout(Duration::from_secs(5))
26/// .with_send_timeout(Duration::from_secs(5))
27/// .with_buffer_sizes(16384, 16384); // 16KB buffers for high-throughput
28/// ```
29#[derive(Clone)]
30pub struct SocketOptions {
31 /// Read buffer size (bytes)
32 ///
33 /// Size of the per-read carve taken from the shared read slab.
34 /// - Default: 32768 (32KB) - cuts recv syscalls on bulk transfers while
35 /// staying at half the 64KB slab so small-read trickle still reclaims
36 /// - Small (4KB): Low-latency with small messages (< 1KB)
37 /// - Large (up to 32KB): High-throughput with large messages (> 8KB)
38 pub read_buffer_size: usize,
39
40 /// Write buffer size (bytes)
41 ///
42 /// Initial capacity of `BytesMut` buffer for sending data.
43 /// - Default: 8192 (8KB) - balanced for most workloads
44 /// - Small (4KB): Low-latency with small messages
45 /// - Large (16KB): High-throughput with large messages
46 pub write_buffer_size: usize,
47
48 /// Receive timeout (`ZMQ_RCVTIMEO`)
49 ///
50 /// Maximum time to wait for a receive operation.
51 /// - `None`: Block indefinitely (default)
52 /// - `Some(Duration::ZERO)`: Non-blocking (return immediately with EAGAIN)
53 /// - `Some(duration)`: Wait up to duration before returning EAGAIN
54 pub recv_timeout: Option<Duration>,
55
56 /// Send timeout (`ZMQ_SNDTIMEO`)
57 ///
58 /// Maximum time to wait for a send operation.
59 /// - `None`: Block indefinitely (default)
60 /// - `Some(Duration::ZERO)`: Non-blocking (return immediately with EAGAIN)
61 /// - `Some(duration)`: Wait up to duration before returning EAGAIN
62 pub send_timeout: Option<Duration>,
63
64 /// Handshake timeout (`ZMQ_HANDSHAKE_IVL`)
65 ///
66 /// Maximum time to complete ZMTP handshake after connection.
67 /// - Default: 30 seconds
68 /// - Set to `Duration::ZERO` to disable timeout
69 pub handshake_timeout: Duration,
70
71 /// Linger timeout (`ZMQ_LINGER`)
72 ///
73 /// Time to wait for pending messages to be sent before closing socket.
74 /// - `None`: Close immediately, discard pending messages
75 /// - `Some(Duration::ZERO)`: Same as None
76 /// - `Some(duration)`: Wait up to duration for messages to be sent
77 pub linger: Option<Duration>,
78
79 /// Reconnect interval (`ZMQ_RECONNECT_IVL`)
80 ///
81 /// Initial reconnection delay after connection loss.
82 /// - Default: 100ms
83 /// - Use with `reconnect_ivl_max` for exponential backoff
84 pub reconnect_ivl: Duration,
85
86 /// Maximum reconnect interval (`ZMQ_RECONNECT_IVL_MAX`)
87 ///
88 /// Maximum reconnection delay for exponential backoff.
89 /// - Default: 0 (no maximum, use `reconnect_ivl` always)
90 /// - When > 0: Doubles `reconnect_ivl` up to this value
91 pub reconnect_ivl_max: Duration,
92
93 /// Connection timeout (`ZMQ_CONNECT_TIMEOUT`)
94 ///
95 /// Maximum time to wait for TCP connection to complete.
96 /// - Default: 0 (use OS default)
97 pub connect_timeout: Duration,
98
99 /// High water mark for receiving (`ZMQ_RCVHWM`)
100 ///
101 /// Maximum number of messages to queue for receiving.
102 /// When reached, socket will block or drop messages depending on socket type.
103 /// - Default: 1000 messages
104 pub recv_hwm: usize,
105
106 /// High water mark for sending (`ZMQ_SNDHWM`)
107 ///
108 /// Maximum number of messages to queue for sending.
109 /// When reached, socket will block or drop messages depending on socket type.
110 /// - Default: 1000 messages
111 pub send_hwm: usize,
112
113 /// Enable immediate connect mode (`ZMQ_IMMEDIATE`)
114 ///
115 /// - `false` (default): Queue messages while connecting
116 /// - `true`: Report error if no connection established
117 pub immediate: bool,
118
119 /// Maximum message size (`ZMQ_MAXMSGSIZE`)
120 ///
121 /// Maximum size of a single message in bytes.
122 /// - `None`: No limit (default)
123 /// - `Some(size)`: Reject messages larger than size
124 pub max_msg_size: Option<usize>,
125
126 /// Socket identity / routing ID (`ZMQ_ROUTING_ID` / `ZMQ_IDENTITY`)
127 ///
128 /// Identity for ROUTER addressing. If None, a random UUID is generated.
129 /// - Default: None (auto-generate)
130 /// - Custom: Set for stable identity across reconnections
131 pub routing_id: Option<bytes::Bytes>,
132
133 /// Connect routing ID (`ZMQ_CONNECT_ROUTING_ID`)
134 ///
135 /// Identity to assign to the next outgoing connection.
136 /// Used by ROUTER sockets to assign a specific identity to a peer.
137 /// - Default: None (auto-generate)
138 /// - Custom: Assign explicit identity to next connection
139 /// - Consumed after each connect operation
140 pub connect_routing_id: Option<bytes::Bytes>,
141
142 /// ROUTER mandatory mode (`ZMQ_ROUTER_MANDATORY`)
143 ///
144 /// - `false` (default): Silently drop messages to unknown peers
145 /// - `true`: Return error when sending to unknown peer
146 pub router_mandatory: bool,
147
148 /// ROUTER handover mode (`ZMQ_ROUTER_HANDOVER`)
149 ///
150 /// - `false` (default): Disconnect old peer when new peer with same identity connects
151 /// - `true`: Hand over pending messages to new peer with same identity
152 pub router_handover: bool,
153
154 /// Probe ROUTER on connect (`ZMQ_PROBE_ROUTER`)
155 ///
156 /// - `false` (default): Normal operation
157 /// - `true`: Send empty message on connect to probe ROUTER identity
158 pub probe_router: bool,
159
160 /// XPUB verbose mode (`ZMQ_XPUB_VERBOSE`)
161 ///
162 /// - `false` (default): Only report new subscriptions
163 /// - `true`: Report all subscription messages (including duplicates)
164 pub xpub_verbose: bool,
165
166 /// XPUB manual mode (`ZMQ_XPUB_MANUAL`)
167 ///
168 /// - `false` (default): Automatic subscription management
169 /// - `true`: Manual subscription control via `send()`
170 pub xpub_manual: bool,
171
172 /// XPUB welcome message (`ZMQ_XPUB_WELCOME_MSG`)
173 ///
174 /// Message to send to new subscribers on connection.
175 /// Useful for last value cache (LVC) patterns.
176 pub xpub_welcome_msg: Option<bytes::Bytes>,
177
178 /// XSUB verbose unsubscribe (`ZMQ_XSUB_VERBOSE_UNSUBSCRIBE`)
179 ///
180 /// - `false` (default): Don't send explicit unsubscribe messages
181 /// - `true`: Send unsubscribe messages upstream
182 pub xsub_verbose_unsubs: bool,
183
184 /// Conflate messages (`ZMQ_CONFLATE`)
185 ///
186 /// - `false` (default): Queue all messages
187 /// - `true`: Keep only last message (overwrite queue)
188 pub conflate: bool,
189
190 /// TCP keepalive (`ZMQ_TCP_KEEPALIVE`)
191 ///
192 /// - `-1` (default): Use OS default
193 /// - `0`: Disable TCP keepalive
194 /// - `1`: Enable TCP keepalive
195 pub tcp_keepalive: i32,
196
197 /// TCP keepalive count (`ZMQ_TCP_KEEPALIVE_CNT`)
198 ///
199 /// Number of keepalive probes before considering connection dead.
200 /// - `-1` (default): Use OS default
201 /// - `> 0`: Number of probes
202 pub tcp_keepalive_cnt: i32,
203
204 /// TCP keepalive idle (`ZMQ_TCP_KEEPALIVE_IDLE`)
205 ///
206 /// Time in seconds before starting keepalive probes.
207 /// - `-1` (default): Use OS default
208 /// - `> 0`: Idle time in seconds
209 pub tcp_keepalive_idle: i32,
210
211 /// TCP keepalive interval (`ZMQ_TCP_KEEPALIVE_INTVL`)
212 ///
213 /// Time in seconds between keepalive probes.
214 /// - `-1` (default): Use OS default
215 /// - `> 0`: Interval in seconds
216 pub tcp_keepalive_intvl: i32,
217
218 /// REQ correlate mode (`ZMQ_REQ_CORRELATE`)
219 ///
220 /// Match replies to requests using message envelope.
221 /// - `false` (default): Accept any reply
222 /// - `true`: Match reply envelope to request
223 pub req_correlate: bool,
224
225 /// REQ relaxed mode (`ZMQ_REQ_RELAXED`)
226 ///
227 /// Allow multiple outstanding requests without strict alternation.
228 /// - `false` (default): Strict send-recv-send-recv pattern
229 /// - `true`: Allow send-send-recv-recv pattern
230 pub req_relaxed: bool,
231
232 /// Multicast rate in kilobits per second (`ZMQ_RATE`)
233 ///
234 /// Maximum send or receive data rate for multicast transports (PGM/EPGM).
235 /// - Default: 100 kbps
236 pub rate: i32,
237
238 /// Multicast recovery interval (`ZMQ_RECOVERY_IVL`)
239 ///
240 /// Maximum time to recover lost messages on multicast transports.
241 /// - Default: 10 seconds
242 pub recovery_ivl: Duration,
243
244 /// OS-level send buffer size (`ZMQ_SNDBUF`)
245 ///
246 /// Size of kernel send buffer. 0 = OS default.
247 /// - Default: 0 (use OS default)
248 pub sndbuf: i32,
249
250 /// OS-level receive buffer size (`ZMQ_RCVBUF`)
251 ///
252 /// Size of kernel receive buffer. 0 = OS default.
253 /// - Default: 0 (use OS default)
254 pub rcvbuf: i32,
255
256 /// Bind listeners with `SO_REUSEPORT` (Unix only).
257 ///
258 /// When `true`, a socket that creates its own listener binds it with
259 /// `SO_REUSEPORT` so multiple acceptors can share one port with in-kernel
260 /// load balancing (scaling accept for high-connection ROUTER/PULL/XPUB).
261 /// - `false` (default): a single listener per address.
262 pub reuse_port: bool,
263
264 /// Multicast TTL (`ZMQ_MULTICAST_HOPS`)
265 ///
266 /// Time-to-live for multicast packets.
267 /// - Default: 1 (local network only)
268 pub multicast_hops: i32,
269
270 /// IP Type of Service (`ZMQ_TOS`)
271 ///
272 /// Sets the `ToS` field in IP headers for `QoS`.
273 /// - Default: 0 (normal service)
274 pub tos: i32,
275
276 /// Maximum multicast transmission unit (`ZMQ_MULTICAST_MAXTPDU`)
277 ///
278 /// Maximum transport data unit for multicast.
279 /// - Default: 1500 bytes
280 pub multicast_maxtpdu: i32,
281
282 /// IPv6 support (`ZMQ_IPV6`)
283 ///
284 /// Enable IPv6 on socket.
285 /// - `false` (default): IPv4 only
286 /// - `true`: IPv6 support enabled
287 pub ipv6: bool,
288
289 /// Bind to device (`ZMQ_BINDTODEVICE`)
290 ///
291 /// Bind socket to specific network interface (Linux only).
292 /// - Default: None (bind to all interfaces)
293 pub bind_to_device: Option<String>,
294
295 // --- Security Options ---
296 /// PLAIN server mode (`ZMQ_PLAIN_SERVER`)
297 ///
298 /// Enable PLAIN authentication as server.
299 /// - `false` (default): Client mode
300 /// - `true`: Server mode (validate credentials)
301 pub plain_server: bool,
302
303 /// PLAIN username (`ZMQ_PLAIN_USERNAME`)
304 ///
305 /// Username for PLAIN authentication (client side).
306 /// - Default: None (no authentication)
307 pub plain_username: Option<String>,
308
309 /// PLAIN password (`ZMQ_PLAIN_PASSWORD`)
310 ///
311 /// Password for PLAIN authentication (client side).
312 /// - Default: None (no authentication)
313 ///
314 /// Wrapped in `Zeroizing` so the password buffer is scrubbed when these
315 /// options (or a clone) are dropped. Read it via [`Self::plain_password`].
316 pub plain_password: Option<zeroize::Zeroizing<String>>,
317
318 /// CURVE server mode (`ZMQ_CURVE_SERVER`)
319 ///
320 /// Enable CURVE encryption as server.
321 /// - `false` (default): Client mode
322 /// - `true`: Server mode (provide server key)
323 pub curve_server: bool,
324
325 /// CURVE public key (`ZMQ_CURVE_PUBLICKEY`)
326 ///
327 /// Local public key for CURVE (32 bytes).
328 /// - Default: None (no encryption)
329 pub curve_publickey: Option<[u8; 32]>,
330
331 /// CURVE secret key (`ZMQ_CURVE_SECRETKEY`)
332 ///
333 /// Local secret key for CURVE (32 bytes).
334 /// - Default: None (no encryption)
335 ///
336 /// Wrapped in `Zeroizing` so the key bytes are scrubbed when these options
337 /// (or a clone) are dropped. Read it via [`Self::curve_secretkey`].
338 pub curve_secretkey: Option<zeroize::Zeroizing<[u8; 32]>>,
339
340 /// CURVE server key (`ZMQ_CURVE_SERVERKEY`)
341 ///
342 /// Server's public key for CURVE client (32 bytes).
343 /// - Default: None (no encryption)
344 /// - Client must set this to verify server identity
345 pub curve_serverkey: Option<[u8; 32]>,
346
347 /// ZAP domain (`ZMQ_ZAP_DOMAIN`)
348 ///
349 /// Security domain for ZAP authentication.
350 /// - Default: "" (global domain)
351 pub zap_domain: String,
352
353 /// Subscriptions (`ZMQ_SUBSCRIBE`)
354 ///
355 /// Subscription filters for SUB/XSUB sockets.
356 /// - Empty vec: No subscriptions (default) - won't receive any messages
357 /// - vec![b""] or vec![`Bytes::new()`]: Subscribe to all messages
358 /// - vec![b"topic1", b"topic2"]: Subscribe to specific topics
359 ///
360 /// Note: SUB sockets MUST subscribe to at least one topic to receive messages.
361 pub subscriptions: Vec<bytes::Bytes>,
362
363 /// Unsubscriptions (`ZMQ_UNSUBSCRIBE`)
364 ///
365 /// Subscription filters to remove for SUB/XSUB sockets.
366 /// Applied after subscriptions during socket configuration.
367 pub unsubscriptions: Vec<bytes::Bytes>,
368
369 /// Maximum reconnection attempts (`ZMQ_RECONNECT_STOP`)
370 ///
371 /// Maximum number of times to attempt reconnection after a disconnect.
372 /// - `None`: Retry indefinitely (default, matches libzmq behaviour)
373 /// - `Some(n)`: Give up and return `NotConnected` after n attempts
374 pub max_reconnect_attempts: Option<u32>,
375
376 /// ZMTP heartbeat interval (`ZMQ_HEARTBEAT_IVL` = 75)
377 ///
378 /// How often to send PING heartbeat commands on an otherwise idle connection.
379 /// - `None`: Disabled (default)
380 /// - `Some(dur)`: Send PING every `dur` of inactivity
381 pub heartbeat_ivl: Option<Duration>,
382
383 /// ZMTP heartbeat TTL (`ZMQ_HEARTBEAT_TTL` = 76)
384 ///
385 /// Time-to-live for the remote peer's heartbeat (sent in PING command).
386 /// The remote will disconnect if it doesn't receive a heartbeat within this interval.
387 /// - `None`: Use `heartbeat_ivl` (default)
388 /// - `Some(dur)`: Override TTL sent to peer
389 pub heartbeat_ttl: Option<Duration>,
390
391 /// ZMTP heartbeat timeout (`ZMQ_HEARTBEAT_TIMEOUT` = 77)
392 ///
393 /// How long to wait for a PONG reply before considering the connection dead.
394 /// - `None`: Use `heartbeat_ivl` (default)
395 /// - `Some(dur)`: Custom timeout (recommended: 2-5x `heartbeat_ivl`)
396 pub heartbeat_timeout: Option<Duration>,
397
398 /// ROUTER raw mode (`ZMQ_ROUTER_RAW` = 41)
399 ///
400 /// Put ROUTER socket into raw mode (no ZMTP handshake, acts like STREAM).
401 /// - `false` (default): Normal ZMTP routing
402 /// - `true`: Raw TCP bridging mode
403 pub router_raw: bool,
404
405 /// STREAM connect/disconnect notifications (`ZMQ_STREAM_NOTIFY` = 73)
406 ///
407 /// Send empty notification frames on connect and disconnect.
408 /// - `true` (default): Send notification frames
409 /// - `false`: Suppress notification frames
410 pub stream_notify: bool,
411
412 /// XPUB no-drop mode (`ZMQ_XPUB_NODROP` = 69)
413 ///
414 /// - `false` (default): Drop messages silently when HWM is reached
415 /// - `true`: Return error (`EAGAIN`) instead of dropping
416 pub xpub_nodrop: bool,
417
418 /// Invert topic matching (`ZMQ_INVERT_MATCHING` = 74)
419 ///
420 /// Invert the subscription filter logic for PUB/SUB and XPUB/XSUB.
421 /// - `false` (default): Deliver messages matching subscriptions
422 /// - `true`: Deliver messages NOT matching any subscription
423 pub invert_matching: bool,
424
425 /// Write coalescing: batch multiple `send()` calls before writing to the kernel.
426 ///
427 /// When enabled, `send()` accumulates encoded messages in an internal buffer
428 /// and only flushes to the kernel when `write_coalesce_threshold` bytes have
429 /// accumulated or when `flush()` is called explicitly.
430 ///
431 /// - `false` (default): each `send()` writes immediately (lowest latency)
432 /// - `true`: messages are batched (higher throughput for small messages)
433 ///
434 /// Always call `flush()` after the last `send()` in a burst to ensure
435 /// all buffered data reaches the peer.
436 pub write_coalescing: bool,
437
438 /// Byte threshold at which the coalesce buffer is flushed automatically.
439 ///
440 /// Only relevant when `write_coalescing` is enabled. The internal send
441 /// buffer is written to the kernel as a single syscall once it reaches
442 /// this many bytes.
443 ///
444 /// - Default: 65536 (64 KB) - one typical TCP segment on loopback
445 pub write_coalesce_threshold: usize,
446
447 /// Frame-body size at or above which the send path switches to a vectored
448 /// write (`writev`) instead of copying the body into the userspace send
449 /// buffer.
450 ///
451 /// For large frames the body copy into the coalescing buffer is the
452 /// dominant per-byte cost on the core. Above this threshold the frame header
453 /// and the refcounted `Bytes` body are written as an iovec, so the body is
454 /// handed straight to the kernel with no intermediate copy. Small frames
455 /// stay on the copy path, where a single `write` of one contiguous buffer
456 /// beats the per-iovec bookkeeping.
457 ///
458 /// Only applies in eager mode (write coalescing disabled) and when the
459 /// connection is not CURVE-encrypted (encryption must transform the body
460 /// into a fresh buffer regardless).
461 ///
462 /// - Default: 32768 (32 KB) - the measured crossover on loopback below which
463 /// copying the body into one contiguous buffer beats a two-segment
464 /// `writev`; tune for your hardware and message sizes
465 pub vectored_write_threshold: usize,
466}
467
468impl fmt::Debug for SocketOptions {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 f.debug_struct("SocketOptions")
471 .field("read_buffer_size", &self.read_buffer_size())
472 .field("write_buffer_size", &self.write_buffer_size)
473 .field("recv_timeout", &self.recv_timeout)
474 .field("send_timeout", &self.send_timeout)
475 .field("handshake_timeout", &self.handshake_timeout)
476 .field("linger", &self.linger)
477 .field("reconnect_ivl", &self.reconnect_ivl)
478 .field("reconnect_ivl_max", &self.reconnect_ivl_max)
479 .field("connect_timeout", &self.connect_timeout)
480 .field("recv_hwm", &self.recv_hwm)
481 .field("send_hwm", &self.send_hwm)
482 .field("immediate", &self.immediate)
483 .field("max_msg_size", &self.max_msg_size)
484 .field("routing_id", &self.routing_id)
485 .field("connect_routing_id", &self.connect_routing_id)
486 .field("router_mandatory", &self.router_mandatory)
487 .field("router_handover", &self.router_handover)
488 .field("probe_router", &self.probe_router)
489 .field("xpub_verbose", &self.xpub_verbose)
490 .field("xpub_manual", &self.xpub_manual)
491 .field("xpub_welcome_msg", &self.xpub_welcome_msg)
492 .field("xsub_verbose_unsubs", &self.xsub_verbose_unsubs)
493 .field("conflate", &self.conflate)
494 .field("tcp_keepalive", &self.tcp_keepalive)
495 .field("tcp_keepalive_cnt", &self.tcp_keepalive_cnt)
496 .field("tcp_keepalive_idle", &self.tcp_keepalive_idle)
497 .field("tcp_keepalive_intvl", &self.tcp_keepalive_intvl)
498 .field("req_correlate", &self.req_correlate)
499 .field("req_relaxed", &self.req_relaxed)
500 .field("rate", &self.rate)
501 .field("recovery_ivl", &self.recovery_ivl)
502 .field("sndbuf", &self.sndbuf)
503 .field("rcvbuf", &self.rcvbuf)
504 .field("reuse_port", &self.reuse_port)
505 .field("multicast_hops", &self.multicast_hops)
506 .field("tos", &self.tos)
507 .field("multicast_maxtpdu", &self.multicast_maxtpdu)
508 .field("ipv6", &self.ipv6)
509 .field("bind_to_device", &self.bind_to_device)
510 .field("plain_server", &self.plain_server)
511 .field("plain_username", &self.plain_username)
512 .field(
513 "plain_password",
514 &self.plain_password.as_ref().map(|_| "[REDACTED]"),
515 )
516 .field("curve_server", &self.curve_server)
517 .field("curve_publickey", &self.curve_publickey)
518 .field(
519 "curve_secretkey",
520 &self.curve_secretkey.as_ref().map(|_| "[REDACTED]"),
521 )
522 .field("curve_serverkey", &self.curve_serverkey)
523 .field("zap_domain", &self.zap_domain)
524 .field("subscriptions", &self.subscriptions)
525 .field("unsubscriptions", &self.unsubscriptions)
526 .field("max_reconnect_attempts", &self.max_reconnect_attempts)
527 .field("heartbeat_ivl", &self.heartbeat_ivl)
528 .field("heartbeat_ttl", &self.heartbeat_ttl)
529 .field("heartbeat_timeout", &self.heartbeat_timeout)
530 .field("router_raw", &self.router_raw)
531 .field("stream_notify", &self.stream_notify)
532 .field("xpub_nodrop", &self.xpub_nodrop)
533 .field("invert_matching", &self.invert_matching)
534 .field("write_coalescing", &self.write_coalescing)
535 .field("write_coalesce_threshold", &self.write_coalesce_threshold)
536 .field("vectored_write_threshold", &self.vectored_write_threshold)
537 .finish()
538 }
539}
540
541impl Default for SocketOptions {
542 fn default() -> Self {
543 Self {
544 recv_timeout: None, // Block indefinitely
545 send_timeout: None, // Block indefinitely
546 handshake_timeout: Duration::from_secs(30),
547 linger: Some(Duration::from_secs(30)), // Wait 30s for pending messages
548 reconnect_ivl: Duration::from_millis(100),
549 reconnect_ivl_max: Duration::ZERO, // No maximum
550 connect_timeout: Duration::ZERO, // Use OS default
551 recv_hwm: 1000,
552 send_hwm: 1000,
553 immediate: false,
554 max_msg_size: None, // No limit
555 // 32 KiB read batch (half the 64 KiB slab). Larger batches cut recv
556 // syscalls on bulk transfers roughly in proportion to their size
557 // (measured over a 64 MiB PUSH/PULL flow: 8 KiB -> 8379 recvfrom,
558 // 16 KiB -> 4178, 32 KiB -> 2104, 64 KiB -> 1037), while the reclaim
559 // keeps short reads from wasting the slab. It is capped below the
560 // slab size: a batch equal to the slab would carve the whole slab per
561 // read, so an uncoalesced trickle of small reads would allocate a
562 // fresh slab each time. 32 KiB leaves room for ~512 small reads per
563 // slab and does not raise the per-connection footprint.
564 read_buffer_size: 32768,
565 write_buffer_size: 8192, // 8KB - balanced default
566 routing_id: None,
567 connect_routing_id: None,
568 router_mandatory: false,
569 router_handover: false,
570 probe_router: false,
571 xpub_verbose: false,
572 xpub_manual: false,
573 xpub_welcome_msg: None,
574 xsub_verbose_unsubs: false,
575 conflate: false,
576 tcp_keepalive: -1, // OS default
577 tcp_keepalive_cnt: -1, // OS default
578 tcp_keepalive_idle: -1, // OS default
579 tcp_keepalive_intvl: -1, // OS default
580 req_correlate: false,
581 req_relaxed: false,
582 rate: 100, // 100 kbps
583 recovery_ivl: Duration::from_secs(10),
584 sndbuf: 0, // OS default
585 rcvbuf: 0, // OS default
586 reuse_port: false,
587 multicast_hops: 1, // Local network only
588 tos: 0, // Normal service
589 multicast_maxtpdu: 1500, // Standard MTU
590 ipv6: false, // IPv4 only
591 bind_to_device: None, // All interfaces
592 // Security
593 plain_server: false,
594 plain_username: None,
595 plain_password: None,
596 curve_server: false,
597 curve_publickey: None,
598 curve_secretkey: None,
599 curve_serverkey: None,
600 zap_domain: String::new(), // Global domain
601 subscriptions: Vec::new(), // No subscriptions
602 unsubscriptions: Vec::new(), // No unsubscriptions
603 max_reconnect_attempts: None, // Retry indefinitely
604 heartbeat_ivl: None,
605 heartbeat_ttl: None,
606 heartbeat_timeout: None,
607 router_raw: false,
608 stream_notify: true,
609 xpub_nodrop: false,
610 invert_matching: false,
611 write_coalescing: false,
612 write_coalesce_threshold: 65536,
613 vectored_write_threshold: 32768,
614 }
615 }
616}
617
618impl SocketOptions {
619 /// Create new socket options with default values (8KB buffers).
620 #[must_use]
621 pub fn new() -> Self {
622 Self::default()
623 }
624
625 /// Create socket options optimized for small messages (< 1KB).
626 ///
627 /// Sets 4KB buffers, suitable for low-latency request-reply patterns.
628 ///
629 /// # Examples
630 ///
631 /// ```
632 /// use monocoque_core::options::SocketOptions;
633 ///
634 /// let opts = SocketOptions::small(); // 4KB buffers for REQ/REP
635 /// ```
636 #[must_use]
637 pub fn small() -> Self {
638 Self {
639 read_buffer_size: 4096,
640 write_buffer_size: 4096,
641 ..Self::default()
642 }
643 }
644
645 /// Create socket options optimized for large messages (> 8KB).
646 ///
647 /// Sets 16KB buffers, suitable for high-throughput async patterns.
648 ///
649 /// # Examples
650 ///
651 /// ```
652 /// use monocoque_core::options::SocketOptions;
653 ///
654 /// let opts = SocketOptions::large(); // 16KB buffers for DEALER/ROUTER
655 /// ```
656 #[must_use]
657 pub fn large() -> Self {
658 Self {
659 read_buffer_size: 16384,
660 write_buffer_size: 16384,
661 ..Self::default()
662 }
663 }
664
665 /// Set receive timeout.
666 ///
667 /// # Examples
668 ///
669 /// ```
670 /// use monocoque_core::options::SocketOptions;
671 /// use std::time::Duration;
672 ///
673 /// // Non-blocking receive
674 /// let opts = SocketOptions::new().with_recv_timeout(Duration::ZERO);
675 ///
676 /// // 5 second timeout
677 /// let opts = SocketOptions::new().with_recv_timeout(Duration::from_secs(5));
678 /// ```
679 pub const fn with_recv_timeout(mut self, timeout: Duration) -> Self {
680 self.recv_timeout = Some(timeout);
681 self
682 }
683
684 /// Set send timeout.
685 pub const fn with_send_timeout(mut self, timeout: Duration) -> Self {
686 self.send_timeout = Some(timeout);
687 self
688 }
689
690 /// Set handshake timeout.
691 pub const fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
692 self.handshake_timeout = timeout;
693 self
694 }
695
696 /// Set linger timeout.
697 pub const fn with_linger(mut self, linger: Option<Duration>) -> Self {
698 self.linger = linger;
699 self
700 }
701
702 /// Set reconnection interval.
703 pub const fn with_reconnect_ivl(mut self, ivl: Duration) -> Self {
704 self.reconnect_ivl = ivl;
705 self
706 }
707
708 /// Set maximum reconnection interval for exponential backoff.
709 pub const fn with_reconnect_ivl_max(mut self, max: Duration) -> Self {
710 self.reconnect_ivl_max = max;
711 self
712 }
713
714 /// Set maximum number of reconnection attempts.
715 ///
716 /// `None` retries indefinitely (default); `Some(n)` gives up after n attempts.
717 pub const fn with_max_reconnect_attempts(mut self, max: Option<u32>) -> Self {
718 self.max_reconnect_attempts = max;
719 self
720 }
721
722 /// Set connection timeout.
723 pub const fn with_connect_timeout(mut self, timeout: Duration) -> Self {
724 self.connect_timeout = timeout;
725 self
726 }
727
728 /// Set heartbeat interval (`ZMQ_HEARTBEAT_IVL`).
729 pub const fn with_heartbeat_ivl(mut self, ivl: Duration) -> Self {
730 self.heartbeat_ivl = Some(ivl);
731 self
732 }
733
734 /// Set heartbeat TTL (`ZMQ_HEARTBEAT_TTL`).
735 pub const fn with_heartbeat_ttl(mut self, ttl: Duration) -> Self {
736 self.heartbeat_ttl = Some(ttl);
737 self
738 }
739
740 /// Set heartbeat timeout (`ZMQ_HEARTBEAT_TIMEOUT`).
741 pub const fn with_heartbeat_timeout(mut self, timeout: Duration) -> Self {
742 self.heartbeat_timeout = Some(timeout);
743 self
744 }
745
746 /// Enable or disable ROUTER raw mode (`ZMQ_ROUTER_RAW`).
747 pub const fn with_router_raw(mut self, raw: bool) -> Self {
748 self.router_raw = raw;
749 self
750 }
751
752 /// Enable or disable STREAM connect/disconnect notifications (`ZMQ_STREAM_NOTIFY`).
753 pub const fn with_stream_notify(mut self, notify: bool) -> Self {
754 self.stream_notify = notify;
755 self
756 }
757
758 /// Enable XPUB no-drop mode (`ZMQ_XPUB_NODROP`).
759 pub const fn with_xpub_nodrop(mut self, nodrop: bool) -> Self {
760 self.xpub_nodrop = nodrop;
761 self
762 }
763
764 /// Enable inverted topic matching (`ZMQ_INVERT_MATCHING`).
765 pub const fn with_invert_matching(mut self, invert: bool) -> Self {
766 self.invert_matching = invert;
767 self
768 }
769
770 /// Enable or disable write coalescing.
771 ///
772 /// When enabled, consecutive `send()` calls accumulate in an internal buffer
773 /// and are written to the kernel in one syscall, reducing per-message overhead
774 /// for small-message workloads. Call `flush()` after the last send in a burst.
775 pub const fn with_write_coalescing(mut self, enabled: bool) -> Self {
776 self.write_coalescing = enabled;
777 self
778 }
779
780 /// Set the byte threshold at which the coalesce buffer flushes automatically.
781 pub const fn with_write_coalesce_threshold(mut self, threshold: usize) -> Self {
782 self.write_coalesce_threshold = threshold;
783 self
784 }
785
786 /// Set the frame-body size at or above which the eager send path uses a
787 /// vectored write (`writev`) instead of copying the body into the send
788 /// buffer. See [`SocketOptions::vectored_write_threshold`]. Set to
789 /// `usize::MAX` to disable vectored writes entirely.
790 pub const fn with_vectored_write_threshold(mut self, threshold: usize) -> Self {
791 self.vectored_write_threshold = threshold;
792 self
793 }
794
795 /// Get the configured PLAIN password, if any.
796 pub fn plain_password(&self) -> Option<&str> {
797 self.plain_password.as_ref().map(|p| p.as_str())
798 }
799
800 /// Get the configured CURVE secret key, if any.
801 pub fn curve_secretkey(&self) -> Option<&[u8; 32]> {
802 self.curve_secretkey.as_deref()
803 }
804
805 /// Get the configured read buffer size after applying the read-slab cap.
806 pub const fn read_buffer_size(&self) -> usize {
807 if self.read_buffer_size > crate::io::READ_SLAB_SIZE {
808 crate::io::READ_SLAB_SIZE
809 } else {
810 self.read_buffer_size
811 }
812 }
813
814 /// Set receive high water mark.
815 pub const fn with_recv_hwm(mut self, hwm: usize) -> Self {
816 self.recv_hwm = hwm;
817 self
818 }
819
820 /// Set send high water mark.
821 pub const fn with_send_hwm(mut self, hwm: usize) -> Self {
822 self.send_hwm = hwm;
823 self
824 }
825
826 /// Enable or disable immediate mode.
827 pub const fn with_immediate(mut self, immediate: bool) -> Self {
828 self.immediate = immediate;
829 self
830 }
831
832 /// Set maximum message size.
833 pub const fn with_max_msg_size(mut self, size: Option<usize>) -> Self {
834 self.max_msg_size = size;
835 self
836 }
837
838 /// Set read buffer size.
839 ///
840 /// # Examples
841 ///
842 /// ```
843 /// use monocoque_core::options::SocketOptions;
844 ///
845 /// // Small buffers for low latency
846 /// let opts = SocketOptions::new().with_read_buffer_size(4096);
847 ///
848 /// // Large buffers for throughput
849 /// let opts = SocketOptions::new().with_read_buffer_size(16384);
850 /// ```
851 pub const fn with_read_buffer_size(mut self, size: usize) -> Self {
852 // A read buffer of 0 (or a byte or two) turns the read loop into a spin
853 // or a false EOF, so floor it to a small minimum. The upper bound stays
854 // the read slab size.
855 const MIN_READ_BUFFER_SIZE: usize = 64;
856 self.read_buffer_size = if size < MIN_READ_BUFFER_SIZE {
857 MIN_READ_BUFFER_SIZE
858 } else if size > crate::io::READ_SLAB_SIZE {
859 crate::io::READ_SLAB_SIZE
860 } else {
861 size
862 };
863 self
864 }
865
866 /// Set write buffer size.
867 pub const fn with_write_buffer_size(mut self, size: usize) -> Self {
868 self.write_buffer_size = size;
869 self
870 }
871
872 /// Set both read and write buffer sizes (convenience method).
873 ///
874 /// # Examples
875 ///
876 /// ```
877 /// use monocoque_core::options::SocketOptions;
878 ///
879 /// // Small buffers for both
880 /// let opts = SocketOptions::new().with_buffer_sizes(4096, 4096);
881 /// ```
882 pub const fn with_buffer_sizes(mut self, read_size: usize, write_size: usize) -> Self {
883 self.read_buffer_size = if read_size > crate::io::READ_SLAB_SIZE {
884 crate::io::READ_SLAB_SIZE
885 } else {
886 read_size
887 };
888 self.write_buffer_size = write_size;
889 self
890 }
891
892 /// Set socket routing ID / identity.
893 ///
894 /// # Examples
895 ///
896 /// ```
897 /// use monocoque_core::options::SocketOptions;
898 /// use bytes::Bytes;
899 ///
900 /// let opts = SocketOptions::new()
901 /// .with_routing_id(Bytes::from_static(b"worker-01"));
902 /// ```
903 pub fn with_routing_id(mut self, id: bytes::Bytes) -> Self {
904 self.routing_id = Some(id);
905 self
906 }
907
908 /// Set connect routing ID for the next connection.
909 ///
910 /// This option is consumed after each connect operation and must be set
911 /// again for subsequent connections.
912 ///
913 /// # Examples
914 ///
915 /// ```
916 /// use monocoque_core::options::SocketOptions;
917 /// use bytes::Bytes;
918 ///
919 /// let opts = SocketOptions::new()
920 /// .with_connect_routing_id(Bytes::from_static(b"client-001"));
921 /// ```
922 pub fn with_connect_routing_id(mut self, id: bytes::Bytes) -> Self {
923 self.connect_routing_id = Some(id);
924 self
925 }
926
927 /// Enable ROUTER mandatory mode.
928 pub const fn with_router_mandatory(mut self, enabled: bool) -> Self {
929 self.router_mandatory = enabled;
930 self
931 }
932
933 /// Enable ROUTER handover mode.
934 pub const fn with_router_handover(mut self, enabled: bool) -> Self {
935 self.router_handover = enabled;
936 self
937 }
938
939 /// Enable ROUTER probe on connect.
940 pub const fn with_probe_router(mut self, enabled: bool) -> Self {
941 self.probe_router = enabled;
942 self
943 }
944
945 /// Enable XPUB verbose mode.
946 pub const fn with_xpub_verbose(mut self, enabled: bool) -> Self {
947 self.xpub_verbose = enabled;
948 self
949 }
950
951 /// Enable XPUB manual mode.
952 pub const fn with_xpub_manual(mut self, enabled: bool) -> Self {
953 self.xpub_manual = enabled;
954 self
955 }
956
957 /// Set XPUB welcome message.
958 pub fn with_xpub_welcome_msg(mut self, msg: bytes::Bytes) -> Self {
959 self.xpub_welcome_msg = Some(msg);
960 self
961 }
962
963 /// Enable XSUB verbose unsubscribe.
964 pub const fn with_xsub_verbose_unsubs(mut self, enabled: bool) -> Self {
965 self.xsub_verbose_unsubs = enabled;
966 self
967 }
968
969 /// Enable message conflation (keep only last message).
970 pub const fn with_conflate(mut self, enabled: bool) -> Self {
971 self.conflate = enabled;
972 self
973 }
974
975 /// Set TCP keepalive mode.
976 ///
977 /// # Arguments
978 ///
979 /// * `mode` - `-1` for OS default, `0` to disable, `1` to enable
980 pub const fn with_tcp_keepalive(mut self, mode: i32) -> Self {
981 self.tcp_keepalive = mode;
982 self
983 }
984
985 /// Set TCP keepalive count (number of probes before timeout).
986 ///
987 /// # Arguments
988 ///
989 /// * `count` - `-1` for OS default, `> 0` for specific count
990 pub const fn with_tcp_keepalive_cnt(mut self, count: i32) -> Self {
991 self.tcp_keepalive_cnt = count;
992 self
993 }
994
995 /// Set TCP keepalive idle time (seconds before first probe).
996 ///
997 /// # Arguments
998 ///
999 /// * `seconds` - `-1` for OS default, `> 0` for specific idle time
1000 pub const fn with_tcp_keepalive_idle(mut self, seconds: i32) -> Self {
1001 self.tcp_keepalive_idle = seconds;
1002 self
1003 }
1004
1005 /// Set TCP keepalive interval (seconds between probes).
1006 ///
1007 /// # Arguments
1008 ///
1009 /// * `seconds` - `-1` for OS default, `> 0` for specific interval
1010 pub const fn with_tcp_keepalive_intvl(mut self, seconds: i32) -> Self {
1011 self.tcp_keepalive_intvl = seconds;
1012 self
1013 }
1014
1015 /// Enable REQ correlation mode (match replies to requests).
1016 pub const fn with_req_correlate(mut self, enabled: bool) -> Self {
1017 self.req_correlate = enabled;
1018 self
1019 }
1020
1021 /// Enable REQ relaxed mode (allow multiple outstanding requests).
1022 pub const fn with_req_relaxed(mut self, enabled: bool) -> Self {
1023 self.req_relaxed = enabled;
1024 self
1025 }
1026
1027 /// Set multicast rate (`ZMQ_RATE`).
1028 pub const fn with_rate(mut self, rate: i32) -> Self {
1029 self.rate = rate;
1030 self
1031 }
1032
1033 /// Set multicast recovery interval (`ZMQ_RECOVERY_IVL`).
1034 pub const fn with_recovery_ivl(mut self, interval: Duration) -> Self {
1035 self.recovery_ivl = interval;
1036 self
1037 }
1038
1039 /// Set OS send buffer size (`ZMQ_SNDBUF`).
1040 pub const fn with_sndbuf(mut self, size: i32) -> Self {
1041 self.sndbuf = size;
1042 self
1043 }
1044
1045 /// Set OS receive buffer size (`ZMQ_RCVBUF`).
1046 pub const fn with_rcvbuf(mut self, size: i32) -> Self {
1047 self.rcvbuf = size;
1048 self
1049 }
1050
1051 /// Bind listeners with `SO_REUSEPORT` (Unix only) so multiple acceptors can
1052 /// share one port. See [`SocketOptions::reuse_port`].
1053 pub const fn with_reuse_port(mut self, enabled: bool) -> Self {
1054 self.reuse_port = enabled;
1055 self
1056 }
1057
1058 /// Set multicast TTL/hops (`ZMQ_MULTICAST_HOPS`).
1059 pub const fn with_multicast_hops(mut self, hops: i32) -> Self {
1060 self.multicast_hops = hops;
1061 self
1062 }
1063
1064 /// Set IP Type of Service (`ZMQ_TOS`).
1065 pub const fn with_tos(mut self, tos: i32) -> Self {
1066 self.tos = tos;
1067 self
1068 }
1069
1070 /// Set multicast maximum TPU (`ZMQ_MULTICAST_MAXTPDU`).
1071 pub const fn with_multicast_maxtpdu(mut self, mtu: i32) -> Self {
1072 self.multicast_maxtpdu = mtu;
1073 self
1074 }
1075
1076 /// Enable IPv6 support (`ZMQ_IPV6`).
1077 pub const fn with_ipv6(mut self, enabled: bool) -> Self {
1078 self.ipv6 = enabled;
1079 self
1080 }
1081
1082 /// Bind to specific device (`ZMQ_BINDTODEVICE`) - Linux only.
1083 pub fn with_bind_to_device(mut self, device: impl Into<String>) -> Self {
1084 self.bind_to_device = Some(device.into());
1085 self
1086 }
1087
1088 // --- Security Options ---
1089
1090 /// Enable PLAIN server mode.
1091 ///
1092 /// # Examples
1093 ///
1094 /// ```
1095 /// use monocoque_core::options::SocketOptions;
1096 ///
1097 /// let opts = SocketOptions::new().with_plain_server(true);
1098 /// ```
1099 pub const fn with_plain_server(mut self, enabled: bool) -> Self {
1100 self.plain_server = enabled;
1101 self
1102 }
1103
1104 /// Set PLAIN client credentials.
1105 ///
1106 /// # Examples
1107 ///
1108 /// ```
1109 /// use monocoque_core::options::SocketOptions;
1110 ///
1111 /// let opts = SocketOptions::new()
1112 /// .with_plain_credentials("admin", "secret123");
1113 /// ```
1114 pub fn with_plain_credentials(
1115 mut self,
1116 username: impl Into<String>,
1117 password: impl Into<String>,
1118 ) -> Self {
1119 self.plain_username = Some(username.into());
1120 self.plain_password = Some(zeroize::Zeroizing::new(password.into()));
1121 self
1122 }
1123
1124 /// Enable CURVE server mode.
1125 ///
1126 /// # Examples
1127 ///
1128 /// ```
1129 /// use monocoque_core::options::SocketOptions;
1130 ///
1131 /// let opts = SocketOptions::new().with_curve_server(true);
1132 /// ```
1133 pub const fn with_curve_server(mut self, enabled: bool) -> Self {
1134 self.curve_server = enabled;
1135 self
1136 }
1137
1138 /// Set CURVE client keys (public + secret).
1139 ///
1140 /// # Examples
1141 ///
1142 /// ```
1143 /// use monocoque_core::options::SocketOptions;
1144 ///
1145 /// let public = [0u8; 32]; // Replace with actual key
1146 /// let secret = [0u8; 32]; // Replace with actual key
1147 /// let opts = SocketOptions::new().with_curve_keypair(public, secret);
1148 /// ```
1149 pub fn with_curve_keypair(mut self, publickey: [u8; 32], secretkey: [u8; 32]) -> Self {
1150 self.curve_publickey = Some(publickey);
1151 self.curve_secretkey = Some(zeroize::Zeroizing::new(secretkey));
1152 self
1153 }
1154
1155 /// Set CURVE server public key (for client).
1156 ///
1157 /// # Examples
1158 ///
1159 /// ```
1160 /// use monocoque_core::options::SocketOptions;
1161 ///
1162 /// let server_key = [0u8; 32]; // Server's public key
1163 /// let opts = SocketOptions::new().with_curve_serverkey(server_key);
1164 /// ```
1165 pub const fn with_curve_serverkey(mut self, serverkey: [u8; 32]) -> Self {
1166 self.curve_serverkey = Some(serverkey);
1167 self
1168 }
1169
1170 /// Set ZAP domain for authentication.
1171 ///
1172 /// # Examples
1173 ///
1174 /// ```
1175 /// use monocoque_core::options::SocketOptions;
1176 ///
1177 /// let opts = SocketOptions::new().with_zap_domain("production");
1178 /// ```
1179 pub fn with_zap_domain(mut self, domain: impl Into<String>) -> Self {
1180 self.zap_domain = domain.into();
1181 self
1182 }
1183
1184 /// Add a subscription filter for SUB/XSUB sockets (`ZMQ_SUBSCRIBE`).
1185 ///
1186 /// SUB sockets MUST subscribe to at least one topic to receive messages.
1187 /// An empty filter (b"" or `Bytes::new()`) subscribes to all messages.
1188 ///
1189 /// # Examples
1190 ///
1191 /// ```
1192 /// use monocoque_core::options::SocketOptions;
1193 /// use bytes::Bytes;
1194 ///
1195 /// // Subscribe to all messages
1196 /// let opts = SocketOptions::new().with_subscribe(Bytes::new());
1197 ///
1198 /// // Subscribe to specific topics
1199 /// let opts = SocketOptions::new()
1200 /// .with_subscribe(Bytes::from("weather."))
1201 /// .with_subscribe(Bytes::from("stocks."));
1202 /// ```
1203 pub fn with_subscribe(mut self, filter: bytes::Bytes) -> Self {
1204 self.subscriptions.push(filter);
1205 self
1206 }
1207
1208 /// Add multiple subscription filters for SUB/XSUB sockets.
1209 ///
1210 /// Convenience method to subscribe to multiple topics at once.
1211 ///
1212 /// # Examples
1213 ///
1214 /// ```
1215 /// use monocoque_core::options::SocketOptions;
1216 /// use bytes::Bytes;
1217 ///
1218 /// let opts = SocketOptions::new()
1219 /// .with_subscriptions(vec![
1220 /// Bytes::from("weather."),
1221 /// Bytes::from("stocks."),
1222 /// ]);
1223 /// ```
1224 pub fn with_subscriptions(mut self, filters: Vec<bytes::Bytes>) -> Self {
1225 self.subscriptions.extend(filters);
1226 self
1227 }
1228
1229 /// Add an unsubscription filter for SUB/XSUB sockets (`ZMQ_UNSUBSCRIBE`).
1230 ///
1231 /// Removes a previously added subscription filter.
1232 ///
1233 /// # Examples
1234 ///
1235 /// ```
1236 /// use monocoque_core::options::SocketOptions;
1237 /// use bytes::Bytes;
1238 ///
1239 /// let opts = SocketOptions::new()
1240 /// .with_subscribe(Bytes::new()) // Subscribe to all
1241 /// .with_unsubscribe(Bytes::from("admin.")); // Except admin topics
1242 /// ```
1243 pub fn with_unsubscribe(mut self, filter: bytes::Bytes) -> Self {
1244 self.unsubscriptions.push(filter);
1245 self
1246 }
1247
1248 // --- Query Methods ---
1249
1250 /// Check if receive operation should be non-blocking.
1251 pub const fn is_recv_nonblocking(&self) -> bool {
1252 matches!(self.recv_timeout, Some(d) if d.is_zero())
1253 }
1254
1255 /// Check if send operation should be non-blocking.
1256 pub const fn is_send_nonblocking(&self) -> bool {
1257 matches!(self.send_timeout, Some(d) if d.is_zero())
1258 }
1259
1260 /// Validate routing ID for use with ROUTER sockets.
1261 ///
1262 /// ROUTER socket identities must:
1263 /// - Be 1-255 bytes long
1264 /// - Not start with null byte (0x00) which is reserved for auto-generated IDs
1265 pub fn validate_router_identity(id: &[u8]) -> std::io::Result<()> {
1266 if id.is_empty() {
1267 return Err(std::io::Error::new(
1268 std::io::ErrorKind::InvalidInput,
1269 "routing ID cannot be empty",
1270 ));
1271 }
1272
1273 if id.len() > 255 {
1274 return Err(std::io::Error::new(
1275 std::io::ErrorKind::InvalidInput,
1276 format!("routing ID cannot exceed 255 bytes (got {})", id.len()),
1277 ));
1278 }
1279
1280 if id[0] == 0x00 {
1281 return Err(std::io::Error::new(
1282 std::io::ErrorKind::InvalidInput,
1283 "routing ID cannot start with null byte (reserved for auto-generated IDs)",
1284 ));
1285 }
1286
1287 Ok(())
1288 }
1289
1290 /// Validate general routing ID (for DEALER, REQ, REP).
1291 ///
1292 /// Less strict than ROUTER identities - allows null prefix.
1293 pub fn validate_routing_id(id: &[u8]) -> std::io::Result<()> {
1294 if id.len() > 255 {
1295 return Err(std::io::Error::new(
1296 std::io::ErrorKind::InvalidInput,
1297 format!("routing ID cannot exceed 255 bytes (got {})", id.len()),
1298 ));
1299 }
1300 Ok(())
1301 }
1302
1303 /// Get the current reconnection interval with exponential backoff.
1304 ///
1305 /// Returns the interval to use, considering exponential backoff
1306 /// and the maximum interval setting.
1307 pub fn next_reconnect_ivl(&self, attempt: u32) -> Duration {
1308 // Match reconnect::ReconnectState::next_delay so the two do not diverge:
1309 // max == 0 disables exponential backoff, and a max at or below the base
1310 // leaves no room to grow. Both cases hold at the base interval rather
1311 // than (as before) clamping the growing value down to a smaller max.
1312 if self.reconnect_ivl_max.is_zero() || self.reconnect_ivl_max <= self.reconnect_ivl {
1313 return self.reconnect_ivl;
1314 }
1315
1316 // Calculate exponential backoff: base * 2^attempt
1317 let backoff = self
1318 .reconnect_ivl
1319 .saturating_mul(2u32.saturating_pow(attempt));
1320
1321 // Cap at maximum interval
1322 backoff.min(self.reconnect_ivl_max)
1323 }
1324}
1325
1326#[cfg(test)]
1327mod tests {
1328 use super::*;
1329
1330 #[test]
1331 fn test_default_options() {
1332 let opts = SocketOptions::default();
1333 assert!(opts.recv_timeout.is_none());
1334 assert!(opts.send_timeout.is_none());
1335 assert_eq!(opts.handshake_timeout, Duration::from_secs(30));
1336 assert_eq!(opts.reconnect_ivl, Duration::from_millis(100));
1337 assert_eq!(opts.recv_hwm, 1000);
1338 assert_eq!(opts.send_hwm, 1000);
1339 }
1340
1341 #[test]
1342 fn test_builder_pattern() {
1343 let opts = SocketOptions::new()
1344 .with_recv_timeout(Duration::from_secs(5))
1345 .with_send_timeout(Duration::from_secs(10))
1346 .with_recv_hwm(2000);
1347
1348 assert_eq!(opts.recv_timeout, Some(Duration::from_secs(5)));
1349 assert_eq!(opts.send_timeout, Some(Duration::from_secs(10)));
1350 assert_eq!(opts.recv_hwm, 2000);
1351 }
1352
1353 #[test]
1354 fn test_nonblocking_checks() {
1355 let blocking = SocketOptions::new();
1356 assert!(!blocking.is_recv_nonblocking());
1357 assert!(!blocking.is_send_nonblocking());
1358
1359 let nonblocking = SocketOptions::new()
1360 .with_recv_timeout(Duration::ZERO)
1361 .with_send_timeout(Duration::ZERO);
1362 assert!(nonblocking.is_recv_nonblocking());
1363 assert!(nonblocking.is_send_nonblocking());
1364 }
1365
1366 #[test]
1367 fn test_exponential_backoff() {
1368 let opts = SocketOptions::new()
1369 .with_reconnect_ivl(Duration::from_millis(100))
1370 .with_reconnect_ivl_max(Duration::from_secs(10));
1371
1372 // First attempt: 100ms
1373 assert_eq!(opts.next_reconnect_ivl(0), Duration::from_millis(100));
1374
1375 // Second attempt: 200ms
1376 assert_eq!(opts.next_reconnect_ivl(1), Duration::from_millis(200));
1377
1378 // Third attempt: 400ms
1379 assert_eq!(opts.next_reconnect_ivl(2), Duration::from_millis(400));
1380
1381 // Eventually caps at 10s
1382 assert_eq!(opts.next_reconnect_ivl(10), Duration::from_secs(10));
1383 }
1384
1385 #[test]
1386 fn test_no_exponential_backoff() {
1387 let opts = SocketOptions::new().with_reconnect_ivl(Duration::from_millis(100));
1388 // reconnect_ivl_max is 0 by default
1389
1390 // Always returns base interval
1391 assert_eq!(opts.next_reconnect_ivl(0), Duration::from_millis(100));
1392 assert_eq!(opts.next_reconnect_ivl(1), Duration::from_millis(100));
1393 assert_eq!(opts.next_reconnect_ivl(10), Duration::from_millis(100));
1394 }
1395
1396 #[test]
1397 fn test_reconnect_ivl_max_below_base_holds_at_base() {
1398 // A max below the base leaves no room to grow: hold at base rather than
1399 // clamping down to the smaller max. This matches ReconnectState and is
1400 // the invariant next_reconnect_ivl previously violated.
1401 let opts = SocketOptions::new()
1402 .with_reconnect_ivl(Duration::from_millis(100))
1403 .with_reconnect_ivl_max(Duration::from_millis(50));
1404
1405 for attempt in 0..5 {
1406 assert_eq!(
1407 opts.next_reconnect_ivl(attempt),
1408 Duration::from_millis(100),
1409 "max below base must hold at the base interval"
1410 );
1411 }
1412 }
1413
1414 #[test]
1415 fn test_routing_id_validation() {
1416 // Valid ROUTER identities
1417 assert!(SocketOptions::validate_router_identity(b"client-001").is_ok());
1418 assert!(SocketOptions::validate_router_identity(&[0x01; 255]).is_ok());
1419
1420 // Invalid: empty
1421 assert!(SocketOptions::validate_router_identity(b"").is_err());
1422
1423 // Invalid: too long
1424 assert!(SocketOptions::validate_router_identity(&[0x01; 256]).is_err());
1425
1426 // Invalid: starts with null byte
1427 assert!(SocketOptions::validate_router_identity(b"\x00client").is_err());
1428 }
1429
1430 #[test]
1431 fn test_general_routing_id_validation() {
1432 // Valid
1433 assert!(SocketOptions::validate_routing_id(b"").is_ok()); // Empty allowed
1434 assert!(SocketOptions::validate_routing_id(b"\x00client").is_ok()); // Null prefix allowed
1435 assert!(SocketOptions::validate_routing_id(&[0x00; 255]).is_ok());
1436
1437 // Invalid: too long
1438 assert!(SocketOptions::validate_routing_id(&[0x01; 256]).is_err());
1439 }
1440
1441 #[test]
1442 fn test_connect_routing_id() {
1443 let opts =
1444 SocketOptions::new().with_connect_routing_id(bytes::Bytes::from_static(b"peer-123"));
1445
1446 assert_eq!(
1447 opts.connect_routing_id,
1448 Some(bytes::Bytes::from_static(b"peer-123"))
1449 );
1450 }
1451
1452 #[test]
1453 fn debug_output_redacts_security_options() {
1454 let opts = SocketOptions::new()
1455 .with_plain_credentials("alice", "super-secret-password")
1456 .with_curve_keypair([1u8; 32], [7u8; 32]);
1457
1458 let debug = format!("{opts:?}");
1459
1460 assert!(
1461 !debug.contains("super-secret-password"),
1462 "SocketOptions Debug output exposes the PLAIN password"
1463 );
1464 assert!(
1465 !debug.contains("curve_secretkey: Some([7, 7, 7"),
1466 "SocketOptions Debug output exposes the CURVE secret key"
1467 );
1468 }
1469
1470 #[test]
1471 fn read_buffer_size_cannot_exceed_read_slab_size() {
1472 let opts = SocketOptions::new().with_read_buffer_size(crate::io::READ_SLAB_SIZE + 1);
1473
1474 assert!(
1475 opts.read_buffer_size <= crate::io::READ_SLAB_SIZE,
1476 "SocketOptions allowed a read buffer size larger than the read slab"
1477 );
1478 }
1479
1480 #[test]
1481 fn test_router_options() {
1482 let opts = SocketOptions::new()
1483 .with_router_mandatory(true)
1484 .with_router_handover(true);
1485
1486 assert!(opts.router_mandatory);
1487 assert!(opts.router_handover);
1488 }
1489
1490 #[test]
1491 fn test_subscription_options() {
1492 // Test with_subscribe
1493 let opts = SocketOptions::new()
1494 .with_subscribe(bytes::Bytes::new()) // Subscribe to all
1495 .with_subscribe(bytes::Bytes::from("weather."))
1496 .with_subscribe(bytes::Bytes::from("stocks."));
1497
1498 assert_eq!(opts.subscriptions.len(), 3);
1499 assert_eq!(opts.subscriptions[0], bytes::Bytes::new());
1500 assert_eq!(opts.subscriptions[1], bytes::Bytes::from("weather."));
1501 assert_eq!(opts.subscriptions[2], bytes::Bytes::from("stocks."));
1502
1503 // Test with_subscriptions
1504 let opts2 = SocketOptions::new().with_subscriptions(vec![
1505 bytes::Bytes::from("topic1"),
1506 bytes::Bytes::from("topic2"),
1507 ]);
1508
1509 assert_eq!(opts2.subscriptions.len(), 2);
1510
1511 // Test with_unsubscribe
1512 let opts3 = opts.with_unsubscribe(bytes::Bytes::from("admin."));
1513 assert_eq!(opts3.unsubscriptions.len(), 1);
1514 assert_eq!(opts3.unsubscriptions[0], bytes::Bytes::from("admin."));
1515 }
1516}