omq-backend-rust 0.1.5 → 0.1.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c552d7ca25d216c752732579deddf1cf357a8bd1b71cfec2be322f5e1d1645ef
4
- data.tar.gz: 76e666f63269f5d69384aa04a6298144b78171c4f69d397a98ebfcb5ff1ccf9e
3
+ metadata.gz: b22c5a39d466ddff0ea811bfadff93fe58e64c3ea08bb213f61419e5efd13cc6
4
+ data.tar.gz: '09ef0d2306516c3809a957f61e234360d9f2bbe34b0389cf874c8d5cdc87abc8'
5
5
  SHA512:
6
- metadata.gz: 4a2974b9286be09db0671bf8784346e88bc58cbcdf00a3a22d58309a8a852e33164d044565315fcde4f869a11e3f294d38ed313f45196967259a1d2e46bfad81
7
- data.tar.gz: e406b0651722cff4b807096ec15b5d8cb9cfd8e5e314285250af47ecaf119b79af3e0aa46265cab53e1b4d6b7ca6eea38f7b03cd31205ed0575d186dd43f9bff
6
+ metadata.gz: bae7c0181f01e714d3fbbb11546bb84dc6642d77661734e5d5eaa5467af153a5437b0a9af1ff0d1b8f894609928d7952a83bcb492ef299e327253acdc6a1f85d
7
+ data.tar.gz: b787de3962b49bf2c784596f8fd82901e0323e74270915ba051359eb73a46022389726cea92b1a6364c7780eaf296a457d8355d90507503a5690f4de4de8410b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.7] - 2026-08-02
6
+
7
+ ### Added
8
+
9
+ - Restored Rust/Ruby backend interop coverage.
10
+
11
+ ### Changed
12
+
13
+ - Updated to `omq-tokio` 0.21.0.
14
+
15
+ ### Fixed
16
+
17
+ - Drained the Ruby-to-Tokio send pump before native socket close.
18
+ - Released Rust send ring slots before awaiting native sends, avoiding
19
+ high-throughput stalls under `omq-tokio` 0.21.0 backpressure.
20
+ - Waited for Rust peer registration before resolving `peer_connected`.
21
+
22
+ ## [0.1.6] - 2026-08-01
23
+
24
+ ### Added
25
+
26
+ - Updated to `omq-tokio` 0.20.3 and `omq-proto` 0.25.0.
27
+ - Enabled OMQ.rs `zstd+tcp://` by default.
28
+ - Forwarded compression bind/connect kwargs to current OMQ.rs options:
29
+ `dict:`, `auto_dict:`, `compression_threshold:`, `max_recv_dict_size:`,
30
+ `compression_offload_threshold:`, and zstd `level:`.
31
+
5
32
  ## [0.1.5] - 2026-07-31
6
33
 
7
34
  ### Fixed
data/README.md CHANGED
@@ -58,6 +58,18 @@ SCATTER/GATHER, CHANNEL.
58
58
  - **NULL** (default)
59
59
  - **CURVE** (CurveZMQ, via [Nuckle](https://github.com/paddor/nuckle))
60
60
 
61
+ ## Compressed transports
62
+
63
+ The Rust backend supports `lz4+tcp://` and `zstd+tcp://`. Compression
64
+ configuration uses the same bind/connect kwargs as the Ruby transports:
65
+ `level:` for zstd compression level (`-8..4`), `dict:` for a static
66
+ send-side dictionary, and `auto_dict: true` for automatic dictionary
67
+ training.
68
+
69
+ Automatic dictionary training is off by default.
70
+ OMQ.rs stores compression configuration per socket, so pass these kwargs on
71
+ the first `bind` or `connect` for a Rust-backed socket.
72
+
61
73
  ## Development
62
74
 
63
75
  ```sh
@@ -11,16 +11,17 @@ name = "omq_backend_rust"
11
11
  crate-type = ["cdylib"]
12
12
 
13
13
  [features]
14
- default = ["plain", "curve", "lz4"]
14
+ default = ["plain", "curve", "lz4", "zstd"]
15
15
  plain = ["omq-tokio/plain"]
16
16
  curve = ["omq-tokio/curve"]
17
17
  lz4 = ["omq-tokio/lz4"]
18
+ zstd = ["omq-tokio/zstd"]
18
19
 
19
20
  [dependencies]
20
- omq-proto = { version = "=0.24.1", default-features = false }
21
- omq-tokio = { version = "=0.20.2", default-features = false }
21
+ omq-proto = { version = "=0.25.0", default-features = false }
22
+ omq-tokio = { version = "=0.21.0", default-features = false }
22
23
  tokio = { version = "1.52.0", features = ["rt", "rt-multi-thread", "time", "sync", "io-util", "net"] }
23
- yring = { version = "0.3.8", features = ["async"] }
24
+ yring = { version = "0.3.11", features = ["async"] }
24
25
 
25
26
  bytes = "1.12.0"
26
27
  flume = { version = "0.12", default-features = false, features = ["async"] }
@@ -48,6 +48,29 @@ pub fn build_options(ruby: &Ruby, hash: RHash) -> Result<omq_tokio::Options, Err
48
48
  if let Some(v) = get_opt::<i64>(ruby, hash, "rcvbuf")? {
49
49
  opts.recv_buffer_size = Some(v as usize);
50
50
  }
51
+ if let Some(v) = get_opt_bytes(ruby, hash, "compression_dict")? {
52
+ if !v.is_empty() {
53
+ opts.compression_dict = Some(Bytes::from(v));
54
+ }
55
+ }
56
+ if let Some(v) = get_opt::<bool>(ruby, hash, "compression_auto_train")? {
57
+ opts.compression_auto_train = v;
58
+ }
59
+ if let Some(v) = get_opt::<i64>(ruby, hash, "compression_threshold")? {
60
+ opts.compression_threshold = Some(v as usize);
61
+ }
62
+ if let Some(v) = get_opt::<i64>(ruby, hash, "compression_level")? {
63
+ opts.compression_level = Some(v as i32);
64
+ }
65
+ if let Some(v) = get_opt::<i64>(ruby, hash, "compression_dict_capacity")? {
66
+ opts.compression_dict_capacity = Some(v as usize);
67
+ }
68
+ if let Some(v) = get_opt::<i64>(ruby, hash, "max_recv_dict_size")? {
69
+ opts.max_recv_dict_size = Some(v as usize);
70
+ }
71
+ if let Some(v) = get_opt::<i64>(ruby, hash, "compression_offload_threshold")? {
72
+ opts.compression_offload_threshold = if v < 0 { None } else { Some(v as usize) };
73
+ }
51
74
  if let Some(v) = get_opt::<String>(ruby, hash, "on_mute")? {
52
75
  opts.on_mute = match v.as_str() {
53
76
  "drop_newest" | "drop" => omq_tokio::OnMute::DropNewest,
@@ -299,20 +299,15 @@ pub fn materialize(
299
299
  let job: Job = Box::new(move || {
300
300
  let sock = Arc::new(InnerSocket::new(socket_type, options));
301
301
 
302
- const SEND_YIELD_INTERVAL: u32 = 256;
303
302
  let s = sock.clone();
304
303
  let sn = send_notify.clone();
305
304
  let send_pump = tokio::spawn(async move {
306
- futures::pin_mut!(send_cons);
307
- let mut batch = 0u32;
305
+ let mut send_cons = send_cons;
308
306
  while let Some(msg) = futures::StreamExt::next(&mut send_cons).await {
309
- let _ = s.send(msg).await;
307
+ send_cons.release();
310
308
  sn.notify();
311
- batch += 1;
312
- if batch >= SEND_YIELD_INTERVAL {
313
- batch = 0;
314
- tokio::task::yield_now().await;
315
- }
309
+ let _ = s.send(msg).await;
310
+ tokio::task::yield_now().await;
316
311
  }
317
312
  sn.notify();
318
313
  });
@@ -343,6 +338,7 @@ pub fn materialize(
343
338
  });
344
339
 
345
340
  let monitor_sock = sock.clone();
341
+ let peer_ready_sock = sock.clone();
346
342
  let monitor_pump = tokio::spawn(async move {
347
343
  let mut stream = monitor_sock.monitor();
348
344
  let mut peer_count: u32 = 0;
@@ -359,6 +355,7 @@ pub fn materialize(
359
355
  had_peers = true;
360
356
  if !peer_connected_fired {
361
357
  peer_connected_fired = true;
358
+ let _ = peer_ready_sock.connections().await;
362
359
  peer_connected_notify.force_wake();
363
360
  }
364
361
  }
@@ -374,6 +371,12 @@ pub fn materialize(
374
371
  subscriber_joined_notify.force_wake();
375
372
  }
376
373
  }
374
+ omq_tokio::MonitorEvent::JoinReceived { .. } => {
375
+ if !subscriber_joined_fired {
376
+ subscriber_joined_fired = true;
377
+ subscriber_joined_notify.force_wake();
378
+ }
379
+ }
377
380
  _ => {}
378
381
  }
379
382
 
@@ -432,24 +435,59 @@ pub fn destroy_socket(
432
435
  io_threads: usize,
433
436
  sock: Arc<InnerSocket>,
434
437
  send_prod: Mutex<yring::AsyncProducer<omq_tokio::Message>>,
435
- send_pump: JoinHandle<()>,
438
+ mut send_pump: JoinHandle<()>,
436
439
  recv_pump: JoinHandle<()>,
437
440
  monitor_pump: JoinHandle<()>,
438
441
  linger: Option<Duration>,
439
442
  ) {
440
443
  recv_pump.abort();
441
444
  monitor_pump.abort();
442
- send_pump.abort();
443
- drop(send_prod);
444
445
  let Ok(handle) = (|| -> std::result::Result<Handle, ()> { Ok(ensure_runtime(io_threads)) })()
445
446
  else {
447
+ send_pump.abort();
448
+ drop(send_prod);
446
449
  return;
447
450
  };
448
451
  let close_timeout = linger
449
452
  .unwrap_or(Duration::from_secs(30))
450
453
  .max(Duration::from_millis(10));
451
- handle.spawn(async move {
454
+ let fut = async move {
455
+ drop(send_prod);
456
+ if tokio::time::timeout(close_timeout, &mut send_pump)
457
+ .await
458
+ .is_err()
459
+ {
460
+ send_pump.abort();
461
+ let _ = send_pump.await;
462
+ }
463
+
452
464
  let s = Arc::try_unwrap(sock).unwrap_or_else(|arc| (*arc).clone());
453
465
  let _ = tokio::time::timeout(close_timeout, s.close()).await;
466
+ };
467
+
468
+ let (otx, orx) = flume::bounded::<()>(1);
469
+ handle.spawn(async move {
470
+ fut.await;
471
+ let _ = otx.send(());
454
472
  });
473
+
474
+ struct RecvBox {
475
+ rx: flume::Receiver<()>,
476
+ }
477
+
478
+ extern "C" fn blocking_recv(data: *mut libc::c_void) -> *mut libc::c_void {
479
+ let rd = unsafe { &mut *(data as *mut RecvBox) };
480
+ let _ = rd.rx.recv();
481
+ std::ptr::null_mut()
482
+ }
483
+
484
+ let mut rd = RecvBox { rx: orx };
485
+ unsafe {
486
+ rb_sys::rb_thread_call_without_gvl(
487
+ Some(blocking_recv),
488
+ &mut rd as *mut RecvBox as *mut libc::c_void,
489
+ None,
490
+ std::ptr::null_mut(),
491
+ );
492
+ }
455
493
  }
@@ -19,12 +19,13 @@ module OMQ
19
19
  @peer_connected = Async::Promise.new
20
20
  @all_peers_gone = Async::Promise.new
21
21
  @subscriber_joined = Async::Promise.new
22
- @connections = []
22
+ @connections = {}
23
23
  @closed = false
24
24
  @parent_task = nil
25
25
  @on_io_thread = false
26
26
  @materialized = false
27
27
  @recv_sentinels = 0
28
+ @compression_options = {}
28
29
 
29
30
  @native = Native::RustSocket.new(socket_type.to_s)
30
31
 
@@ -47,16 +48,18 @@ module OMQ
47
48
  end
48
49
 
49
50
 
50
- def bind(endpoint, parent: nil, **)
51
+ def bind(endpoint, parent: nil, **opts)
51
52
  capture_parent_task(parent: parent)
53
+ apply_endpoint_options!(opts)
52
54
  ensure_materialized
53
55
  resolved = @native.bind(endpoint)
54
56
  URI.parse(resolved)
55
57
  end
56
58
 
57
59
 
58
- def connect(endpoint, parent: nil, **)
60
+ def connect(endpoint, parent: nil, **opts)
59
61
  capture_parent_task(parent: parent)
62
+ apply_endpoint_options!(opts)
60
63
  ensure_materialized
61
64
  @native.connect(endpoint)
62
65
  URI.parse(endpoint)
@@ -219,6 +222,7 @@ module OMQ
219
222
  monitor_io.wait_readable
220
223
  monitor_io.read_nonblock(256, exception: false)
221
224
  while (data = @native.try_recv_monitor)
225
+ track_connection_event(data)
222
226
  @monitor_queue.enqueue(MonitorEvent.new(**data))
223
227
  end
224
228
  end
@@ -226,6 +230,23 @@ module OMQ
226
230
  end
227
231
 
228
232
 
233
+ def track_connection_event(data)
234
+ detail = data[:detail] || {}
235
+ connection_id = detail[:connection_id]
236
+
237
+ case data[:type]
238
+ when :handshake_succeeded
239
+ @connections[connection_id || Object.new] = true
240
+ when :disconnected
241
+ if connection_id
242
+ @connections.delete(connection_id)
243
+ else
244
+ @connections.shift
245
+ end
246
+ end
247
+ end
248
+
249
+
229
250
  def extract_options
230
251
  h = {}
231
252
  h["send_hwm"] = @options.send_hwm
@@ -241,6 +262,7 @@ module OMQ
241
262
  h["sndbuf"] = @options.sndbuf
242
263
  h["rcvbuf"] = @options.rcvbuf
243
264
  h["on_mute"] = @options.on_mute.to_s
265
+ h.merge!(@compression_options)
244
266
 
245
267
  ri = @options.reconnect_interval
246
268
  if ri.is_a?(Range)
@@ -256,6 +278,88 @@ module OMQ
256
278
  end
257
279
 
258
280
 
281
+ def apply_endpoint_options!(opts)
282
+ compression = extract_endpoint_compression_options(opts)
283
+ return if compression.empty?
284
+
285
+ if @materialized
286
+ existing = compression.keys.to_h do |key|
287
+ [key, @compression_options.fetch(key, default_compression_option(key))]
288
+ end
289
+ return if compression == existing
290
+
291
+ raise ArgumentError,
292
+ "Rust backend compression options must be set before first bind/connect"
293
+ end
294
+
295
+ @compression_options.merge!(compression)
296
+ end
297
+
298
+
299
+ def extract_endpoint_compression_options(opts)
300
+ out = {}
301
+
302
+ if opts.key?(:level)
303
+ validate_zstd_level!(opts[:level])
304
+ out["compression_level"] = opts[:level]
305
+ end
306
+ out["compression_dict"] = opts[:dict].b if opts.key?(:dict) && opts[:dict]
307
+
308
+ if opts.key?(:auto_dict)
309
+ auto_dict = opts[:auto_dict]
310
+ if auto_dict && opts[:dict]
311
+ raise ArgumentError, "cannot combine auto_dict: and dict:"
312
+ end
313
+
314
+ case auto_dict
315
+ when nil, false
316
+ out["compression_auto_train"] = false
317
+ when true
318
+ out["compression_auto_train"] = true
319
+ when Hash
320
+ if auto_dict.key?(:trigger)
321
+ raise ArgumentError,
322
+ "Rust backend does not support auto_dict: trigger"
323
+ end
324
+ validate_positive!("auto_dict capacity", auto_dict[:capacity]) if auto_dict[:capacity]
325
+ out["compression_auto_train"] = true
326
+ out["compression_dict_capacity"] = auto_dict[:capacity] if auto_dict[:capacity]
327
+ else
328
+ raise TypeError, "auto_dict: must be true, false, or a Hash; got #{auto_dict.class}"
329
+ end
330
+ end
331
+
332
+ if opts.key?(:compression_threshold)
333
+ out["compression_threshold"] = opts[:compression_threshold]
334
+ end
335
+ out["max_recv_dict_size"] = opts[:max_recv_dict_size] if opts.key?(:max_recv_dict_size)
336
+ if opts.key?(:compression_offload_threshold)
337
+ out["compression_offload_threshold"] = opts[:compression_offload_threshold] || -1
338
+ end
339
+
340
+ out
341
+ end
342
+
343
+
344
+ def default_compression_option(key)
345
+ key == "compression_auto_train" ? false : nil
346
+ end
347
+
348
+
349
+ def validate_positive!(label, value)
350
+ return if value.respond_to?(:positive?) && value.positive?
351
+
352
+ raise ArgumentError, "#{label} must be positive"
353
+ end
354
+
355
+
356
+ def validate_zstd_level!(level)
357
+ return if level.is_a?(Integer) && (-8..4).cover?(level)
358
+
359
+ raise ArgumentError, "zstd compression level must be -8..4, got #{level.inspect}"
360
+ end
361
+
362
+
259
363
  def extract_mechanism(h)
260
364
  mech = @options.mechanism
261
365
  case mech
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OMQ
4
4
  module Rust
5
- VERSION = "0.1.5"
5
+ VERSION = "0.1.7"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-backend-rust
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger