omq-backend-rust 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec3e72f8903532375903dac4637b6f4ebc1c9b09a0f2d1d1475420797464e800
4
- data.tar.gz: d4cb76d8140f6f10d3b4eda7f9f720a7901407dcf0ea45da3e7a48989a137ca4
3
+ metadata.gz: 4f12671673afe657015b59cf65873132c88c5cbb15cf34962b54318fb944310c
4
+ data.tar.gz: 3b7de77e6e01dc461fa0aa969870472442a2f9b3b4657fa5b35ca1fe90bd8940
5
5
  SHA512:
6
- metadata.gz: 5921c0fcc3fd18908cbc142ae481aa4e61d8005ab68f2ebc61fd316f65089f8d6453acddb76e073a2c06e0a47581900878a47d5c0dc2b7899bb2bd815a500fec
7
- data.tar.gz: 7401afef781cc1000b175dd1e28f89241e2002abfc0a415d8bedd1d5a341398dce599574a6ced626f17a8c94ab3add861699f254e4f32a2d8f439322add19272
6
+ metadata.gz: 77dda34fcf69ef4d048d74276b1ae5cd455b23086de0a826f751862014e0d048dd8f27ccc2042f5332ff91a5410f11243f687450ca0093e58ab19377b64ec9dd
7
+ data.tar.gz: 2536562047da85d828e732a03c5a551a85b2b0774821f3ada32f890ba48f4453696c0243ef5e66248cbdee763623d1cf0f9b45bb74ac5e9aedba6aab014558ed
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.0] - 2026-08-26
6
+
7
+ ### Added
8
+
9
+ - Added JRuby support through OMQ.java Maven artifacts for Linux x86_64,
10
+ macOS aarch64, macOS x86_64, and Windows x86_64.
11
+
12
+ ### Changed
13
+
14
+ - Replaced the private MRI/TruffleRuby native extension with the first-class
15
+ `omq-rs` binding.
16
+
5
17
  ## [0.2.0] - 2026-08-08
6
18
 
7
19
  ### Added
data/README.md CHANGED
@@ -6,12 +6,16 @@
6
6
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
7
7
 
8
8
  Rust-backed engine for [OMQ](https://github.com/zeromq/omq.rb). Same socket API,
9
- but networking runs on a [Tokio](https://tokio.rs/) runtime inside a native
10
- extension compiled via [rb_sys](https://github.com/oxidize-rb/rb-sys).
9
+ but networking runs through the first-class
10
+ [omq-rs](https://github.com/paddor/omq.rs/tree/main/bindings/ruby) binding on
11
+ MRI and TruffleRuby, or through OMQ.java on JRuby.
11
12
 
12
13
  ## Install
13
14
 
14
- Requires a Rust toolchain (stable) at gem install time.
15
+ The `omq-rs` dependency compiles OMQ.rs during installation on MRI and
16
+ TruffleRuby. JRuby uses OMQ.java on Java 25 or newer. The Java gem selects the
17
+ matching OMQ.java artifact on Linux x86_64, macOS aarch64, macOS x86_64, and
18
+ Windows x86_64.
15
19
 
16
20
  ```ruby
17
21
  # Gemfile
@@ -73,8 +77,8 @@ the first `bind` or `connect` for a Rust-backed socket.
73
77
  ## Development
74
78
 
75
79
  ```sh
76
- OMQ_DEV=1 bundle install
77
- OMQ_DEV=1 bundle exec rake
80
+ OMQ_RS_PATH=/path/to/omq.rs/bindings/ruby bundle install
81
+ OMQ_RS_PATH=/path/to/omq.rs/bindings/ruby bundle exec rake
78
82
  ```
79
83
 
80
84
  ## License
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "omq"
4
- require_relative "../rust/omq_backend_rust"
5
4
  require_relative "../rust/version"
6
- require_relative "../rust/engine"
7
5
 
8
- module OMQ
9
- module Rust
10
- @io_threads = 1
6
+ if RUBY_ENGINE == "jruby"
7
+ require_relative "../rust/java/engine"
8
+ else
9
+ require "omq/rs"
10
+ require_relative "../rust/engine"
11
+ end
12
+
13
+ if RUBY_ENGINE == "jruby"
14
+ module OMQ
15
+ module Rust
16
+ @io_threads = 1
11
17
 
12
- class << self
13
- attr_accessor :io_threads
18
+ class << self
19
+ attr_accessor :io_threads
20
+ end
14
21
  end
15
22
  end
16
23
  end
@@ -27,8 +27,10 @@ module OMQ
27
27
  @materialized = false
28
28
  @recv_sentinels = 0
29
29
  @compression_options = {}
30
-
31
- @native = Native::RustSocket.new(socket_type.to_s)
30
+ @socket = nil
31
+ @monitor_task = nil
32
+ @monitor_watching = false
33
+ @had_peer = false
32
34
 
33
35
  @routing = RoutingStub.new(self)
34
36
  end
@@ -55,7 +57,7 @@ module OMQ
55
57
  capture_parent_task(parent: parent)
56
58
  apply_endpoint_options!(opts)
57
59
  ensure_materialized
58
- resolved = @native.bind(endpoint)
60
+ resolved = @socket.bind(endpoint)
59
61
  URI.parse(resolved)
60
62
  end
61
63
 
@@ -64,54 +66,39 @@ module OMQ
64
66
  capture_parent_task(parent: parent)
65
67
  apply_endpoint_options!(opts)
66
68
  ensure_materialized
67
- @native.connect(endpoint)
69
+ @socket.connect(endpoint)
68
70
  URI.parse(endpoint)
69
71
  end
70
72
 
71
73
 
72
74
  def disconnect(endpoint)
73
- @native.disconnect(endpoint)
75
+ @socket.disconnect(endpoint)
74
76
  end
75
77
 
76
78
 
77
79
  def unbind(endpoint)
78
- @native.unbind(endpoint)
80
+ @socket.unbind(endpoint)
79
81
  end
80
82
 
81
83
 
82
84
  def enqueue_send(parts)
83
85
  ensure_materialized
84
- result = @native.enqueue_send(parts)
85
- return if result == :ok
86
-
87
- @send_signal_r ||= io_for_native_fd(@native.send_fd)
88
- loop do
89
- result = @native.enqueue_send(parts)
90
- return if result == :ok
91
-
92
- @send_signal_r.wait_readable
93
- @send_signal_r.read_nonblock(256, exception: false)
94
- end
86
+ @socket.send(normalize_outgoing(parts))
87
+ nil
95
88
  end
96
89
 
97
90
 
98
91
  def dequeue_recv
99
92
  ensure_materialized
100
93
 
101
- if @recv_batch && !@recv_batch.empty?
102
- return @recv_batch.shift
103
- end
104
-
105
- msg = try_recv_batch
94
+ msg = try_recv
106
95
  return msg if msg
107
96
 
108
97
  return take_recv_sentinel if @recv_sentinels.positive?
109
98
 
110
99
  loop do
111
- @recv_signal_r.wait_readable
112
- @recv_signal_r.read_nonblock(256, exception: false)
113
-
114
- msg = try_recv_batch
100
+ @socket.wait_readable
101
+ msg = try_recv
115
102
  return msg if msg
116
103
 
117
104
  return take_recv_sentinel if @recv_sentinels.positive?
@@ -121,7 +108,7 @@ module OMQ
121
108
 
122
109
  def dequeue_recv_sentinel
123
110
  @recv_sentinels += 1
124
- @native.wake_recv if @materialized
111
+ @socket.wake_recv if @materialized
125
112
  nil
126
113
  end
127
114
 
@@ -134,9 +121,8 @@ module OMQ
134
121
  @all_peers_gone.resolve(nil) unless @all_peers_gone.resolved?
135
122
  @subscriber_joined.resolve(nil) unless @subscriber_joined.resolved?
136
123
  FdWatcher.unwatch_owner(self) unless Reactor.native_fiber_scheduler?
137
- @native.close
138
- close_io_wrapper(@recv_signal_r)
139
- close_io_wrapper(@send_signal_r)
124
+ @socket&.close
125
+ @monitor_task&.stop
140
126
  end
141
127
 
142
128
 
@@ -179,19 +165,10 @@ module OMQ
179
165
  return if @materialized
180
166
 
181
167
  capture_parent_task unless @parent_task
182
- Native.send(:io_threads=, OMQ::Rust.io_threads)
183
- @native.set_options(extract_options)
184
- @native.materialize
185
- @recv_signal_r = io_for_native_fd(@native.recv_fd)
186
- @materialized = true
187
-
188
- @routing.replay_pending(@native)
189
-
190
- spawn_lifecycle_watcher(@native.peer_connected_fd, @peer_connected)
191
- spawn_lifecycle_watcher(@native.all_peers_gone_fd, @all_peers_gone)
192
- spawn_lifecycle_watcher(@native.subscriber_joined_fd, @subscriber_joined)
193
-
194
- start_monitor_forwarder if @monitor_queue
168
+ @socket = OMQ::Rust.socket(@socket_type, **extract_options.transform_keys(&:to_sym))
169
+ @materialized = true
170
+ @routing.replay_pending(@socket)
171
+ start_monitor_forwarder
195
172
  end
196
173
 
197
174
 
@@ -201,89 +178,82 @@ module OMQ
201
178
  end
202
179
 
203
180
 
204
- def try_recv_batch
205
- batch = @native.try_recv_batch
206
- return unless batch
181
+ def try_recv
182
+ message = @socket.try_recv
183
+ return unless message
184
+ return message unless @socket_type.to_sym == :SERVER
207
185
 
208
- msg = batch.shift
209
- @recv_batch = batch unless batch.empty?
210
- msg
186
+ routing_id, *parts = message
187
+ [[routing_id].pack("N"), *parts]
211
188
  end
212
189
 
213
190
 
214
- def spawn_lifecycle_watcher(fd, promise)
215
- if Reactor.native_fiber_scheduler?
216
- io = io_for_native_fd(fd)
217
- @parent_task.async(transient: true) do
218
- io.wait_readable
219
- promise.resolve(true) unless promise.resolved? || @closed
220
- rescue IOError, Errno::EBADF
221
- ensure
222
- close_io_wrapper(io)
223
- end
224
- else
225
- FdWatcher.watch_once(fd, owner: self) do
226
- promise.resolve(true) unless promise.resolved? || @closed
227
- end
191
+ def normalize_outgoing(parts)
192
+ return parts unless @socket_type.to_sym == :SERVER
193
+
194
+ routing_id, *body = parts
195
+ unless routing_id.is_a?(String) && routing_id.bytesize == 4
196
+ raise ArgumentError, "SERVER routing ID must be a 4-byte String"
228
197
  end
198
+ [routing_id.unpack1("N"), *body]
229
199
  end
230
200
 
231
201
 
232
202
  def start_monitor_forwarder
203
+ return if @monitor_task || @monitor_watching
204
+
233
205
  if Reactor.native_fiber_scheduler?
234
- monitor_io = io_for_native_fd(@native.monitor_fd)
235
- @parent_task.async(transient: true, annotation: "rust-monitor") do
236
- until @closed
237
- monitor_io.wait_readable
238
- monitor_io.read_nonblock(256, exception: false)
239
- while (data = @native.try_recv_monitor)
240
- track_connection_event(data)
241
- @monitor_queue.enqueue(MonitorEvent.new(**data))
242
- end
243
- end
244
- rescue IOError, Errno::EBADF
245
- ensure
246
- close_io_wrapper(monitor_io)
206
+ @monitor_task = @parent_task.async(transient: true, annotation: "rust-monitor") do
207
+ monitor_loop
247
208
  end
248
209
  else
249
- FdWatcher.watch_loop(@native.monitor_fd, owner: self) do |io|
210
+ @monitor_watching = true
211
+ FdWatcher.watch_loop(@socket.monitor_fd, owner: self) do |io|
250
212
  io.read_nonblock(256, exception: false)
251
- while (data = @native.try_recv_monitor)
252
- track_connection_event(data)
253
- @monitor_queue.enqueue(MonitorEvent.new(**data))
213
+ while (data = @socket.try_monitor_event)
214
+ handle_monitor_event(data)
254
215
  end
216
+ @socket.monitor_fd unless @closed
255
217
  end
256
218
  end
257
219
  end
258
220
 
259
221
 
260
- def io_for_native_fd(fd)
261
- IO.for_fd(fd, autoclose: false)
262
- end
222
+ def monitor_loop
223
+ until @closed
224
+ data = @socket.monitor.recv
225
+ break unless data
263
226
 
264
-
265
- def close_io_wrapper(io)
266
- return unless io && !io.closed?
267
-
268
- io.close
269
- rescue IOError, SystemCallError
227
+ handle_monitor_event(data)
228
+ end
229
+ rescue IOError, Errno::EBADF
230
+ raise unless @closed
270
231
  end
271
232
 
272
233
 
273
- def track_connection_event(data)
274
- detail = data[:detail] || {}
275
- connection_id = detail[:connection_id]
234
+ def handle_monitor_event(data)
235
+ type = data.fetch(:event)
236
+ endpoint = data[:endpoint]
237
+ connection_id = data[:connection_id]
276
238
 
277
- case data[:type]
239
+ case type
278
240
  when :handshake_succeeded
241
+ @had_peer = true
279
242
  @connections[connection_id || Object.new] = true
243
+ @peer_connected.resolve(true) unless @peer_connected.resolved?
280
244
  when :disconnected
281
- if connection_id
282
- @connections.delete(connection_id)
283
- else
284
- @connections.shift
245
+ connection_id ? @connections.delete(connection_id) : @connections.shift
246
+ if @had_peer && @connections.empty? && !@all_peers_gone.resolved?
247
+ @all_peers_gone.resolve(true)
285
248
  end
249
+ when :subscribe_received
250
+ @subscriber_joined.resolve(true) unless @subscriber_joined.resolved?
286
251
  end
252
+
253
+ return unless @monitor_queue
254
+
255
+ detail = data.except(:event, :endpoint)
256
+ @monitor_queue.enqueue(MonitorEvent.new(type:, endpoint:, detail: detail.empty? ? nil : detail))
287
257
  end
288
258
 
289
259
 
@@ -444,9 +414,9 @@ module OMQ
444
414
 
445
415
 
446
416
  def subscribe(prefix)
447
- native = @engine.instance_variable_get(:@native)
417
+ socket = @engine.instance_variable_get(:@socket)
448
418
  if @engine.instance_variable_get(:@materialized)
449
- native.subscribe(prefix.b)
419
+ socket.subscribe(prefix.b)
450
420
  else
451
421
  @pending_subscribe << prefix.b
452
422
  end
@@ -454,14 +424,14 @@ module OMQ
454
424
 
455
425
 
456
426
  def unsubscribe(prefix)
457
- @engine.instance_variable_get(:@native).unsubscribe(prefix.b)
427
+ @engine.instance_variable_get(:@socket).unsubscribe(prefix.b)
458
428
  end
459
429
 
460
430
 
461
431
  def join(group)
462
- native = @engine.instance_variable_get(:@native)
432
+ socket = @engine.instance_variable_get(:@socket)
463
433
  if @engine.instance_variable_get(:@materialized)
464
- native.join(group)
434
+ socket.join(group)
465
435
  else
466
436
  @pending_join << group
467
437
  end
@@ -469,14 +439,14 @@ module OMQ
469
439
 
470
440
 
471
441
  def leave(group)
472
- @engine.instance_variable_get(:@native).leave(group)
442
+ @engine.instance_variable_get(:@socket).leave(group)
473
443
  end
474
444
 
475
445
 
476
- def replay_pending(native)
477
- @pending_subscribe.each { |p| native.subscribe(p) }
446
+ def replay_pending(socket)
447
+ @pending_subscribe.each { |p| socket.subscribe(p) }
478
448
  @pending_subscribe.clear
479
- @pending_join.each { |g| native.join(g) }
449
+ @pending_join.each { |g| socket.join(g) }
480
450
  @pending_join.clear
481
451
  end
482
452
  end