omq-rs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ use std::os::fd::RawFd;
2
+ use std::sync::atomic::{AtomicBool, Ordering};
3
+
4
+ pub struct PipeNotify {
5
+ read_fd: RawFd,
6
+ write_fd: RawFd,
7
+ parking: AtomicBool,
8
+ }
9
+
10
+ unsafe impl Send for PipeNotify {}
11
+ unsafe impl Sync for PipeNotify {}
12
+
13
+ impl PipeNotify {
14
+ pub fn new() -> Self {
15
+ let mut fds = [0i32; 2];
16
+ let ret = create_pipe(&mut fds);
17
+ assert!(
18
+ ret == 0,
19
+ "pipe2 failed: {}",
20
+ std::io::Error::last_os_error()
21
+ );
22
+ Self {
23
+ read_fd: fds[0],
24
+ write_fd: fds[1],
25
+ parking: AtomicBool::new(false),
26
+ }
27
+ }
28
+
29
+ pub fn notify(&self) {
30
+ if self.parking.swap(false, Ordering::AcqRel) {
31
+ self.write_byte();
32
+ }
33
+ }
34
+
35
+ pub fn force_wake(&self) {
36
+ self.write_byte();
37
+ }
38
+
39
+ pub fn read_fd(&self) -> RawFd {
40
+ self.read_fd
41
+ }
42
+
43
+ pub fn park_begin(&self) {
44
+ self.parking.store(true, Ordering::Release);
45
+ }
46
+
47
+ pub fn cancel_park(&self) {
48
+ self.parking.store(false, Ordering::Release);
49
+ }
50
+
51
+ pub fn clear(&self) {
52
+ let mut buf = [0u8; 64];
53
+ loop {
54
+ let ret = unsafe {
55
+ libc::read(
56
+ self.read_fd,
57
+ buf.as_mut_ptr().cast::<libc::c_void>(),
58
+ buf.len(),
59
+ )
60
+ };
61
+ if ret > 0 {
62
+ continue;
63
+ }
64
+ if ret < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
65
+ continue;
66
+ }
67
+ break;
68
+ }
69
+ }
70
+
71
+ fn write_byte(&self) {
72
+ let val: u8 = 1;
73
+ loop {
74
+ let ret =
75
+ unsafe { libc::write(self.write_fd, (&raw const val).cast::<libc::c_void>(), 1) };
76
+ if ret >= 0 {
77
+ break;
78
+ }
79
+ if std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ #[cfg(any(target_os = "linux", target_os = "android"))]
87
+ fn create_pipe(fds: &mut [RawFd; 2]) -> i32 {
88
+ unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) }
89
+ }
90
+
91
+ #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
92
+ fn create_pipe(fds: &mut [RawFd; 2]) -> i32 {
93
+ let ret = unsafe { libc::pipe(fds.as_mut_ptr()) };
94
+ if ret != 0 {
95
+ return ret;
96
+ }
97
+
98
+ for fd in *fds {
99
+ unsafe {
100
+ libc::fcntl(fd, libc::F_SETFL, libc::O_NONBLOCK);
101
+ libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
102
+ }
103
+ }
104
+ 0
105
+ }
106
+
107
+ impl Drop for PipeNotify {
108
+ fn drop(&mut self) {
109
+ unsafe {
110
+ libc::close(self.read_fd);
111
+ libc::close(self.write_fd);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,434 @@
1
+ use std::time::Duration;
2
+
3
+ use bytes::Bytes;
4
+
5
+ use rb_sys::VALUE;
6
+
7
+ use crate::rb::{self, RbResult, RubyErr};
8
+
9
+ #[expect(
10
+ clippy::too_many_lines,
11
+ reason = "flat option mapping mirrors omq-proto Options fields"
12
+ )]
13
+ pub fn build_options(hash: VALUE) -> RbResult<omq_tokio::Options> {
14
+ rb::check_hash(hash)?;
15
+
16
+ let mut opts = omq_tokio::Options::default();
17
+
18
+ if let Some(v) = get_opt_u32(hash, "send_hwm")? {
19
+ opts.send_hwm = v;
20
+ }
21
+ if let Some(v) = get_opt_u32(hash, "recv_hwm")? {
22
+ opts.recv_hwm = v;
23
+ }
24
+ if let Some(v) = get_rate_limit(hash, "recv_rate_limit")? {
25
+ opts.recv_rate_limit = Some(v);
26
+ }
27
+ if let Some(v) = get_rate_limit(hash, "recv_ip_rate_limit")? {
28
+ opts.recv_ip_rate_limit = Some(v);
29
+ }
30
+ if let Some(v) = get_opt_string(hash, "workload_profile")? {
31
+ opts.workload_profile = Some(match v.as_str() {
32
+ "throughput" => omq_proto::options::WorkloadProfile::Throughput,
33
+ "latency" => omq_proto::options::WorkloadProfile::Latency,
34
+ _ => {
35
+ return Err(RubyErr::arg(
36
+ "workload_profile must be :throughput or :latency",
37
+ ));
38
+ }
39
+ });
40
+ }
41
+ if let Some(v) = get_opt_f64(hash, "linger")? {
42
+ opts.linger = if v.is_infinite() && v.is_sign_positive() {
43
+ None
44
+ } else {
45
+ Some(duration_from_seconds("linger", v)?)
46
+ };
47
+ }
48
+ if let Some(v) = get_opt_bytes(hash, "identity")?
49
+ && !v.is_empty()
50
+ {
51
+ opts.identity = Bytes::from(v);
52
+ }
53
+ if let Some(v) = get_opt_bool(hash, "router_mandatory")? {
54
+ opts.router_mandatory = v;
55
+ }
56
+ if let Some(v) = get_opt_bool(hash, "conflate")? {
57
+ opts.conflate = v;
58
+ }
59
+ if let Some(v) = get_opt_duration(hash, "heartbeat_interval")? {
60
+ opts.heartbeat_interval = Some(v);
61
+ }
62
+ if let Some(v) = get_opt_duration(hash, "heartbeat_ttl")? {
63
+ opts.heartbeat_ttl = Some(v);
64
+ }
65
+ if let Some(v) = get_opt_duration(hash, "heartbeat_timeout")? {
66
+ opts.heartbeat_timeout = Some(v);
67
+ }
68
+ if let Some(v) = get_opt_duration(hash, "handshake_timeout")? {
69
+ opts.handshake_timeout = Some(v);
70
+ }
71
+ if let Some(v) = get_opt_usize(hash, "max_pending_handshakes")? {
72
+ opts.max_pending_handshakes = v;
73
+ }
74
+ if let Some(v) = get_opt_usize(hash, "max_message_size")? {
75
+ opts.max_message_size = Some(v);
76
+ }
77
+ if let Some(v) = get_opt_usize(hash, "sndbuf")? {
78
+ opts.send_buffer_size = Some(v);
79
+ }
80
+ if let Some(v) = get_opt_usize(hash, "rcvbuf")? {
81
+ opts.recv_buffer_size = Some(v);
82
+ }
83
+ if let Some(v) = get_opt_usize(hash, "large_message_threshold")? {
84
+ opts.large_message_threshold = Some(v);
85
+ }
86
+ if let Some(v) = get_opt_usize(hash, "arena_threshold")? {
87
+ opts.arena_threshold = Some(v);
88
+ }
89
+ if let Some(v) = get_opt_usize(hash, "transmit_slot_cap")? {
90
+ opts.transmit_slot_cap = Some(v);
91
+ }
92
+ if let Some(v) = get_opt_bool(hash, "xpub_nodrop")? {
93
+ opts.xpub_nodrop = v;
94
+ }
95
+ if let Some(v) = get_opt_bool(hash, "reconnect_stop_conn_refused")? {
96
+ opts.reconnect_stop_conn_refused = v;
97
+ }
98
+ if let Some(v) = get_opt_bytes(hash, "compression_dict")? {
99
+ opts.compression_dict = (!v.is_empty()).then(|| Bytes::from(v));
100
+ }
101
+ if let Some(v) = get_opt_bool(hash, "compression_auto_train")? {
102
+ opts.compression_auto_train = v;
103
+ }
104
+ if let Some(v) = get_opt_usize(hash, "compression_threshold")? {
105
+ opts.compression_threshold = Some(v);
106
+ }
107
+ if let Some(v) = get_opt_i64(hash, "compression_level")? {
108
+ opts.compression_level =
109
+ Some(i32::try_from(v).map_err(|_| RubyErr::arg("compression_level must fit in i32"))?);
110
+ }
111
+ if let Some(v) = get_opt_usize(hash, "compression_dict_capacity")? {
112
+ opts.compression_dict_capacity = Some(v);
113
+ }
114
+ if let Some(v) = get_opt_usize(hash, "max_recv_dict_size")? {
115
+ opts.max_recv_dict_size = Some(v);
116
+ }
117
+ if let Some(v) = get_opt_i64(hash, "compression_offload_threshold")? {
118
+ opts.compression_offload_threshold = if v < 0 { None } else { Some(v as usize) };
119
+ }
120
+ if let Some(v) = get_opt_string(hash, "on_mute")? {
121
+ opts.on_mute = match v.as_str() {
122
+ "drop_newest" | "drop" => omq_tokio::OnMute::DropNewest,
123
+ "drop_oldest" => omq_tokio::OnMute::DropOldest,
124
+ "block" => omq_tokio::OnMute::Block,
125
+ _ => {
126
+ return Err(RubyErr::arg(
127
+ "on_mute must be :block, :drop_newest, or :drop_oldest",
128
+ ));
129
+ }
130
+ };
131
+ }
132
+
133
+ if let Some(v) = get_opt_f64(hash, "reconnect_interval")? {
134
+ opts.reconnect = omq_proto::options::ReconnectPolicy::Fixed(duration_from_seconds(
135
+ "reconnect_interval",
136
+ v,
137
+ )?);
138
+ }
139
+ if let Some(min) = get_opt_f64(hash, "reconnect_interval_min")? {
140
+ let max = get_opt_f64(hash, "reconnect_interval_max")?.unwrap_or(min * 16.0);
141
+ opts.reconnect = omq_proto::options::ReconnectPolicy::Exponential {
142
+ min: duration_from_seconds("reconnect_interval min", min)?,
143
+ max: duration_from_seconds("reconnect_interval max", max)?,
144
+ };
145
+ }
146
+
147
+ let mut mechanism_type = get_opt_string(hash, "mechanism_type")?;
148
+ if mechanism_type.is_none() {
149
+ let curve = option_present(
150
+ hash,
151
+ &[
152
+ "curve_server",
153
+ "curve_publickey",
154
+ "curve_public_key",
155
+ "curve_secretkey",
156
+ "curve_secret_key",
157
+ "curve_serverkey",
158
+ "curve_server_key",
159
+ ],
160
+ )?;
161
+ let plain = option_present(hash, &["plain_server", "plain_username", "plain_password"])?;
162
+ mechanism_type = if curve {
163
+ Some("curve".to_owned())
164
+ } else if plain {
165
+ Some("plain".to_owned())
166
+ } else {
167
+ None
168
+ };
169
+ }
170
+ if let Some(mechanism_type) = mechanism_type {
171
+ apply_mechanism(hash, &mechanism_type, &mut opts)?;
172
+ }
173
+
174
+ opts.validate()
175
+ .map_err(|error| RubyErr::arg(error.to_string()))?;
176
+ Ok(opts)
177
+ }
178
+
179
+ fn apply_mechanism(hash: VALUE, mech_type: &str, opts: &mut omq_tokio::Options) -> RbResult<()> {
180
+ match mech_type {
181
+ "null" => {}
182
+
183
+ #[cfg(feature = "curve")]
184
+ "curve" => {
185
+ let is_server =
186
+ get_opt_bool_alias(hash, &["curve_server", "mechanism_server"])?.unwrap_or(false);
187
+ let pub_key = get_opt_bytes_alias(
188
+ hash,
189
+ &[
190
+ "curve_publickey",
191
+ "curve_public_key",
192
+ "mechanism_public_key",
193
+ ],
194
+ )?;
195
+ let sec_key = get_opt_bytes_alias(
196
+ hash,
197
+ &[
198
+ "curve_secretkey",
199
+ "curve_secret_key",
200
+ "mechanism_secret_key",
201
+ ],
202
+ )?;
203
+
204
+ if is_server {
205
+ let public = parse_curve_public_key(
206
+ &pub_key
207
+ .ok_or_else(|| RubyErr::arg("CURVE server requires curve_publickey"))?,
208
+ "curve_publickey",
209
+ )?;
210
+ let secret = curve_secret_key(
211
+ &sec_key
212
+ .ok_or_else(|| RubyErr::arg("CURVE server requires curve_secretkey"))?,
213
+ "curve_secretkey",
214
+ )?;
215
+ validate_curve_keypair(&public, &secret)?;
216
+ opts.mechanism = omq_proto::MechanismSetup::CurveServer {
217
+ our_keypair: omq_proto::CurveKeypair { public, secret },
218
+ options: omq_proto::CurveServerOptions::default(),
219
+ };
220
+ } else {
221
+ let server_key = get_opt_bytes_alias(
222
+ hash,
223
+ &[
224
+ "curve_serverkey",
225
+ "curve_server_key",
226
+ "mechanism_server_key",
227
+ ],
228
+ )?;
229
+ let public = parse_curve_public_key(
230
+ &pub_key
231
+ .ok_or_else(|| RubyErr::arg("CURVE client requires curve_publickey"))?,
232
+ "curve_publickey",
233
+ )?;
234
+ let secret = curve_secret_key(
235
+ &sec_key
236
+ .ok_or_else(|| RubyErr::arg("CURVE client requires curve_secretkey"))?,
237
+ "curve_secretkey",
238
+ )?;
239
+ validate_curve_keypair(&public, &secret)?;
240
+ let server_public = parse_curve_public_key(
241
+ &server_key
242
+ .ok_or_else(|| RubyErr::arg("CURVE client requires curve_serverkey"))?,
243
+ "curve_serverkey",
244
+ )?;
245
+ opts.mechanism = omq_proto::MechanismSetup::CurveClient {
246
+ our_keypair: omq_proto::CurveKeypair { public, secret },
247
+ server_public,
248
+ };
249
+ }
250
+ }
251
+
252
+ #[cfg(feature = "plain")]
253
+ "plain" => {
254
+ if get_opt_bool_alias(hash, &["plain_server", "mechanism_server"])?.unwrap_or(false) {
255
+ opts.mechanism = omq_proto::MechanismSetup::PlainServer {
256
+ authenticator: omq_proto::Authenticator::new(|_| true),
257
+ };
258
+ } else {
259
+ let username =
260
+ get_opt_string_alias(hash, &["plain_username", "mechanism_username"])?
261
+ .ok_or_else(|| RubyErr::arg("PLAIN client requires plain_username"))?;
262
+ let password =
263
+ get_opt_string_alias(hash, &["plain_password", "mechanism_password"])?
264
+ .ok_or_else(|| RubyErr::arg("PLAIN client requires plain_password"))?;
265
+ opts.mechanism = omq_proto::MechanismSetup::PlainClient { username, password };
266
+ }
267
+ }
268
+
269
+ _ => return Err(RubyErr::arg(format!("unknown mechanism_type: {mech_type}"))),
270
+ }
271
+ Ok(())
272
+ }
273
+
274
+ #[cfg(feature = "curve")]
275
+ pub(crate) fn parse_curve_public_key(
276
+ bytes: &[u8],
277
+ label: &str,
278
+ ) -> RbResult<omq_proto::CurvePublicKey> {
279
+ if let Ok(raw) = <[u8; 32]>::try_from(bytes) {
280
+ return Ok(omq_proto::CurvePublicKey::from_bytes(raw));
281
+ }
282
+ let z85 = std::str::from_utf8(bytes)
283
+ .map_err(|_| RubyErr::arg(format!("{label} must be raw bytes or Z85 ASCII")))?;
284
+ omq_proto::CurvePublicKey::from_z85(z85)
285
+ .map_err(|error| RubyErr::arg(format!("invalid {label}: {error}")))
286
+ }
287
+
288
+ #[cfg(feature = "curve")]
289
+ fn curve_secret_key(bytes: &[u8], label: &str) -> RbResult<omq_proto::CurveSecretKey> {
290
+ if let Ok(raw) = <[u8; 32]>::try_from(bytes) {
291
+ return Ok(omq_proto::CurveSecretKey::from_bytes(raw));
292
+ }
293
+ let z85 = std::str::from_utf8(bytes)
294
+ .map_err(|_| RubyErr::arg(format!("{label} must be raw bytes or Z85 ASCII")))?;
295
+ omq_proto::CurveSecretKey::from_z85(z85)
296
+ .map_err(|error| RubyErr::arg(format!("invalid {label}: {error}")))
297
+ }
298
+
299
+ #[cfg(feature = "curve")]
300
+ fn validate_curve_keypair(
301
+ public: &omq_proto::CurvePublicKey,
302
+ secret: &omq_proto::CurveSecretKey,
303
+ ) -> RbResult<()> {
304
+ if secret.derive_public().as_bytes() == public.as_bytes() {
305
+ Ok(())
306
+ } else {
307
+ Err(RubyErr::arg("CURVE public and secret keys do not match"))
308
+ }
309
+ }
310
+
311
+ fn option_present(hash: VALUE, keys: &[&str]) -> RbResult<bool> {
312
+ for key in keys {
313
+ if rb::hash_get(hash, key)?.is_some() {
314
+ return Ok(true);
315
+ }
316
+ }
317
+ Ok(false)
318
+ }
319
+
320
+ fn get_opt_bytes_alias(hash: VALUE, keys: &[&str]) -> RbResult<Option<Vec<u8>>> {
321
+ for key in keys {
322
+ if rb::hash_get(hash, key)?.is_some() {
323
+ return get_opt_bytes(hash, key);
324
+ }
325
+ }
326
+ Ok(None)
327
+ }
328
+
329
+ fn get_opt_string_alias(hash: VALUE, keys: &[&str]) -> RbResult<Option<String>> {
330
+ for key in keys {
331
+ if rb::hash_get(hash, key)?.is_some() {
332
+ return get_opt_string(hash, key);
333
+ }
334
+ }
335
+ Ok(None)
336
+ }
337
+
338
+ fn get_opt_bool_alias(hash: VALUE, keys: &[&str]) -> RbResult<Option<bool>> {
339
+ for key in keys {
340
+ if rb::hash_get(hash, key)?.is_some() {
341
+ return get_opt_bool(hash, key);
342
+ }
343
+ }
344
+ Ok(None)
345
+ }
346
+
347
+ fn get_opt_bytes(hash: VALUE, key: &str) -> RbResult<Option<Vec<u8>>> {
348
+ match rb::hash_get(hash, key)? {
349
+ Some(v) if v == rb::qnil() => Ok(None),
350
+ Some(v) => Ok(Some(rb::value_to_bytes(v)?)),
351
+ None => Ok(None),
352
+ }
353
+ }
354
+
355
+ fn get_opt_string(hash: VALUE, key: &str) -> RbResult<Option<String>> {
356
+ match rb::hash_get(hash, key)? {
357
+ Some(v) if v == rb::qnil() => Ok(None),
358
+ Some(v) => Ok(Some(rb::value_to_string(v)?)),
359
+ None => Ok(None),
360
+ }
361
+ }
362
+
363
+ fn get_opt_i64(hash: VALUE, key: &str) -> RbResult<Option<i64>> {
364
+ match rb::hash_get(hash, key)? {
365
+ Some(v) if v == rb::qnil() => Ok(None),
366
+ Some(v) => Ok(Some(rb::value_to_i64(v)?)),
367
+ None => Ok(None),
368
+ }
369
+ }
370
+
371
+ fn get_opt_f64(hash: VALUE, key: &str) -> RbResult<Option<f64>> {
372
+ match rb::hash_get(hash, key)? {
373
+ Some(v) if v == rb::qnil() => Ok(None),
374
+ Some(v) => Ok(Some(rb::value_to_f64(v)?)),
375
+ None => Ok(None),
376
+ }
377
+ }
378
+
379
+ fn get_opt_usize(hash: VALUE, key: &str) -> RbResult<Option<usize>> {
380
+ let Some(v) = get_opt_i64(hash, key)? else {
381
+ return Ok(None);
382
+ };
383
+
384
+ usize::try_from(v)
385
+ .map(Some)
386
+ .map_err(|_| RubyErr::arg(format!("{key} must be non-negative")))
387
+ }
388
+
389
+ fn get_opt_u32(hash: VALUE, key: &str) -> RbResult<Option<u32>> {
390
+ let Some(v) = get_opt_i64(hash, key)? else {
391
+ return Ok(None);
392
+ };
393
+
394
+ u32::try_from(v)
395
+ .map(Some)
396
+ .map_err(|_| RubyErr::arg(format!("{key} must fit in a 32-bit unsigned integer")))
397
+ }
398
+
399
+ fn get_rate_limit(hash: VALUE, key: &str) -> RbResult<Option<omq_proto::MessageRateLimit>> {
400
+ let Some(value) = rb::hash_get(hash, key)? else {
401
+ return Ok(None);
402
+ };
403
+ if value == rb::qnil() {
404
+ return Ok(None);
405
+ }
406
+ rb::check_hash(value)?;
407
+
408
+ let rate = get_opt_u32(value, "messages_per_second")?
409
+ .or(get_opt_u32(value, "rate")?)
410
+ .ok_or_else(|| RubyErr::arg(format!("{key} requires :messages_per_second")))?;
411
+ let burst = get_opt_u32(value, "burst")?
412
+ .ok_or_else(|| RubyErr::arg(format!("{key} requires :burst")))?;
413
+ Ok(Some(omq_proto::MessageRateLimit::new(rate, burst)))
414
+ }
415
+
416
+ fn get_opt_bool(hash: VALUE, key: &str) -> RbResult<Option<bool>> {
417
+ match rb::hash_get(hash, key)? {
418
+ Some(v) if v == rb::qnil() => Ok(None),
419
+ Some(v) => Ok(Some(rb::value_to_bool(v)?)),
420
+ None => Ok(None),
421
+ }
422
+ }
423
+
424
+ fn get_opt_duration(hash: VALUE, key: &str) -> RbResult<Option<Duration>> {
425
+ match get_opt_f64(hash, key)? {
426
+ Some(v) => Ok(Some(duration_from_seconds(key, v)?)),
427
+ None => Ok(None),
428
+ }
429
+ }
430
+
431
+ fn duration_from_seconds(label: &str, value: f64) -> RbResult<Duration> {
432
+ Duration::try_from_secs_f64(value)
433
+ .map_err(|_| RubyErr::arg(format!("{label} must be finite and non-negative")))
434
+ }