omq 0.12.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 +84 -1
- data/README.md +27 -0
- data/lib/omq/drop_queue.rb +3 -0
- data/lib/omq/engine/connection_setup.rb +70 -0
- data/lib/omq/engine/heartbeat.rb +40 -0
- data/lib/omq/engine/maintenance.rb +35 -0
- data/lib/omq/engine/reconnect.rb +82 -0
- data/lib/omq/engine/recv_pump.rb +119 -0
- data/lib/omq/engine.rb +139 -304
- 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 +36 -0
- data/lib/omq/routing/dealer.rb +15 -10
- data/lib/omq/routing/fair_queue.rb +172 -0
- data/lib/omq/routing/fair_recv.rb +27 -0
- data/lib/omq/routing/fan_out.rb +127 -74
- data/lib/omq/routing/pair.rb +47 -20
- data/lib/omq/routing/pub.rb +12 -6
- data/lib/omq/routing/pull.rb +12 -4
- data/lib/omq/routing/push.rb +3 -12
- data/lib/omq/routing/rep.rb +41 -51
- data/lib/omq/routing/req.rb +15 -10
- data/lib/omq/routing/round_robin.rb +82 -63
- data/lib/omq/routing/router.rb +32 -48
- data/lib/omq/routing/sub.rb +18 -5
- data/lib/omq/routing/xpub.rb +15 -3
- data/lib/omq/routing/xsub.rb +53 -27
- data/lib/omq/routing.rb +29 -11
- data/lib/omq/socket.rb +25 -7
- data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
- data/lib/omq/transport/inproc.rb +41 -217
- 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 +14 -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,9 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
+
|
|
38
|
+
## 0.13.0
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **`Engine` internals: `ConnectionRecord` + lifecycle state** — three parallel
|
|
43
|
+
per-connection ivars (`@connections` Array, `@connection_endpoints`,
|
|
44
|
+
`@connection_promises`) replaced by a single `@connections` Hash keyed by
|
|
45
|
+
connection, with values `ConnectionRecord = Data.define(:endpoint, :done)`.
|
|
46
|
+
`@connected_endpoints` renamed to `@dialed` (`Set`). `@closed`/`@closing`
|
|
47
|
+
booleans replaced by a `@state` symbol (`:open`/`:closing`/`:closed`).
|
|
48
|
+
Net: −4 instance variables.
|
|
49
|
+
- **`@connections` in `FanOut`, `Sub`, `XSub` routing strategies changed from
|
|
50
|
+
`Array` to `Set`** — O(1) `#delete` on peer disconnect; semantics already
|
|
51
|
+
required uniqueness.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **FanOut send queues no longer drop messages** — per-connection send queues in
|
|
56
|
+
`FanOut` (PUB/XPUB/RADIO) used `DropQueue` (`Thread::SizedQueue`) which never
|
|
57
|
+
blocked the publisher fiber. When burst-sending beyond `send_hwm`, the sender
|
|
58
|
+
ran without yielding and messages were silently dropped. Switched to
|
|
59
|
+
`Async::LimitedQueue` (`:block`) so the publisher yields when a per-connection
|
|
60
|
+
queue is full, giving the send pump fiber a chance to drain it.
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- **Benchmark suite redesign** — replaced ASCII plots (unicode_plot) with JSONL
|
|
65
|
+
result storage and a colored terminal regression report. Results are appended
|
|
66
|
+
to `bench/results.jsonl` (gitignored, machine-local). New commands:
|
|
67
|
+
`ruby bench/run_all.rb` (run all patterns), `ruby bench/report.rb` (compare
|
|
68
|
+
last runs, highlight regressions/improvements).
|
|
4
69
|
|
|
5
70
|
### Added
|
|
6
71
|
|
|
72
|
+
- **Per-peer HWM** — send and receive high-water marks now apply per connected
|
|
73
|
+
peer (RFC 28/29/30). Each peer gets its own bounded send queue and its own
|
|
74
|
+
bounded recv queue. A slow or muted peer no longer steals capacity from
|
|
75
|
+
other peers. `FairQueue` + `SignalingQueue` aggregate per-connection recv
|
|
76
|
+
queues with fair round-robin delivery; `RoundRobin` and `FanOut` mixins
|
|
77
|
+
maintain per-connection send queues with dedicated send pump fibers.
|
|
78
|
+
`PUSH`/`DEALER`/`PAIR` buffer messages in a staging queue when no peers are
|
|
79
|
+
connected yet, draining into the first peer's queue on connect.
|
|
80
|
+
- **`FairQueue`** — new aggregator class (`lib/omq/routing/fair_queue.rb`)
|
|
81
|
+
that fair-queues across per-connection bounded queues. Pending messages from
|
|
82
|
+
a disconnected peer are drained before the queue is discarded.
|
|
83
|
+
- **`Socket.bind` / `Socket.connect` class-method fix** — now pass the
|
|
84
|
+
endpoint via `@`/`>` prefix into the constructor so any post-attach
|
|
85
|
+
initialization in subclasses (e.g. XSUB's `subscribe:` kwarg) runs after
|
|
86
|
+
the connection is established.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
7
90
|
- **QoS infrastructure** — `Options#qos` attribute (default 0) and inproc
|
|
8
91
|
command queue support for QoS-enabled connections. The
|
|
9
92
|
[omq-qos](https://github.com/paddor/omq-qos) gem activates delivery
|
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?
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class Engine
|
|
5
|
+
# Performs ZMTP handshake and registers a new connection.
|
|
6
|
+
#
|
|
7
|
+
class ConnectionSetup
|
|
8
|
+
# @param io [#read, #write, #close] underlying transport stream
|
|
9
|
+
# @param engine [Engine]
|
|
10
|
+
# @param as_server [Boolean]
|
|
11
|
+
# @param endpoint [String, nil]
|
|
12
|
+
# @param done [Async::Promise, nil] resolved when connection is lost
|
|
13
|
+
# @return [Connection]
|
|
14
|
+
#
|
|
15
|
+
def self.run(io, engine, as_server:, endpoint: nil, done: nil)
|
|
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)
|
|
37
|
+
conn.handshake!
|
|
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)
|
|
42
|
+
conn
|
|
43
|
+
rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
|
|
44
|
+
@engine.emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
|
|
45
|
+
conn&.close
|
|
46
|
+
raise
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_connection(io, as_server)
|
|
52
|
+
Protocol::ZMTP::Connection.new(
|
|
53
|
+
io,
|
|
54
|
+
socket_type: @engine.socket_type.to_s,
|
|
55
|
+
identity: @engine.options.identity,
|
|
56
|
+
as_server: as_server,
|
|
57
|
+
mechanism: @engine.options.mechanism&.dup,
|
|
58
|
+
max_message_size: @engine.options.max_message_size,
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
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)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class Engine
|
|
5
|
+
# Spawns a heartbeat task for a connection.
|
|
6
|
+
#
|
|
7
|
+
# Sends PING frames at +interval+ seconds and closes the connection
|
|
8
|
+
# if no traffic is seen within +timeout+ seconds.
|
|
9
|
+
#
|
|
10
|
+
module Heartbeat
|
|
11
|
+
# @param parent_task [Async::Task]
|
|
12
|
+
# @param conn [Connection]
|
|
13
|
+
# @param options [Options]
|
|
14
|
+
# @param tasks [Array]
|
|
15
|
+
#
|
|
16
|
+
def self.start(parent_task, conn, options, tasks)
|
|
17
|
+
interval = options.heartbeat_interval
|
|
18
|
+
return unless interval
|
|
19
|
+
|
|
20
|
+
ttl = options.heartbeat_ttl || interval
|
|
21
|
+
timeout = options.heartbeat_timeout || interval
|
|
22
|
+
conn.touch_heartbeat
|
|
23
|
+
|
|
24
|
+
tasks << parent_task.async(transient: true, annotation: "heartbeat") do
|
|
25
|
+
loop do
|
|
26
|
+
sleep interval
|
|
27
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl, context: "".b))
|
|
28
|
+
if conn.heartbeat_expired?(timeout)
|
|
29
|
+
conn.close
|
|
30
|
+
break
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
rescue Async::Stop
|
|
34
|
+
rescue *CONNECTION_LOST
|
|
35
|
+
# connection closed
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class Engine
|
|
5
|
+
# Schedules reconnect attempts with exponential back-off.
|
|
6
|
+
#
|
|
7
|
+
# Runs a background task that loops until a connection is established
|
|
8
|
+
# or the engine is closed.
|
|
9
|
+
#
|
|
10
|
+
class Reconnect
|
|
11
|
+
# @param endpoint [String]
|
|
12
|
+
# @param options [Options]
|
|
13
|
+
# @param parent_task [Async::Task]
|
|
14
|
+
# @param engine [Engine]
|
|
15
|
+
# @param delay [Numeric, nil] initial delay (defaults to reconnect_interval)
|
|
16
|
+
#
|
|
17
|
+
def self.schedule(endpoint, options, parent_task, engine, delay: nil)
|
|
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
|
|
31
|
+
|
|
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
|
|
43
|
+
loop do
|
|
44
|
+
break if @engine.closed?
|
|
45
|
+
sleep delay if delay > 0
|
|
46
|
+
break if @engine.closed?
|
|
47
|
+
begin
|
|
48
|
+
@engine.transport_for(@endpoint).connect(@endpoint, @engine)
|
|
49
|
+
break
|
|
50
|
+
rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
|
|
51
|
+
delay = next_delay(delay, max_delay)
|
|
52
|
+
@engine.emit_monitor_event(:connect_retried, endpoint: @endpoint, detail: { interval: delay })
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
rescue Async::Stop
|
|
56
|
+
rescue => error
|
|
57
|
+
@engine.signal_fatal_error(error)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def init_delay(delay)
|
|
64
|
+
ri = @options.reconnect_interval
|
|
65
|
+
if ri.is_a?(Range)
|
|
66
|
+
[delay || ri.begin, ri.end]
|
|
67
|
+
else
|
|
68
|
+
[delay || ri, nil]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def next_delay(delay, max_delay)
|
|
74
|
+
ri = @options.reconnect_interval
|
|
75
|
+
delay = delay * 2
|
|
76
|
+
delay = [delay, max_delay].min if max_delay
|
|
77
|
+
delay = (ri.is_a?(Range) ? ri.begin : ri) if delay == 0
|
|
78
|
+
delay
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class Engine
|
|
5
|
+
# Recv pump for a connection.
|
|
6
|
+
#
|
|
7
|
+
# For inproc DirectPipe: wires the direct recv path (no fiber spawned).
|
|
8
|
+
# For TCP/IPC: spawns a transient task that reads messages from the
|
|
9
|
+
# connection and enqueues them into +recv_queue+.
|
|
10
|
+
#
|
|
11
|
+
# The two-method structure (with/without transform) is intentional for
|
|
12
|
+
# YJIT: it gives the JIT a monomorphic call per routing strategy instead
|
|
13
|
+
# of a megamorphic `transform.call` dispatch inside a shared loop.
|
|
14
|
+
#
|
|
15
|
+
class RecvPump
|
|
16
|
+
FAIRNESS_MESSAGES = 64
|
|
17
|
+
FAIRNESS_BYTES = 1 << 20 # 1 MB
|
|
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
|
+
#
|
|
29
|
+
def self.start(parent_task, conn, recv_queue, engine, transform)
|
|
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
|
|
57
|
+
return nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if transform
|
|
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
|
|
83
|
+
end
|
|
84
|
+
task.yield
|
|
85
|
+
end
|
|
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
|
|
107
|
+
end
|
|
108
|
+
task.yield
|
|
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)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|