omq 0.6.5 → 0.8.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 +78 -1
- data/README.md +76 -81
- data/lib/omq/cli/base_runner.rb +54 -21
- data/lib/omq/cli/client_server.rb +14 -9
- data/lib/omq/cli/config.rb +7 -4
- data/lib/omq/cli/pair.rb +1 -1
- data/lib/omq/cli/pipe.rb +37 -21
- data/lib/omq/cli/req_rep.rb +8 -4
- data/lib/omq/cli/router_dealer.rb +12 -6
- data/lib/omq/cli.rb +100 -34
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/engine.rb +69 -22
- data/lib/omq/zmtp/routing/peer.rb +3 -2
- data/lib/omq/zmtp/routing/rep.rb +2 -3
- data/lib/omq/zmtp/routing/req.rb +3 -7
- data/lib/omq/zmtp/routing/router.rb +3 -2
- data/lib/omq/zmtp/routing/server.rb +3 -2
- data/lib/omq/zmtp.rb +16 -13
- data/lib/omq.rb +1 -0
- metadata +15 -8
- data/lib/omq/zmtp/codec/command.rb +0 -210
- data/lib/omq/zmtp/codec/frame.rb +0 -89
- data/lib/omq/zmtp/codec/greeting.rb +0 -78
- data/lib/omq/zmtp/codec.rb +0 -18
- data/lib/omq/zmtp/connection.rb +0 -282
- data/lib/omq/zmtp/mechanism/null.rb +0 -72
- data/lib/omq/zmtp/valid_peers.rb +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 852e5a45a7b9f61b32004aa2079e1289291c3ac2ecc5efeb78911d88d3b63b2e
|
|
4
|
+
data.tar.gz: b475a15807b8e98d4a852ec78b07ecaa4a0b15e68d3aec2fd9b2702533cb5d5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0afb8f440937ae11bebe4ce3ecc1df599103f27300ed721b14437d708bd1cba1cd5b0aeb6873bd7281d054c42f6353abc29b1c8c542f4708152c2ad6b047b5f0
|
|
7
|
+
data.tar.gz: b3477d54c520f7ed09dc4647b84841c9937d863036c4301afc3110bd959fd794bbd91db56d0204f317cd66ef62bf63ad8b1772521dc48f96912ae01e9bea8f81
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-03-31
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **CURVE mechanism moved to protocol-zmtp** — `OMQ::ZMTP::Mechanism::Curve`
|
|
8
|
+
is now `Protocol::ZMTP::Mechanism::Curve` with a required `crypto:` parameter.
|
|
9
|
+
Pass `crypto: RbNaCl` (libsodium) or `crypto: Nuckle` (pure Ruby). The
|
|
10
|
+
omq-curve and omq-kurve gems are superseded.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# Before (omq-curve)
|
|
14
|
+
require "omq/curve"
|
|
15
|
+
rep.mechanism = OMQ::Curve.server(pub, sec)
|
|
16
|
+
|
|
17
|
+
# After (protocol-zmtp + any NaCl backend)
|
|
18
|
+
require "protocol/zmtp/mechanism/curve"
|
|
19
|
+
require "nuckle" # or: require "rbnacl"
|
|
20
|
+
rep.mechanism = Protocol::ZMTP::Mechanism::Curve.server(pub, sec, crypto: Nuckle)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Protocol layer extracted into protocol-zmtp gem** — Codec (Frame,
|
|
26
|
+
Greeting, Command), Connection, Mechanism::Null, Mechanism::Curve,
|
|
27
|
+
ValidPeers, and Z85 now live in the
|
|
28
|
+
[protocol-zmtp](https://github.com/paddor/protocol-zmtp) gem. OMQ
|
|
29
|
+
re-exports them under `OMQ::ZMTP::` for backwards compatibility.
|
|
30
|
+
protocol-zmtp has zero runtime dependencies.
|
|
31
|
+
- **Unified CURVE mechanism** — one implementation with a pluggable
|
|
32
|
+
`crypto:` backend replaces the two near-identical copies in omq-curve
|
|
33
|
+
(RbNaCl) and omq-kurve (Nuckle). 1,088 → 467 lines (57% reduction).
|
|
34
|
+
- **Heartbeat ownership** — `Connection#start_heartbeat` removed.
|
|
35
|
+
Connection tracks timestamps only; the engine drives the PING/PONG loop.
|
|
36
|
+
- **CI no longer needs libsodium** — CURVE tests use
|
|
37
|
+
[nuckle](https://github.com/paddor/nuckle) (pure Ruby) by default.
|
|
38
|
+
Cross-backend interop tests run when rbnacl is available.
|
|
39
|
+
|
|
40
|
+
## 0.7.0 — 2026-03-30
|
|
41
|
+
|
|
42
|
+
### Breaking
|
|
43
|
+
|
|
44
|
+
- **`-e` is now `--recv-eval`** — evaluates incoming messages only.
|
|
45
|
+
Send-only sockets (PUSH, PUB, SCATTER, RADIO) must use `-E` /
|
|
46
|
+
`--send-eval` instead of `-e`.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **`-E` / `--send-eval`** — eval Ruby for each outgoing message.
|
|
51
|
+
REQ can now transform requests independently from replies.
|
|
52
|
+
ROUTER/SERVER/PEER: `-E` does dynamic routing (first element =
|
|
53
|
+
identity), mutually exclusive with `--target`.
|
|
54
|
+
- **`OMQ.outgoing` / `OMQ.incoming`** — registration API for script
|
|
55
|
+
handlers loaded via `-r`. Blocks receive message parts as a block
|
|
56
|
+
argument (`|msg|`). Setup via closures, teardown via `at_exit`.
|
|
57
|
+
CLI flags override registered handlers.
|
|
58
|
+
- **[CLI.md](CLI.md)** — comprehensive CLI documentation.
|
|
59
|
+
- **[GETTING_STARTED.md](GETTING_STARTED.md)** — renamed from
|
|
60
|
+
`ZGUIDE_SUMMARY.md` for discoverability.
|
|
61
|
+
- **Multi-peer pipe with `--in`/`--out`** — modal switches that assign
|
|
62
|
+
subsequent `-b`/`-c` to the PULL (input) or PUSH (output) side.
|
|
63
|
+
Enables fan-in, fan-out, and mixed bind/connect per side.
|
|
64
|
+
Backward compatible — without `--in`/`--out`, the positional
|
|
65
|
+
2-endpoint syntax works as before.
|
|
66
|
+
|
|
67
|
+
### Improved
|
|
68
|
+
|
|
69
|
+
- **YJIT recv pump** — replaced lambda/proc `transform:` parameter in
|
|
70
|
+
`Engine#start_recv_pump` with block captures. No-transform path
|
|
71
|
+
(PUSH/PULL, PUB/SUB) is now branch-free. ~2.5x YJIT speedup on
|
|
72
|
+
inproc, ~2x on ipc/tcp.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **Frozen array from `recv_msg_raw`** — ROUTER/SERVER receiver crashed
|
|
77
|
+
with `FrozenError` when shifting identity off frozen message arrays.
|
|
78
|
+
`#recv_msg_raw` now dups the array.
|
|
79
|
+
|
|
3
80
|
## 0.6.5 — 2026-03-30
|
|
4
81
|
|
|
5
82
|
### Fixed
|
|
@@ -462,4 +539,4 @@ Initial release. Pure Ruby implementation of ZMTP 3.1 (ZeroMQ) using Async.
|
|
|
462
539
|
- Linger on close (drain send queue before closing)
|
|
463
540
|
- `max_message_size` enforcement
|
|
464
541
|
- Works inside Async reactors or standalone (shared IO thread)
|
|
465
|
-
- Optional CURVE encryption via the [
|
|
542
|
+
- Optional CURVE encryption via the [protocol-zmtp](https://github.com/paddor/protocol-zmtp) gem
|
data/README.md
CHANGED
|
@@ -1,39 +1,44 @@
|
|
|
1
|
-
# OMQ
|
|
1
|
+
# OMQ — ZeroMQ in pure Ruby
|
|
2
2
|
|
|
3
3
|
[](https://github.com/zeromq/omq/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/omq)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://www.ruby-lang.org)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
`gem install omq` — that's it. No libzmq, no compiler, no system packages. Just Ruby.
|
|
9
9
|
|
|
10
|
-
|
|
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.
|
|
11
|
+
|
|
12
|
+
> **234k msg/s** inproc | **49k msg/s** ipc | **36k msg/s** tcp
|
|
11
13
|
>
|
|
12
|
-
> **
|
|
14
|
+
> **12 µs** inproc latency | **51 µs** ipc | **62 µs** tcp
|
|
13
15
|
>
|
|
14
|
-
> Ruby 4.0 + YJIT on a Linux VM
|
|
16
|
+
> Ruby 4.0 + YJIT on a Linux VM — see [`bench/`](bench/) for full results
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## What is ZeroMQ?
|
|
21
|
+
|
|
22
|
+
Brokerless message-oriented middleware. No central server, no extra hop — processes talk directly to each other, cutting latency in half compared to broker-based systems. You get the patterns you'd normally build on top of RabbitMQ or Redis — pub/sub, work distribution, request/reply, fan-out — but decentralized, with no single point of failure.
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
- **All socket types** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair + draft types (client/server, radio/dish, scatter/gather, peer, channel)
|
|
22
|
-
- **Async-native** — built on [Async](https://github.com/socketry/async) fibers, also works with plain threads
|
|
23
|
-
- **Ruby-idiomatic API** — messages as `Array<String>`, errors as exceptions, timeouts as `IO::TimeoutError`
|
|
24
|
-
- **All transports** — tcp, ipc, inproc
|
|
24
|
+
Networking is hard. ZeroMQ abstracts away reconnection, queuing, load balancing, and framing so you can focus on what your system actually does. Start with threads talking over `inproc://`, split into processes with `ipc://`, scale across machines with `tcp://` — same code, same API, just change the URL.
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
If you've ever wired up services with raw TCP, HTTP polling, or Redis pub/sub and wished it was simpler, this is what you've been looking for.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
See [GETTING_STARTED.md](GETTING_STARTED.md) for a ~30 min walkthrough of all major patterns with working code.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
- **Fiber Scheduler** — non-blocking I/O without callbacks or threads (`Async` builds on this)
|
|
32
|
-
- **`io-stream`** — buffered I/O with read-ahead, from the Async ecosystem
|
|
30
|
+
## Highlights
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
- **Zero dependencies on C** — no extensions, no FFI, no libzmq. `gem install` just works everywhere
|
|
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)
|
|
35
|
+
- **Every socket pattern** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair, and all draft types
|
|
36
|
+
- **Every transport** — tcp, ipc (Unix domain sockets), inproc (in-process queues)
|
|
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
|
+
- **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
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
For architecture internals, see [DESIGN.md](DESIGN.md).
|
|
37
42
|
|
|
38
43
|
## Install
|
|
39
44
|
|
|
@@ -45,10 +50,6 @@ gem install omq
|
|
|
45
50
|
gem 'omq'
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
## Learning ZeroMQ
|
|
49
|
-
|
|
50
|
-
New to ZeroMQ? See [ZGUIDE_SUMMARY.md](ZGUIDE_SUMMARY.md) — a ~30 min read covering all major patterns with working OMQ code examples.
|
|
51
|
-
|
|
52
53
|
## Quick Start
|
|
53
54
|
|
|
54
55
|
### Request / Reply
|
|
@@ -67,7 +68,7 @@ Async do |task|
|
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
req << 'hello'
|
|
70
|
-
|
|
71
|
+
p req.receive # => ["HELLO"]
|
|
71
72
|
ensure
|
|
72
73
|
req&.close
|
|
73
74
|
rep&.close
|
|
@@ -83,7 +84,7 @@ Async do |task|
|
|
|
83
84
|
sub.subscribe('') # subscribe to all
|
|
84
85
|
|
|
85
86
|
task.async { pub << 'news flash' }
|
|
86
|
-
|
|
87
|
+
p sub.receive # => ["news flash"]
|
|
87
88
|
ensure
|
|
88
89
|
pub&.close
|
|
89
90
|
sub&.close
|
|
@@ -98,51 +99,56 @@ Async do
|
|
|
98
99
|
pull = OMQ::PULL.bind('inproc://pipeline')
|
|
99
100
|
|
|
100
101
|
push << 'work item'
|
|
101
|
-
|
|
102
|
+
p pull.receive # => ["work item"]
|
|
102
103
|
ensure
|
|
103
104
|
push&.close
|
|
104
105
|
pull&.close
|
|
105
106
|
end
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
### Without Async (IO thread)
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|---------|---------|-----------|-----------|
|
|
112
|
-
| Request/Reply | `REQ`, `REP` | bidirectional | yes |
|
|
113
|
-
| Publish/Subscribe | `PUB`, `SUB`, `XPUB`, `XSUB` | unidirectional | yes |
|
|
114
|
-
| Pipeline | `PUSH`, `PULL` | unidirectional | yes |
|
|
115
|
-
| Routing | `DEALER`, `ROUTER` | bidirectional | yes |
|
|
116
|
-
| Exclusive pair | `PAIR` | bidirectional | yes |
|
|
117
|
-
| Client/Server | `CLIENT`, `SERVER` | bidirectional | no |
|
|
118
|
-
| Group messaging | `RADIO`, `DISH` | unidirectional | no |
|
|
119
|
-
| Pipeline (draft) | `SCATTER`, `GATHER` | unidirectional | no |
|
|
120
|
-
| Peer-to-peer | `PEER` | bidirectional | no |
|
|
121
|
-
| Channel (draft) | `CHANNEL` | bidirectional | no |
|
|
122
|
-
|
|
123
|
-
All classes live under `OMQ::`. For the purists, `ØMQ` is an alias:
|
|
111
|
+
OMQ spawns a shared `omq-io` thread when used outside an Async reactor — no boilerplate needed:
|
|
124
112
|
|
|
125
113
|
```ruby
|
|
126
|
-
|
|
114
|
+
require 'omq'
|
|
115
|
+
|
|
116
|
+
push = OMQ::PUSH.bind('tcp://127.0.0.1:5557')
|
|
117
|
+
pull = OMQ::PULL.connect('tcp://127.0.0.1:5557')
|
|
118
|
+
|
|
119
|
+
push << 'hello'
|
|
120
|
+
p pull.receive # => ["hello"]
|
|
121
|
+
|
|
122
|
+
push.close
|
|
123
|
+
pull.close
|
|
127
124
|
```
|
|
128
125
|
|
|
129
|
-
|
|
126
|
+
The IO thread runs all pumps, reconnection, and heartbeating in the background. When you're inside an `Async` block, OMQ uses the existing reactor instead.
|
|
130
127
|
|
|
131
|
-
|
|
128
|
+
## Socket Types
|
|
132
129
|
|
|
133
|
-
|
|
130
|
+
All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes live under `OMQ::` (alias: `ØMQ`).
|
|
134
131
|
|
|
135
|
-
|
|
136
|
-
|--------|-----|-----|
|
|
137
|
-
| 244k/s | 47k/s | 36k/s |
|
|
132
|
+
#### Standard (multipart messages)
|
|
138
133
|
|
|
139
|
-
|
|
134
|
+
| Pattern | Send | Receive | When HWM full |
|
|
135
|
+
|---------|------|---------|---------------|
|
|
136
|
+
| **REQ** / **REP** | Round-robin / route-back | Fair-queue | Block |
|
|
137
|
+
| **PUB** / **SUB** | Fan-out to subscribers | Subscription filter | Drop |
|
|
138
|
+
| **PUSH** / **PULL** | Round-robin to workers | Fair-queue | Block |
|
|
139
|
+
| **DEALER** / **ROUTER** | Round-robin / identity-route | Fair-queue | Block |
|
|
140
|
+
| **XPUB** / **XSUB** | Fan-out (subscription events) | Fair-queue | Drop |
|
|
141
|
+
| **PAIR** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|--------|-----|-----|
|
|
143
|
-
| 9 µs | 47 µs | 61 µs |
|
|
143
|
+
#### Draft (single-frame only)
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
| Pattern | Send | Receive | When HWM full |
|
|
146
|
+
|---------|------|---------|---------------|
|
|
147
|
+
| **CLIENT** / **SERVER** | Round-robin / routing-ID | Fair-queue | Block |
|
|
148
|
+
| **RADIO** / **DISH** | Group fan-out | Group filter | Drop |
|
|
149
|
+
| **SCATTER** / **GATHER** | Round-robin | Fair-queue | Block |
|
|
150
|
+
| **PEER** | Routing-ID | Fair-queue | Block |
|
|
151
|
+
| **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
|
|
146
152
|
|
|
147
153
|
## omq — CLI tool
|
|
148
154
|
|
|
@@ -152,7 +158,7 @@ See [`bench/`](bench/) for full results and scripts.
|
|
|
152
158
|
# Echo server
|
|
153
159
|
omq rep -b tcp://:5555 --echo
|
|
154
160
|
|
|
155
|
-
# Upcase server
|
|
161
|
+
# Upcase server — -e evals Ruby on each incoming message
|
|
156
162
|
omq rep -b tcp://:5555 -e '$F.map(&:upcase)'
|
|
157
163
|
|
|
158
164
|
# Client
|
|
@@ -163,50 +169,39 @@ echo "hello" | omq req -c tcp://localhost:5555
|
|
|
163
169
|
omq sub -b tcp://:5556 -s "weather." &
|
|
164
170
|
echo "weather.nyc 72F" | omq pub -c tcp://localhost:5556 -d 0.3
|
|
165
171
|
|
|
166
|
-
# Pipeline with filtering
|
|
167
|
-
# /regexp/ matches against $_, next skips, break stops
|
|
172
|
+
# Pipeline with filtering
|
|
168
173
|
tail -f /var/log/syslog | omq push -c tcp://collector:5557
|
|
169
174
|
omq pull -b tcp://:5557 -e 'next unless /error/; $F'
|
|
170
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
|
+
|
|
171
183
|
# Pipe: PULL → eval → PUSH in one process
|
|
172
184
|
omq pipe -c ipc://@work -c ipc://@sink -e '$F.map(&:upcase)'
|
|
173
185
|
|
|
174
186
|
# Pipe with Ractor workers for CPU parallelism (-P = all CPUs)
|
|
175
187
|
omq pipe -c ipc://@work -c ipc://@sink -P -r./fib -e 'fib(Integer($_)).to_s'
|
|
188
|
+
```
|
|
176
189
|
|
|
177
|
-
|
|
178
|
-
omq pipe -c ipc://@work -c ipc://@sink --transient -e '$F'
|
|
179
|
-
|
|
180
|
-
# JSONL for structured data
|
|
181
|
-
echo '["key","value"]' | omq push -c tcp://localhost:5557 -J
|
|
182
|
-
omq pull -b tcp://:5557 -J
|
|
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`:
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
```ruby
|
|
193
|
+
# my_handler.rb
|
|
194
|
+
db = DB.connect("postgres://localhost/app")
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# prints: OMQ_SERVER_KEY='...'
|
|
191
|
-
omq req -c tcp://localhost:5555 --curve-server-key '...'
|
|
196
|
+
OMQ.incoming { db.query($F.first) }
|
|
197
|
+
at_exit { db.close }
|
|
192
198
|
```
|
|
193
199
|
|
|
194
|
-
The `-e` flag runs Ruby inside the socket instance — the full socket API (`self <<`, `send`, `subscribe`, ...) is available. `$F` is the message parts array, `$_` is the first part. Use `-r` to require gems:
|
|
195
|
-
|
|
196
200
|
```sh
|
|
197
|
-
omq
|
|
198
|
-
-e 'JSON.parse($F.first)["temperature"]'
|
|
199
|
-
|
|
200
|
-
# BEGIN/END blocks (like awk) — accumulate and summarize
|
|
201
|
-
omq pull -b tcp://:5557 \
|
|
202
|
-
-e 'BEGIN{ @sum = 0 } @sum += Integer($_); next END{ puts @sum }'
|
|
201
|
+
omq pull -b tcp://:5557 -r./my_handler.rb
|
|
203
202
|
```
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
## Interop with native ZMQ
|
|
208
|
-
|
|
209
|
-
OMQ speaks ZMTP 3.1 on the wire and interoperates with libzmq, CZMQ, pyzmq, etc. over **tcp** and **ipc**. The `inproc://` transport is OMQ-internal (in-process Ruby queues) and is not visible to native ZMQ running in the same process — use `ipc://` to talk across library boundaries.
|
|
204
|
+
See [CLI.md](CLI.md) for full documentation, or `omq --help` / `omq --examples`.
|
|
210
205
|
|
|
211
206
|
## Development
|
|
212
207
|
|
data/lib/omq/cli/base_runner.rb
CHANGED
|
@@ -28,9 +28,11 @@ module OMQ
|
|
|
28
28
|
sleep(config.delay) if config.delay && config.recv_only?
|
|
29
29
|
wait_for_peer if needs_peer_wait?
|
|
30
30
|
|
|
31
|
-
@sock.instance_exec(&@
|
|
31
|
+
@sock.instance_exec(&@send_begin_proc) if @send_begin_proc
|
|
32
|
+
@sock.instance_exec(&@recv_begin_proc) if @recv_begin_proc
|
|
32
33
|
run_loop(task)
|
|
33
|
-
@sock.instance_exec(&@
|
|
34
|
+
@sock.instance_exec(&@send_end_proc) if @send_end_proc
|
|
35
|
+
@sock.instance_exec(&@recv_end_proc) if @recv_end_proc
|
|
34
36
|
ensure
|
|
35
37
|
@sock&.close
|
|
36
38
|
end
|
|
@@ -201,19 +203,19 @@ module OMQ
|
|
|
201
203
|
end
|
|
202
204
|
end
|
|
203
205
|
elsif config.data || config.file
|
|
204
|
-
parts =
|
|
206
|
+
parts = eval_send_expr(read_next)
|
|
205
207
|
send_msg(parts) if parts
|
|
206
208
|
elsif stdin_ready?
|
|
207
209
|
loop do
|
|
208
210
|
parts = read_next
|
|
209
211
|
break unless parts
|
|
210
|
-
parts =
|
|
212
|
+
parts = eval_send_expr(parts)
|
|
211
213
|
send_msg(parts) if parts
|
|
212
214
|
i += 1
|
|
213
215
|
break if n && n > 0 && i >= n
|
|
214
216
|
end
|
|
215
|
-
elsif @
|
|
216
|
-
parts =
|
|
217
|
+
elsif @send_eval_proc
|
|
218
|
+
parts = eval_send_expr(nil)
|
|
217
219
|
send_msg(parts) if parts
|
|
218
220
|
end
|
|
219
221
|
end
|
|
@@ -221,11 +223,11 @@ module OMQ
|
|
|
221
223
|
|
|
222
224
|
def send_tick
|
|
223
225
|
raw = read_next_or_nil
|
|
224
|
-
if raw.nil? && !@
|
|
226
|
+
if raw.nil? && !@send_eval_proc
|
|
225
227
|
@send_tick_eof = true
|
|
226
228
|
return 0
|
|
227
229
|
end
|
|
228
|
-
parts =
|
|
230
|
+
parts = eval_send_expr(raw)
|
|
229
231
|
send_msg(parts) if parts
|
|
230
232
|
1
|
|
231
233
|
end
|
|
@@ -237,7 +239,7 @@ module OMQ
|
|
|
237
239
|
loop do
|
|
238
240
|
parts = recv_msg
|
|
239
241
|
break if parts.nil?
|
|
240
|
-
parts =
|
|
242
|
+
parts = eval_recv_expr(parts)
|
|
241
243
|
output(parts)
|
|
242
244
|
i += 1
|
|
243
245
|
break if n && n > 0 && i >= n
|
|
@@ -246,7 +248,7 @@ module OMQ
|
|
|
246
248
|
|
|
247
249
|
|
|
248
250
|
def wait_for_loops(receiver, sender)
|
|
249
|
-
if config.data || config.file || config.
|
|
251
|
+
if config.data || config.file || config.send_expr || config.recv_expr || config.target
|
|
250
252
|
sender.wait
|
|
251
253
|
receiver.stop
|
|
252
254
|
elsif config.count && config.count > 0
|
|
@@ -281,7 +283,8 @@ module OMQ
|
|
|
281
283
|
|
|
282
284
|
|
|
283
285
|
def recv_msg_raw
|
|
284
|
-
@sock.receive
|
|
286
|
+
msg = @sock.receive
|
|
287
|
+
msg&.dup
|
|
285
288
|
end
|
|
286
289
|
|
|
287
290
|
|
|
@@ -320,7 +323,7 @@ module OMQ
|
|
|
320
323
|
def read_next_or_nil
|
|
321
324
|
if config.data || config.file
|
|
322
325
|
read_next
|
|
323
|
-
elsif @
|
|
326
|
+
elsif @send_eval_proc
|
|
324
327
|
nil
|
|
325
328
|
else
|
|
326
329
|
read_next
|
|
@@ -358,11 +361,30 @@ module OMQ
|
|
|
358
361
|
|
|
359
362
|
|
|
360
363
|
def compile_expr
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
@
|
|
364
|
-
@
|
|
365
|
-
|
|
364
|
+
compile_one_expr(:send, config.send_expr)
|
|
365
|
+
compile_one_expr(:recv, config.recv_expr)
|
|
366
|
+
@send_eval_proc ||= wrap_registered_proc(OMQ.outgoing_proc)
|
|
367
|
+
@recv_eval_proc ||= wrap_registered_proc(OMQ.incoming_proc)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def wrap_registered_proc(block)
|
|
372
|
+
return unless block
|
|
373
|
+
proc do |msg|
|
|
374
|
+
$_ = msg&.first
|
|
375
|
+
block.call(msg)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def compile_one_expr(direction, src)
|
|
381
|
+
return unless src
|
|
382
|
+
expr, begin_body, end_body = extract_blocks(src)
|
|
383
|
+
instance_variable_set(:"@#{direction}_begin_proc", eval("proc { #{begin_body} }")) if begin_body
|
|
384
|
+
instance_variable_set(:"@#{direction}_end_proc", eval("proc { #{end_body} }")) if end_body
|
|
385
|
+
if expr && !expr.strip.empty?
|
|
386
|
+
instance_variable_set(:"@#{direction}_eval_proc", eval("proc { $_ = $F&.first; #{expr} }"))
|
|
387
|
+
end
|
|
366
388
|
end
|
|
367
389
|
|
|
368
390
|
|
|
@@ -398,10 +420,21 @@ module OMQ
|
|
|
398
420
|
|
|
399
421
|
SENT = Object.new.freeze # sentinel: eval already sent the reply
|
|
400
422
|
|
|
401
|
-
def
|
|
402
|
-
return parts unless @
|
|
423
|
+
def eval_send_expr(parts)
|
|
424
|
+
return parts unless @send_eval_proc
|
|
425
|
+
run_eval(@send_eval_proc, parts)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def eval_recv_expr(parts)
|
|
430
|
+
return parts unless @recv_eval_proc
|
|
431
|
+
run_eval(@recv_eval_proc, parts)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def run_eval(eval_proc, parts)
|
|
403
436
|
$F = parts
|
|
404
|
-
result = @sock.instance_exec(
|
|
437
|
+
result = @sock.instance_exec(parts, &eval_proc)
|
|
405
438
|
return nil if result.nil?
|
|
406
439
|
return SENT if result.equal?(@sock)
|
|
407
440
|
return [result] if config.format == :marshal
|
|
@@ -411,7 +444,7 @@ module OMQ
|
|
|
411
444
|
else [result.to_str]
|
|
412
445
|
end
|
|
413
446
|
rescue => e
|
|
414
|
-
$stderr.puts "omq:
|
|
447
|
+
$stderr.puts "omq: eval error: #{e.message} (#{e.class})"
|
|
415
448
|
exit 3
|
|
416
449
|
end
|
|
417
450
|
|
|
@@ -11,7 +11,7 @@ module OMQ
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def run_loop(task)
|
|
14
|
-
if config.echo || config.
|
|
14
|
+
if config.echo || config.recv_expr || @recv_eval_proc || config.data || config.file || !config.stdin_is_tty
|
|
15
15
|
reply_loop
|
|
16
16
|
else
|
|
17
17
|
monitor_loop(task)
|
|
@@ -28,8 +28,8 @@ module OMQ
|
|
|
28
28
|
routing_id = parts.shift
|
|
29
29
|
body = @fmt.decompress(parts)
|
|
30
30
|
|
|
31
|
-
if config.
|
|
32
|
-
reply =
|
|
31
|
+
if config.recv_expr || @recv_eval_proc
|
|
32
|
+
reply = eval_recv_expr(body)
|
|
33
33
|
output([display_routing_id(routing_id), *(reply || [""])])
|
|
34
34
|
@sock.send_to(routing_id, @fmt.compress(reply || [""]).first)
|
|
35
35
|
elsif config.echo
|
|
@@ -56,7 +56,7 @@ module OMQ
|
|
|
56
56
|
break if parts.nil?
|
|
57
57
|
routing_id = parts.shift
|
|
58
58
|
parts = @fmt.decompress(parts)
|
|
59
|
-
result =
|
|
59
|
+
result = eval_recv_expr([display_routing_id(routing_id), *parts])
|
|
60
60
|
output(result)
|
|
61
61
|
i += 1
|
|
62
62
|
break if n && n > 0 && i >= n
|
|
@@ -71,18 +71,18 @@ module OMQ
|
|
|
71
71
|
Async::Loop.quantized(interval: config.interval) do
|
|
72
72
|
parts = read_next
|
|
73
73
|
break unless parts
|
|
74
|
-
|
|
74
|
+
send_targeted_or_eval(parts)
|
|
75
75
|
i += 1
|
|
76
76
|
break if n && n > 0 && i >= n
|
|
77
77
|
end
|
|
78
78
|
elsif config.data || config.file
|
|
79
79
|
parts = read_next
|
|
80
|
-
|
|
80
|
+
send_targeted_or_eval(parts) if parts
|
|
81
81
|
else
|
|
82
82
|
loop do
|
|
83
83
|
parts = read_next
|
|
84
84
|
break unless parts
|
|
85
|
-
|
|
85
|
+
send_targeted_or_eval(parts)
|
|
86
86
|
i += 1
|
|
87
87
|
break if n && n > 0 && i >= n
|
|
88
88
|
end
|
|
@@ -93,8 +93,13 @@ module OMQ
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def
|
|
97
|
-
if
|
|
96
|
+
def send_targeted_or_eval(parts)
|
|
97
|
+
if @send_eval_proc
|
|
98
|
+
parts = eval_send_expr(parts)
|
|
99
|
+
return unless parts
|
|
100
|
+
routing_id = resolve_target(parts.shift)
|
|
101
|
+
@sock.send_to(routing_id, @fmt.compress(parts).first || "")
|
|
102
|
+
elsif config.target
|
|
98
103
|
parts = @fmt.compress(parts)
|
|
99
104
|
@sock.send_to(resolve_target(config.target), parts.first || "")
|
|
100
105
|
else
|
data/lib/omq/cli/config.rb
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
module CLI
|
|
5
|
+
SEND_ONLY = %w[pub push scatter radio].freeze
|
|
6
|
+
RECV_ONLY = %w[sub pull gather dish].freeze
|
|
7
|
+
|
|
5
8
|
Endpoint = Data.define(:url, :bind?) do
|
|
6
9
|
def connect? = !bind?
|
|
7
10
|
end
|
|
@@ -12,6 +15,8 @@ module OMQ
|
|
|
12
15
|
:endpoints,
|
|
13
16
|
:connects,
|
|
14
17
|
:binds,
|
|
18
|
+
:in_endpoints,
|
|
19
|
+
:out_endpoints,
|
|
15
20
|
:data,
|
|
16
21
|
:file,
|
|
17
22
|
:format,
|
|
@@ -29,7 +34,8 @@ module OMQ
|
|
|
29
34
|
:heartbeat_ivl,
|
|
30
35
|
:conflate,
|
|
31
36
|
:compress,
|
|
32
|
-
:
|
|
37
|
+
:send_expr,
|
|
38
|
+
:recv_expr,
|
|
33
39
|
:parallel,
|
|
34
40
|
:transient,
|
|
35
41
|
:verbose,
|
|
@@ -41,9 +47,6 @@ module OMQ
|
|
|
41
47
|
:has_zstd,
|
|
42
48
|
:stdin_is_tty,
|
|
43
49
|
) do
|
|
44
|
-
SEND_ONLY = %w[pub push scatter radio].freeze
|
|
45
|
-
RECV_ONLY = %w[sub pull gather dish].freeze
|
|
46
|
-
|
|
47
50
|
def send_only? = SEND_ONLY.include?(type_name)
|
|
48
51
|
def recv_only? = RECV_ONLY.include?(type_name)
|
|
49
52
|
end
|