omq-backend-rust 0.3.0-java

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9895c2e753f379324c116f3082c76ab5d562e9d5b7e00539073bdfdd90fa2ac9
4
+ data.tar.gz: 11cdd6699b6e7efa69a2e8e84a65641406732d3fffb33326311925c009b98c24
5
+ SHA512:
6
+ metadata.gz: 58c0e0103fe6cffd7d73373045cdce94f113f9c7d520e7a7de909e7a311618318790e5bf7c01685efb83f7bea927b5af7588474189942c29f1f38a4f2ea36d3b
7
+ data.tar.gz: 95dc600672be8c9ee8a5341b79ce090856ed8129053437fb4ca340d70e039cea9f980cc751ebffb1504f035ac827a1aee4ac837fc0da909e30db379e1c487a41
data/CHANGELOG.md ADDED
@@ -0,0 +1,123 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
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
+
17
+ ## [0.2.0] - 2026-08-08
18
+
19
+ ### Added
20
+
21
+ - Added TruffleRuby support for the Rust backend.
22
+ - Added a shared fallback file descriptor watcher for Rubies without a
23
+ native `Fiber.scheduler`.
24
+ - Added TruffleRuby CI coverage for the Rust backend.
25
+
26
+ ### Changed
27
+
28
+ - Updated to `omq-tokio` 0.21.1 and `omq-proto` 0.25.1.
29
+ - Replaced Magnus with a small `rb-sys` helper layer that uses public
30
+ Ruby C API calls.
31
+
32
+ ### Fixed
33
+
34
+ - Raised Ruby exceptions through public C API entry points for
35
+ TruffleRuby compatibility.
36
+ - Rejected invalid negative numeric options before they can overflow Rust
37
+ sizes or panic on invalid durations.
38
+
39
+ ## [0.1.7] - 2026-08-02
40
+
41
+ ### Added
42
+
43
+ - Restored Rust/Ruby backend interop coverage.
44
+
45
+ ### Changed
46
+
47
+ - Updated to `omq-tokio` 0.21.0.
48
+
49
+ ### Fixed
50
+
51
+ - Drained the Ruby-to-Tokio send pump before native socket close.
52
+ - Released Rust send ring slots before awaiting native sends, avoiding
53
+ high-throughput stalls under `omq-tokio` 0.21.0 backpressure.
54
+ - Waited for Rust peer registration before resolving `peer_connected`.
55
+
56
+ ## [0.1.6] - 2026-08-01
57
+
58
+ ### Added
59
+
60
+ - Updated to `omq-tokio` 0.20.3 and `omq-proto` 0.25.0.
61
+ - Enabled OMQ.rs `zstd+tcp://` by default.
62
+ - Forwarded compression bind/connect kwargs to current OMQ.rs options:
63
+ `dict:`, `auto_dict:`, `compression_threshold:`, `max_recv_dict_size:`,
64
+ `compression_offload_threshold:`, and zstd `level:`.
65
+
66
+ ## [0.1.5] - 2026-07-31
67
+
68
+ ### Fixed
69
+
70
+ - Used `IO#wait_readable` for Rust backend receive waits and routed
71
+ `close_read` wakeups through the native receive notifier.
72
+ - Updated to `omq-tokio` 0.20.2, fixing large byte-stream `PUB` fan-out
73
+ delivery over IPC and TCP.
74
+
75
+ ## [0.1.4] - 2026-07-30
76
+
77
+ ### Fixed
78
+
79
+ - Accepted Ruby string identities when materializing Rust sockets.
80
+
81
+ ## [0.1.3] - 2026-07-30
82
+
83
+ ### Added
84
+
85
+ - Added `require "omq/backend/rust"` as the canonical backend load path.
86
+
87
+ ### Removed
88
+
89
+ - Removed the old `require "omq/rust"` load path.
90
+
91
+ ## 0.1.2 - 2026-07-28
92
+
93
+ ### Changed
94
+
95
+ - Updated to `omq-tokio` 0.20.1 and `omq-proto` 0.24.1.
96
+ - Marked the native extension Ractor-safe for Ruby 4 Ractors.
97
+ - Adapted CURVE server setup to `CurveServerOptions`.
98
+
99
+ ## 0.1.1 - 2026-07-23
100
+
101
+ ### Changed
102
+
103
+ - Updated to `omq-tokio` 0.19.3 and `omq-proto` 0.23.2.
104
+ - Removed obsolete Blake3ZMQ support.
105
+ - Moved release source to the `zeromq/omq.rb` monorepo.
106
+
107
+ ## v0.1.0 — 2026-06-24
108
+
109
+ Initial release.
110
+
111
+ ### Added
112
+
113
+ - **`OMQ::Rust::Engine`** — drop-in OMQ engine backed by omq-tokio. Pass
114
+ `backend: :rust` to any OMQ socket constructor.
115
+ - All standard socket types: REQ/REP, PUB/SUB, PUSH/PULL, DEALER/ROUTER,
116
+ XPUB/XSUB, PAIR.
117
+ - All draft socket types: CLIENT/SERVER, RADIO/DISH, SCATTER/GATHER, CHANNEL.
118
+ - TCP and IPC transports.
119
+ - CURVE (CurveZMQ) and BLAKE3ZMQ security mechanisms.
120
+ - Full cross-backend interop with the default Ruby engine.
121
+ - Lifecycle promises: `peer_connected`, `all_peers_gone`, `subscriber_joined`.
122
+ - Monitor event forwarding from the Tokio runtime.
123
+ - Configurable IO thread count via `OMQ::Rust.io_threads`.
data/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025-2026, Patrik Wenger
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # omq-backend-rust
2
+
3
+ [![CI](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml/badge.svg)](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/omq-backend-rust?color=e9573f)](https://rubygems.org/gems/omq-backend-rust)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
7
+
8
+ Rust-backed engine for [OMQ](https://github.com/zeromq/omq.rb). Same socket API,
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.
12
+
13
+ ## Install
14
+
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.
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem "omq-backend-rust"
23
+ ```
24
+
25
+ ```sh
26
+ gem install omq-backend-rust
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ruby
32
+ require "omq"
33
+ require "omq/backend/rust"
34
+
35
+ Async do
36
+ push = OMQ::PUSH.new(backend: :rust)
37
+ pull = OMQ::PULL.new(backend: :rust)
38
+
39
+ port = pull.bind("tcp://127.0.0.1:0").port
40
+ push.connect("tcp://127.0.0.1:#{port}")
41
+ push.peer_connected.wait
42
+
43
+ push << "hello from Rust"
44
+ p pull.receive # => ["hello from Rust"]
45
+ ensure
46
+ push&.close
47
+ pull&.close
48
+ end
49
+ ```
50
+
51
+ The `:rust` backend is fully interoperable with the default `:ruby` backend.
52
+ Mix backends freely within the same process.
53
+
54
+ ## Supported socket types
55
+
56
+ All standard and draft ZMTP socket types: REQ/REP, PUB/SUB, PUSH/PULL,
57
+ DEALER/ROUTER, XPUB/XSUB, PAIR, CLIENT/SERVER, RADIO/DISH,
58
+ SCATTER/GATHER, CHANNEL.
59
+
60
+ ## Security mechanisms
61
+
62
+ - **NULL** (default)
63
+ - **CURVE** (CurveZMQ, via [Nuckle](https://github.com/paddor/nuckle))
64
+
65
+ ## Compressed transports
66
+
67
+ The Rust backend supports `lz4+tcp://` and `zstd+tcp://`. Compression
68
+ configuration uses the same bind/connect kwargs as the Ruby transports:
69
+ `level:` for zstd compression level (`-8..4`), `dict:` for a static
70
+ send-side dictionary, and `auto_dict: true` for automatic dictionary
71
+ training.
72
+
73
+ Automatic dictionary training is off by default.
74
+ OMQ.rs stores compression configuration per socket, so pass these kwargs on
75
+ the first `bind` or `connect` for a Rust-backed socket.
76
+
77
+ ## Development
78
+
79
+ ```sh
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
82
+ ```
83
+
84
+ ## License
85
+
86
+ [ISC](LICENSE)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "omq"
4
+ require_relative "../rust/version"
5
+
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
17
+
18
+ class << self
19
+ attr_accessor :io_threads
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ OMQ::Backend.register(:rust, OMQ::Rust::Engine)
@@ -0,0 +1,642 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jar_dependencies"
4
+ require "thread"
5
+ require "uri"
6
+ require "java"
7
+
8
+ require_relative "../version"
9
+ require_relative "platform"
10
+
11
+ require_jar "io.github.paddor", "omq-java",
12
+ OMQ::Rust::Java.classifier, OMQ::Rust::OMQ_JAVA_VERSION
13
+
14
+ module OMQ
15
+ module Rust
16
+ module Java
17
+ Duration = ::Java::JavaTime::Duration
18
+ JavaOMQ = ::Java::IoOmq::OMQ
19
+ SocketOptions = ::Java::IoOmq::SocketOptions
20
+ SocketType = ::Java::IoOmq::SocketType
21
+ Message = ::Java::IoOmq::Message
22
+ CurveKeypair = ::Java::IoOmq::CurveKeypair
23
+ OnMute = ::Java::IoOmq::OnMute
24
+ @context_mutex = Mutex.new
25
+
26
+ class << self
27
+ def context
28
+ @context_mutex.synchronize do
29
+ @context ||= JavaOMQ.context(OMQ::Rust.io_threads.to_i).tap do
30
+ at_exit { @context&.close rescue nil } unless @context_at_exit
31
+ @context_at_exit = true
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ class Promise
38
+ def initialize(&resolver)
39
+ @mutex = Mutex.new
40
+ @cv = ConditionVariable.new
41
+ @resolved = false
42
+ @value = nil
43
+ @resolver = resolver
44
+ end
45
+
46
+
47
+ def resolved?
48
+ @mutex.synchronize { @resolved }
49
+ end
50
+
51
+
52
+ def resolve(value = nil)
53
+ @mutex.synchronize do
54
+ return @value if @resolved
55
+
56
+ @resolved = true
57
+ @value = value
58
+ @cv.broadcast
59
+ end
60
+ end
61
+
62
+
63
+ def wait
64
+ until resolved?
65
+ if @resolver
66
+ @resolver.call(self)
67
+ else
68
+ @mutex.synchronize { @cv.wait(@mutex, 0.05) unless @resolved }
69
+ end
70
+ end
71
+
72
+ @value
73
+ end
74
+ end
75
+
76
+ class Engine
77
+ POLL_SECONDS = 0.005
78
+ POLL_DURATION = Duration.ofMillis((POLL_SECONDS * 1000).to_i)
79
+
80
+ attr_reader :options, :connections, :routing, :socket_type
81
+ attr_reader :peer_connected, :all_peers_gone, :parent_task
82
+ attr_reader :on_io_thread
83
+ alias on_io_thread? on_io_thread
84
+ attr_writer :reconnect_enabled
85
+ attr_accessor :subscriber_joined
86
+
87
+
88
+ def initialize(socket_type, options)
89
+ @socket_type = socket_type
90
+ @options = options
91
+ @connections = {}
92
+ @closed = false
93
+ @parent_task = nil
94
+ @on_io_thread = false
95
+ @materialized = false
96
+ @recv_sentinels = 0
97
+ @compression_options = {}
98
+
99
+ @peer_connected = Promise.new do |promise|
100
+ wait_connected(1)
101
+ promise.resolve(true) unless @closed
102
+ end
103
+ @all_peers_gone = Promise.new do |promise|
104
+ wait_all_peers_gone
105
+ promise.resolve(true) unless @closed
106
+ end
107
+ @subscriber_joined = Promise.new do |promise|
108
+ wait_subscribed(1)
109
+ promise.resolve(true) unless @closed
110
+ end
111
+
112
+ @routing = RoutingStub.new(self)
113
+ end
114
+
115
+
116
+ def capture_parent_task(parent: nil)
117
+ @parent_task ||= parent
118
+ end
119
+
120
+
121
+ def bind(endpoint, parent: nil, **opts)
122
+ capture_parent_task(parent: parent)
123
+ apply_endpoint_options!(opts)
124
+ ensure_materialized
125
+ URI.parse(with_java_errors { @native.bind(endpoint) })
126
+ end
127
+
128
+
129
+ def connect(endpoint, parent: nil, **opts)
130
+ capture_parent_task(parent: parent)
131
+ apply_endpoint_options!(opts)
132
+ ensure_materialized
133
+ with_java_errors { @native.connect(endpoint) }
134
+ resolve_peer_connected if endpoint.start_with?("inproc://")
135
+ URI.parse(endpoint)
136
+ end
137
+
138
+
139
+ def disconnect(endpoint)
140
+ ensure_materialized
141
+ with_java_errors { @native.disconnect(endpoint) }
142
+ end
143
+
144
+
145
+ def unbind(endpoint)
146
+ ensure_materialized
147
+ with_java_errors { @native.unbind(endpoint) }
148
+ end
149
+
150
+
151
+ def enqueue_send(parts)
152
+ ensure_materialized
153
+ msg = java_message(parts)
154
+
155
+ if (timeout = @options.write_timeout)
156
+ ok = with_java_errors { @native.send(msg, duration_from_seconds("write_timeout", timeout)) }
157
+ raise IO::TimeoutError, "operation timed out" unless ok
158
+ else
159
+ with_java_errors { @native.send(msg) }
160
+ end
161
+
162
+ nil
163
+ end
164
+
165
+
166
+ def dequeue_recv
167
+ ensure_materialized
168
+ @recv_deadline = monotonic_time + @options.read_timeout.to_f if @options.read_timeout
169
+
170
+ loop do
171
+ return take_recv_sentinel if @recv_sentinels.positive?
172
+
173
+ optional = with_java_errors { @native.tryReceive }
174
+ return ruby_parts(optional.get) if optional.isPresent
175
+
176
+ raise IO::TimeoutError, "operation timed out" if recv_deadline_expired?
177
+
178
+ sleep recv_poll_seconds
179
+ end
180
+ ensure
181
+ @recv_deadline = nil
182
+ end
183
+
184
+
185
+ def dequeue_recv_sentinel
186
+ @recv_sentinels += 1
187
+ nil
188
+ end
189
+
190
+
191
+ def close
192
+ return if @closed
193
+
194
+ @closed = true
195
+ @peer_connected.resolve(nil)
196
+ @all_peers_gone.resolve(nil)
197
+ @subscriber_joined.resolve(nil)
198
+ with_java_errors { @native&.close }
199
+ @connections.clear
200
+ nil
201
+ end
202
+
203
+
204
+ alias stop close
205
+
206
+
207
+ def closed?
208
+ @closed
209
+ end
210
+
211
+
212
+ def subscribe(prefix)
213
+ @routing.subscribe(prefix)
214
+ end
215
+
216
+
217
+ def unsubscribe(prefix)
218
+ @routing.unsubscribe(prefix)
219
+ end
220
+
221
+
222
+ def emit_monitor_event(_type, endpoint: nil, detail: nil)
223
+ end
224
+
225
+
226
+ def monitor_queue=(queue)
227
+ @monitor_queue = queue
228
+ end
229
+
230
+
231
+ def verbose_monitor=(val)
232
+ @verbose_monitor = val
233
+ end
234
+
235
+
236
+ private
237
+
238
+
239
+ def ensure_materialized
240
+ return if @materialized
241
+
242
+ @native = Java.context.socket(SocketType.valueOf(@socket_type.to_s), java_options)
243
+ @materialized = true
244
+ @routing.replay_pending(@native)
245
+ end
246
+
247
+
248
+ def java_message(parts)
249
+ frames = parts.map { |part| part.to_java_bytes }
250
+ return Message.of(frames.first) if frames.size == 1
251
+
252
+ Message.multipart(*frames)
253
+ end
254
+
255
+
256
+ def ruby_parts(message)
257
+ parts = []
258
+ message.partCount.times do |index|
259
+ parts << String.from_java_bytes(message.part(index)).b.freeze
260
+ end
261
+ parts.freeze
262
+ end
263
+
264
+
265
+ def take_recv_sentinel
266
+ @recv_sentinels -= 1
267
+ nil
268
+ end
269
+
270
+
271
+ def recv_deadline_expired?
272
+ return false unless @options.read_timeout
273
+
274
+ monotonic_time >= @recv_deadline
275
+ end
276
+
277
+
278
+ def recv_poll_seconds
279
+ return POLL_SECONDS unless @options.read_timeout
280
+
281
+ [[@recv_deadline - monotonic_time, POLL_SECONDS].min, 0].max
282
+ end
283
+
284
+
285
+ def monotonic_time
286
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
287
+ end
288
+
289
+
290
+ def wait_connected(count)
291
+ ensure_materialized
292
+
293
+ loop do
294
+ result = @native.waitConnected(count, POLL_DURATION)
295
+ resolve_peer_connected if result.to_i.positive?
296
+ return result
297
+ rescue ::Java::IoOmq::TimeoutException
298
+ return nil if @closed
299
+ rescue ::Java::IoOmq::ClosedException
300
+ return nil
301
+ end
302
+ end
303
+
304
+
305
+ def resolve_peer_connected
306
+ @connections[:peer] = true
307
+ @peer_connected.resolve(true) unless @peer_connected.resolved?
308
+ end
309
+
310
+
311
+ def wait_subscribed(count)
312
+ ensure_materialized
313
+
314
+ loop do
315
+ result = @native.waitSubscribed(count, POLL_DURATION)
316
+ return result
317
+ rescue ::Java::IoOmq::TimeoutException
318
+ return nil if @closed
319
+ rescue ::Java::IoOmq::ClosedException
320
+ return nil
321
+ end
322
+ end
323
+
324
+
325
+ def wait_all_peers_gone
326
+ @peer_connected.wait
327
+
328
+ loop do
329
+ @native.waitConnected(1, POLL_DURATION)
330
+ sleep POLL_SECONDS
331
+ rescue ::Java::IoOmq::TimeoutException
332
+ @connections.clear
333
+ return true
334
+ rescue ::Java::IoOmq::ClosedException
335
+ return nil
336
+ end
337
+ end
338
+
339
+
340
+ def java_options
341
+ builder = SocketOptions.builder
342
+ apply_socket_options(builder)
343
+ apply_compression_options(builder)
344
+ apply_mechanism(builder)
345
+ builder.build
346
+ end
347
+
348
+
349
+ def apply_socket_options(builder)
350
+ builder.sendHighWaterMark([integer_option("send_hwm", @options.send_hwm), 0].max)
351
+ builder.receiveHighWaterMark([integer_option("recv_hwm", @options.recv_hwm), 0].max)
352
+
353
+ if @options.linger == Float::INFINITY
354
+ builder.lingerForever
355
+ else
356
+ builder.linger(duration_from_seconds("linger", @options.linger))
357
+ end
358
+
359
+ builder.identity(bytes(@options.identity)) if @options.identity && !@options.identity.empty?
360
+ builder.routerMandatory(!!@options.router_mandatory)
361
+ builder.conflate(!!@options.conflate)
362
+ builder.maxMessageSize(nonnegative_integer("max_message_size", @options.max_message_size)) if @options.max_message_size
363
+ builder.sendBufferSize(nonnegative_integer("sndbuf", @options.sndbuf)) if @options.sndbuf
364
+ builder.receiveBufferSize(nonnegative_integer("rcvbuf", @options.rcvbuf)) if @options.rcvbuf
365
+ builder.onMute(on_mute)
366
+
367
+ apply_duration_option(builder, :heartbeatInterval, "heartbeat_interval", @options.heartbeat_interval)
368
+ apply_duration_option(builder, :heartbeatTtl, "heartbeat_ttl", @options.heartbeat_ttl)
369
+ apply_duration_option(builder, :heartbeatTimeout, "heartbeat_timeout", @options.heartbeat_timeout)
370
+ apply_reconnect_option(builder)
371
+ end
372
+
373
+
374
+ def apply_reconnect_option(builder)
375
+ reconnect = @options.reconnect_interval
376
+
377
+ case reconnect
378
+ when Range
379
+ builder.reconnectExponential(
380
+ duration_from_seconds("reconnect_interval min", reconnect.begin),
381
+ duration_from_seconds("reconnect_interval max", reconnect.end),
382
+ )
383
+ when nil, false
384
+ builder.reconnectDisabled
385
+ else
386
+ builder.reconnectInterval(duration_from_seconds("reconnect_interval", reconnect))
387
+ end
388
+ end
389
+
390
+
391
+ def apply_duration_option(builder, method, label, value)
392
+ builder.public_send(method, duration_from_seconds(label, value)) if value
393
+ end
394
+
395
+
396
+ def apply_compression_options(builder)
397
+ if @compression_options.key?("compression_auto_train")
398
+ builder.compressionAutoTrain(!!@compression_options["compression_auto_train"])
399
+ end
400
+ if (value = @compression_options["compression_threshold"])
401
+ builder.compressionThreshold(nonnegative_integer("compression_threshold", value))
402
+ end
403
+ if (value = @compression_options["compression_level"])
404
+ builder.compressionLevel(integer_option("compression_level", value))
405
+ end
406
+ if (value = @compression_options["compression_dict"])
407
+ builder.compressionDict(bytes(value))
408
+ end
409
+ if (value = @compression_options["compression_dict_capacity"])
410
+ builder.compressionDictCapacity(nonnegative_integer("compression_dict_capacity", value))
411
+ end
412
+ if (value = @compression_options["max_recv_dict_size"])
413
+ builder.maxReceiveDictSize(nonnegative_integer("max_recv_dict_size", value))
414
+ end
415
+
416
+ return unless @compression_options.key?("compression_offload_threshold")
417
+
418
+ value = @compression_options["compression_offload_threshold"]
419
+ if value.nil? || value.to_i.negative?
420
+ builder.noCompressionOffload
421
+ else
422
+ builder.compressionOffloadThreshold(nonnegative_integer("compression_offload_threshold", value))
423
+ end
424
+ end
425
+
426
+
427
+ def apply_mechanism(builder)
428
+ mech = @options.mechanism
429
+ klass = mech.class.name
430
+ return unless klass&.include?("Curve")
431
+
432
+ require "protocol/zmtp/z85"
433
+
434
+ keypair = CurveKeypair.new(
435
+ z85_key(mech.instance_variable_get(:@permanent_public), "public key"),
436
+ z85_key(mech.instance_variable_get(:@permanent_secret), "secret key"),
437
+ )
438
+
439
+ if mech.instance_variable_get(:@as_server)
440
+ builder.curveServer(keypair)
441
+ else
442
+ builder.curveClient(keypair, z85_key(mech.instance_variable_get(:@server_public), "server key"))
443
+ end
444
+ end
445
+
446
+
447
+ def z85_key(key, label)
448
+ raw = key&.to_s&.b
449
+ raise ArgumentError, "#{label} must be exactly 32 bytes" unless raw&.bytesize == 32
450
+
451
+ Protocol::ZMTP::Z85.encode(raw)
452
+ end
453
+
454
+
455
+ def on_mute
456
+ case @options.on_mute&.to_sym
457
+ when :drop_newest, :drop
458
+ OnMute::DROP_NEWEST
459
+ when :drop_oldest
460
+ OnMute::DROP_OLDEST
461
+ else
462
+ OnMute::BLOCK
463
+ end
464
+ end
465
+
466
+
467
+ def duration_from_seconds(label, value)
468
+ seconds = Float(value)
469
+ raise ArgumentError, "#{label} must be finite and non-negative" unless seconds.finite? && !seconds.negative?
470
+
471
+ Duration.ofNanos((seconds * 1_000_000_000).round)
472
+ end
473
+
474
+
475
+ def nonnegative_integer(label, value)
476
+ value = integer_option(label, value)
477
+ raise ArgumentError, "#{label} must be non-negative" if value.negative?
478
+
479
+ value
480
+ end
481
+
482
+
483
+ def integer_option(label, value)
484
+ Integer(value)
485
+ rescue ArgumentError, TypeError
486
+ raise ArgumentError, "#{label} must be an Integer"
487
+ end
488
+
489
+
490
+ def bytes(value)
491
+ value.to_s.b.to_java_bytes
492
+ end
493
+
494
+
495
+ def apply_endpoint_options!(opts)
496
+ compression = extract_endpoint_compression_options(opts)
497
+ return if compression.empty?
498
+
499
+ if @materialized
500
+ existing = compression.keys.to_h do |key|
501
+ [key, @compression_options.fetch(key, default_compression_option(key))]
502
+ end
503
+ return if compression == existing
504
+
505
+ raise ArgumentError,
506
+ "Rust backend compression options must be set before first bind/connect"
507
+ end
508
+
509
+ @compression_options.merge!(compression)
510
+ end
511
+
512
+
513
+ def extract_endpoint_compression_options(opts)
514
+ out = {}
515
+
516
+ if opts.key?(:level)
517
+ validate_zstd_level!(opts[:level])
518
+ out["compression_level"] = opts[:level]
519
+ end
520
+ out["compression_dict"] = opts[:dict].b if opts.key?(:dict) && opts[:dict]
521
+
522
+ if opts.key?(:auto_dict)
523
+ auto_dict = opts[:auto_dict]
524
+ if auto_dict && opts[:dict]
525
+ raise ArgumentError, "cannot combine auto_dict: and dict:"
526
+ end
527
+
528
+ case auto_dict
529
+ when nil, false
530
+ out["compression_auto_train"] = false
531
+ when true
532
+ out["compression_auto_train"] = true
533
+ when Hash
534
+ if auto_dict.key?(:trigger)
535
+ raise ArgumentError,
536
+ "Rust backend does not support auto_dict: trigger"
537
+ end
538
+ validate_positive!("auto_dict capacity", auto_dict[:capacity]) if auto_dict[:capacity]
539
+ out["compression_auto_train"] = true
540
+ out["compression_dict_capacity"] = auto_dict[:capacity] if auto_dict[:capacity]
541
+ else
542
+ raise TypeError, "auto_dict: must be true, false, or a Hash; got #{auto_dict.class}"
543
+ end
544
+ end
545
+
546
+ out["compression_threshold"] = opts[:compression_threshold] if opts.key?(:compression_threshold)
547
+ out["max_recv_dict_size"] = opts[:max_recv_dict_size] if opts.key?(:max_recv_dict_size)
548
+ if opts.key?(:compression_offload_threshold)
549
+ out["compression_offload_threshold"] = opts[:compression_offload_threshold] || -1
550
+ end
551
+
552
+ out
553
+ end
554
+
555
+
556
+ def default_compression_option(key)
557
+ key == "compression_auto_train" ? false : nil
558
+ end
559
+
560
+
561
+ def validate_positive!(label, value)
562
+ return if value.respond_to?(:positive?) && value.positive?
563
+
564
+ raise ArgumentError, "#{label} must be positive"
565
+ end
566
+
567
+
568
+ def validate_zstd_level!(level)
569
+ return if level.is_a?(Integer) && (-8..4).cover?(level)
570
+
571
+ raise ArgumentError, "zstd compression level must be -8..4, got #{level.inspect}"
572
+ end
573
+
574
+
575
+ def with_java_errors
576
+ yield
577
+ rescue ::Java::IoOmq::TimeoutException => error
578
+ raise IO::TimeoutError, error.message
579
+ rescue ::Java::IoOmq::ClosedException => error
580
+ raise IOError, error.message
581
+ rescue ::Java::IoOmq::InvalidEndpointException => error
582
+ raise ArgumentError, error.message
583
+ rescue ::Java::IoOmq::OMQException => error
584
+ raise RuntimeError, error.message
585
+ end
586
+
587
+ class RoutingStub
588
+ def initialize(engine)
589
+ @engine = engine
590
+ @pending_subscribe = []
591
+ @pending_join = []
592
+ end
593
+
594
+
595
+ def subscriber_joined
596
+ @engine.subscriber_joined
597
+ end
598
+
599
+
600
+ def subscribe(prefix)
601
+ native = @engine.instance_variable_get(:@native)
602
+ if @engine.instance_variable_get(:@materialized)
603
+ native.subscribe(prefix.b.to_java_bytes)
604
+ else
605
+ @pending_subscribe << prefix.b
606
+ end
607
+ end
608
+
609
+
610
+ def unsubscribe(prefix)
611
+ @engine.instance_variable_get(:@native).unsubscribe(prefix.b.to_java_bytes)
612
+ end
613
+
614
+
615
+ def join(group)
616
+ native = @engine.instance_variable_get(:@native)
617
+ if @engine.instance_variable_get(:@materialized)
618
+ native.join(group.b.to_java_bytes)
619
+ else
620
+ @pending_join << group.b
621
+ end
622
+ end
623
+
624
+
625
+ def leave(group)
626
+ @engine.instance_variable_get(:@native).leave(group.b.to_java_bytes)
627
+ end
628
+
629
+
630
+ def replay_pending(native)
631
+ @pending_subscribe.each { |prefix| native.subscribe(prefix.to_java_bytes) }
632
+ @pending_subscribe.clear
633
+ @pending_join.each { |group| native.join(group.to_java_bytes) }
634
+ @pending_join.clear
635
+ end
636
+ end
637
+ end
638
+ end
639
+
640
+ Engine = Java::Engine
641
+ end
642
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ require_relative "../version"
6
+
7
+ module OMQ
8
+ module Rust
9
+ module Java
10
+ class UnsupportedPlatformError < LoadError; end
11
+
12
+ class << self
13
+ def classifier
14
+ override = ENV.fetch("OMQ_JAVA_CLASSIFIER", "").strip
15
+ return validate_classifier(override) unless override.empty?
16
+
17
+ classifier_for(os_name: host_os_name, os_arch: host_os_arch)
18
+ end
19
+
20
+
21
+ def classifier_for(os_name:, os_arch:)
22
+ os = normalize_os(os_name)
23
+ arch = normalize_arch(os_arch)
24
+
25
+ classifier = "#{os}-#{arch}" if os && arch
26
+ return classifier if OMQ::Rust::OMQ_JAVA_CLASSIFIERS.include?(classifier)
27
+
28
+ raise UnsupportedPlatformError,
29
+ "unsupported OMQ.java platform: #{os_name.inspect}/#{os_arch.inspect} " \
30
+ "(supported: #{OMQ::Rust::OMQ_JAVA_CLASSIFIERS.join(", ")})"
31
+ end
32
+
33
+
34
+ private
35
+
36
+
37
+ def validate_classifier(classifier)
38
+ return classifier if OMQ::Rust::OMQ_JAVA_CLASSIFIERS.include?(classifier)
39
+
40
+ raise UnsupportedPlatformError,
41
+ "unsupported OMQ.java classifier: #{classifier.inspect} " \
42
+ "(supported: #{OMQ::Rust::OMQ_JAVA_CLASSIFIERS.join(", ")})"
43
+ end
44
+
45
+
46
+ def host_os_name
47
+ java_system_property("os.name") ||
48
+ RbConfig::CONFIG["host_os"] ||
49
+ RbConfig::CONFIG["target_os"] ||
50
+ RUBY_PLATFORM
51
+ end
52
+
53
+
54
+ def host_os_arch
55
+ java_system_property("os.arch") ||
56
+ RbConfig::CONFIG["host_cpu"] ||
57
+ RbConfig::CONFIG["target_cpu"] ||
58
+ RUBY_PLATFORM
59
+ end
60
+
61
+
62
+ def java_system_property(name)
63
+ return unless RUBY_ENGINE == "jruby"
64
+
65
+ require "java"
66
+ ::Java::JavaLang::System.get_property(name)
67
+ rescue LoadError, NameError
68
+ nil
69
+ end
70
+
71
+
72
+ def normalize_os(value)
73
+ case value.to_s.downcase
74
+ when /linux/
75
+ "linux"
76
+ when /darwin|mac\s*os|macos/
77
+ "macos"
78
+ when /windows|mswin|mingw|cygwin/
79
+ "windows"
80
+ end
81
+ end
82
+
83
+
84
+ def normalize_arch(value)
85
+ case value.to_s.downcase
86
+ when "amd64", "x64", "x86-64", "x86_64"
87
+ "x86_64"
88
+ when "aarch64", "arm64"
89
+ "aarch64"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Backend
5
+ module Rust
6
+ VERSION = "0.3.0"
7
+ end
8
+ end
9
+
10
+ module Rust
11
+ OMQ_JAVA_VERSION = "0.3.0"
12
+ OMQ_JAVA_CLASSIFIERS = [
13
+ "linux-x86_64",
14
+ "macos-aarch64",
15
+ "macos-x86_64",
16
+ "windows-x86_64",
17
+ ].freeze
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omq-backend-rust
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: java
6
+ authors:
7
+ - Patrik Wenger
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: omq
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.28'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.28'
26
+ - !ruby/object:Gem::Dependency
27
+ name: jar-dependencies
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.5.7
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.5.7
40
+ description: Drop-in Rust backend for OMQ on JRuby. Same socket API (REQ/REP, PUB/SUB,
41
+ PUSH/PULL, DEALER/ROUTER, and all draft types), but networking runs through the
42
+ OMQ.java Maven artifact.
43
+ email:
44
+ - paddor@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE
51
+ - README.md
52
+ - lib/omq/backend/rust.rb
53
+ - lib/omq/rust/java/engine.rb
54
+ - lib/omq/rust/java/platform.rb
55
+ - lib/omq/rust/version.rb
56
+ homepage: https://github.com/zeromq/omq.rb/tree/main/gems/omq-backend-rust
57
+ licenses:
58
+ - ISC
59
+ metadata: {}
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.3'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements:
74
+ - jar io.github.paddor:omq-java:linux-x86_64, 0.3.0
75
+ - jar io.github.paddor:omq-java:macos-aarch64, 0.3.0
76
+ - jar io.github.paddor:omq-java:macos-x86_64, 0.3.0
77
+ - jar io.github.paddor:omq-java:windows-x86_64, 0.3.0
78
+ rubygems_version: 4.0.16
79
+ specification_version: 4
80
+ summary: Rust-backed engine for OMQ using OMQ.java on JRuby
81
+ test_files: []