omq 0.22.1 → 0.24.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 +162 -0
- data/README.md +17 -21
- data/lib/omq/channel.rb +35 -0
- data/lib/omq/client_server.rb +72 -0
- data/lib/omq/constants.rb +68 -0
- data/lib/omq/engine/connection_lifecycle.rb +22 -8
- data/lib/omq/engine/heartbeat.rb +3 -4
- data/lib/omq/engine/maintenance.rb +4 -5
- data/lib/omq/engine/reconnect.rb +12 -11
- data/lib/omq/engine/recv_pump.rb +10 -10
- data/lib/omq/engine/socket_lifecycle.rb +26 -9
- data/lib/omq/engine.rb +202 -90
- data/lib/omq/peer.rb +49 -0
- data/lib/omq/pub_sub.rb +2 -2
- data/lib/omq/radio_dish.rb +122 -0
- data/lib/omq/reactor.rb +14 -5
- data/lib/omq/readable.rb +5 -1
- data/lib/omq/routing/channel.rb +110 -0
- data/lib/omq/routing/client.rb +70 -0
- data/lib/omq/routing/conn_send_pump.rb +5 -8
- data/lib/omq/routing/dealer.rb +3 -15
- data/lib/omq/routing/dish.rb +94 -0
- data/lib/omq/routing/fan_out.rb +12 -16
- data/lib/omq/routing/gather.rb +60 -0
- data/lib/omq/routing/pair.rb +7 -26
- data/lib/omq/routing/peer.rb +95 -0
- data/lib/omq/routing/pub.rb +2 -13
- data/lib/omq/routing/pull.rb +3 -15
- data/lib/omq/routing/push.rb +4 -13
- data/lib/omq/routing/radio.rb +187 -0
- data/lib/omq/routing/rep.rb +5 -19
- data/lib/omq/routing/req.rb +6 -18
- data/lib/omq/routing/round_robin.rb +15 -19
- data/lib/omq/routing/router.rb +5 -19
- data/lib/omq/routing/scatter.rb +76 -0
- data/lib/omq/routing/server.rb +90 -0
- data/lib/omq/routing/sub.rb +3 -15
- data/lib/omq/routing/xpub.rb +2 -13
- data/lib/omq/routing/xsub.rb +8 -25
- data/lib/omq/scatter_gather.rb +56 -0
- data/lib/omq/socket.rb +8 -23
- data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +26 -24
- data/lib/omq/transport/inproc.rb +22 -14
- data/lib/omq/transport/ipc.rb +41 -13
- data/lib/omq/transport/tcp.rb +59 -23
- data/lib/omq/transport/udp.rb +281 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +11 -42
- data/lib/omq.rb +9 -64
- metadata +17 -3
- data/lib/omq/monitor_event.rb +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed87cc3a3243100b7977fd58f102b87918de8568cf8739642afc6377bb3f76e5
|
|
4
|
+
data.tar.gz: f01b30844ae48ffe26ec3934fef780521937a7611e1d00c87b333136fa5641ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 853d3171298de868ad3de28fe284c7970ddf196203bbb217fc91fa62cebb9d39c1d278b53d4259d0dc505b09457624ea68f4a9a8a0e654850c9266139a1fa05e
|
|
7
|
+
data.tar.gz: ad8a2cec518ceebac32055aeab7fd159d982421e9983670ec28182094f4fe2338f89cd7a1609d02cd8720f01c121159fec7e2489b8c816f9f16adf9d950c34c4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,167 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0 — 2026-04-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Caller owns message parts.** `Writable#send` no longer deep-freezes or
|
|
8
|
+
binary-coerces the caller's input. The contract is now libzmq-style:
|
|
9
|
+
don't mutate parts after sending. `#receive` likewise returns mutable
|
|
10
|
+
arrays of mutable strings. This removes a full-payload allocation per
|
|
11
|
+
message (`.b.freeze`) on the send path and a per-frame freeze on the
|
|
12
|
+
receive path.
|
|
13
|
+
|
|
14
|
+
- **No more implicit `#to_s` / nil coercion.** Passing a non-string part
|
|
15
|
+
(e.g. Integer, Symbol, nil) will raise `NoMethodError` at the wire layer
|
|
16
|
+
instead of being silently converted. The `EMPTY_PART` constant is gone.
|
|
17
|
+
|
|
18
|
+
- **Reactor fast path for `#send` / `#receive`.** When the socket was
|
|
19
|
+
bound/connected from an Async fiber, hot-path I/O skips `Reactor.run`
|
|
20
|
+
entirely and calls the engine directly (with an `Async::Task#with_timeout`
|
|
21
|
+
wrapper only when a timeout is configured). The shared IO thread is used
|
|
22
|
+
only when the socket was created from a non-Async thread.
|
|
23
|
+
|
|
24
|
+
### Performance
|
|
25
|
+
|
|
26
|
+
Combined effect of caller-owns-data + Reactor fast path on inproc:
|
|
27
|
+
|
|
28
|
+
- PUSH/PULL inproc 1-peer: **+105% to +128%** msg/s across payload sizes
|
|
29
|
+
- PUSH/PULL inproc 3-peer: **+63% to +111%** msg/s
|
|
30
|
+
- PUSH/PULL ipc: +5% to +17%
|
|
31
|
+
- TCP numbers unchanged (OS/syscall-dominated)
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
|
|
35
|
+
- `Writable#freeze_message` and `#frozen_binary` private helpers.
|
|
36
|
+
- `Writable::EMPTY_PART` constant.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## 0.23.1 — 2026-04-18
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **SCATTER double-tracked each peer.** `Routing::Scatter#connection_added`
|
|
44
|
+
appended to `@connections` and then called `add_round_robin_send_connection`,
|
|
45
|
+
which appends again — so every connected peer had two entries in the list.
|
|
46
|
+
`#connection_removed` deleted only one on disconnect, leaving a stale entry
|
|
47
|
+
behind. Fixed by dropping the duplicate append.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## 0.23.0 — 2026-04-17
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **Draft socket types now ship with `omq` itself.** `OMQ::CLIENT`/`SERVER`,
|
|
55
|
+
`OMQ::RADIO`/`DISH`, `OMQ::SCATTER`/`GATHER`, `OMQ::CHANNEL`, and
|
|
56
|
+
`OMQ::PEER` are back in OMQ. They were previously distributed as separate
|
|
57
|
+
`omq-rfc-*` gems, which was a PITA to maintain. Their source is now part of
|
|
58
|
+
`omq`. They are **not** loaded by `require "omq"` — opt in with one of:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
require "omq/client_server"
|
|
62
|
+
require "omq/radio_dish" # also registers the udp:// transport
|
|
63
|
+
require "omq/scatter_gather"
|
|
64
|
+
require "omq/channel"
|
|
65
|
+
require "omq/peer"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
These requires must run at process startup (before any socket is bound
|
|
69
|
+
or connected), since the underlying registries (`Routing`,
|
|
70
|
+
`Engine.transports`) freeze on first use. The five `omq-rfc-*` gems are
|
|
71
|
+
superseded and will not receive further releases. Per-pattern docs live
|
|
72
|
+
under [`doc/socket-types/`](doc/socket-types/).
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`Socket#bind` / `#connect` now return a `URI`** (the resolved endpoint).
|
|
78
|
+
`#bind` returns the listener's resolved URI — for `tcp://host:0` this
|
|
79
|
+
carries the auto-selected port via `uri.port`. `#connect` returns the
|
|
80
|
+
parsed input URI. The `last_tcp_port` and `last_endpoint` accessors are
|
|
81
|
+
removed; callers should capture the URI from `#bind` instead. Note: stdlib
|
|
82
|
+
`URI.parse` is lossy on abstract IPC endpoints (`ipc://@name`) — the `@`
|
|
83
|
+
is parsed as userinfo and dropped on `to_s`. For abstract IPC, use the
|
|
84
|
+
input string for connect rather than re-serializing the URI.
|
|
85
|
+
`Socket#inspect` now shows `bound=[...]` (the listener endpoints) instead
|
|
86
|
+
of `last_endpoint=...`.
|
|
87
|
+
|
|
88
|
+
- **Transport interface: `.bind`/`.connect` replaced by `.listener`/`.dialer`
|
|
89
|
+
factory methods** returning stateful `Listener`/`Dialer` objects. The
|
|
90
|
+
engine now stores a per-endpoint `@dialers` map (was a `@dialed` Set)
|
|
91
|
+
and a `@listeners` hash keyed by endpoint (was an Array). Reconnect
|
|
92
|
+
calls `dialer.connect` directly — no transport lookup or option replay
|
|
93
|
+
on every retry. `Transport::Inproc` keeps its synchronous `.connect`
|
|
94
|
+
fast-path; only TCP/IPC gain `Dialer` classes.
|
|
95
|
+
|
|
96
|
+
- **`Engine#bind` / `#connect` accept transport-specific kwargs** via
|
|
97
|
+
`**opts`, forwarded to the transport's `.listener` / `.dialer`. Socket
|
|
98
|
+
`#bind` / `#connect` pass them through. Enables per-connection
|
|
99
|
+
transport configuration (e.g., TLS context) without polluting
|
|
100
|
+
`Options`.
|
|
101
|
+
|
|
102
|
+
- **`ConnectionLifecycle#ready!` calls `transport_obj.wrap_connection(conn)`
|
|
103
|
+
if defined** — hook for transports that need to wrap the buffered
|
|
104
|
+
stream after handshake (e.g., TLS).
|
|
105
|
+
|
|
106
|
+
- **Transports self-register in `Engine.transports`.** Each transport
|
|
107
|
+
file (`tcp`, `ipc`, `inproc`) now adds its own scheme entry at load
|
|
108
|
+
time. `lib/omq.rb` requires transports after `engine.rb` so the
|
|
109
|
+
`Engine` constant is available. External transport plugins follow
|
|
110
|
+
the same pattern.
|
|
111
|
+
|
|
112
|
+
- **`Engine` gains delegate methods that hide internal layout** from
|
|
113
|
+
callers: `#subscribe`, `#unsubscribe`, `#subscriber_joined` forward
|
|
114
|
+
to the routing strategy; `#record_disconnect_reason(conn, error)`
|
|
115
|
+
wraps the `@connections` lookup; `Inproc::DirectPipe#wire_direct_recv`
|
|
116
|
+
replaces two separate attribute setters previously poked from the
|
|
117
|
+
recv pump. Callers no longer chain through `engine.routing.*` or
|
|
118
|
+
`engine.connections[conn]`.
|
|
119
|
+
|
|
120
|
+
- **`SocketLifecycle#resolve_all_peers_gone_if_empty` renamed to
|
|
121
|
+
`#maybe_resolve_all_peers_gone`.** The composite `unless` was split
|
|
122
|
+
into two early-returns for readability. A new `#force_close!` handles
|
|
123
|
+
`Engine#stop`'s crash path, collapsing two `@lifecycle.*` calls into
|
|
124
|
+
one.
|
|
125
|
+
|
|
126
|
+
- **Module-level constants consolidated into `lib/omq/constants.rb`.**
|
|
127
|
+
`MonitorEvent`, `DEBUG`, `SocketDeadError`, `CONNECTION_LOST`,
|
|
128
|
+
`CONNECTION_FAILED`, and `OMQ.freeze_for_ractors!` now live in one
|
|
129
|
+
file. `lib/omq/monitor_event.rb` is deleted; `lib/omq.rb` just
|
|
130
|
+
requires `omq/constants`.
|
|
131
|
+
|
|
132
|
+
### Removed
|
|
133
|
+
|
|
134
|
+
- **`Engine#tasks` array** (and every `@tasks << ...` append site)
|
|
135
|
+
deleted. `Async::Barrier` already tracks every spawned task and
|
|
136
|
+
exposes `#size`, `#empty?`, and `#stop`. `Heartbeat.start`,
|
|
137
|
+
`Maintenance.start`, and `Reconnect#run` drop their `tasks`
|
|
138
|
+
parameter. Teardown collapses to `@lifecycle.barrier&.stop`.
|
|
139
|
+
|
|
140
|
+
- **Routing strategies and TCP listener drop their `@tasks` arrays
|
|
141
|
+
too.** Same `Async::Barrier` rollout applied to every routing
|
|
142
|
+
strategy and `Transport::TCP::Listener`. Per-connection pumps
|
|
143
|
+
(send/recv/reaper/group/subscription listener) ride the
|
|
144
|
+
per-connection lifecycle barrier; Radio's socket-level send pump
|
|
145
|
+
rides `engine.barrier` via a new `parent:` kwarg on
|
|
146
|
+
`Engine#spawn_pump_task`. The redundant `@conn_send_tasks` hashes
|
|
147
|
+
in RoundRobin, FanOut, Rep, Router, Peer, and Server are gone, as
|
|
148
|
+
are all routing-strategy `#stop` methods and the matching
|
|
149
|
+
`routing.stop rescue nil` calls in `Engine#close`/`#stop`.
|
|
150
|
+
`ConnSendPump.start` drops its `tasks` parameter. Channel's send
|
|
151
|
+
pump moves from loose `spawn_pump_task` to `spawn_conn_pump_task`,
|
|
152
|
+
so its disconnect rescue is now centralized in `Engine`. Net: 24
|
|
153
|
+
files, −340/+121.
|
|
154
|
+
|
|
155
|
+
### Fixed
|
|
156
|
+
|
|
157
|
+
- **`bench/report.rb` preserves chronological run order.** Named run IDs
|
|
158
|
+
(e.g. `baseline-append`) previously sorted alphabetically after ISO
|
|
159
|
+
timestamps, hiding the most recent run. Now uses insertion order.
|
|
160
|
+
|
|
161
|
+
- **`zmtp_30_compat_test` waits for XSUB connection** before sending
|
|
162
|
+
`SUBSCRIBE`, removing a race where the subscribe arrived before the
|
|
163
|
+
handshake completed.
|
|
164
|
+
|
|
3
165
|
## 0.22.1 — 2026-04-16
|
|
4
166
|
|
|
5
167
|
### Changed
|
data/README.md
CHANGED
|
@@ -20,7 +20,7 @@ live in the same process, on the same machine, or across the network.
|
|
|
20
20
|
Reconnects, queuing, and back-pressure are handled for you; you write the
|
|
21
21
|
interesting part.
|
|
22
22
|
|
|
23
|
-
New to ZeroMQ? Start with [GETTING_STARTED.md](GETTING_STARTED.md) — a ~30 min
|
|
23
|
+
New to ZeroMQ? Start with [GETTING_STARTED.md](doc/GETTING_STARTED.md) — a ~30 min
|
|
24
24
|
walkthrough of every major pattern with working code.
|
|
25
25
|
|
|
26
26
|
## Highlights
|
|
@@ -45,7 +45,7 @@ walkthrough of every major pattern with working code.
|
|
|
45
45
|
connect, peers come and go. ZeroMQ reconnects automatically and queued
|
|
46
46
|
messages drain when peers arrive
|
|
47
47
|
|
|
48
|
-
For architecture internals, see [DESIGN.md](DESIGN.md).
|
|
48
|
+
For architecture internals, see [DESIGN.md](doc/DESIGN.md).
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
@@ -176,20 +176,22 @@ wire. Classes live under `OMQ::` (alias: `ØMQ`).
|
|
|
176
176
|
> **Work-stealing, not round-robin.** Outbound load balancing uses one shared
|
|
177
177
|
> send queue per socket drained by N racing pump fibers, so a slow peer can't
|
|
178
178
|
> stall the pipeline. Under tight bursts on small `n`, distribution isn't
|
|
179
|
-
> strict RR. See [DESIGN.md](DESIGN.md#per-socket-hwm-not-per-connection) and
|
|
180
|
-
> [Libzmq quirks](DESIGN.md#libzmq-quirks-omq-avoids) for the reasoning.
|
|
179
|
+
> strict RR. See [DESIGN.md](doc/DESIGN.md#per-socket-hwm-not-per-connection) and
|
|
180
|
+
> [Libzmq quirks](doc/DESIGN.md#libzmq-quirks-omq-avoids) for the reasoning.
|
|
181
181
|
|
|
182
182
|
#### Draft (single-frame only)
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
Bundled with `omq` but not loaded by `require "omq"` — opt in with the matching
|
|
185
|
+
`require` line. See [`doc/socket-types/`](doc/socket-types/) for per-pattern
|
|
186
|
+
usage.
|
|
185
187
|
|
|
186
|
-
| Pattern | Send | Receive | When HWM full |
|
|
187
|
-
|
|
188
|
-
| **CLIENT** / **SERVER** | Work-stealing / routing-ID | Fair-queue | Block |
|
|
189
|
-
| **RADIO** / **DISH** | Group fan-out | Group filter | Drop |
|
|
190
|
-
| **SCATTER** / **GATHER** | Work-stealing | Fair-queue | Block |
|
|
191
|
-
| **PEER** | Routing-ID | Fair-queue | Block |
|
|
192
|
-
| **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block |
|
|
188
|
+
| Pattern | Send | Receive | When HWM full | Opt-in `require` |
|
|
189
|
+
|---------|------|---------|---------------|------------------|
|
|
190
|
+
| **CLIENT** / **SERVER** | Work-stealing / routing-ID | Fair-queue | Block | `require "omq/client_server"` |
|
|
191
|
+
| **RADIO** / **DISH** | Group fan-out | Group filter | Drop | `require "omq/radio_dish"` (also registers `udp://`) |
|
|
192
|
+
| **SCATTER** / **GATHER** | Work-stealing | Fair-queue | Block | `require "omq/scatter_gather"` |
|
|
193
|
+
| **PEER** | Routing-ID | Fair-queue | Block | `require "omq/peer"` |
|
|
194
|
+
| **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block | `require "omq/channel"` |
|
|
193
195
|
|
|
194
196
|
## CLI
|
|
195
197
|
|
|
@@ -220,7 +222,7 @@ See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentati
|
|
|
220
222
|
Optional plug-ins that extend the ZMTP wire protocol. Each is a separate gem;
|
|
221
223
|
load the ones you need.
|
|
222
224
|
|
|
223
|
-
- **[omq-
|
|
225
|
+
- **[omq-zstd](https://github.com/paddor/omq-zstd)** — transparent
|
|
224
226
|
Zstandard compression on the wire, negotiated per peer via READY properties.
|
|
225
227
|
|
|
226
228
|
## Development
|
|
@@ -233,7 +235,7 @@ bundle exec rake
|
|
|
233
235
|
### Full development setup
|
|
234
236
|
|
|
235
237
|
Set `OMQ_DEV=1` to tell Bundler to load sibling projects from source
|
|
236
|
-
(protocol-zmtp, nuckle,
|
|
238
|
+
(protocol-zmtp, omq-zstd ,nuckle, etc.) instead of released gems.
|
|
237
239
|
This is required for running benchmarks and for testing changes across
|
|
238
240
|
the stack.
|
|
239
241
|
|
|
@@ -241,13 +243,7 @@ the stack.
|
|
|
241
243
|
# clone OMQ and its sibling repos into the same parent directory
|
|
242
244
|
git clone https://github.com/paddor/omq.git
|
|
243
245
|
git clone https://github.com/paddor/protocol-zmtp.git
|
|
244
|
-
git clone https://github.com/paddor/omq-
|
|
245
|
-
git clone https://github.com/paddor/omq-rfc-clientserver.git
|
|
246
|
-
git clone https://github.com/paddor/omq-rfc-radiodish.git
|
|
247
|
-
git clone https://github.com/paddor/omq-rfc-scattergather.git
|
|
248
|
-
git clone https://github.com/paddor/omq-rfc-channel.git
|
|
249
|
-
git clone https://github.com/paddor/omq-rfc-p2p.git
|
|
250
|
-
git clone https://github.com/paddor/omq-rfc-qos.git
|
|
246
|
+
git clone https://github.com/paddor/omq-zstd.git
|
|
251
247
|
git clone https://github.com/paddor/omq-ffi.git
|
|
252
248
|
git clone https://github.com/paddor/omq-ractor.git
|
|
253
249
|
git clone https://github.com/paddor/nuckle.git
|
data/lib/omq/channel.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OMQ CHANNEL socket type (ZeroMQ RFC 52).
|
|
4
|
+
#
|
|
5
|
+
# Not loaded by +require "omq"+; opt in with:
|
|
6
|
+
#
|
|
7
|
+
# require "omq/channel"
|
|
8
|
+
|
|
9
|
+
require "omq"
|
|
10
|
+
require_relative "routing/channel"
|
|
11
|
+
|
|
12
|
+
module OMQ
|
|
13
|
+
# Exclusive 1-to-1 bidirectional socket (ZeroMQ RFC 52).
|
|
14
|
+
#
|
|
15
|
+
# Allows exactly one peer connection. Both sides can send and receive.
|
|
16
|
+
class CHANNEL < Socket
|
|
17
|
+
include Readable
|
|
18
|
+
include Writable
|
|
19
|
+
include SingleFrame
|
|
20
|
+
|
|
21
|
+
# Creates a new CHANNEL socket.
|
|
22
|
+
#
|
|
23
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
|
|
24
|
+
# @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
|
|
25
|
+
# @param backend [Object, nil] optional transport backend
|
|
26
|
+
def initialize(endpoints = nil, linger: Float::INFINITY, backend: nil)
|
|
27
|
+
init_engine(:CHANNEL, backend: backend)
|
|
28
|
+
@options.linger = linger
|
|
29
|
+
attach_endpoints(endpoints, default: :connect)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Routing.register(:CHANNEL, Routing::Channel)
|
|
35
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OMQ CLIENT/SERVER socket types (ZeroMQ RFC 41).
|
|
4
|
+
#
|
|
5
|
+
# Not loaded by +require "omq"+; opt in with:
|
|
6
|
+
#
|
|
7
|
+
# require "omq/client_server"
|
|
8
|
+
|
|
9
|
+
require "omq"
|
|
10
|
+
require_relative "routing/client"
|
|
11
|
+
require_relative "routing/server"
|
|
12
|
+
|
|
13
|
+
module OMQ
|
|
14
|
+
# Asynchronous client socket for the CLIENT/SERVER pattern (ZeroMQ RFC 41).
|
|
15
|
+
#
|
|
16
|
+
# Round-robins outgoing messages across connected SERVER peers.
|
|
17
|
+
class CLIENT < Socket
|
|
18
|
+
include Readable
|
|
19
|
+
include Writable
|
|
20
|
+
include SingleFrame
|
|
21
|
+
|
|
22
|
+
# Creates a new CLIENT socket.
|
|
23
|
+
#
|
|
24
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to connect to
|
|
25
|
+
# @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
|
|
26
|
+
# @param backend [Object, nil] optional transport backend
|
|
27
|
+
def initialize(endpoints = nil, linger: Float::INFINITY, backend: nil)
|
|
28
|
+
init_engine(:CLIENT, backend: backend)
|
|
29
|
+
@options.linger = linger
|
|
30
|
+
attach_endpoints(endpoints, default: :connect)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Asynchronous server socket for the CLIENT/SERVER pattern (ZeroMQ RFC 41).
|
|
36
|
+
#
|
|
37
|
+
# Assigns a 4-byte routing ID to each connected CLIENT and supports
|
|
38
|
+
# directed replies via #send_to.
|
|
39
|
+
class SERVER < Socket
|
|
40
|
+
include Readable
|
|
41
|
+
include Writable
|
|
42
|
+
include SingleFrame
|
|
43
|
+
|
|
44
|
+
# Creates a new SERVER socket.
|
|
45
|
+
#
|
|
46
|
+
# @param endpoints [String, Array<String>, nil] endpoint(s) to bind to
|
|
47
|
+
# @param linger [Numeric] linger period in seconds (Float::INFINITY = wait forever, 0 = drop)
|
|
48
|
+
# @param backend [Object, nil] optional transport backend
|
|
49
|
+
def initialize(endpoints = nil, linger: Float::INFINITY, backend: nil)
|
|
50
|
+
init_engine(:SERVER, backend: backend)
|
|
51
|
+
@options.linger = linger
|
|
52
|
+
attach_endpoints(endpoints, default: :bind)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Sends a message to a specific peer by routing ID.
|
|
57
|
+
#
|
|
58
|
+
# @param routing_id [String] 4-byte routing ID
|
|
59
|
+
# @param message [String] message body
|
|
60
|
+
# @return [self]
|
|
61
|
+
#
|
|
62
|
+
def send_to(routing_id, message)
|
|
63
|
+
parts = [routing_id, message]
|
|
64
|
+
Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
Routing.register(:CLIENT, Routing::Client)
|
|
71
|
+
Routing.register(:SERVER, Routing::Server)
|
|
72
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "io/stream"
|
|
5
|
+
|
|
6
|
+
module OMQ
|
|
7
|
+
# When OMQ_DEBUG is set, silent rescue clauses print the exception
|
|
8
|
+
# to stderr so transport/engine bugs surface immediately.
|
|
9
|
+
DEBUG = !!ENV["OMQ_DEBUG"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Raised when an internal pump task crashes unexpectedly.
|
|
13
|
+
# The socket is no longer usable; the original error is available via #cause.
|
|
14
|
+
#
|
|
15
|
+
class SocketDeadError < RuntimeError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Lifecycle event emitted by {Socket#monitor}.
|
|
20
|
+
#
|
|
21
|
+
# @!attribute [r] type
|
|
22
|
+
# @return [Symbol] event type (:listening, :connected, :disconnected, etc.)
|
|
23
|
+
# @!attribute [r] endpoint
|
|
24
|
+
# @return [String, nil] the endpoint involved
|
|
25
|
+
# @!attribute [r] detail
|
|
26
|
+
# @return [Hash, nil] extra context (e.g. { error: }, { interval: }, etc.)
|
|
27
|
+
#
|
|
28
|
+
MonitorEvent = Data.define(:type, :endpoint, :detail) do
|
|
29
|
+
def initialize(type:, endpoint: nil, detail: nil) = super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Errors raised when a peer disconnects or resets the connection.
|
|
34
|
+
# Not frozen at load time — transport plugins append to this before
|
|
35
|
+
# the first bind/connect, which freezes both arrays.
|
|
36
|
+
CONNECTION_LOST = [
|
|
37
|
+
EOFError,
|
|
38
|
+
IOError,
|
|
39
|
+
Errno::EPIPE,
|
|
40
|
+
Errno::ECONNRESET,
|
|
41
|
+
Errno::ECONNABORTED,
|
|
42
|
+
Errno::ENOTCONN,
|
|
43
|
+
IO::Stream::ConnectionResetError,
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Errors raised when a peer cannot be reached.
|
|
48
|
+
CONNECTION_FAILED = [
|
|
49
|
+
Errno::ECONNREFUSED,
|
|
50
|
+
Errno::ENOENT,
|
|
51
|
+
Errno::ETIMEDOUT,
|
|
52
|
+
Errno::EHOSTUNREACH,
|
|
53
|
+
Errno::ENETUNREACH,
|
|
54
|
+
Errno::EPROTOTYPE, # IPC: existing socket file is SOCK_DGRAM, not SOCK_STREAM
|
|
55
|
+
Socket::ResolutionError,
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Freezes module-level state so OMQ sockets can be used inside Ractors.
|
|
60
|
+
# Call this once before spawning any Ractors that create OMQ sockets.
|
|
61
|
+
#
|
|
62
|
+
def self.freeze_for_ractors!
|
|
63
|
+
CONNECTION_LOST.freeze
|
|
64
|
+
CONNECTION_FAILED.freeze
|
|
65
|
+
Engine.transports.freeze
|
|
66
|
+
Routing.registry.freeze
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -5,7 +5,7 @@ module OMQ
|
|
|
5
5
|
# Owns the full arc of *one* connection: handshake → ready → closed.
|
|
6
6
|
#
|
|
7
7
|
# Scope boundary: ConnectionLifecycle tracks a single peer link
|
|
8
|
-
# (one ZMTP connection or one inproc
|
|
8
|
+
# (one ZMTP connection or one inproc Pipe). SocketLifecycle
|
|
9
9
|
# owns the socket-wide state above it — first-peer/last-peer
|
|
10
10
|
# signaling, reconnect enable flag, the parent task tree, and the
|
|
11
11
|
# open → closing → closed transitions that gate close-time drain.
|
|
@@ -43,7 +43,7 @@ module OMQ
|
|
|
43
43
|
}.freeze
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
# @return [Protocol::ZMTP::Connection, Transport::Inproc::
|
|
46
|
+
# @return [Protocol::ZMTP::Connection, Transport::Inproc::Pipe, nil]
|
|
47
47
|
attr_reader :conn
|
|
48
48
|
|
|
49
49
|
|
|
@@ -101,7 +101,7 @@ module OMQ
|
|
|
101
101
|
conn.handshake!
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
Heartbeat.start(@barrier, conn, @engine.options
|
|
104
|
+
Heartbeat.start(@barrier, conn, @engine.options)
|
|
105
105
|
ready!(conn)
|
|
106
106
|
@conn
|
|
107
107
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST, Async::TimeoutError => error
|
|
@@ -120,9 +120,9 @@ module OMQ
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
# Registers an already-connected inproc pipe as :ready.
|
|
123
|
-
# No handshake — inproc
|
|
123
|
+
# No handshake — inproc Pipe bypasses ZMTP entirely.
|
|
124
124
|
#
|
|
125
|
-
# @param pipe [Transport::Inproc::
|
|
125
|
+
# @param pipe [Transport::Inproc::Pipe]
|
|
126
126
|
#
|
|
127
127
|
def ready_direct!(pipe)
|
|
128
128
|
ready!(pipe)
|
|
@@ -161,7 +161,17 @@ module OMQ
|
|
|
161
161
|
|
|
162
162
|
|
|
163
163
|
def ready!(conn)
|
|
164
|
-
|
|
164
|
+
if @engine.connection_wrapper
|
|
165
|
+
conn = @engine.connection_wrapper.call(conn)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if @endpoint
|
|
169
|
+
transport_obj = @engine.transport_object_for(@endpoint)
|
|
170
|
+
if transport_obj.respond_to?(:wrap_connection)
|
|
171
|
+
conn = transport_obj.wrap_connection(conn)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
165
175
|
@conn = conn
|
|
166
176
|
@engine.connections[@conn] = self
|
|
167
177
|
@engine.emit_monitor_event(:handshake_succeeded, endpoint: @endpoint)
|
|
@@ -169,7 +179,7 @@ module OMQ
|
|
|
169
179
|
@engine.peer_connected.resolve(@conn)
|
|
170
180
|
transition!(:ready)
|
|
171
181
|
|
|
172
|
-
# No supervisor if nothing to supervise: inproc
|
|
182
|
+
# No supervisor if nothing to supervise: inproc Pipes
|
|
173
183
|
# wire the recv/send paths synchronously (no task-based pumps),
|
|
174
184
|
# and isolated unit tests use a FakeEngine without pumps at all.
|
|
175
185
|
# Waiting on an empty barrier returns immediately and would
|
|
@@ -205,6 +215,7 @@ module OMQ
|
|
|
205
215
|
|
|
206
216
|
def tear_down!(reconnect:, reason: nil)
|
|
207
217
|
return if @state == :closed
|
|
218
|
+
|
|
208
219
|
transition!(:closed)
|
|
209
220
|
@engine.connections.delete(@conn)
|
|
210
221
|
@engine.routing.connection_removed(@conn) if @conn
|
|
@@ -212,7 +223,7 @@ module OMQ
|
|
|
212
223
|
detail = reason ? { error: reason, reason: reason.message } : nil
|
|
213
224
|
@engine.emit_monitor_event(:disconnected, endpoint: @endpoint, detail: detail)
|
|
214
225
|
@done&.resolve(true)
|
|
215
|
-
@engine.
|
|
226
|
+
@engine.maybe_resolve_all_peers_gone
|
|
216
227
|
@engine.maybe_reconnect(@endpoint) if reconnect
|
|
217
228
|
|
|
218
229
|
# Cancel every sibling pump of this connection. The caller is
|
|
@@ -236,11 +247,14 @@ module OMQ
|
|
|
236
247
|
|
|
237
248
|
def transition!(new_state)
|
|
238
249
|
allowed = TRANSITIONS[@state]
|
|
250
|
+
|
|
239
251
|
unless allowed&.include?(new_state)
|
|
240
252
|
raise InvalidTransition, "#{@state} → #{new_state}"
|
|
241
253
|
end
|
|
254
|
+
|
|
242
255
|
@state = new_state
|
|
243
256
|
end
|
|
257
|
+
|
|
244
258
|
end
|
|
245
259
|
end
|
|
246
260
|
end
|
data/lib/omq/engine/heartbeat.rb
CHANGED
|
@@ -9,11 +9,10 @@ module OMQ
|
|
|
9
9
|
#
|
|
10
10
|
module Heartbeat
|
|
11
11
|
# @param parent [Async::Task, Async::Barrier] parent to spawn under
|
|
12
|
-
# @param conn [Connection]
|
|
12
|
+
# @param conn [Protocol::ZMTP::Connection]
|
|
13
13
|
# @param options [Options]
|
|
14
|
-
# @param tasks [Array]
|
|
15
14
|
#
|
|
16
|
-
def self.start(parent, conn, options
|
|
15
|
+
def self.start(parent, conn, options)
|
|
17
16
|
interval = options.heartbeat_interval
|
|
18
17
|
return unless interval
|
|
19
18
|
|
|
@@ -21,7 +20,7 @@ module OMQ
|
|
|
21
20
|
timeout = options.heartbeat_timeout || interval
|
|
22
21
|
conn.touch_heartbeat
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
parent.async(transient: true, annotation: "heartbeat") do
|
|
25
24
|
loop do
|
|
26
25
|
sleep interval
|
|
27
26
|
conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl))
|
|
@@ -12,17 +12,15 @@ module OMQ
|
|
|
12
12
|
module Maintenance
|
|
13
13
|
# @param parent_task [Async::Task]
|
|
14
14
|
# @param mechanism [#maintenance, nil]
|
|
15
|
-
# @param tasks [Array<Async::Task>]
|
|
16
15
|
#
|
|
17
|
-
def self.start(parent_task, mechanism
|
|
16
|
+
def self.start(parent_task, mechanism)
|
|
18
17
|
return unless mechanism.respond_to?(:maintenance)
|
|
19
|
-
spec = mechanism.maintenance
|
|
20
|
-
return unless spec
|
|
18
|
+
spec = mechanism.maintenance or return spec
|
|
21
19
|
|
|
22
20
|
interval = spec[:interval]
|
|
23
21
|
callable = spec[:task]
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
parent_task.async(transient: true, annotation: "mechanism maintenance") do
|
|
26
24
|
Async::Loop.quantized(interval: interval) do
|
|
27
25
|
callable.call
|
|
28
26
|
end
|
|
@@ -30,6 +28,7 @@ module OMQ
|
|
|
30
28
|
# clean shutdown
|
|
31
29
|
end
|
|
32
30
|
end
|
|
31
|
+
|
|
33
32
|
end
|
|
34
33
|
end
|
|
35
34
|
end
|
data/lib/omq/engine/reconnect.rb
CHANGED
|
@@ -8,25 +8,25 @@ module OMQ
|
|
|
8
8
|
# or the engine is closed.
|
|
9
9
|
#
|
|
10
10
|
class Reconnect
|
|
11
|
-
# @param
|
|
11
|
+
# @param dialer [Transport::TCP::Dialer, etc.] stateful dialer factory
|
|
12
12
|
# @param options [Options]
|
|
13
13
|
# @param parent_task [Async::Task]
|
|
14
14
|
# @param engine [Engine]
|
|
15
15
|
# @param delay [Numeric, nil] initial delay (defaults to reconnect_interval)
|
|
16
16
|
#
|
|
17
|
-
def self.schedule(
|
|
18
|
-
new(engine,
|
|
17
|
+
def self.schedule(dialer, options, parent_task, engine, delay: nil)
|
|
18
|
+
new(engine, dialer, options).run(parent_task, delay: delay)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
# @param engine [Engine]
|
|
23
|
-
# @param
|
|
23
|
+
# @param dialer [Transport::TCP::Dialer, etc.] stateful dialer factory
|
|
24
24
|
# @param options [Options]
|
|
25
25
|
#
|
|
26
|
-
def initialize(engine,
|
|
27
|
-
@engine
|
|
28
|
-
@
|
|
29
|
-
@options
|
|
26
|
+
def initialize(engine, dialer, options)
|
|
27
|
+
@engine = engine
|
|
28
|
+
@dialer = dialer
|
|
29
|
+
@options = options
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
|
|
@@ -37,7 +37,8 @@ module OMQ
|
|
|
37
37
|
# @return [void]
|
|
38
38
|
#
|
|
39
39
|
def run(parent_task, delay: nil)
|
|
40
|
-
|
|
40
|
+
endpoint = @dialer.endpoint
|
|
41
|
+
parent_task.async(transient: true, annotation: "reconnect #{endpoint}") do
|
|
41
42
|
retry_loop(delay: delay)
|
|
42
43
|
rescue Async::Stop
|
|
43
44
|
rescue => error
|
|
@@ -57,12 +58,12 @@ module OMQ
|
|
|
57
58
|
sleep quantized_wait(delay) if delay > 0
|
|
58
59
|
break if @engine.closed?
|
|
59
60
|
begin
|
|
60
|
-
@
|
|
61
|
+
@dialer.connect
|
|
61
62
|
break
|
|
62
63
|
rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
|
|
63
64
|
delay = next_delay(delay, max_delay)
|
|
64
65
|
@engine.emit_monitor_event :connect_retried,
|
|
65
|
-
endpoint: @endpoint, detail: { interval: delay }
|
|
66
|
+
endpoint: @dialer.endpoint, detail: { interval: delay }
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
end
|