omq 0.22.0 → 0.23.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 +139 -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 +11 -3
- 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 +3 -4
- data/lib/omq/engine/socket_lifecycle.rb +26 -9
- data/lib/omq/engine.rb +196 -85
- 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/routing/channel.rb +110 -0
- data/lib/omq/routing/client.rb +70 -0
- data/lib/omq/routing/conn_send_pump.rb +12 -10
- data/lib/omq/routing/dealer.rb +1 -13
- data/lib/omq/routing/dish.rb +94 -0
- data/lib/omq/routing/fan_out.rb +14 -13
- data/lib/omq/routing/gather.rb +60 -0
- data/lib/omq/routing/pair.rb +7 -24
- data/lib/omq/routing/peer.rb +95 -0
- data/lib/omq/routing/pub.rb +0 -11
- data/lib/omq/routing/pull.rb +1 -13
- data/lib/omq/routing/push.rb +1 -10
- data/lib/omq/routing/radio.rb +187 -0
- data/lib/omq/routing/rep.rb +10 -20
- data/lib/omq/routing/req.rb +5 -17
- data/lib/omq/routing/round_robin.rb +17 -18
- data/lib/omq/routing/router.rb +3 -17
- data/lib/omq/routing/scatter.rb +77 -0
- data/lib/omq/routing/server.rb +90 -0
- data/lib/omq/routing/sub.rb +1 -13
- data/lib/omq/routing/xpub.rb +0 -11
- data/lib/omq/routing/xsub.rb +6 -23
- data/lib/omq/routing.rb +5 -2
- data/lib/omq/scatter_gather.rb +56 -0
- data/lib/omq/socket.rb +8 -23
- data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
- data/lib/omq/transport/inproc.rb +11 -3
- 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.rb +9 -64
- metadata +16 -2
- 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: 2a0e72756aae2179b8c790d5b65c54a82b516a17d11f760187a6fefecf6bf2c1
|
|
4
|
+
data.tar.gz: d6ce860b558977939b28c3d1f23d7bfc0740cf602fb63a519107a59cdca3c8c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa8a7c98147aa4f0fea5fad3004e8a964105a2005ed645d03b0fd1f0fba67bfc4952a3f4d14926fbaf4293efdcada93235b2ba59956737ef0ced5c1b97d35ea2
|
|
7
|
+
data.tar.gz: b5878650300c7135e04ec8fffcadef6d5101f77c8f2a7e3e6c1407faebe57714b613398c3fecf71b561cb8bdb6c25a70646d812c395db46f78827e97f796c8e7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,144 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.23.0 — 2026-04-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Draft socket types now ship with `omq` itself.** `OMQ::CLIENT`/`SERVER`,
|
|
8
|
+
`OMQ::RADIO`/`DISH`, `OMQ::SCATTER`/`GATHER`, `OMQ::CHANNEL`, and
|
|
9
|
+
`OMQ::PEER` are back in OMQ. They were previously distributed as separate
|
|
10
|
+
`omq-rfc-*` gems, which was a PITA to maintain. Their source is now part of
|
|
11
|
+
`omq`. They are **not** loaded by `require "omq"` — opt in with one of:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "omq/client_server"
|
|
15
|
+
require "omq/radio_dish" # also registers the udp:// transport
|
|
16
|
+
require "omq/scatter_gather"
|
|
17
|
+
require "omq/channel"
|
|
18
|
+
require "omq/peer"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
These requires must run at process startup (before any socket is bound
|
|
22
|
+
or connected), since the underlying registries (`Routing`,
|
|
23
|
+
`Engine.transports`) freeze on first use. The five `omq-rfc-*` gems are
|
|
24
|
+
superseded and will not receive further releases. Per-pattern docs live
|
|
25
|
+
under [`doc/socket-types/`](doc/socket-types/).
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **`Socket#bind` / `#connect` now return a `URI`** (the resolved endpoint).
|
|
31
|
+
`#bind` returns the listener's resolved URI — for `tcp://host:0` this
|
|
32
|
+
carries the auto-selected port via `uri.port`. `#connect` returns the
|
|
33
|
+
parsed input URI. The `last_tcp_port` and `last_endpoint` accessors are
|
|
34
|
+
removed; callers should capture the URI from `#bind` instead. Note: stdlib
|
|
35
|
+
`URI.parse` is lossy on abstract IPC endpoints (`ipc://@name`) — the `@`
|
|
36
|
+
is parsed as userinfo and dropped on `to_s`. For abstract IPC, use the
|
|
37
|
+
input string for connect rather than re-serializing the URI.
|
|
38
|
+
`Socket#inspect` now shows `bound=[...]` (the listener endpoints) instead
|
|
39
|
+
of `last_endpoint=...`.
|
|
40
|
+
|
|
41
|
+
- **Transport interface: `.bind`/`.connect` replaced by `.listener`/`.dialer`
|
|
42
|
+
factory methods** returning stateful `Listener`/`Dialer` objects. The
|
|
43
|
+
engine now stores a per-endpoint `@dialers` map (was a `@dialed` Set)
|
|
44
|
+
and a `@listeners` hash keyed by endpoint (was an Array). Reconnect
|
|
45
|
+
calls `dialer.connect` directly — no transport lookup or option replay
|
|
46
|
+
on every retry. `Transport::Inproc` keeps its synchronous `.connect`
|
|
47
|
+
fast-path; only TCP/IPC gain `Dialer` classes.
|
|
48
|
+
|
|
49
|
+
- **`Engine#bind` / `#connect` accept transport-specific kwargs** via
|
|
50
|
+
`**opts`, forwarded to the transport's `.listener` / `.dialer`. Socket
|
|
51
|
+
`#bind` / `#connect` pass them through. Enables per-connection
|
|
52
|
+
transport configuration (e.g., TLS context) without polluting
|
|
53
|
+
`Options`.
|
|
54
|
+
|
|
55
|
+
- **`ConnectionLifecycle#ready!` calls `transport_obj.wrap_connection(conn)`
|
|
56
|
+
if defined** — hook for transports that need to wrap the buffered
|
|
57
|
+
stream after handshake (e.g., TLS).
|
|
58
|
+
|
|
59
|
+
- **Transports self-register in `Engine.transports`.** Each transport
|
|
60
|
+
file (`tcp`, `ipc`, `inproc`) now adds its own scheme entry at load
|
|
61
|
+
time. `lib/omq.rb` requires transports after `engine.rb` so the
|
|
62
|
+
`Engine` constant is available. External transport plugins follow
|
|
63
|
+
the same pattern.
|
|
64
|
+
|
|
65
|
+
- **`Engine` gains delegate methods that hide internal layout** from
|
|
66
|
+
callers: `#subscribe`, `#unsubscribe`, `#subscriber_joined` forward
|
|
67
|
+
to the routing strategy; `#record_disconnect_reason(conn, error)`
|
|
68
|
+
wraps the `@connections` lookup; `Inproc::DirectPipe#wire_direct_recv`
|
|
69
|
+
replaces two separate attribute setters previously poked from the
|
|
70
|
+
recv pump. Callers no longer chain through `engine.routing.*` or
|
|
71
|
+
`engine.connections[conn]`.
|
|
72
|
+
|
|
73
|
+
- **`SocketLifecycle#resolve_all_peers_gone_if_empty` renamed to
|
|
74
|
+
`#maybe_resolve_all_peers_gone`.** The composite `unless` was split
|
|
75
|
+
into two early-returns for readability. A new `#force_close!` handles
|
|
76
|
+
`Engine#stop`'s crash path, collapsing two `@lifecycle.*` calls into
|
|
77
|
+
one.
|
|
78
|
+
|
|
79
|
+
- **Module-level constants consolidated into `lib/omq/constants.rb`.**
|
|
80
|
+
`MonitorEvent`, `DEBUG`, `SocketDeadError`, `CONNECTION_LOST`,
|
|
81
|
+
`CONNECTION_FAILED`, and `OMQ.freeze_for_ractors!` now live in one
|
|
82
|
+
file. `lib/omq/monitor_event.rb` is deleted; `lib/omq.rb` just
|
|
83
|
+
requires `omq/constants`.
|
|
84
|
+
|
|
85
|
+
### Removed
|
|
86
|
+
|
|
87
|
+
- **`Engine#tasks` array** (and every `@tasks << ...` append site)
|
|
88
|
+
deleted. `Async::Barrier` already tracks every spawned task and
|
|
89
|
+
exposes `#size`, `#empty?`, and `#stop`. `Heartbeat.start`,
|
|
90
|
+
`Maintenance.start`, and `Reconnect#run` drop their `tasks`
|
|
91
|
+
parameter. Teardown collapses to `@lifecycle.barrier&.stop`.
|
|
92
|
+
|
|
93
|
+
- **Routing strategies and TCP listener drop their `@tasks` arrays
|
|
94
|
+
too.** Same `Async::Barrier` rollout applied to every routing
|
|
95
|
+
strategy and `Transport::TCP::Listener`. Per-connection pumps
|
|
96
|
+
(send/recv/reaper/group/subscription listener) ride the
|
|
97
|
+
per-connection lifecycle barrier; Radio's socket-level send pump
|
|
98
|
+
rides `engine.barrier` via a new `parent:` kwarg on
|
|
99
|
+
`Engine#spawn_pump_task`. The redundant `@conn_send_tasks` hashes
|
|
100
|
+
in RoundRobin, FanOut, Rep, Router, Peer, and Server are gone, as
|
|
101
|
+
are all routing-strategy `#stop` methods and the matching
|
|
102
|
+
`routing.stop rescue nil` calls in `Engine#close`/`#stop`.
|
|
103
|
+
`ConnSendPump.start` drops its `tasks` parameter. Channel's send
|
|
104
|
+
pump moves from loose `spawn_pump_task` to `spawn_conn_pump_task`,
|
|
105
|
+
so its disconnect rescue is now centralized in `Engine`. Net: 24
|
|
106
|
+
files, −340/+121.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- **`bench/report.rb` preserves chronological run order.** Named run IDs
|
|
111
|
+
(e.g. `baseline-append`) previously sorted alphabetically after ISO
|
|
112
|
+
timestamps, hiding the most recent run. Now uses insertion order.
|
|
113
|
+
|
|
114
|
+
- **`zmtp_30_compat_test` waits for XSUB connection** before sending
|
|
115
|
+
`SUBSCRIBE`, removing a race where the subscribe arrived before the
|
|
116
|
+
handshake completed.
|
|
117
|
+
|
|
118
|
+
## 0.22.1 — 2026-04-16
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- **Reuse batch arrays in send pumps.** All send pumps (RoundRobin,
|
|
123
|
+
Pair, ConnSendPump, FanOut, FanOut-conflate) now pre-allocate a
|
|
124
|
+
single batch array and clear it between cycles instead of
|
|
125
|
+
allocating a fresh `[msg]` per dequeue.
|
|
126
|
+
|
|
127
|
+
- **`Routing.dequeue_batch`** consolidates the blocking-dequeue +
|
|
128
|
+
non-blocking-sweep pattern that was duplicated across four call
|
|
129
|
+
sites into one method. `dequeue_batch_capped` does the same for
|
|
130
|
+
the byte/message-capped RoundRobin variant.
|
|
131
|
+
|
|
132
|
+
- **REP envelope stored as `[conn, envelope]`** instead of a Hash,
|
|
133
|
+
and reply assembly uses `<<` + `concat` instead of double splat.
|
|
134
|
+
|
|
135
|
+
- **Heartbeat drops redundant `context: "".b`** — the default is
|
|
136
|
+
now `EMPTY_BINARY` in protocol-zmtp.
|
|
137
|
+
|
|
138
|
+
- **Bench harness accepts `OMQ_BENCH_SIZES`, `OMQ_BENCH_TRANSPORTS`,
|
|
139
|
+
and `OMQ_BENCH_PEERS`** env vars to scope runs without editing
|
|
140
|
+
code.
|
|
141
|
+
|
|
3
142
|
## 0.22.0 — 2026-04-15
|
|
4
143
|
|
|
5
144
|
### Fixed
|
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.b.freeze, message.b.freeze]
|
|
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
|
|
@@ -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
|
|
@@ -161,7 +161,15 @@ module OMQ
|
|
|
161
161
|
|
|
162
162
|
|
|
163
163
|
def ready!(conn)
|
|
164
|
-
conn
|
|
164
|
+
conn = @engine.connection_wrapper.call(conn) if @engine.connection_wrapper
|
|
165
|
+
|
|
166
|
+
if @endpoint
|
|
167
|
+
transport_obj = @engine.transport_object_for(@endpoint)
|
|
168
|
+
if transport_obj.respond_to?(:wrap_connection)
|
|
169
|
+
conn = transport_obj.wrap_connection(conn)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
165
173
|
@conn = conn
|
|
166
174
|
@engine.connections[@conn] = self
|
|
167
175
|
@engine.emit_monitor_event(:handshake_succeeded, endpoint: @endpoint)
|
|
@@ -212,7 +220,7 @@ module OMQ
|
|
|
212
220
|
detail = reason ? { error: reason, reason: reason.message } : nil
|
|
213
221
|
@engine.emit_monitor_event(:disconnected, endpoint: @endpoint, detail: detail)
|
|
214
222
|
@done&.resolve(true)
|
|
215
|
-
@engine.
|
|
223
|
+
@engine.maybe_resolve_all_peers_gone
|
|
216
224
|
@engine.maybe_reconnect(@endpoint) if reconnect
|
|
217
225
|
|
|
218
226
|
# Cancel every sibling pump of this connection. The caller is
|
data/lib/omq/engine/heartbeat.rb
CHANGED
|
@@ -11,9 +11,8 @@ module OMQ
|
|
|
11
11
|
# @param parent [Async::Task, Async::Barrier] parent to spawn under
|
|
12
12
|
# @param conn [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,10 +20,10 @@ 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
|
-
conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl
|
|
26
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl))
|
|
28
27
|
if conn.heartbeat_expired?(timeout)
|
|
29
28
|
conn.close
|
|
30
29
|
break
|
|
@@ -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
|
data/lib/omq/engine/recv_pump.rb
CHANGED
|
@@ -61,8 +61,7 @@ module OMQ
|
|
|
61
61
|
#
|
|
62
62
|
def start(parent_task, transform)
|
|
63
63
|
if @conn.is_a?(Transport::Inproc::DirectPipe) && @conn.peer
|
|
64
|
-
@conn.peer.
|
|
65
|
-
@conn.peer.direct_recv_transform = transform
|
|
64
|
+
@conn.peer.wire_direct_recv(@recv_queue, transform)
|
|
66
65
|
return nil
|
|
67
66
|
end
|
|
68
67
|
|
|
@@ -125,7 +124,7 @@ module OMQ
|
|
|
125
124
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
|
|
126
125
|
# expected disconnect — stash reason for the :disconnected
|
|
127
126
|
# monitor event, let the lifecycle reconnect as usual
|
|
128
|
-
engine.
|
|
127
|
+
engine.record_disconnect_reason(conn, error)
|
|
129
128
|
rescue => error
|
|
130
129
|
engine.signal_fatal_error(error)
|
|
131
130
|
end
|
|
@@ -172,7 +171,7 @@ module OMQ
|
|
|
172
171
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
|
|
173
172
|
# expected disconnect — stash reason for the :disconnected
|
|
174
173
|
# monitor event, let the lifecycle reconnect as usual
|
|
175
|
-
engine.
|
|
174
|
+
engine.record_disconnect_reason(conn, error)
|
|
176
175
|
rescue => error
|
|
177
176
|
engine.signal_fatal_error(error)
|
|
178
177
|
end
|
|
@@ -28,11 +28,11 @@ module OMQ
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
TRANSITIONS = {
|
|
31
|
-
new: %i[open closed]
|
|
32
|
-
open: %i[closing closed]
|
|
33
|
-
closing: %i[closed]
|
|
34
|
-
closed: []
|
|
35
|
-
}.freeze
|
|
31
|
+
new: %i[open closed],
|
|
32
|
+
open: %i[closing closed],
|
|
33
|
+
closing: %i[closed],
|
|
34
|
+
closed: [],
|
|
35
|
+
}.transform_values(&:freeze).freeze
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# @return [Symbol]
|
|
@@ -74,8 +74,8 @@ module OMQ
|
|
|
74
74
|
@peer_connected = Async::Promise.new
|
|
75
75
|
@all_peers_gone = Async::Promise.new
|
|
76
76
|
@reconnect_enabled = true
|
|
77
|
-
@parent_task = nil
|
|
78
77
|
@on_io_thread = false
|
|
78
|
+
@parent_task = nil
|
|
79
79
|
@barrier = nil
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -106,6 +106,7 @@ module OMQ
|
|
|
106
106
|
#
|
|
107
107
|
def capture_parent_task(parent: nil, linger:)
|
|
108
108
|
return false if @parent_task
|
|
109
|
+
|
|
109
110
|
if parent
|
|
110
111
|
@parent_task = parent
|
|
111
112
|
elsif Async::Task.current?
|
|
@@ -115,6 +116,7 @@ module OMQ
|
|
|
115
116
|
@on_io_thread = true
|
|
116
117
|
Reactor.track_linger(linger)
|
|
117
118
|
end
|
|
119
|
+
|
|
118
120
|
@barrier = Async::Barrier.new(parent: @parent_task)
|
|
119
121
|
transition!(:open)
|
|
120
122
|
true
|
|
@@ -134,10 +136,23 @@ module OMQ
|
|
|
134
136
|
end
|
|
135
137
|
|
|
136
138
|
|
|
137
|
-
#
|
|
139
|
+
# Hard close from any alive state — used by {Engine#stop}'s crash
|
|
140
|
+
# path, which skips the linger drain. Goes `:open → :closed`
|
|
141
|
+
# directly when needed.
|
|
142
|
+
def force_close!
|
|
143
|
+
transition!(:closed)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Resolves `all_peers_gone` once we had peers and the connection map
|
|
148
|
+
# is empty. Called by {Engine#maybe_resolve_all_peers_gone} after a
|
|
149
|
+
# teardown removes a peer.
|
|
150
|
+
#
|
|
138
151
|
# @param connections [Hash] current connection map
|
|
139
|
-
def
|
|
140
|
-
return unless @peer_connected.resolved?
|
|
152
|
+
def maybe_resolve_all_peers_gone(connections)
|
|
153
|
+
return unless @peer_connected.resolved?
|
|
154
|
+
return unless connections.empty?
|
|
155
|
+
|
|
141
156
|
@all_peers_gone.resolve(true)
|
|
142
157
|
end
|
|
143
158
|
|
|
@@ -147,9 +162,11 @@ module OMQ
|
|
|
147
162
|
|
|
148
163
|
def transition!(new_state)
|
|
149
164
|
allowed = TRANSITIONS[@state]
|
|
165
|
+
|
|
150
166
|
unless allowed&.include?(new_state)
|
|
151
167
|
raise InvalidTransition, "#{@state} → #{new_state}"
|
|
152
168
|
end
|
|
169
|
+
|
|
153
170
|
@state = new_state
|
|
154
171
|
end
|
|
155
172
|
|