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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +173 -0
- data/README.md +21 -19
- data/exe/omq +6 -0
- data/lib/omq/cli/base_runner.rb +423 -0
- data/lib/omq/cli/channel.rb +8 -0
- data/lib/omq/cli/client_server.rb +106 -0
- data/lib/omq/cli/config.rb +51 -0
- data/lib/omq/cli/formatter.rb +75 -0
- data/lib/omq/cli/pair.rb +31 -0
- data/lib/omq/cli/peer.rb +8 -0
- data/lib/omq/cli/pipe.rb +249 -0
- data/lib/omq/cli/pub_sub.rb +14 -0
- data/lib/omq/cli/push_pull.rb +14 -0
- data/lib/omq/cli/radio_dish.rb +27 -0
- data/lib/omq/cli/req_rep.rb +77 -0
- data/lib/omq/cli/router_dealer.rb +70 -0
- data/lib/omq/cli/scatter_gather.rb +14 -0
- data/lib/omq/cli.rb +444 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +2 -2
- data/lib/omq/socket.rb +74 -27
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/connection.rb +24 -3
- data/lib/omq/zmtp/engine.rb +179 -17
- data/lib/omq/zmtp/options.rb +4 -3
- data/lib/omq/zmtp/reactor.rb +10 -5
- data/lib/omq/zmtp/routing/channel.rb +8 -2
- data/lib/omq/zmtp/routing/fan_out.rb +38 -8
- data/lib/omq/zmtp/routing/pair.rb +8 -2
- data/lib/omq/zmtp/routing/peer.rb +7 -1
- data/lib/omq/zmtp/routing/push.rb +14 -7
- data/lib/omq/zmtp/routing/radio.rb +32 -11
- data/lib/omq/zmtp/routing/rep.rb +11 -7
- data/lib/omq/zmtp/routing/req.rb +1 -2
- data/lib/omq/zmtp/routing/round_robin.rb +35 -1
- data/lib/omq/zmtp/routing/router.rb +7 -1
- data/lib/omq/zmtp/routing/scatter.rb +16 -3
- data/lib/omq/zmtp/routing/server.rb +7 -1
- data/lib/omq/zmtp/routing/xsub.rb +7 -1
- data/lib/omq/zmtp/transport/inproc.rb +40 -5
- data/lib/omq/zmtp/transport/ipc.rb +9 -7
- data/lib/omq/zmtp/transport/tcp.rb +14 -7
- data/lib/omq/zmtp/writable.rb +21 -4
- data/lib/omq.rb +7 -0
- metadata +18 -3
- data/exe/omqcat +0 -532
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6cbe3ab5a57c05043fa8f088c106c54ada9268286fe812b4fb43bf0d4ad4bdff
|
|
4
|
+
data.tar.gz: 7ef9f87c80ee05ea36bc60386cc7221ad4869fadc4840d5dcc58c218bdebfabc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
147
|
+
## omq — CLI tool
|
|
146
148
|
|
|
147
|
-
`
|
|
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
|
-
|
|
153
|
+
omq rep -b tcp://:5555 --echo
|
|
152
154
|
|
|
153
155
|
# Upcase server in one line
|
|
154
|
-
|
|
156
|
+
omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
|
|
155
157
|
|
|
156
158
|
# Client
|
|
157
|
-
echo "hello" |
|
|
159
|
+
echo "hello" | omq req -c tcp://localhost:5555
|
|
158
160
|
# => HELLO
|
|
159
161
|
|
|
160
162
|
# PUB/SUB
|
|
161
|
-
|
|
162
|
-
echo "weather.nyc 72F" |
|
|
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 |
|
|
166
|
-
|
|
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" |
|
|
170
|
-
|
|
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"]' |
|
|
175
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
+
omq push -c tcp://remote:5557 -z < data.txt
|
|
181
|
+
omq pull -b tcp://:5557 -z
|
|
180
182
|
|
|
181
183
|
# CURVE encryption
|
|
182
|
-
|
|
184
|
+
omq rep -b tcp://:5555 -D "secret" --curve-server
|
|
183
185
|
# prints: OMQ_SERVER_KEY='...'
|
|
184
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|