omq 0.5.1 → 0.6.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +173 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +444 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +24 -3
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +10 -5
  28. data/lib/omq/zmtp/routing/channel.rb +8 -2
  29. data/lib/omq/zmtp/routing/fan_out.rb +38 -8
  30. data/lib/omq/zmtp/routing/pair.rb +8 -2
  31. data/lib/omq/zmtp/routing/peer.rb +7 -1
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +32 -11
  34. data/lib/omq/zmtp/routing/rep.rb +11 -7
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +35 -1
  37. data/lib/omq/zmtp/routing/router.rb +7 -1
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +7 -1
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/transport/inproc.rb +40 -5
  42. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  43. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  44. data/lib/omq/zmtp/writable.rb +21 -4
  45. data/lib/omq.rb +7 -0
  46. metadata +18 -3
  47. data/exe/omqcat +0 -532
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76cecd3650f9fb73d699a027570421f589452a40cf6bbf207af02afb2bc6df08
4
- data.tar.gz: 0252e01bacd5278defe46ed9ab4750b3e33ca71e928beb13f1092c23a7c2ce62
3
+ metadata.gz: 6cbe3ab5a57c05043fa8f088c106c54ada9268286fe812b4fb43bf0d4ad4bdff
4
+ data.tar.gz: 7ef9f87c80ee05ea36bc60386cc7221ad4869fadc4840d5dcc58c218bdebfabc
5
5
  SHA512:
6
- metadata.gz: de2508cbe4e22c6dc93bdeab4626fed0c5280cf74eaaf4c4f886f566719a3c732f5db9b766c201f7e9d6eb7bb00ee4ed2d2392eec232abe88c7847148b889fc5
7
- data.tar.gz: 65e96cef70697abe90588e5e35f4563e55ef183bb0be44276c6cb62ec007f6accb4e482a08c14d89b15ec3f542d75c78bacc87c6174ca6f4e69b588ddb540730
6
+ metadata.gz: d84d69dafd6b1f8e98e22726556f38865e61b9e047e39ea88ab0546dbac224887ae11645728b193b457f99401f6643133dcff357cef03856cf4308818ae05288
7
+ data.tar.gz: 647d4a79a3cb4ae55611f95bbefbb598dd700244820b5a20158a87c3430ba54434dfe2e7565727ceff37f53bbebc72e7f33f19ac0a7134749dec2a28331d3cf5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,178 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 — 2026-03-30
4
+
5
+ ### Added
6
+
7
+ - **`OMQ::SocketDeadError`** — raised on `#send`/`#receive` after an
8
+ internal pump task crashes. The original exception is available via
9
+ `#cause`. The socket is permanently bricked.
10
+ - **`Engine#spawn_pump_task`** — replaces bare `parent_task.async(transient: true)`
11
+ in all 10 routing strategies. Catches unexpected exceptions and forwards
12
+ them via `signal_fatal_error` so blocked `#send`/`#receive` callers see
13
+ the real error instead of deadlocking.
14
+ - **`Socket#close_read`** — pushes a nil sentinel into the recv queue,
15
+ causing a blocked `#receive` to return nil. Used by `--transient` to
16
+ drain remaining messages before exit instead of killing the task.
17
+ - **`send_pump_idle?`** on all routing classes — tracks whether the send
18
+ pump has an in-flight batch. `Engine#drain_send_queues` now waits for
19
+ both `send_queue.empty?` and `send_pump_idle?`, preventing message loss
20
+ during linger close.
21
+ - **Grace period after `peer_connected`** — senders that bind or connect
22
+ to multiple endpoints sleep one `reconnect_interval` (100ms) after the
23
+ first peer handshake, giving latecomers time to connect before messages
24
+ start flowing.
25
+ - **`-P/--parallel [N]` for `omq pipe`** — spawns N Ractor workers
26
+ (default: nproc) in a single process for true CPU parallelism. Each
27
+ Ractor runs its own Async reactor with independent PULL/PUSH sockets.
28
+ `$F` in `-e` expressions is transparently rewritten for Ractor isolation.
29
+ - **`BEGIN{}`/`END{}` blocks in `-e` expressions** — like awk, run setup
30
+ before the message loop and teardown after. Supports nested braces.
31
+ Example: `-e 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'`
32
+ - **`--reconnect-ivl`** — set reconnect interval from the CLI, accepts a
33
+ fixed value (`0.5`) or a range for exponential backoff (`0.1..2`).
34
+ - **`--transient`** — exit when all peers disconnect (after at least one
35
+ message has been sent/received). Useful for pipeline sinks and workers.
36
+ - **`--examples`** — annotated usage examples, paged via `$PAGER` or `less`.
37
+ `--help` now shows help + examples (paged); `-h` shows help only.
38
+ - **`-r` relative paths** — `-r./lib.rb` and `-r../lib.rb` resolve via
39
+ `File.expand_path` instead of `$LOAD_PATH`.
40
+ - **`peer_connected` / `all_peers_gone`** — `Async::Promise` hooks on
41
+ `Socket` for connection lifecycle tracking.
42
+ - **`reconnect_enabled=`** — disable auto-reconnect per socket.
43
+ - **Pipeline benchmark** — 4-worker fib pipeline via `omq` CLI
44
+ (`bench/cli/pipeline.sh`). ~300–1800 msg/s depending on N.
45
+ - **DESIGN.md** — architecture overview covering task trees, send pump
46
+ batching, ZMTP wire protocol, transports, and the fallacies of
47
+ distributed computing.
48
+ - **Draft socket types in omqcat** — CLIENT, SERVER, RADIO, DISH, SCATTER,
49
+ GATHER, CHANNEL, and PEER are now supported in the CLI tool.
50
+ - `-j`/`--join GROUP` for DISH (like `--subscribe` for SUB)
51
+ - `-g`/`--group GROUP` for RADIO publishing
52
+ - `--target` extended to SERVER and PEER (accepts `0x` hex for binary routing IDs)
53
+ - `--echo` and `-e` on SERVER/PEER reply to the originating client via `send_to`
54
+ - CLIENT uses request-reply loop (send then receive)
55
+ - **Unified `--timeout`** — replaces `--recv-timeout`/`--send-timeout` with a
56
+ single `-t`/`--timeout` flag that applies to both directions.
57
+ - **`--linger`** — configurable drain time on close (default 5s).
58
+ - **Exit codes** — 0 = success, 1 = error, 2 = timeout.
59
+ - **CLI unit tests** — 74 tests covering Formatter, routing helpers,
60
+ validation, and option parsing.
61
+ - **Quantized `--interval`** — uses `Async::Loop.quantized` for
62
+ wall-clock-aligned, start-to-start timing (no drift).
63
+ - **`-e` as data source** — eval expressions can generate messages without
64
+ `--data`, `--file`, or stdin. E.g. `omq pub -e 'Time.now.to_s' -i 1`.
65
+ - **`$_` in eval** — set to the first frame of `$F` inside `-e` expressions,
66
+ following Ruby convention.
67
+ - **`wait_for_peer`** — connecting sockets wait for the first peer handshake
68
+ before sending. Replaces the need for manual `--delay` on PUB, PUSH, etc.
69
+ - **`OMQ_DEV` env var** — unified dev-mode flag for loading local omq and
70
+ omq-curve source via `require_relative` (replaces `DEV_ENV`).
71
+ - **`--marshal` / `-M`** — Ruby Marshal stream format. Sends any Ruby
72
+ object over the wire; receiver deserializes and prints `inspect` output.
73
+ E.g. `omq pub -e 'Time.now' -M` / `omq sub -M`.
74
+ - **`-e` single-shot** — eval runs once and exits when no other data
75
+ source is present. Supports `self << msg` for direct socket sends.
76
+ - **`subscriber_joined`** — `Async::Promise` on PUB/XPUB that resolves
77
+ when the first subscription arrives. CLI PUB waits for it before sending.
78
+ - **`#to_str` enforcement** — message parts must be string-like; passing
79
+ integers or symbols raises `NoMethodError` instead of silently coercing.
80
+ - **`-e` error handling** — eval errors abort with exit code 3.
81
+ - **`--raw` outputs ZMTP frames** — flags + length + body per frame,
82
+ suitable for `hexdump -C`. Compression remains transparent.
83
+ - **ROUTER `router_mandatory` by default** — CLI ROUTER rejects sends to
84
+ unknown identities and waits for first peer before sending.
85
+ - **`--timeout` applies to `wait_for_peer`** — `-t` now bounds the initial
86
+ connection wait via `Async::TimeoutError`.
87
+
88
+ ### Improved
89
+
90
+ - **Received messages are always frozen** — `Connection#receive_message`
91
+ (TCP/IPC) now returns a frozen array of frozen strings, matching the
92
+ inproc fast-path. REP and REQ recv transforms rewritten to avoid
93
+ in-place mutation (`Array#shift` → slicing).
94
+ - **CLI refactored into 16 files** — the 1162-line `cli.rb` monolith is
95
+ decomposed into `CLI::Config` (frozen `Data.define`), `CLI::Formatter`,
96
+ `CLI::BaseRunner` (shared infrastructure), and one runner class per
97
+ socket type combo (PushRunner, PullRunner, ReqRunner, RepRunner, etc.).
98
+ Each runner models its behavior as a single `#run_loop` override.
99
+ - **`--transient` uses `close_read` instead of `task.stop`** — recv-only
100
+ and bidirectional sockets drain their recv queue via nil sentinel before
101
+ exiting, preventing message loss on disconnect. Send-only sockets still
102
+ use `task.stop`.
103
+ - **Pipeline benchmark** — natural startup order (producer → workers →
104
+ sink), workers use `--transient -t 1` (timeout covers workers that
105
+ connect after the producer is already gone). Verified correct at 5M messages
106
+ (56k msg/s sustained, zero message loss).
107
+ - **Renamed `omqcat` → `omq`** — the CLI executable is now `omq`, matching
108
+ the gem name.
109
+ - **Per-connection task subtrees** — each connection gets an isolated Async
110
+ task whose children (heartbeat, recv pump, reaper) are cleaned up
111
+ automatically when the connection dies. No reparenting.
112
+ - **Flat task tree** — send pump spawned at socket level (singleton), not
113
+ inside connection subtrees. Accept loops use `defer_stop` to prevent
114
+ socket leaks on stop.
115
+ - **`compile_expr`** — `-e` expressions compiled once as a proc,
116
+ `instance_exec` per message (was `instance_eval` per message).
117
+ - **Close lifecycle** — stop listeners before drain only when connections
118
+ exist; keep listeners open with zero connections so late-arriving peers
119
+ can receive queued messages during linger.
120
+ - **Reconnect guard** — `@closing` flag suppresses reconnect during close.
121
+ - **Task annotations** — all pump tasks carry descriptive annotations
122
+ (send pump, recv pump, reaper, heartbeat, reconnect, tcp/ipc accept).
123
+ - **Rename monitor → reaper** — clearer name for PUSH/SCATTER dead-peer
124
+ detection tasks.
125
+ - **Extracted `OMQ::CLI` module** — `exe/omq` is a thin wrapper;
126
+ bulk of the CLI lives in `lib/omq/cli.rb` (loaded via `require "omq/cli"`,
127
+ not auto-loaded by `require "omq"`).
128
+ - `Formatter` class for encode/decode/compress/decompress
129
+ - `Runner` is stateful with `@sock`, cleaner method signatures
130
+ - **Quoted format uses `String#dump`/`undump`** — fixes backslash escaping
131
+ bug, proper round-tripping of all byte values.
132
+ - **Hex routing IDs** — binary identities display as `0xdeadbeef` instead
133
+ of lossy Z85 encoding. `--target 0x...` decodes hex on input.
134
+ - **Compression-safe routing** — routing ID and delimiter frames are no
135
+ longer compressed/decompressed in ROUTER, SERVER, and PEER loops.
136
+ - **`require_relative` in CLI** — `exe/omq` loads the local source tree
137
+ instead of the installed gem.
138
+ - **`output` skips nil** — `-e` returning nil no longer prints a blank line.
139
+ - **Removed `#count_reached?`** — inlined for clarity.
140
+ - **System tests overhauled** — `test/omqcat` → `test/cli`, all IPC
141
+ abstract namespace, `set -eu`, stderr captured, no sleeps (except
142
+ ROUTER --target), under 10s.
143
+
144
+ ### Fixed
145
+
146
+ - **Inproc DEALER→REP broker deadlock** — `Writable#send` freezes the
147
+ message array, but the REP recv transform mutated it in-place via
148
+ `Array#shift`. On the inproc fast-path the frozen array passed through
149
+ the DEALER send pump unchanged, causing `FrozenError` that silently
150
+ killed the send pump task and deadlocked the broker.
151
+ - **Pump errors swallowed silently** — all send/recv pump tasks ran as
152
+ `transient: true` Async tasks, so unexpected exceptions (bugs) were
153
+ logged but never surfaced to the caller. The socket would deadlock
154
+ instead of raising. Now `Engine#signal_fatal_error` stores the error
155
+ and unblocks the recv queue; subsequent `#send`/`#receive` calls
156
+ re-raise it as `SocketDeadError`. Expected errors (`Async::Stop`,
157
+ `ProtocolError`, `CONNECTION_LOST`) are still handled normally.
158
+ - **Pipe `--transient` drains too early** — `all_peers_gone` fired while
159
+ `pull.receive` was blocked, hanging the worker forever. Now the transient
160
+ monitor pushes a nil sentinel via `close_read`, which unblocks the
161
+ blocked dequeue and lets the loop drain naturally.
162
+ - **Linger drain missed in-flight batches** — `drain_send_queues` only
163
+ checked `send_queue.empty?`, but the send pump may have already dequeued
164
+ messages into a local batch. Now also checks `send_pump_idle?`.
165
+ - **Socket option delegators not Ractor-safe** — `define_method` with a
166
+ block captured state from the main Ractor, causing `Ractor::IsolationError`
167
+ when calling setters like `recv_timeout=`. Replaced with `Forwardable`.
168
+ - **Pipe endpoint ordering** — `omq pipe -b url1 -c url2` assigned PULL
169
+ to `url2` and PUSH to `url1` (backwards) because connects were
170
+ concatenated before binds. Now uses ordered `Config#endpoints`.
171
+ - **Linger drain kills reconnect tasks** — `Engine#close` set `@closed = true`
172
+ before draining send queues, causing reconnect tasks to bail immediately.
173
+ Messages queued before any peer connected were silently dropped. Now `@closed`
174
+ is set after draining, so reconnection continues during the linger period.
175
+
3
176
  ## 0.5.1 — 2026-03-28
4
177
 
5
178
  ### Improved
data/README.md CHANGED
@@ -33,6 +33,8 @@ Modern Ruby has closed the gap:
33
33
 
34
34
  When [CZTop](https://github.com/paddor/cztop) was written, none of this existed. Today, a pure Ruby ZMTP implementation is fast enough for production use — and you get `gem install` with no compiler toolchain, no system packages, and no segfaults.
35
35
 
36
+ See [DESIGN.md](DESIGN.md) for architecture details: task trees, send pump batching, the ZMTP wire protocol, and how OMQ handles the fallacies of distributed computing.
37
+
36
38
  ## Install
37
39
 
38
40
  No system libraries needed — just Ruby:
@@ -142,56 +144,56 @@ Benchmarked with benchmark-ips on Linux x86_64 (Ruby 4.0.2 +YJIT):
142
144
 
143
145
  See [`bench/`](bench/) for full results and scripts.
144
146
 
145
- ## omqcat — CLI tool
147
+ ## omq — CLI tool
146
148
 
147
- `omqcat` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
149
+ `omq` is a command-line tool for sending and receiving messages on any OMQ socket. Like `nngcat` from libnng, but with Ruby superpowers.
148
150
 
149
151
  ```sh
150
152
  # Echo server
151
- omqcat rep -b tcp://:5555 --echo
153
+ omq rep -b tcp://:5555 --echo
152
154
 
153
155
  # Upcase server in one line
154
- omqcat rep -b tcp://:5555 -e '$F.map(&:upcase)'
156
+ omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
155
157
 
156
158
  # Client
157
- echo "hello" | omqcat req -c tcp://localhost:5555
159
+ echo "hello" | omq req -c tcp://localhost:5555
158
160
  # => HELLO
159
161
 
160
162
  # PUB/SUB
161
- omqcat sub -b tcp://:5556 -s "weather." &
162
- echo "weather.nyc 72F" | omqcat pub -c tcp://localhost:5556 -d 0.3
163
+ omq sub -b tcp://:5556 -s "weather." &
164
+ echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
163
165
 
164
166
  # Pipeline with filtering
165
- tail -f /var/log/syslog | omqcat push -c tcp://collector:5557
166
- omqcat pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
167
+ tail -f /var/log/syslog | omq push -c tcp://collector:5557
168
+ omq pull -b tcp://:5557 -e '$F.first.include?("error") ? $F : nil'
167
169
 
168
170
  # Multipart messages via tabs
169
- printf "routing-key\tpayload data" | omqcat push -c tcp://localhost:5557
170
- omqcat pull -b tcp://:5557
171
+ printf "routing-key\tpayload data" | omq push -c tcp://localhost:5557
172
+ omq pull -b tcp://:5557
171
173
  # => routing-key payload data
172
174
 
173
175
  # JSONL for structured data
174
- echo '["key","value"]' | omqcat push -c tcp://localhost:5557 -J
175
- omqcat pull -b tcp://:5557 -J
176
+ echo '["key","value"]' | omq push -c tcp://localhost:5557 -J
177
+ omq pull -b tcp://:5557 -J
176
178
 
177
179
  # Zstandard compression
178
- omqcat push -c tcp://remote:5557 -z < data.txt
179
- omqcat pull -b tcp://:5557 -z
180
+ omq push -c tcp://remote:5557 -z < data.txt
181
+ omq pull -b tcp://:5557 -z
180
182
 
181
183
  # CURVE encryption
182
- omqcat rep -b tcp://:5555 -D "secret" --curve-server
184
+ omq rep -b tcp://:5555 -D "secret" --curve-server
183
185
  # prints: OMQ_SERVER_KEY='...'
184
- omqcat req -c tcp://localhost:5555 --curve-server-key '...'
186
+ omq req -c tcp://localhost:5555 --curve-server-key '...'
185
187
  ```
186
188
 
187
189
  The `-e` flag runs Ruby inside the socket instance — the full socket API (`self <<`, `send`, `subscribe`, ...) is available. Use `-r` to require gems:
188
190
 
189
191
  ```sh
190
- omqcat sub -c tcp://localhost:5556 -s "" -r json \
192
+ omq sub -c tcp://localhost:5556 -s "" -r json \
191
193
  -e 'JSON.parse($F.first)["temperature"]'
192
194
  ```
193
195
 
194
- Formats: `--ascii` (default, tab-separated), `--quoted`, `--raw`, `--jsonl`, `--msgpack`. See `omqcat --help` for all options.
196
+ Formats: `--ascii` (default, tab-separated), `--quoted`, `--raw`, `--jsonl`, `--msgpack`. See `omq --help` for all options.
195
197
 
196
198
  ## Interop with native ZMQ
197
199
 
data/exe/omq ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ Warning[:experimental] = false
4
+
5
+ require_relative "../lib/omq/cli"
6
+ OMQ::CLI.run