omq 0.22.1 → 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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -0
  3. data/README.md +17 -21
  4. data/lib/omq/channel.rb +35 -0
  5. data/lib/omq/client_server.rb +72 -0
  6. data/lib/omq/constants.rb +68 -0
  7. data/lib/omq/engine/connection_lifecycle.rb +11 -3
  8. data/lib/omq/engine/heartbeat.rb +2 -3
  9. data/lib/omq/engine/maintenance.rb +4 -5
  10. data/lib/omq/engine/reconnect.rb +12 -11
  11. data/lib/omq/engine/recv_pump.rb +3 -4
  12. data/lib/omq/engine/socket_lifecycle.rb +26 -9
  13. data/lib/omq/engine.rb +196 -85
  14. data/lib/omq/peer.rb +49 -0
  15. data/lib/omq/pub_sub.rb +2 -2
  16. data/lib/omq/radio_dish.rb +122 -0
  17. data/lib/omq/reactor.rb +14 -5
  18. data/lib/omq/routing/channel.rb +110 -0
  19. data/lib/omq/routing/client.rb +70 -0
  20. data/lib/omq/routing/conn_send_pump.rb +4 -7
  21. data/lib/omq/routing/dealer.rb +1 -13
  22. data/lib/omq/routing/dish.rb +94 -0
  23. data/lib/omq/routing/fan_out.rb +5 -9
  24. data/lib/omq/routing/gather.rb +60 -0
  25. data/lib/omq/routing/pair.rb +3 -22
  26. data/lib/omq/routing/peer.rb +95 -0
  27. data/lib/omq/routing/pub.rb +0 -11
  28. data/lib/omq/routing/pull.rb +1 -13
  29. data/lib/omq/routing/push.rb +1 -10
  30. data/lib/omq/routing/radio.rb +187 -0
  31. data/lib/omq/routing/rep.rb +3 -17
  32. data/lib/omq/routing/req.rb +4 -16
  33. data/lib/omq/routing/round_robin.rb +11 -15
  34. data/lib/omq/routing/router.rb +3 -17
  35. data/lib/omq/routing/scatter.rb +77 -0
  36. data/lib/omq/routing/server.rb +90 -0
  37. data/lib/omq/routing/sub.rb +1 -13
  38. data/lib/omq/routing/xpub.rb +0 -11
  39. data/lib/omq/routing/xsub.rb +6 -23
  40. data/lib/omq/scatter_gather.rb +56 -0
  41. data/lib/omq/socket.rb +8 -23
  42. data/lib/omq/transport/inproc/direct_pipe.rb +17 -15
  43. data/lib/omq/transport/inproc.rb +11 -3
  44. data/lib/omq/transport/ipc.rb +41 -13
  45. data/lib/omq/transport/tcp.rb +59 -23
  46. data/lib/omq/transport/udp.rb +281 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq.rb +9 -64
  49. metadata +16 -2
  50. data/lib/omq/monitor_event.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f58b9e0c8c49bcdfb8dd80ec5eda6a6b6e5b09f04a2a7851001449449ba448fd
4
- data.tar.gz: 0f6cbd451adc8b1d1cde0771d6535c5959f09e4b3f0442164b0544964755795e
3
+ metadata.gz: 2a0e72756aae2179b8c790d5b65c54a82b516a17d11f760187a6fefecf6bf2c1
4
+ data.tar.gz: d6ce860b558977939b28c3d1f23d7bfc0740cf602fb63a519107a59cdca3c8c3
5
5
  SHA512:
6
- metadata.gz: d7d13f97eeacf998f1e0137bdbc5dd4a471195ff628fd7da97720a019e30c2a9c200fab761c38943bbbada056bf81880e6ca2dd87d3936f150f0fa22566cebe5
7
- data.tar.gz: f97931ecb4893ecd0155c37ea18793c9414bd57de6918496ef7af18bad714720aa2e146531788956265d6f85bc3e18f6dd403f71df8683913d322c94a6dd1bdb
6
+ metadata.gz: fa8a7c98147aa4f0fea5fad3004e8a964105a2005ed645d03b0fd1f0fba67bfc4952a3f4d14926fbaf4293efdcada93235b2ba59956737ef0ced5c1b97d35ea2
7
+ data.tar.gz: b5878650300c7135e04ec8fffcadef6d5101f77c8f2a7e3e6c1407faebe57714b613398c3fecf71b561cb8bdb6c25a70646d812c395db46f78827e97f796c8e7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,120 @@
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
+
3
118
  ## 0.22.1 — 2026-04-16
4
119
 
5
120
  ### 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
- Each draft pattern lives in its own geminstall only the ones you use.
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 | Gem |
187
- |---------|------|---------|---------------|-----|
188
- | **CLIENT** / **SERVER** | Work-stealing / routing-ID | Fair-queue | Block | [`omq-rfc-clientserver`](https://github.com/paddor/omq-rfc-clientserver) |
189
- | **RADIO** / **DISH** | Group fan-out | Group filter | Drop | [`omq-rfc-radiodish`](https://github.com/paddor/omq-rfc-radiodish) |
190
- | **SCATTER** / **GATHER** | Work-stealing | Fair-queue | Block | [`omq-rfc-scattergather`](https://github.com/paddor/omq-rfc-scattergather) |
191
- | **PEER** | Routing-ID | Fair-queue | Block | [`omq-rfc-p2p`](https://github.com/paddor/omq-rfc-p2p) |
192
- | **CHANNEL** | Exclusive 1-to-1 | Exclusive 1-to-1 | Block | [`omq-rfc-channel`](https://github.com/paddor/omq-rfc-channel) |
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-rfc-zstd](https://github.com/paddor/omq-rfc-zstd)** — transparent
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, omq-rfc-\*, etc.) instead of released gems.
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-rfc-zstd.git
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
@@ -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, @engine.tasks)
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 = @engine.connection_wrapper.call(conn) if @engine.connection_wrapper
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.resolve_all_peers_gone_if_empty
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
@@ -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, tasks)
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
- tasks << parent.async(transient: true, annotation: "heartbeat") do
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, tasks)
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
- tasks << parent_task.async(transient: true, annotation: "mechanism maintenance") do
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
@@ -8,25 +8,25 @@ module OMQ
8
8
  # or the engine is closed.
9
9
  #
10
10
  class Reconnect
11
- # @param endpoint [String]
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(endpoint, options, parent_task, engine, delay: nil)
18
- new(engine, endpoint, options).run(parent_task, delay: delay)
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 endpoint [String]
23
+ # @param dialer [Transport::TCP::Dialer, etc.] stateful dialer factory
24
24
  # @param options [Options]
25
25
  #
26
- def initialize(engine, endpoint, options)
27
- @engine = engine
28
- @endpoint = endpoint
29
- @options = 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
- @engine.tasks << parent_task.async(transient: true, annotation: "reconnect #{@endpoint}") do
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
- @engine.transport_for(@endpoint).connect(@endpoint, @engine)
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
@@ -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.direct_recv_queue = @recv_queue
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.connections[conn]&.record_disconnect_reason(error)
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.connections[conn]&.record_disconnect_reason(error)
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].freeze,
32
- open: %i[closing closed].freeze,
33
- closing: %i[closed].freeze,
34
- closed: [].freeze,
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
- # Resolves `all_peers_gone` if we had peers and now have none.
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 resolve_all_peers_gone_if_empty(connections)
140
- return unless @peer_connected.resolved? && connections.empty?
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