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 +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +12 -0
- data/ext/omq_backend_rust/Cargo.toml +5 -4
- data/ext/omq_backend_rust/src/options.rs +23 -0
- data/ext/omq_backend_rust/src/runtime.rs +51 -13
- data/lib/omq/rust/engine.rb +107 -3
- data/lib/omq/rust/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b22c5a39d466ddff0ea811bfadff93fe58e64c3ea08bb213f61419e5efd13cc6
|
|
4
|
+
data.tar.gz: '09ef0d2306516c3809a957f61e234360d9f2bbe34b0389cf874c8d5cdc87abc8'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
21
|
-
omq-tokio = { version = "=0.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
send_cons.release();
|
|
310
308
|
sn.notify();
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
}
|
data/lib/omq/rust/engine.rb
CHANGED
|
@@ -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
|
data/lib/omq/rust/version.rb
CHANGED