omq 0.8.0 → 0.10.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +87 -0
  3. data/README.md +9 -49
  4. data/lib/omq/channel.rb +3 -3
  5. data/lib/omq/client_server.rb +6 -6
  6. data/lib/omq/engine.rb +641 -0
  7. data/lib/omq/options.rb +46 -0
  8. data/lib/omq/pair.rb +2 -2
  9. data/lib/omq/peer.rb +3 -3
  10. data/lib/omq/pub_sub.rb +6 -6
  11. data/lib/omq/push_pull.rb +2 -2
  12. data/lib/omq/radio_dish.rb +2 -2
  13. data/lib/omq/reactor.rb +128 -0
  14. data/lib/omq/readable.rb +42 -0
  15. data/lib/omq/req_rep.rb +4 -4
  16. data/lib/omq/router_dealer.rb +4 -4
  17. data/lib/omq/routing/channel.rb +83 -0
  18. data/lib/omq/routing/client.rb +56 -0
  19. data/lib/omq/routing/dealer.rb +57 -0
  20. data/lib/omq/routing/dish.rb +78 -0
  21. data/lib/omq/routing/fan_out.rb +131 -0
  22. data/lib/omq/routing/gather.rb +46 -0
  23. data/lib/omq/routing/pair.rb +86 -0
  24. data/lib/omq/routing/peer.rb +101 -0
  25. data/lib/omq/routing/pub.rb +60 -0
  26. data/lib/omq/routing/pull.rb +46 -0
  27. data/lib/omq/routing/push.rb +81 -0
  28. data/lib/omq/routing/radio.rb +140 -0
  29. data/lib/omq/routing/rep.rb +101 -0
  30. data/lib/omq/routing/req.rb +65 -0
  31. data/lib/omq/routing/round_robin.rb +168 -0
  32. data/lib/omq/routing/router.rb +110 -0
  33. data/lib/omq/routing/scatter.rb +82 -0
  34. data/lib/omq/routing/server.rb +101 -0
  35. data/lib/omq/routing/sub.rb +78 -0
  36. data/lib/omq/routing/xpub.rb +72 -0
  37. data/lib/omq/routing/xsub.rb +83 -0
  38. data/lib/omq/routing.rb +66 -0
  39. data/lib/omq/scatter_gather.rb +4 -4
  40. data/lib/omq/single_frame.rb +18 -0
  41. data/lib/omq/socket.rb +24 -9
  42. data/lib/omq/transport/inproc.rb +355 -0
  43. data/lib/omq/transport/ipc.rb +117 -0
  44. data/lib/omq/transport/tcp.rb +111 -0
  45. data/lib/omq/version.rb +1 -1
  46. data/lib/omq/writable.rb +65 -0
  47. data/lib/omq.rb +60 -4
  48. metadata +38 -58
  49. data/exe/omq +0 -6
  50. data/lib/omq/cli/base_runner.rb +0 -459
  51. data/lib/omq/cli/channel.rb +0 -8
  52. data/lib/omq/cli/client_server.rb +0 -111
  53. data/lib/omq/cli/config.rb +0 -54
  54. data/lib/omq/cli/formatter.rb +0 -75
  55. data/lib/omq/cli/pair.rb +0 -31
  56. data/lib/omq/cli/peer.rb +0 -8
  57. data/lib/omq/cli/pipe.rb +0 -265
  58. data/lib/omq/cli/pub_sub.rb +0 -14
  59. data/lib/omq/cli/push_pull.rb +0 -14
  60. data/lib/omq/cli/radio_dish.rb +0 -27
  61. data/lib/omq/cli/req_rep.rb +0 -83
  62. data/lib/omq/cli/router_dealer.rb +0 -76
  63. data/lib/omq/cli/scatter_gather.rb +0 -14
  64. data/lib/omq/cli.rb +0 -540
  65. data/lib/omq/zmtp/engine.rb +0 -551
  66. data/lib/omq/zmtp/options.rb +0 -48
  67. data/lib/omq/zmtp/reactor.rb +0 -131
  68. data/lib/omq/zmtp/readable.rb +0 -29
  69. data/lib/omq/zmtp/routing/channel.rb +0 -81
  70. data/lib/omq/zmtp/routing/client.rb +0 -56
  71. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  72. data/lib/omq/zmtp/routing/dish.rb +0 -80
  73. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  74. data/lib/omq/zmtp/routing/gather.rb +0 -48
  75. data/lib/omq/zmtp/routing/pair.rb +0 -84
  76. data/lib/omq/zmtp/routing/peer.rb +0 -100
  77. data/lib/omq/zmtp/routing/pub.rb +0 -62
  78. data/lib/omq/zmtp/routing/pull.rb +0 -48
  79. data/lib/omq/zmtp/routing/push.rb +0 -80
  80. data/lib/omq/zmtp/routing/radio.rb +0 -139
  81. data/lib/omq/zmtp/routing/rep.rb +0 -101
  82. data/lib/omq/zmtp/routing/req.rb +0 -65
  83. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  84. data/lib/omq/zmtp/routing/router.rb +0 -109
  85. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  86. data/lib/omq/zmtp/routing/server.rb +0 -100
  87. data/lib/omq/zmtp/routing/sub.rb +0 -80
  88. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  89. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  90. data/lib/omq/zmtp/routing.rb +0 -65
  91. data/lib/omq/zmtp/single_frame.rb +0 -20
  92. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  93. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  94. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  95. data/lib/omq/zmtp/writable.rb +0 -61
  96. data/lib/omq/zmtp.rb +0 -81
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 852e5a45a7b9f61b32004aa2079e1289291c3ac2ecc5efeb78911d88d3b63b2e
4
- data.tar.gz: b475a15807b8e98d4a852ec78b07ecaa4a0b15e68d3aec2fd9b2702533cb5d5f
3
+ metadata.gz: 3a81ea3e22e2133f508016129454fca72393068b259400e3d47aaf92d0948136
4
+ data.tar.gz: 71839df55e8ae9edb9db9710b77c5449853cde909c60867958a46e8350f27ead
5
5
  SHA512:
6
- metadata.gz: 0afb8f440937ae11bebe4ce3ecc1df599103f27300ed721b14437d708bd1cba1cd5b0aeb6873bd7281d054c42f6353abc29b1c8c542f4708152c2ad6b047b5f0
7
- data.tar.gz: b3477d54c520f7ed09dc4647b84841c9937d863036c4301afc3110bd959fd794bbd91db56d0204f317cd66ef62bf63ad8b1772521dc48f96912ae01e9bea8f81
6
+ metadata.gz: 9e9d603fb6f44b53626e37d88da5dea1297f932bfbc7b3af0b32e5ab4ebdc6c2151f4b7482bca70d5060d31ffc49ba3c771603b6ed1b4dd17ed51733339de67d
7
+ data.tar.gz: b40525791795b8e040a4a275a628c1cdca42eab43fcabfab76407de97f669ff9c1b21d05490509c0e0f5cd0b53505c7f85281343b2c312bdb89fd3eee7d4af75
data/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.10.0 — 2026-04-01
6
+
7
+ ### Added
8
+
9
+ - **Auto-close sockets via Async task tree** — all engine tasks (accept
10
+ loops, connection tasks, send/recv pumps, heartbeats, reconnect loops,
11
+ reapers) now live under the caller's Async task. When the `Async` block
12
+ exits, tasks are stopped and `ensure` blocks close IO resources.
13
+ Explicit `Socket#close` is no longer required (but remains available
14
+ and idempotent).
15
+ - **Non-Async usage** — sockets work outside `Async do…end`. A shared IO
16
+ thread hosts the task tree; all blocking operations (bind, connect,
17
+ send, receive, close) are dispatched to it transparently via
18
+ `Reactor.run`. The IO thread shuts down cleanly at process exit,
19
+ respecting the longest linger across all sockets.
20
+ - **Recv prefetching** — `#receive` internally drains up to 64 messages
21
+ per queue dequeue, buffering the excess behind a Mutex. Subsequent
22
+ calls return from the buffer without touching the queue. Thread-safe
23
+ on JRuby. TCP 64B pipelined: 30k → 221k msg/s (7x).
24
+
25
+ ### Changed
26
+
27
+ - **Transports are pure IO** — TCP and IPC transports no longer spawn
28
+ tasks. They create server sockets and return them; Engine owns the
29
+ accept loops.
30
+ - **Reactor simplified** — `spawn_pump` and `PumpHandle` removed.
31
+ Reactor exposes `root_task` (shared IO thread's root Async task)
32
+ and `run` (cross-thread dispatch). `stop!` respects max linger.
33
+ - **Flatten `OMQ::ZMTP` namespace into `OMQ`** — with the ZMTP protocol
34
+ layer extracted to `protocol-zmtp`, the `ZMTP` sub-namespace no longer
35
+ makes sense. Engine, routing, transport, and mixins now live directly
36
+ under `OMQ::`. Protocol-zmtp types are referenced as `Protocol::ZMTP::*`.
37
+
38
+ ### Performance
39
+
40
+ - **Direct pipe bypass for single-peer inproc** — PAIR, CHANNEL, and
41
+ single-peer RoundRobin types (PUSH, REQ, DEALER, CLIENT, SCATTER)
42
+ enqueue directly into the receiver's recv queue, skipping the
43
+ send_queue and send pump entirely.
44
+ Inproc PUSH/PULL: 200k → 980k msg/s (5x).
45
+ - **Uncapped send queue drain** — the send pump drains the entire queue
46
+ per cycle instead of capping at 64 messages. IO::Stream auto-flushes
47
+ at 64 KB, so writes hit the wire naturally under load.
48
+ IPC latency −12%, TCP latency −10%.
49
+ - **Remove `.b` allocations from PUB/SUB subscription matching** —
50
+ `FanOut#subscribed?` no longer creates temporary binary strings per
51
+ comparison; both topic and prefix are guaranteed binary at rest.
52
+ - **Reuse `written` Set and `latest` Hash across batches** in all send
53
+ pumps (fan-out, round-robin, router, server, peer, rep, radio),
54
+ eliminating per-batch object allocation.
55
+ - **O(1) `connection_removed` for identity-routed sockets** — Router,
56
+ Server, and Peer now maintain a reverse index instead of scanning.
57
+ - **`freeze_message` fast path** — skip `.b.freeze` when the string is
58
+ already a frozen binary string.
59
+ - **Pre-frozen empty frame constants** for REQ/REP delimiter frames.
60
+
61
+ ### Fixed
62
+
63
+ - **Reapers no longer crash on inproc DirectPipe** — PUSH and SCATTER
64
+ reapers skipped for DirectPipe connections that have no receive queue
65
+ (latent bug previously masked by transient task error swallowing).
66
+ - **`send_pump_idle?` made public** on all routing strategies — was
67
+ accidentally private, crashing `Engine#drain_send_queues` with
68
+ linger > 0.
69
+
70
+ ## 0.9.0 — 2026-03-31
71
+
72
+ ### Breaking
73
+
74
+ - **CLI extracted into omq-cli gem** — the `omq` executable, all CLI
75
+ code (`lib/omq/cli/`), tests, and `CLI.md` have moved to the
76
+ [omq-cli](https://github.com/paddor/omq-cli) gem. `gem install omq`
77
+ no longer provides the `omq` command — use `gem install omq-cli`.
78
+ - **`OMQ.outgoing` / `OMQ.incoming`** registration API moved to omq-cli.
79
+ Library-only users are unaffected (these were CLI-specific).
80
+
81
+ ### Changed
82
+
83
+ - **Gemspec is library-only** — no `exe/`, no `bindir`, no `executables`.
84
+ - **README** — restored title, replaced inline CLI section with a
85
+ pointer to omq-cli, fixed ZMTP attribution for protocol-zmtp.
86
+ - **DESIGN.md** — acknowledged protocol-zmtp, clarified transient
87
+ task / linger interaction, removed ZMTP wire protocol section (now in
88
+ protocol-zmtp), simplified inproc description, removed CLI section.
89
+
3
90
  ## 0.8.0 — 2026-03-31
4
91
 
5
92
  ### Breaking
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # OMQ — ZeroMQ in pure Ruby
1
+ # OMQ — Where did the C dependency go!?
2
2
 
3
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)
@@ -7,7 +7,7 @@
7
7
 
8
8
  `gem install omq` — that's it. No libzmq, no compiler, no system packages. Just Ruby.
9
9
 
10
- OMQ implements the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire protocol from scratch using [Async](https://github.com/socketry/async) fibers. It speaks native ZeroMQ on the wire and interoperates with libzmq, pyzmq, CZMQ, and everything else in the ZMQ ecosystem.
10
+ OMQ builds ZeroMQ socket patterns on top of [protocol-zmtp](https://github.com/paddor/protocol-zmtp) (a pure Ruby [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) codec) using [Async](https://github.com/socketry/async) fibers. It speaks native ZeroMQ on the wire and interoperates with libzmq, pyzmq, CZMQ, and everything else in the ZMQ ecosystem.
11
11
 
12
12
  > **234k msg/s** inproc | **49k msg/s** ipc | **36k msg/s** tcp
13
13
  >
@@ -31,12 +31,12 @@ See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all ma
31
31
 
32
32
  - **Zero dependencies on C** — no extensions, no FFI, no libzmq. `gem install` just works everywhere
33
33
  - **Fast** — YJIT-optimized hot paths, batched sends, 234k msg/s inproc with 12 µs latency
34
- - **`omq` CLI** — pipe, filter, and transform messages from the terminal with Ruby eval, Ractor parallelism, and [script handlers](CLI.md#script-handlers--r)
34
+ - **[`omq` CLI](https://github.com/paddor/omq-cli)** — `gem install omq-cli` for a command-line tool with Ruby eval, Ractor parallelism, and script handlers
35
35
  - **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
36
36
  - **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
37
37
  - **Async-native** — built on fibers, non-blocking from the ground up. A shared IO thread handles sockets outside of Async — no reactor needed for simple scripts
38
38
  - **Wire-compatible** — interoperates with libzmq, pyzmq, CZMQ over tcp and ipc
39
- - **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects and requeues automatically
39
+ - **Bind/connect order doesn't matter** — connect before bind, bind before connect, peers come and go. ZeroMQ reconnects automatically and queued messages drain when peers arrive
40
40
 
41
41
  For architecture internals, see [DESIGN.md](DESIGN.md).
42
42
 
@@ -150,58 +150,18 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes li
150
150
  | **PEER** | Routing-ID | Fair-queue | Block |
151
151
  | **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
152
152
 
153
- ## omq — CLI tool
153
+ ## CLI
154
154
 
155
- `omq` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
155
+ Install [omq-cli](https://github.com/paddor/omq-cli) for a command-line tool that sends, receives, pipes, and transforms ZeroMQ messages from the terminal:
156
156
 
157
157
  ```sh
158
- # Echo server
159
- omq rep -b tcp://:5555 --echo
160
-
161
- # Upcase server — -e evals Ruby on each incoming message
162
- omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
158
+ gem install omq-cli
163
159
 
164
- # Client
160
+ omq rep -b tcp://:5555 --echo
165
161
  echo "hello" | omq req -c tcp://localhost:5555
166
- # => HELLO
167
-
168
- # PUB/SUB
169
- omq sub -b tcp://:5556 -s "weather." &
170
- echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
171
-
172
- # Pipeline with filtering
173
- tail -f /var/log/syslog | omq push -c tcp://collector:5557
174
- omq pull -b tcp://:5557 -e 'next unless /error/; $F'
175
-
176
- # Transform outgoing messages with -E
177
- echo hello | omq push -c tcp://localhost:5557 -E '$F.map(&:upcase)'
178
-
179
- # REQ: transform request and reply independently
180
- echo hello | omq req -c tcp://localhost:5555 \
181
- -E '$F.map(&:upcase)' -e '$F.map(&:reverse)'
182
-
183
- # Pipe: PULL → eval → PUSH in one process
184
- omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
185
-
186
- # Pipe with Ractor workers for CPU parallelism (-P = all CPUs)
187
- omq pipe -c ipc://@work -c ipc://@sink -P -r./fib -e 'fib(Integer($_)).to_s'
188
- ```
189
-
190
- `-e` (recv-eval) transforms incoming messages, `-E` (send-eval) transforms outgoing messages. `$F` is the message parts array, `$_` is the first part. Use `-r` to require gems or load scripts that register handlers via `OMQ.incoming` / `OMQ.outgoing`:
191
-
192
- ```ruby
193
- # my_handler.rb
194
- db = DB.connect("postgres://localhost/app")
195
-
196
- OMQ.incoming { db.query($F.first) }
197
- at_exit { db.close }
198
- ```
199
-
200
- ```sh
201
- omq pull -b tcp://:5557 -r./my_handler.rb
202
162
  ```
203
163
 
204
- See [CLI.md](CLI.md) for full documentation, or `omq --help` / `omq --examples`.
164
+ See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentation.
205
165
 
206
166
  ## Development
207
167
 
data/lib/omq/channel.rb CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  module OMQ
4
4
  class CHANNEL < Socket
5
- include ZMTP::Readable
6
- include ZMTP::Writable
7
- include ZMTP::SingleFrame
5
+ include Readable
6
+ include Writable
7
+ include SingleFrame
8
8
 
9
9
  def initialize(endpoints = nil, linger: 0)
10
10
  _init_engine(:CHANNEL, linger: linger)
@@ -2,9 +2,9 @@
2
2
 
3
3
  module OMQ
4
4
  class CLIENT < Socket
5
- include ZMTP::Readable
6
- include ZMTP::Writable
7
- include ZMTP::SingleFrame
5
+ include Readable
6
+ include Writable
7
+ include SingleFrame
8
8
 
9
9
  def initialize(endpoints = nil, linger: 0)
10
10
  _init_engine(:CLIENT, linger: linger)
@@ -13,9 +13,9 @@ module OMQ
13
13
  end
14
14
 
15
15
  class SERVER < Socket
16
- include ZMTP::Readable
17
- include ZMTP::Writable
18
- include ZMTP::SingleFrame
16
+ include Readable
17
+ include Writable
18
+ include SingleFrame
19
19
 
20
20
  def initialize(endpoints = nil, linger: 0)
21
21
  _init_engine(:SERVER, linger: linger)