omq 0.13.0 → 0.14.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 +35 -0
- data/README.md +27 -0
- data/lib/omq/drop_queue.rb +3 -0
- data/lib/omq/engine/connection_setup.rb +38 -15
- data/lib/omq/engine/heartbeat.rb +1 -1
- data/lib/omq/engine/maintenance.rb +35 -0
- data/lib/omq/engine/reconnect.rb +38 -12
- data/lib/omq/engine/recv_pump.rb +89 -46
- data/lib/omq/engine.rb +42 -0
- data/lib/omq/options.rb +44 -0
- data/lib/omq/pair.rb +6 -0
- data/lib/omq/pub_sub.rb +25 -0
- data/lib/omq/push_pull.rb +17 -0
- data/lib/omq/queue_interface.rb +1 -0
- data/lib/omq/readable.rb +2 -0
- data/lib/omq/req_rep.rb +13 -0
- data/lib/omq/router_dealer.rb +12 -0
- data/lib/omq/routing/conn_send_pump.rb +1 -1
- data/lib/omq/routing/dealer.rb +7 -0
- data/lib/omq/routing/fair_queue.rb +28 -0
- data/lib/omq/routing/fan_out.rb +19 -7
- data/lib/omq/routing/pair.rb +9 -1
- data/lib/omq/routing/pub.rb +8 -0
- data/lib/omq/routing/pull.rb +7 -0
- data/lib/omq/routing/rep.rb +11 -1
- data/lib/omq/routing/req.rb +7 -0
- data/lib/omq/routing/round_robin.rb +4 -3
- data/lib/omq/routing/router.rb +10 -1
- data/lib/omq/routing/sub.rb +10 -0
- data/lib/omq/routing/xpub.rb +8 -0
- data/lib/omq/routing/xsub.rb +13 -3
- data/lib/omq/routing.rb +26 -11
- data/lib/omq/socket.rb +23 -5
- data/lib/omq/transport/inproc/direct_pipe.rb +12 -1
- data/lib/omq/transport/inproc.rb +5 -0
- data/lib/omq/transport/ipc.rb +7 -1
- data/lib/omq/transport/tcp.rb +12 -7
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +2 -0
- data/lib/omq.rb +4 -1
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 997f3d83d6ddc56d44341f69743f4f30b9e122f046718246de78da75969ec6fa
|
|
4
|
+
data.tar.gz: 65f4f071d952c477630bfae4c25f136abfa79350099b58b1be4ca79ec500f2e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d92503d56e3106987bda522497f42630d7999bca8ba1ddd5190129d0dc567c5dac1714a8f8566601ac1fa309d0e142f87936342667b36588f81e39f785a2b502
|
|
7
|
+
data.tar.gz: bed4e86b9404a7ecc175c50ba85a2b7ad8daaea1490c8f39a449c8f92c197b039326d73708279838c1678a29235c7a5ff30c1bf36903306d452dccd743059b76
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 — 2026-04-07
|
|
4
|
+
|
|
5
|
+
- **Fix recv pump crash with connection wrappers** — `start_direct` called
|
|
6
|
+
`msg.sum(&:bytesize)` unconditionally, crashing when a `connection_wrapper`
|
|
7
|
+
(e.g. omq-ractor's `MarshalConnection`) returns deserialized Ruby objects.
|
|
8
|
+
Byte counting now uses `conn.instance_of?(Protocol::ZMTP::Connection)` to
|
|
9
|
+
skip non-ZMTP connections (inproc, Ractor bridges).
|
|
10
|
+
- Remove TLS transport dependency from Gemfile.
|
|
11
|
+
- YARD documentation on all public methods and classes.
|
|
12
|
+
- Code style: expand `else X` one-liners, enforce two blank lines between
|
|
13
|
+
methods and constants.
|
|
14
|
+
- Benchmarks: add per-run timeout (default 30s, `OMQ_BENCH_TIMEOUT` env var)
|
|
15
|
+
and abort if a group produces no results.
|
|
16
|
+
|
|
17
|
+
- Add `Engine::Maintenance` — spawns a periodic `Async::Loop.quantized` timer
|
|
18
|
+
that calls the mechanism's `#maintenance` callback (if defined). Enables
|
|
19
|
+
automatic cookie key rotation for CurveZMQ and BLAKE3ZMQ server mechanisms.
|
|
20
|
+
- **YJIT: remove redundant `is_a?` guards in recv pump** — the non-transform
|
|
21
|
+
branch no longer type-checks every message; `conn.receive_message` always
|
|
22
|
+
returns `Array<String>`.
|
|
23
|
+
- **YJIT: `FanOut#subscribed?` fast path for subscribe-all** — connections
|
|
24
|
+
subscribed to `""` are tracked in a `@subscribe_all` Set, short-circuiting
|
|
25
|
+
the per-message prefix scan with an O(1) lookup.
|
|
26
|
+
- **YJIT: remove safe navigation in hot enqueue paths** — `&.enqueue` calls
|
|
27
|
+
in `FanOut#fan_out_enqueue` and `RoundRobin#enqueue_round_robin` replaced
|
|
28
|
+
with direct calls; queues are guaranteed to exist for live connections.
|
|
29
|
+
- **Fix PUB/SUB fan-out over inproc and IPC** — restore `respond_to?(:write_wire)`
|
|
30
|
+
guard in `FanOut#start_conn_send_pump` so DirectPipe connections use
|
|
31
|
+
`#write_message` instead of the wire-optimized path. Add `DirectPipe#encrypted?`
|
|
32
|
+
(returns `false`) for the mechanism query.
|
|
33
|
+
- **Code audit: never-instantiated classes** — `RecvPump`, `ConnectionSetup`,
|
|
34
|
+
and `Reconnect` refactored from class-method namespaces to proper instances
|
|
35
|
+
that capture shared state. `Heartbeat`, `Maintenance`, and `ConnSendPump`
|
|
36
|
+
changed from classes to modules (single `self.` method, never instantiated).
|
|
37
|
+
|
|
3
38
|
## 0.13.0
|
|
4
39
|
|
|
5
40
|
### Changed
|
data/README.md
CHANGED
|
@@ -197,6 +197,33 @@ bundle install
|
|
|
197
197
|
bundle exec rake
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
### Full development setup
|
|
201
|
+
|
|
202
|
+
Set `OMQ_DEV=1` to tell Bundler to load sibling projects from source
|
|
203
|
+
(protocol-zmtp, nuckle, omq-rfc-\*, etc.) instead of released gems.
|
|
204
|
+
This is required for running benchmarks and for testing changes across
|
|
205
|
+
the stack.
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
# clone OMQ and its sibling repos into the same parent directory
|
|
209
|
+
git clone https://github.com/paddor/omq.git
|
|
210
|
+
git clone https://github.com/paddor/protocol-zmtp.git
|
|
211
|
+
git clone https://github.com/paddor/nuckle.git
|
|
212
|
+
git clone https://github.com/paddor/omq-rfc-blake3zmq.git
|
|
213
|
+
git clone https://github.com/paddor/omq-rfc-channel.git
|
|
214
|
+
git clone https://github.com/paddor/omq-rfc-clientserver.git
|
|
215
|
+
git clone https://github.com/paddor/omq-rfc-p2p.git
|
|
216
|
+
git clone https://github.com/paddor/omq-rfc-qos.git
|
|
217
|
+
git clone https://github.com/paddor/omq-rfc-radiodish.git
|
|
218
|
+
git clone https://github.com/paddor/omq-rfc-scattergather.git
|
|
219
|
+
git clone https://github.com/paddor/omq-ffi.git
|
|
220
|
+
git clone https://github.com/paddor/omq-ractor.git
|
|
221
|
+
|
|
222
|
+
cd omq
|
|
223
|
+
OMQ_DEV=1 bundle install
|
|
224
|
+
OMQ_DEV=1 bundle exec rake
|
|
225
|
+
```
|
|
226
|
+
|
|
200
227
|
## License
|
|
201
228
|
|
|
202
229
|
[ISC](LICENSE)
|
data/lib/omq/drop_queue.rb
CHANGED
|
@@ -18,6 +18,7 @@ module OMQ
|
|
|
18
18
|
@strategy = strategy
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
|
|
21
22
|
# Enqueues an item. Drops according to the configured strategy if full.
|
|
22
23
|
#
|
|
23
24
|
# @param item [Object]
|
|
@@ -33,6 +34,7 @@ module OMQ
|
|
|
33
34
|
retry
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
|
|
36
38
|
# Removes and returns the next item, blocking if empty.
|
|
37
39
|
#
|
|
38
40
|
# @return [Object]
|
|
@@ -45,6 +47,7 @@ module OMQ
|
|
|
45
47
|
end
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
|
|
48
51
|
# @return [Boolean]
|
|
49
52
|
#
|
|
50
53
|
def empty?
|
|
@@ -13,34 +13,57 @@ module OMQ
|
|
|
13
13
|
# @return [Connection]
|
|
14
14
|
#
|
|
15
15
|
def self.run(io, engine, as_server:, endpoint: nil, done: nil)
|
|
16
|
-
|
|
16
|
+
new(engine).run(io, as_server: as_server, endpoint: endpoint, done: done)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# @param engine [Engine]
|
|
21
|
+
#
|
|
22
|
+
def initialize(engine)
|
|
23
|
+
@engine = engine
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Performs the ZMTP handshake, starts heartbeat, and registers the connection.
|
|
28
|
+
#
|
|
29
|
+
# @param io [#read, #write, #close]
|
|
30
|
+
# @param as_server [Boolean]
|
|
31
|
+
# @param endpoint [String, nil]
|
|
32
|
+
# @param done [Async::Promise, nil] resolved when connection is lost
|
|
33
|
+
# @return [Connection]
|
|
34
|
+
#
|
|
35
|
+
def run(io, as_server:, endpoint: nil, done: nil)
|
|
36
|
+
conn = build_connection(io, as_server)
|
|
17
37
|
conn.handshake!
|
|
18
|
-
Heartbeat.start(engine.parent_task, conn, engine.options, engine.tasks)
|
|
19
|
-
conn = engine.connection_wrapper.call(conn) if engine.connection_wrapper
|
|
20
|
-
register(conn,
|
|
21
|
-
engine.emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
|
|
38
|
+
Heartbeat.start(@engine.parent_task, conn, @engine.options, @engine.tasks)
|
|
39
|
+
conn = @engine.connection_wrapper.call(conn) if @engine.connection_wrapper
|
|
40
|
+
register(conn, endpoint, done)
|
|
41
|
+
@engine.emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
|
|
22
42
|
conn
|
|
23
43
|
rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
|
|
24
|
-
engine.emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
|
|
44
|
+
@engine.emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
|
|
25
45
|
conn&.close
|
|
26
46
|
raise
|
|
27
47
|
end
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_connection(io, as_server)
|
|
30
52
|
Protocol::ZMTP::Connection.new(
|
|
31
53
|
io,
|
|
32
|
-
socket_type: engine.socket_type.to_s,
|
|
33
|
-
identity: engine.options.identity,
|
|
54
|
+
socket_type: @engine.socket_type.to_s,
|
|
55
|
+
identity: @engine.options.identity,
|
|
34
56
|
as_server: as_server,
|
|
35
|
-
mechanism: engine.options.mechanism&.dup,
|
|
36
|
-
max_message_size: engine.options.max_message_size,
|
|
57
|
+
mechanism: @engine.options.mechanism&.dup,
|
|
58
|
+
max_message_size: @engine.options.max_message_size,
|
|
37
59
|
)
|
|
38
60
|
end
|
|
39
61
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
engine.
|
|
43
|
-
engine.
|
|
62
|
+
|
|
63
|
+
def register(conn, endpoint, done)
|
|
64
|
+
@engine.connections[conn] = Engine::ConnectionRecord.new(endpoint: endpoint, done: done)
|
|
65
|
+
@engine.routing.connection_added(conn)
|
|
66
|
+
@engine.peer_connected.resolve(conn)
|
|
44
67
|
end
|
|
45
68
|
end
|
|
46
69
|
end
|
data/lib/omq/engine/heartbeat.rb
CHANGED
|
@@ -7,7 +7,7 @@ module OMQ
|
|
|
7
7
|
# Sends PING frames at +interval+ seconds and closes the connection
|
|
8
8
|
# if no traffic is seen within +timeout+ seconds.
|
|
9
9
|
#
|
|
10
|
-
|
|
10
|
+
module Heartbeat
|
|
11
11
|
# @param parent_task [Async::Task]
|
|
12
12
|
# @param conn [Connection]
|
|
13
13
|
# @param options [Options]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/loop"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
class Engine
|
|
7
|
+
# Spawns a periodic maintenance task for the parent mechanism.
|
|
8
|
+
#
|
|
9
|
+
# The mechanism declares maintenance needs via +#maintenance+,
|
|
10
|
+
# which returns +{ interval:, task: }+ or nil.
|
|
11
|
+
#
|
|
12
|
+
module Maintenance
|
|
13
|
+
# @param parent_task [Async::Task]
|
|
14
|
+
# @param mechanism [#maintenance, nil]
|
|
15
|
+
# @param tasks [Array<Async::Task>]
|
|
16
|
+
#
|
|
17
|
+
def self.start(parent_task, mechanism, tasks)
|
|
18
|
+
return unless mechanism.respond_to?(:maintenance)
|
|
19
|
+
spec = mechanism.maintenance
|
|
20
|
+
return unless spec
|
|
21
|
+
|
|
22
|
+
interval = spec[:interval]
|
|
23
|
+
callable = spec[:task]
|
|
24
|
+
|
|
25
|
+
tasks << parent_task.async(transient: true, annotation: "mechanism maintenance") do
|
|
26
|
+
Async::Loop.quantized(interval: interval) do
|
|
27
|
+
callable.call
|
|
28
|
+
end
|
|
29
|
+
rescue Async::Stop
|
|
30
|
+
# clean shutdown
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/omq/engine/reconnect.rb
CHANGED
|
@@ -11,33 +11,57 @@ module OMQ
|
|
|
11
11
|
# @param endpoint [String]
|
|
12
12
|
# @param options [Options]
|
|
13
13
|
# @param parent_task [Async::Task]
|
|
14
|
-
# @param engine [Engine]
|
|
14
|
+
# @param engine [Engine]
|
|
15
15
|
# @param delay [Numeric, nil] initial delay (defaults to reconnect_interval)
|
|
16
16
|
#
|
|
17
17
|
def self.schedule(endpoint, options, parent_task, engine, delay: nil)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
new(engine, endpoint, options).run(parent_task, delay: delay)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# @param engine [Engine]
|
|
23
|
+
# @param endpoint [String]
|
|
24
|
+
# @param options [Options]
|
|
25
|
+
#
|
|
26
|
+
def initialize(engine, endpoint, options)
|
|
27
|
+
@engine = engine
|
|
28
|
+
@endpoint = endpoint
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
|
|
33
|
+
# Spawns a background task that retries the connection with exponential backoff.
|
|
34
|
+
#
|
|
35
|
+
# @param parent_task [Async::Task]
|
|
36
|
+
# @param delay [Numeric, nil] initial delay override
|
|
37
|
+
# @return [void]
|
|
38
|
+
#
|
|
39
|
+
def run(parent_task, delay: nil)
|
|
40
|
+
delay, max_delay = init_delay(delay)
|
|
41
|
+
|
|
42
|
+
@engine.tasks << parent_task.async(transient: true, annotation: "reconnect #{@endpoint}") do
|
|
22
43
|
loop do
|
|
23
|
-
break if engine.closed?
|
|
44
|
+
break if @engine.closed?
|
|
24
45
|
sleep delay if delay > 0
|
|
25
|
-
break if engine.closed?
|
|
46
|
+
break if @engine.closed?
|
|
26
47
|
begin
|
|
27
|
-
engine.transport_for(endpoint).connect(endpoint, engine)
|
|
48
|
+
@engine.transport_for(@endpoint).connect(@endpoint, @engine)
|
|
28
49
|
break
|
|
29
50
|
rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
|
|
30
|
-
delay = next_delay(delay, max_delay
|
|
31
|
-
engine.emit_monitor_event(:connect_retried, endpoint: endpoint, detail: { interval: delay })
|
|
51
|
+
delay = next_delay(delay, max_delay)
|
|
52
|
+
@engine.emit_monitor_event(:connect_retried, endpoint: @endpoint, detail: { interval: delay })
|
|
32
53
|
end
|
|
33
54
|
end
|
|
34
55
|
rescue Async::Stop
|
|
35
56
|
rescue => error
|
|
36
|
-
engine.signal_fatal_error(error)
|
|
57
|
+
@engine.signal_fatal_error(error)
|
|
37
58
|
end
|
|
38
59
|
end
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def init_delay(delay)
|
|
64
|
+
ri = @options.reconnect_interval
|
|
41
65
|
if ri.is_a?(Range)
|
|
42
66
|
[delay || ri.begin, ri.end]
|
|
43
67
|
else
|
|
@@ -45,7 +69,9 @@ module OMQ
|
|
|
45
69
|
end
|
|
46
70
|
end
|
|
47
71
|
|
|
48
|
-
|
|
72
|
+
|
|
73
|
+
def next_delay(delay, max_delay)
|
|
74
|
+
ri = @options.reconnect_interval
|
|
49
75
|
delay = delay * 2
|
|
50
76
|
delay = [delay, max_delay].min if max_delay
|
|
51
77
|
delay = (ri.is_a?(Range) ? ri.begin : ri) if delay == 0
|
data/lib/omq/engine/recv_pump.rb
CHANGED
|
@@ -2,73 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class Engine
|
|
5
|
-
#
|
|
5
|
+
# Recv pump for a connection.
|
|
6
6
|
#
|
|
7
7
|
# For inproc DirectPipe: wires the direct recv path (no fiber spawned).
|
|
8
8
|
# For TCP/IPC: spawns a transient task that reads messages from the
|
|
9
9
|
# connection and enqueues them into +recv_queue+.
|
|
10
10
|
#
|
|
11
|
-
# The two-
|
|
11
|
+
# The two-method structure (with/without transform) is intentional for
|
|
12
12
|
# YJIT: it gives the JIT a monomorphic call per routing strategy instead
|
|
13
13
|
# of a megamorphic `transform.call` dispatch inside a shared loop.
|
|
14
14
|
#
|
|
15
|
-
# @param parent_task [Async::Task]
|
|
16
|
-
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
17
|
-
# @param recv_queue [SignalingQueue]
|
|
18
|
-
# @param engine [Engine] for connection_lost / signal_fatal_error callbacks
|
|
19
|
-
# @param transform [Proc, nil]
|
|
20
|
-
# @return [Async::Task, nil]
|
|
21
|
-
#
|
|
22
15
|
class RecvPump
|
|
23
16
|
FAIRNESS_MESSAGES = 64
|
|
24
17
|
FAIRNESS_BYTES = 1 << 20 # 1 MB
|
|
25
18
|
|
|
19
|
+
|
|
20
|
+
# Public entry point — callers use the class method.
|
|
21
|
+
#
|
|
22
|
+
# @param parent_task [Async::Task]
|
|
23
|
+
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
24
|
+
# @param recv_queue [SignalingQueue]
|
|
25
|
+
# @param engine [Engine]
|
|
26
|
+
# @param transform [Proc, nil]
|
|
27
|
+
# @return [Async::Task, nil]
|
|
28
|
+
#
|
|
26
29
|
def self.start(parent_task, conn, recv_queue, engine, transform)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
new(conn, recv_queue, engine).start(parent_task, transform)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# @param conn [Connection, Transport::Inproc::DirectPipe]
|
|
35
|
+
# @param recv_queue [Routing::SignalingQueue]
|
|
36
|
+
# @param engine [Engine]
|
|
37
|
+
#
|
|
38
|
+
def initialize(conn, recv_queue, engine)
|
|
39
|
+
@conn = conn
|
|
40
|
+
@recv_queue = recv_queue
|
|
41
|
+
@engine = engine
|
|
42
|
+
@count_bytes = conn.instance_of?(Protocol::ZMTP::Connection)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Starts the recv pump. For inproc DirectPipe, wires the direct path
|
|
47
|
+
# (no task spawned). For TCP/IPC, spawns a fiber that reads messages.
|
|
48
|
+
#
|
|
49
|
+
# @param parent_task [Async::Task]
|
|
50
|
+
# @param transform [Proc, nil] optional per-message transform
|
|
51
|
+
# @return [Async::Task, nil]
|
|
52
|
+
#
|
|
53
|
+
def start(parent_task, transform)
|
|
54
|
+
if @conn.is_a?(Transport::Inproc::DirectPipe) && @conn.peer
|
|
55
|
+
@conn.peer.direct_recv_queue = @recv_queue
|
|
56
|
+
@conn.peer.direct_recv_transform = transform
|
|
30
57
|
return nil
|
|
31
58
|
end
|
|
32
59
|
|
|
33
60
|
if transform
|
|
34
|
-
parent_task
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
start_with_transform(parent_task, transform)
|
|
62
|
+
else
|
|
63
|
+
start_direct(parent_task)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def start_with_transform(parent_task, transform)
|
|
71
|
+
conn, recv_queue, count_bytes = @conn, @recv_queue, @count_bytes
|
|
72
|
+
|
|
73
|
+
parent_task.async(transient: true, annotation: "recv pump") do |task|
|
|
74
|
+
loop do
|
|
75
|
+
count = 0
|
|
76
|
+
bytes = 0
|
|
77
|
+
while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
|
|
78
|
+
msg = conn.receive_message
|
|
79
|
+
msg = transform.call(msg).freeze
|
|
80
|
+
recv_queue.enqueue(msg)
|
|
81
|
+
count += 1
|
|
82
|
+
bytes += msg.sum(&:bytesize) if count_bytes
|
|
46
83
|
end
|
|
47
|
-
|
|
48
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
49
|
-
engine.connection_lost(conn)
|
|
50
|
-
rescue => error
|
|
51
|
-
engine.signal_fatal_error(error)
|
|
84
|
+
task.yield
|
|
52
85
|
end
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
86
|
+
rescue Async::Stop
|
|
87
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
88
|
+
@engine.connection_lost(conn)
|
|
89
|
+
rescue => error
|
|
90
|
+
@engine.signal_fatal_error(error)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def start_direct(parent_task)
|
|
96
|
+
conn, recv_queue, count_bytes = @conn, @recv_queue, @count_bytes
|
|
97
|
+
|
|
98
|
+
parent_task.async(transient: true, annotation: "recv pump") do |task|
|
|
99
|
+
loop do
|
|
100
|
+
count = 0
|
|
101
|
+
bytes = 0
|
|
102
|
+
while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
|
|
103
|
+
msg = conn.receive_message
|
|
104
|
+
recv_queue.enqueue(msg)
|
|
105
|
+
count += 1
|
|
106
|
+
bytes += msg.sum(&:bytesize) if count_bytes
|
|
65
107
|
end
|
|
66
|
-
|
|
67
|
-
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
68
|
-
engine.connection_lost(conn)
|
|
69
|
-
rescue => error
|
|
70
|
-
engine.signal_fatal_error(error)
|
|
108
|
+
task.yield
|
|
71
109
|
end
|
|
110
|
+
rescue Async::Stop
|
|
111
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST
|
|
112
|
+
@engine.connection_lost(conn)
|
|
113
|
+
rescue => error
|
|
114
|
+
@engine.signal_fatal_error(error)
|
|
72
115
|
end
|
|
73
116
|
end
|
|
74
117
|
end
|
data/lib/omq/engine.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "engine/recv_pump"
|
|
|
5
5
|
require_relative "engine/heartbeat"
|
|
6
6
|
require_relative "engine/reconnect"
|
|
7
7
|
require_relative "engine/connection_setup"
|
|
8
|
+
require_relative "engine/maintenance"
|
|
8
9
|
|
|
9
10
|
module OMQ
|
|
10
11
|
# Per-socket orchestrator.
|
|
@@ -23,6 +24,7 @@ module OMQ
|
|
|
23
24
|
attr_reader :transports
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
|
|
26
28
|
# Per-connection metadata: the endpoint it was established on and an
|
|
27
29
|
# optional Promise resolved when the connection is lost (used by
|
|
28
30
|
# {#spawn_connection} to await connection teardown).
|
|
@@ -79,10 +81,23 @@ module OMQ
|
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
|
|
84
|
+
# @return [Async::Promise] resolves when first peer completes handshake
|
|
85
|
+
# @return [Async::Promise] resolves when all peers disconnect (after having had peers)
|
|
86
|
+
# @return [Hash{Connection => ConnectionRecord}] active connections
|
|
87
|
+
# @return [Async::Task, nil] root task for spawning subtrees
|
|
88
|
+
# @return [Array<Async::Task>] background tasks (pumps, heartbeat, reconnect)
|
|
89
|
+
#
|
|
82
90
|
attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task, :tasks
|
|
83
91
|
|
|
92
|
+
# @!attribute [w] reconnect_enabled
|
|
93
|
+
# @param value [Boolean] enable or disable auto-reconnect
|
|
94
|
+
# @!attribute [w] monitor_queue
|
|
95
|
+
# @param value [Async::Queue, nil] queue for monitor events
|
|
96
|
+
#
|
|
84
97
|
attr_writer :reconnect_enabled, :monitor_queue
|
|
85
98
|
|
|
99
|
+
# @return [Boolean] true if the engine has been closed
|
|
100
|
+
#
|
|
86
101
|
def closed? = @state == :closed
|
|
87
102
|
|
|
88
103
|
# Optional proc that wraps new connections (e.g. for serialization).
|
|
@@ -371,15 +386,30 @@ module OMQ
|
|
|
371
386
|
@on_io_thread = true
|
|
372
387
|
Reactor.track_linger(@options.linger)
|
|
373
388
|
end
|
|
389
|
+
Maintenance.start(@parent_task, @options.mechanism, @tasks)
|
|
374
390
|
end
|
|
375
391
|
|
|
376
392
|
|
|
393
|
+
# Emits a lifecycle event to the monitor queue, if one is attached.
|
|
394
|
+
#
|
|
395
|
+
# @param type [Symbol] event type (e.g. :listening, :connected, :disconnected)
|
|
396
|
+
# @param endpoint [String, nil] the endpoint involved
|
|
397
|
+
# @param detail [Hash, nil] extra context
|
|
398
|
+
# @return [void]
|
|
399
|
+
#
|
|
377
400
|
def emit_monitor_event(type, endpoint: nil, detail: nil)
|
|
378
401
|
return unless @monitor_queue
|
|
379
402
|
@monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
|
|
380
403
|
rescue Async::Stop, ClosedQueueError
|
|
381
404
|
end
|
|
382
405
|
|
|
406
|
+
|
|
407
|
+
# Looks up the transport module for an endpoint URI.
|
|
408
|
+
#
|
|
409
|
+
# @param endpoint [String] endpoint URI (e.g. "tcp://...", "inproc://...")
|
|
410
|
+
# @return [Module] the transport module
|
|
411
|
+
# @raise [ArgumentError] if the scheme is not registered
|
|
412
|
+
#
|
|
383
413
|
def transport_for(endpoint)
|
|
384
414
|
scheme = endpoint[/\A([^:]+):\/\//, 1]
|
|
385
415
|
self.class.transports[scheme] or
|
|
@@ -401,6 +431,7 @@ module OMQ
|
|
|
401
431
|
@tasks << task if task
|
|
402
432
|
end
|
|
403
433
|
|
|
434
|
+
|
|
404
435
|
def drain_send_queues(timeout)
|
|
405
436
|
return unless @routing.respond_to?(:send_queues_drained?)
|
|
406
437
|
deadline = timeout ? Async::Clock.now + timeout : nil
|
|
@@ -410,21 +441,25 @@ module OMQ
|
|
|
410
441
|
end
|
|
411
442
|
end
|
|
412
443
|
|
|
444
|
+
|
|
413
445
|
def maybe_reconnect(endpoint)
|
|
414
446
|
return unless endpoint && @dialed.include?(endpoint)
|
|
415
447
|
return unless @state == :open && @reconnect_enabled
|
|
416
448
|
Reconnect.schedule(endpoint, @options, @parent_task, self)
|
|
417
449
|
end
|
|
418
450
|
|
|
451
|
+
|
|
419
452
|
def schedule_reconnect(endpoint, delay: nil)
|
|
420
453
|
Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
|
|
421
454
|
end
|
|
422
455
|
|
|
456
|
+
|
|
423
457
|
def validate_endpoint!(endpoint)
|
|
424
458
|
transport = transport_for(endpoint)
|
|
425
459
|
transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
|
|
426
460
|
end
|
|
427
461
|
|
|
462
|
+
|
|
428
463
|
def start_accept_loops(listener)
|
|
429
464
|
return unless listener.respond_to?(:start_accept_loops)
|
|
430
465
|
listener.start_accept_loops(@parent_task) do |io|
|
|
@@ -432,16 +467,19 @@ module OMQ
|
|
|
432
467
|
end
|
|
433
468
|
end
|
|
434
469
|
|
|
470
|
+
|
|
435
471
|
def stop_listeners
|
|
436
472
|
@listeners.each(&:stop)
|
|
437
473
|
@listeners.clear
|
|
438
474
|
end
|
|
439
475
|
|
|
476
|
+
|
|
440
477
|
def close_connections
|
|
441
478
|
@connections.each_key(&:close)
|
|
442
479
|
@connections.clear
|
|
443
480
|
end
|
|
444
481
|
|
|
482
|
+
|
|
445
483
|
def close_connections_at(endpoint)
|
|
446
484
|
conns = @connections.filter_map { |conn, e| conn if e.endpoint == endpoint }
|
|
447
485
|
conns.each do |conn|
|
|
@@ -451,24 +489,28 @@ module OMQ
|
|
|
451
489
|
end
|
|
452
490
|
end
|
|
453
491
|
|
|
492
|
+
|
|
454
493
|
def stop_tasks
|
|
455
494
|
@routing.stop rescue nil
|
|
456
495
|
@tasks.each { |t| t.stop rescue nil }
|
|
457
496
|
@tasks.clear
|
|
458
497
|
end
|
|
459
498
|
|
|
499
|
+
|
|
460
500
|
def freeze_error_lists!
|
|
461
501
|
return if OMQ::CONNECTION_LOST.frozen?
|
|
462
502
|
OMQ::CONNECTION_LOST.freeze
|
|
463
503
|
OMQ::CONNECTION_FAILED.freeze
|
|
464
504
|
end
|
|
465
505
|
|
|
506
|
+
|
|
466
507
|
def close_monitor_queue
|
|
467
508
|
return unless @monitor_queue
|
|
468
509
|
@monitor_queue.push(nil)
|
|
469
510
|
end
|
|
470
511
|
end
|
|
471
512
|
|
|
513
|
+
|
|
472
514
|
# Register built-in transports.
|
|
473
515
|
Engine.transports["tcp"] = Transport::TCP
|
|
474
516
|
Engine.transports["ipc"] = Transport::IPC
|