omq 0.3.2 → 0.4.1

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: 27a1d700594261a36d3212b00ff84e15be3053a32db07a04a48aa0eb52fec902
4
- data.tar.gz: 2797e4863af5cfcde9fe446c79929705e6ab17656ba016532b54d6c6b57564b9
3
+ metadata.gz: dd6af0ae414feae27d1119fe4f1144b680e7e039229d512fe2915cd0289fc4dc
4
+ data.tar.gz: b68f9a321eb93350fc175bd603f8d4c29e23518b6b2dfd2a13072eade5904c8f
5
5
  SHA512:
6
- metadata.gz: a116b039dc996a0099bbb86e8c138fd71f03051bd23c142b653e1c8e932e541245476bed51a47183ecb3723f9f86b30f36b6e3956b77494f8d77c924354f39ad
7
- data.tar.gz: bdd7a62bb68390c942ebb1517981ca69d0efab007f3be7d3149fcccb525ad9b2cb279689ba2ec640db0f6057d6d7219e804074bb5ba4eb4d8339517bf6de5805
6
+ metadata.gz: 3e17e9b4d867a7d2972b8caa4eff31dfd3c77095c60e9e5d9fdb53bc2646458dbfff244228065c145f0eb818d32f68bc196a7d0099fa4a5f2f02142f4c9a94b2
7
+ data.tar.gz: d0a7cc664076ea333433e5e14b5d1701219e4bf15200302cf0ab4420e928d50b2fe8d08d35889d69de6f35f7715b40f423195d33055a67f15af18404ee850536
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 — 2026-03-27
4
+
5
+ ### Improved
6
+
7
+ - Explicit flush after `send_message`/`send_command` instead of
8
+ `minimum_write_size: 0` workaround — enables write buffering
9
+ (multi-frame messages coalesced into fewer syscalls).
10
+ **+68% inproc throughput** (145k → 244k msg/s),
11
+ **-40% inproc latency** (15 → 9 µs)
12
+
13
+ ### Fixed
14
+
15
+ - Require `async ~> 2.38` for `Promise#wait?` (was `~> 2`)
16
+
17
+ ## 0.4.0 — 2026-03-27
18
+
19
+ ### Added (omqcat)
20
+
21
+ - `--curve-server` flag — generates ephemeral keypair, prints
22
+ `OMQ_SERVER_KEY=...` to stderr for easy copy-paste
23
+ - `--curve-server-key KEY` flag — CURVE client mode from the CLI
24
+ - `--echo` flag for REP — explicit echo mode
25
+ - REP reads stdin/`-F` as reply source (one line per reply, exits at EOF)
26
+ - REP without a reply source now aborts with a helpful error message
27
+
28
+ ### Changed
29
+
30
+ - CURVE env vars renamed: `OMQ_SERVER_KEY`, `OMQ_SERVER_PUBLIC`,
31
+ `OMQ_SERVER_SECRET` (was `SERVER_KEY`, `SERVER_PUBLIC`, `SERVER_SECRET`)
32
+ - REP with `--echo`/`-D`/`-e` serves forever by default (like a server).
33
+ Use `-n 1` for one-shot, `-n` to limit exchanges. Stdin/`-F` replies
34
+ naturally terminate at EOF.
35
+
3
36
  ## 0.3.2 — 2026-03-26
4
37
 
5
38
  ### Improved
data/README.md CHANGED
@@ -7,11 +7,11 @@
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/exe/omqcat CHANGED
@@ -30,7 +30,7 @@ opts = {
30
30
  identity: nil,
31
31
  target: nil,
32
32
  interval: nil,
33
- count: 0,
33
+ count: nil,
34
34
  delay: nil,
35
35
  recv_timeout: nil,
36
36
  send_timeout: nil,
@@ -48,7 +48,8 @@ parser = OptionParser.new do |o|
48
48
  o.on("-c", "--connect URL", "Connect to endpoint (repeatable)") { |v| opts[:connects] << v }
49
49
  o.on("-b", "--bind URL", "Bind to endpoint (repeatable)") { |v| opts[:binds] << v }
50
50
 
51
- o.separator "\nData source:"
51
+ o.separator "\nData source (REP: reply source):"
52
+ o.on( "--echo", "Echo received messages back (REP)") { opts[:echo] = true }
52
53
  o.on("-D", "--data DATA", "Message data (literal string)") { |v| opts[:data] = v }
53
54
  o.on("-F", "--file FILE", "Read message from file (- = stdin)") { |v| opts[:file] = v }
54
55
 
@@ -80,6 +81,11 @@ parser = OptionParser.new do |o|
80
81
  o.on("-e", "--eval EXPR", "Eval Ruby for each message ($F = parts)") { |v| opts[:expr] = v }
81
82
  o.on("-r", "--require LIB", "Require a Ruby library (repeatable)") { |v| require v }
82
83
 
84
+ o.separator "\nCURVE encryption (requires omq-curve gem):"
85
+ o.on("--curve-server", "Enable CURVE as server (generates keypair)") { opts[:curve_server] = true }
86
+ o.on("--curve-server-key KEY", "Enable CURVE as client (server's Z85 public key)") { |v| opts[:curve_server_key] = v }
87
+ o.separator " Env vars: OMQ_SERVER_KEY (client), OMQ_SERVER_PUBLIC + OMQ_SERVER_SECRET (server)"
88
+
83
89
  o.separator "\nOther:"
84
90
  o.on("-v", "--verbose", "Print connection events to stderr") { opts[:verbose] = true }
85
91
  o.on("-q", "--quiet", "Suppress message output") { opts[:quiet] = true }
@@ -216,21 +222,31 @@ end
216
222
 
217
223
  # ── CURVE setup ─────────────────────────────────────────────────────
218
224
 
219
- def setup_curve(sock)
220
- if ENV["SERVER_KEY"]
225
+ def setup_curve(sock, opts)
226
+ server_key_z85 = opts[:curve_server_key] || ENV["OMQ_SERVER_KEY"]
227
+ server_mode = opts[:curve_server] || (ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"])
228
+
229
+ if server_key_z85
230
+ # Client mode
221
231
  require "omq/curve"
222
- server_key = OMQ::Z85.decode(ENV["SERVER_KEY"])
232
+ server_key = OMQ::Z85.decode(server_key_z85)
223
233
  client_key = RbNaCl::PrivateKey.generate
224
234
  sock.mechanism = OMQ::Curve.client(
225
235
  client_key.public_key.to_s, client_key.to_s, server_key: server_key
226
236
  )
227
- $stderr.puts "CURVE client mode" if $VERBOSE
228
- elsif ENV["SERVER_PUBLIC"] && ENV["SERVER_SECRET"]
237
+ elsif server_mode
238
+ # Server mode
229
239
  require "omq/curve"
230
- server_pub = OMQ::Z85.decode(ENV["SERVER_PUBLIC"])
231
- server_sec = OMQ::Z85.decode(ENV["SERVER_SECRET"])
240
+ if ENV["OMQ_SERVER_PUBLIC"] && ENV["OMQ_SERVER_SECRET"]
241
+ server_pub = OMQ::Z85.decode(ENV["OMQ_SERVER_PUBLIC"])
242
+ server_sec = OMQ::Z85.decode(ENV["OMQ_SERVER_SECRET"])
243
+ else
244
+ key = RbNaCl::PrivateKey.generate
245
+ server_pub = key.public_key.to_s
246
+ server_sec = key.to_s
247
+ end
232
248
  sock.mechanism = OMQ::Curve.server(server_pub, server_sec)
233
- $stderr.puts "SERVER_KEY=#{ENV["SERVER_PUBLIC"]}"
249
+ $stderr.puts "OMQ_SERVER_KEY='#{OMQ::Z85.encode(server_pub)}'"
234
250
  end
235
251
  rescue LoadError
236
252
  abort "omq-curve gem required for CURVE encryption: gem install omq-curve"
@@ -281,6 +297,10 @@ def eval_expr(parts, sock, opts)
281
297
  end
282
298
  end
283
299
 
300
+ def count_reached?(i, opts)
301
+ opts[:count] && opts[:count] > 0 && i >= opts[:count]
302
+ end
303
+
284
304
  def output(parts, opts)
285
305
  return if opts[:quiet]
286
306
  parts = [""] if parts.nil?
@@ -304,7 +324,7 @@ def send_loop(sock, opts)
304
324
  sleep(opts[:delay]) if opts[:delay] && i == 0
305
325
  send_msg(sock, parts, opts) if parts
306
326
  i += 1
307
- break if opts[:count] > 0 && i >= opts[:count]
327
+ break if count_reached?(i, opts)
308
328
  if opts[:interval]
309
329
  sleep(opts[:interval])
310
330
  else
@@ -319,7 +339,7 @@ def send_loop(sock, opts)
319
339
  sleep(opts[:delay]) if opts[:delay] && i == 0
320
340
  send_msg(sock, parts, opts) if parts
321
341
  i += 1
322
- break if opts[:count] > 0 && i >= opts[:count]
342
+ break if count_reached?(i, opts)
323
343
  sleep(opts[:interval]) if opts[:interval]
324
344
  end
325
345
  end
@@ -332,7 +352,7 @@ def recv_loop(sock, opts)
332
352
  parts = eval_expr(parts, sock, opts)
333
353
  output(parts, opts)
334
354
  i += 1
335
- break if opts[:count] > 0 && i >= opts[:count]
355
+ break if count_reached?(i, opts)
336
356
  end
337
357
  end
338
358
 
@@ -347,7 +367,7 @@ def req_loop(sock, opts)
347
367
  reply = eval_expr(reply, sock, opts)
348
368
  output(reply, opts)
349
369
  i += 1
350
- break if opts[:count] > 0 && i >= opts[:count]
370
+ break if count_reached?(i, opts)
351
371
  if opts[:interval]
352
372
  sleep(opts[:interval])
353
373
  elsif !opts[:data] && !opts[:file]
@@ -366,13 +386,19 @@ def rep_loop(sock, opts)
366
386
  reply = eval_expr(msg, sock, opts)
367
387
  output(reply, opts)
368
388
  send_msg(sock, reply || [""], opts)
369
- else
389
+ elsif opts[:echo]
390
+ output(msg, opts)
391
+ send_msg(sock, msg, opts)
392
+ elsif opts[:data] || opts[:file] || !$stdin.tty?
393
+ reply = read_next(opts)
394
+ break unless reply # EOF on stdin/-F
370
395
  output(msg, opts)
371
- reply = opts[:data] ? parse_input(opts[:data] + "\n", opts[:format]) : msg
372
396
  send_msg(sock, reply, opts)
397
+ else
398
+ abort "REP needs a reply source: --echo, --data, --file, -e, or stdin pipe"
373
399
  end
374
400
  i += 1
375
- break if opts[:count] > 0 && i >= opts[:count]
401
+ break if count_reached?(i, opts)
376
402
  end
377
403
  end
378
404
 
@@ -384,7 +410,7 @@ def pair_loop(sock, opts, task)
384
410
  parts = eval_expr(parts, sock, opts)
385
411
  output(parts, opts)
386
412
  i += 1
387
- break if opts[:count] > 0 && i >= opts[:count]
413
+ break if count_reached?(i, opts)
388
414
  end
389
415
  end
390
416
 
@@ -397,13 +423,13 @@ def pair_loop(sock, opts, task)
397
423
  sleep(opts[:delay]) if opts[:delay] && i == 0
398
424
  send_msg(sock, parts, opts) if parts
399
425
  i += 1
400
- break if opts[:count] > 0 && i >= opts[:count]
426
+ break if count_reached?(i, opts)
401
427
  break if (opts[:data] || opts[:file]) && !opts[:interval]
402
428
  sleep(opts[:interval]) if opts[:interval]
403
429
  end
404
430
  end
405
431
 
406
- if opts[:count] > 0
432
+ if opts[:count] && opts[:count] > 0
407
433
  receiver.wait
408
434
  sender.stop
409
435
  else
@@ -425,7 +451,7 @@ def router_loop(sock, opts, task)
425
451
  result = eval_expr([id_display, *parts], sock, opts)
426
452
  output(result, opts)
427
453
  i += 1
428
- break if opts[:count] > 0 && i >= opts[:count]
454
+ break if count_reached?(i, opts)
429
455
  end
430
456
  end
431
457
 
@@ -440,7 +466,7 @@ def router_loop(sock, opts, task)
440
466
  end
441
467
  send_msg(sock, parts, opts)
442
468
  i += 1
443
- break if opts[:count] > 0 && i >= opts[:count]
469
+ break if count_reached?(i, opts)
444
470
  break if (opts[:data] || opts[:file]) && !opts[:interval]
445
471
  sleep(opts[:interval]) if opts[:interval]
446
472
  end
@@ -448,7 +474,7 @@ def router_loop(sock, opts, task)
448
474
 
449
475
  # If count is set, the receiver will exit when count is reached.
450
476
  # Otherwise, wait for Ctrl-C.
451
- if opts[:count] > 0
477
+ if opts[:count] && opts[:count] > 0
452
478
  receiver.wait
453
479
  sender.stop
454
480
  else
@@ -473,7 +499,7 @@ Async do |task|
473
499
  sock.send_timeout = opts[:send_timeout] if opts[:send_timeout]
474
500
  sock.identity = opts[:identity] if opts[:identity]
475
501
 
476
- setup_curve(sock)
502
+ setup_curve(sock, opts)
477
503
 
478
504
  opts[:binds].each do |url|
479
505
  sock.bind(url)
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.3.2"
4
+ VERSION = "0.4.1"
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
 
@@ -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.3.2
4
+ version: 0.4.1
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