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