omq 0.4.0 → 0.4.2
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 +39 -1
- data/README.md +15 -10
- data/lib/omq/push_pull.rb +4 -4
- data/lib/omq/socket.rb +6 -1
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/connection.rb +3 -1
- data/lib/omq/zmtp/engine.rb +2 -2
- data/lib/omq/zmtp/mechanism/null.rb +2 -0
- data/lib/omq/zmtp/routing/round_robin.rb +9 -4
- data/lib/omq/zmtp/transport/inproc.rb +19 -2
- data/lib/omq/zmtp/transport/ipc.rb +2 -2
- data/lib/omq/zmtp/transport/tcp.rb +2 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0dc74658712882f8d0eb8f58b0c9a37c959d50c87116677d23be39904f7002e7
|
|
4
|
+
data.tar.gz: 1ba17796cac6b1cd594ebbee874428e0675153d461d168e0f69f07abab608ef8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3628a755a53010690ae407ac790eb83ef7592ac5cac34bddff8a62782c20cb6092c3dd300ae71a2dfff324eaf8457ba319305b6892ca259f8990ce63267c74e0
|
|
7
|
+
data.tar.gz: 76502cad1a484cf8224285fbf4259e9ba8e5daa396caa42ac3716b50ada2e72a4c31b8d256208ab7410f5e48727216fee526efbf6166e3a1a32bb7bc1701cacb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 — 2026-03-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Send pump dies permanently on connection loss — `rescue` was outside
|
|
8
|
+
the loop, so a single `CONNECTION_LOST` killed the pump and all
|
|
9
|
+
subsequent messages queued but never sent
|
|
10
|
+
- NULL handshake deadlocks with buffered IO — missing `io.flush` after
|
|
11
|
+
greeting and READY writes caused both peers to block on read
|
|
12
|
+
- Inproc DirectPipe drops messages when send pump runs before
|
|
13
|
+
`direct_recv_queue` is wired — now buffers to `@pending_direct` and
|
|
14
|
+
drains on assignment
|
|
15
|
+
- HWM and timeout options set after construction had no effect because
|
|
16
|
+
`Async::LimitedQueue` was already allocated with the default
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `send_hwm:`, `send_timeout:` constructor kwargs for `PUSH`
|
|
21
|
+
- `recv_hwm:`, `recv_timeout:` constructor kwargs for `PULL`
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Use `Async::Clock.now` instead of `Process.clock_gettime` internally
|
|
26
|
+
|
|
27
|
+
## 0.4.1 — 2026-03-27
|
|
28
|
+
|
|
29
|
+
### Improved
|
|
30
|
+
|
|
31
|
+
- Explicit flush after `send_message`/`send_command` instead of
|
|
32
|
+
`minimum_write_size: 0` workaround — enables write buffering
|
|
33
|
+
(multi-frame messages coalesced into fewer syscalls).
|
|
34
|
+
**+68% inproc throughput** (145k → 244k msg/s),
|
|
35
|
+
**-40% inproc latency** (15 → 9 µs)
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Require `async ~> 2.38` for `Promise#wait?` (was `~> 2`)
|
|
40
|
+
|
|
3
41
|
## 0.4.0 — 2026-03-27
|
|
4
42
|
|
|
5
43
|
### Added (omqcat)
|
|
@@ -123,4 +161,4 @@ Initial release. Pure Ruby implementation of ZMTP 3.1 (ZeroMQ) using Async.
|
|
|
123
161
|
- Linger on close (drain send queue before closing)
|
|
124
162
|
- `max_message_size` enforcement
|
|
125
163
|
- Works inside Async reactors or standalone (shared IO thread)
|
|
126
|
-
- Optional CURVE encryption via the [omq-curve](https://github.com/
|
|
164
|
+
- Optional CURVE encryption via the [omq-curve](https://github.com/zeromq/omq-curve) gem
|
data/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# OMQ! Where did the C dependency go?!
|
|
2
2
|
|
|
3
|
-
[](https://github.com/zeromq/omq/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/omq)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://www.ruby-lang.org)
|
|
7
7
|
|
|
8
8
|
Pure Ruby implementation of the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire protocol ([ZeroMQ](https://zeromq.org/)) using the [Async](https://github.com/socketry/async) gem. No native libraries required.
|
|
9
9
|
|
|
10
|
-
> **
|
|
10
|
+
> **244k msg/s** inproc | **47k msg/s** ipc | **36k msg/s** tcp
|
|
11
11
|
>
|
|
12
|
-
> **
|
|
12
|
+
> **9 µs** inproc latency | **47 µs** ipc | **61 µs** tcp
|
|
13
13
|
>
|
|
14
|
-
> Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel) — [
|
|
14
|
+
> Ruby 4.0 + YJIT on a Linux VM on a 2019 MacBook Pro (Intel) — [~340k msg/s with io_uring](bench/README.md#io_uring)
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -121,19 +121,19 @@ req = ØMQ::REQ.new(">tcp://localhost:5555")
|
|
|
121
121
|
|
|
122
122
|
## Performance
|
|
123
123
|
|
|
124
|
-
Benchmarked with benchmark-ips on Linux x86_64 (Ruby 4.0.
|
|
124
|
+
Benchmarked with benchmark-ips on Linux x86_64 (Ruby 4.0.2 +YJIT):
|
|
125
125
|
|
|
126
126
|
#### Throughput (push/pull, 64 B messages)
|
|
127
127
|
|
|
128
128
|
| inproc | ipc | tcp |
|
|
129
129
|
|--------|-----|-----|
|
|
130
|
-
|
|
|
130
|
+
| 244k/s | 47k/s | 36k/s |
|
|
131
131
|
|
|
132
132
|
#### Latency (req/rep roundtrip)
|
|
133
133
|
|
|
134
134
|
| inproc | ipc | tcp |
|
|
135
135
|
|--------|-----|-----|
|
|
136
|
-
|
|
|
136
|
+
| 9 µs | 47 µs | 61 µs |
|
|
137
137
|
|
|
138
138
|
See [`bench/`](bench/) for full results and scripts.
|
|
139
139
|
|
|
@@ -142,7 +142,10 @@ See [`bench/`](bench/) for full results and scripts.
|
|
|
142
142
|
`omqcat` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
|
|
143
143
|
|
|
144
144
|
```sh
|
|
145
|
-
# Echo server
|
|
145
|
+
# Echo server
|
|
146
|
+
omqcat rep -b tcp://:5555 --echo
|
|
147
|
+
|
|
148
|
+
# Upcase server in one line
|
|
146
149
|
omqcat rep -b tcp://:5555 -e '$F.map(&:upcase)'
|
|
147
150
|
|
|
148
151
|
# Client
|
|
@@ -170,8 +173,10 @@ omqcat pull -b tcp://:5557 -J
|
|
|
170
173
|
omqcat push -c tcp://remote:5557 -z < data.txt
|
|
171
174
|
omqcat pull -b tcp://:5557 -z
|
|
172
175
|
|
|
173
|
-
# CURVE encryption
|
|
174
|
-
|
|
176
|
+
# CURVE encryption
|
|
177
|
+
omqcat rep -b tcp://:5555 -D "secret" --curve-server
|
|
178
|
+
# prints: OMQ_SERVER_KEY='...'
|
|
179
|
+
omqcat req -c tcp://localhost:5555 --curve-server-key '...'
|
|
175
180
|
```
|
|
176
181
|
|
|
177
182
|
The `-e` flag runs Ruby inside the socket instance — the full socket API (`self <<`, `send`, `subscribe`, ...) is available. Use `-r` to require gems:
|
data/lib/omq/push_pull.rb
CHANGED
|
@@ -4,8 +4,8 @@ module OMQ
|
|
|
4
4
|
class PUSH < Socket
|
|
5
5
|
include ZMTP::Writable
|
|
6
6
|
|
|
7
|
-
def initialize(endpoints = nil, linger: 0)
|
|
8
|
-
_init_engine(:PUSH, linger: linger)
|
|
7
|
+
def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil)
|
|
8
|
+
_init_engine(:PUSH, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout)
|
|
9
9
|
_attach(endpoints, default: :connect)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -13,8 +13,8 @@ module OMQ
|
|
|
13
13
|
class PULL < Socket
|
|
14
14
|
include ZMTP::Readable
|
|
15
15
|
|
|
16
|
-
def initialize(endpoints = nil, linger: 0)
|
|
17
|
-
_init_engine(:PULL, linger: linger)
|
|
16
|
+
def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil)
|
|
17
|
+
_init_engine(:PULL, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout)
|
|
18
18
|
_attach(endpoints, default: :bind)
|
|
19
19
|
end
|
|
20
20
|
end
|
data/lib/omq/socket.rb
CHANGED
|
@@ -164,8 +164,13 @@ module OMQ
|
|
|
164
164
|
# @param socket_type [Symbol]
|
|
165
165
|
# @param linger [Integer]
|
|
166
166
|
#
|
|
167
|
-
def _init_engine(socket_type, linger:
|
|
167
|
+
def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
|
|
168
|
+
send_timeout: nil, recv_timeout: nil)
|
|
168
169
|
@options = ZMTP::Options.new(linger: linger)
|
|
170
|
+
@options.send_hwm = send_hwm if send_hwm
|
|
171
|
+
@options.recv_hwm = recv_hwm if recv_hwm
|
|
172
|
+
@options.send_timeout = send_timeout if send_timeout
|
|
173
|
+
@options.recv_timeout = recv_timeout if recv_timeout
|
|
169
174
|
@engine = ZMTP::Engine.new(socket_type, @options)
|
|
170
175
|
end
|
|
171
176
|
end
|
data/lib/omq/version.rb
CHANGED
data/lib/omq/zmtp/connection.rb
CHANGED
|
@@ -92,6 +92,7 @@ module OMQ
|
|
|
92
92
|
@io.write(Codec::Frame.new(part, more: more).to_wire)
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
|
+
@io.flush
|
|
95
96
|
end
|
|
96
97
|
end
|
|
97
98
|
|
|
@@ -156,6 +157,7 @@ module OMQ
|
|
|
156
157
|
else
|
|
157
158
|
@io.write(command.to_frame.to_wire)
|
|
158
159
|
end
|
|
160
|
+
@io.flush
|
|
159
161
|
end
|
|
160
162
|
end
|
|
161
163
|
|
|
@@ -216,7 +218,7 @@ module OMQ
|
|
|
216
218
|
end
|
|
217
219
|
|
|
218
220
|
def monotonic_now
|
|
219
|
-
|
|
221
|
+
Async::Clock.now
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
# Sends one frame to the wire.
|
data/lib/omq/zmtp/engine.rb
CHANGED
|
@@ -243,11 +243,11 @@ module OMQ
|
|
|
243
243
|
#
|
|
244
244
|
def drain_send_queues(timeout)
|
|
245
245
|
return unless @routing.respond_to?(:send_queue)
|
|
246
|
-
deadline = timeout ?
|
|
246
|
+
deadline = timeout ? Async::Clock.now + timeout : nil
|
|
247
247
|
|
|
248
248
|
until @routing.send_queue.empty?
|
|
249
249
|
if deadline
|
|
250
|
-
remaining = deadline -
|
|
250
|
+
remaining = deadline - Async::Clock.now
|
|
251
251
|
break if remaining <= 0
|
|
252
252
|
end
|
|
253
253
|
sleep 0.001
|
|
@@ -26,6 +26,7 @@ module OMQ
|
|
|
26
26
|
def handshake!(io, as_server:, socket_type:, identity:)
|
|
27
27
|
# Send our greeting
|
|
28
28
|
io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
|
|
29
|
+
io.flush
|
|
29
30
|
|
|
30
31
|
# Read peer greeting
|
|
31
32
|
greeting_data = io.read_exactly(Codec::Greeting::SIZE)
|
|
@@ -38,6 +39,7 @@ module OMQ
|
|
|
38
39
|
# Send our READY command
|
|
39
40
|
ready_cmd = Codec::Command.ready(socket_type: socket_type, identity: identity)
|
|
40
41
|
io.write(ready_cmd.to_frame.to_wire)
|
|
42
|
+
io.flush
|
|
41
43
|
|
|
42
44
|
# Read peer READY command
|
|
43
45
|
frame = Codec::Frame.read_from(io)
|
|
@@ -56,13 +56,18 @@ module OMQ
|
|
|
56
56
|
@tasks << Reactor.spawn_pump do
|
|
57
57
|
loop do
|
|
58
58
|
parts = @send_queue.dequeue
|
|
59
|
-
|
|
60
|
-
conn.send_message(transform_send(parts))
|
|
59
|
+
send_with_retry(parts)
|
|
61
60
|
end
|
|
62
|
-
rescue *ZMTP::CONNECTION_LOST
|
|
63
|
-
# connection lost mid-write
|
|
64
61
|
end
|
|
65
62
|
end
|
|
63
|
+
|
|
64
|
+
def send_with_retry(parts)
|
|
65
|
+
conn = next_connection
|
|
66
|
+
conn.send_message(transform_send(parts))
|
|
67
|
+
rescue *ZMTP::CONNECTION_LOST
|
|
68
|
+
@engine.connection_lost(conn)
|
|
69
|
+
retry
|
|
70
|
+
end
|
|
66
71
|
end
|
|
67
72
|
end
|
|
68
73
|
end
|
|
@@ -202,7 +202,7 @@ module OMQ
|
|
|
202
202
|
# @return [Async::LimitedQueue, nil] when set, {#send_message}
|
|
203
203
|
# enqueues directly here instead of using the internal queue
|
|
204
204
|
#
|
|
205
|
-
|
|
205
|
+
attr_reader :direct_recv_queue
|
|
206
206
|
|
|
207
207
|
# @return [Proc, nil] optional transform applied before
|
|
208
208
|
# enqueuing into {#direct_recv_queue}
|
|
@@ -224,6 +224,20 @@ module OMQ
|
|
|
224
224
|
@peer = nil
|
|
225
225
|
@direct_recv_queue = nil
|
|
226
226
|
@direct_recv_transform = nil
|
|
227
|
+
@pending_direct = nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Sets the direct recv queue. Drains any messages that were
|
|
231
|
+
# buffered before the queue was available.
|
|
232
|
+
#
|
|
233
|
+
def direct_recv_queue=(queue)
|
|
234
|
+
@direct_recv_queue = queue
|
|
235
|
+
if queue && @pending_direct
|
|
236
|
+
@pending_direct.each do |msg|
|
|
237
|
+
queue.enqueue(msg)
|
|
238
|
+
end
|
|
239
|
+
@pending_direct = nil
|
|
240
|
+
end
|
|
227
241
|
end
|
|
228
242
|
|
|
229
243
|
# Sends a multi-frame message.
|
|
@@ -240,8 +254,11 @@ module OMQ
|
|
|
240
254
|
if @direct_recv_queue
|
|
241
255
|
msg = @direct_recv_transform ? @direct_recv_transform.call(parts) : parts
|
|
242
256
|
@direct_recv_queue.enqueue(msg)
|
|
243
|
-
|
|
257
|
+
elsif @send_queue
|
|
244
258
|
@send_queue.enqueue(parts)
|
|
259
|
+
else
|
|
260
|
+
msg = @direct_recv_transform ? @direct_recv_transform.call(parts) : parts
|
|
261
|
+
(@pending_direct ||= []) << msg
|
|
245
262
|
end
|
|
246
263
|
end
|
|
247
264
|
|
|
@@ -32,7 +32,7 @@ module OMQ
|
|
|
32
32
|
loop do
|
|
33
33
|
client = server.accept
|
|
34
34
|
Reactor.run do
|
|
35
|
-
engine.handle_accepted(IO::Stream::Buffered.wrap(client
|
|
35
|
+
engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: endpoint)
|
|
36
36
|
rescue ProtocolError, *ZMTP::CONNECTION_LOST
|
|
37
37
|
# peer disconnected during handshake
|
|
38
38
|
rescue
|
|
@@ -57,7 +57,7 @@ module OMQ
|
|
|
57
57
|
path = parse_path(endpoint)
|
|
58
58
|
sock_path = to_socket_path(path)
|
|
59
59
|
sock = UNIXSocket.new(sock_path)
|
|
60
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(sock
|
|
60
|
+
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
private
|
|
@@ -29,7 +29,7 @@ module OMQ
|
|
|
29
29
|
loop do
|
|
30
30
|
client = server.accept
|
|
31
31
|
Reactor.run do
|
|
32
|
-
engine.handle_accepted(IO::Stream::Buffered.wrap(client
|
|
32
|
+
engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: resolved)
|
|
33
33
|
rescue ProtocolError, *ZMTP::CONNECTION_LOST
|
|
34
34
|
# peer disconnected during handshake
|
|
35
35
|
rescue
|
|
@@ -53,7 +53,7 @@ module OMQ
|
|
|
53
53
|
def connect(endpoint, engine)
|
|
54
54
|
host, port = parse_endpoint(endpoint)
|
|
55
55
|
sock = TCPSocket.new(host, port)
|
|
56
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(sock
|
|
56
|
+
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
private
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '2'
|
|
18
|
+
version: '2.38'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '2'
|
|
25
|
+
version: '2.38'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: io-stream
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -88,7 +88,7 @@ files:
|
|
|
88
88
|
- lib/omq/zmtp/transport/tcp.rb
|
|
89
89
|
- lib/omq/zmtp/valid_peers.rb
|
|
90
90
|
- lib/omq/zmtp/writable.rb
|
|
91
|
-
homepage: https://github.com/
|
|
91
|
+
homepage: https://github.com/zeromq/omq
|
|
92
92
|
licenses:
|
|
93
93
|
- ISC
|
|
94
94
|
metadata: {}
|