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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d49a1c7caa7a1548615285ae294341b31833fd02bb60a7fa7cbfc0ed5c707d79
4
- data.tar.gz: db36537c5dab13faa9cbdbe611c282cbf9dd96726e88145f80c3aaa7bf3df8a5
3
+ metadata.gz: 0dc74658712882f8d0eb8f58b0c9a37c959d50c87116677d23be39904f7002e7
4
+ data.tar.gz: 1ba17796cac6b1cd594ebbee874428e0675153d461d168e0f69f07abab608ef8
5
5
  SHA512:
6
- metadata.gz: 3e3a9e5a0b9ee3857a2946e7468ea2ca30c87544fa75965c79b1ea905c3a8d969aeb94b49282acd502d2ffbb6a0db5b2952f7a18a17e90363f1c5ed13cfbecd4
7
- data.tar.gz: 1cb5f13312278d55684f22c420f17287a922633eebdfd143336415aafdc725dd7a38e23b5f9387cc55bf28acc0b125840c5770ef9112556b97b7c3cca8578ee6
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/paddor/omq-curve) gem
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
- [![CI](https://github.com/paddor/omq/actions/workflows/ci.yml/badge.svg)](https://github.com/paddor/omq/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/zeromq/omq/actions/workflows/ci.yml/badge.svg)](https://github.com/zeromq/omq/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://img.shields.io/gem/v/omq?color=e9573f)](https://rubygems.org/gems/omq)
5
5
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
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
  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
- > **145k msg/s** inproc | **40k msg/s** ipc | **32k msg/s** tcp
10
+ > **244k msg/s** inproc | **47k msg/s** ipc | **36k msg/s** tcp
11
11
  >
12
- > **15 µs** inproc latency | **62 µs** ipc | **88 µs** tcp
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) — [223k msg/s with io_uring](bench/README.md#io_uring)
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.1 +YJIT):
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
- | 184k/s | 35k/s | 18k/s |
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
- | 13 µs | 70 µs | 97 µs |
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 in one line
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 (auto-detected from env vars)
174
- SERVER_KEY=... omqcat req -c tcp://secure:5555 -D "secret"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.2"
5
5
  end
@@ -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
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
221
+ Async::Clock.now
220
222
  end
221
223
 
222
224
  # Sends one frame to the wire.
@@ -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 ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
246
+ deadline = timeout ? Async::Clock.now + timeout : nil
247
247
 
248
248
  until @routing.send_queue.empty?
249
249
  if deadline
250
- remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
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
- conn = next_connection
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
- attr_accessor :direct_recv_queue
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
- else
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, minimum_write_size: 0), endpoint: endpoint)
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, minimum_write_size: 0), endpoint: endpoint)
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, minimum_write_size: 0), endpoint: resolved)
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, minimum_write_size: 0), endpoint: endpoint)
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.0
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/paddor/omq
91
+ homepage: https://github.com/zeromq/omq
92
92
  licenses:
93
93
  - ISC
94
94
  metadata: {}