omq 0.11.0 → 0.13.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +3 -1
  4. data/lib/omq/drop_queue.rb +54 -0
  5. data/lib/omq/engine/connection_setup.rb +47 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/reconnect.rb +56 -0
  8. data/lib/omq/engine/recv_pump.rb +76 -0
  9. data/lib/omq/engine.rb +145 -371
  10. data/lib/omq/monitor_event.rb +16 -0
  11. data/lib/omq/options.rb +5 -3
  12. data/lib/omq/pub_sub.rb +9 -8
  13. data/lib/omq/routing/conn_send_pump.rb +36 -0
  14. data/lib/omq/routing/dealer.rb +8 -10
  15. data/lib/omq/routing/fair_queue.rb +144 -0
  16. data/lib/omq/routing/fair_recv.rb +27 -0
  17. data/lib/omq/routing/fan_out.rb +116 -63
  18. data/lib/omq/routing/pair.rb +39 -20
  19. data/lib/omq/routing/pub.rb +5 -7
  20. data/lib/omq/routing/pull.rb +5 -4
  21. data/lib/omq/routing/push.rb +3 -10
  22. data/lib/omq/routing/rep.rb +31 -51
  23. data/lib/omq/routing/req.rb +15 -12
  24. data/lib/omq/routing/round_robin.rb +82 -72
  25. data/lib/omq/routing/router.rb +23 -48
  26. data/lib/omq/routing/sub.rb +8 -6
  27. data/lib/omq/routing/xpub.rb +8 -4
  28. data/lib/omq/routing/xsub.rb +43 -27
  29. data/lib/omq/routing.rb +44 -11
  30. data/lib/omq/socket.rb +46 -5
  31. data/lib/omq/transport/inproc/direct_pipe.rb +162 -0
  32. data/lib/omq/transport/inproc.rb +37 -200
  33. data/lib/omq/transport/ipc.rb +16 -4
  34. data/lib/omq/transport/tcp.rb +31 -8
  35. data/lib/omq/version.rb +1 -1
  36. data/lib/omq.rb +5 -19
  37. metadata +11 -16
  38. data/lib/omq/channel.rb +0 -14
  39. data/lib/omq/client_server.rb +0 -37
  40. data/lib/omq/peer.rb +0 -26
  41. data/lib/omq/radio_dish.rb +0 -74
  42. data/lib/omq/routing/channel.rb +0 -83
  43. data/lib/omq/routing/client.rb +0 -56
  44. data/lib/omq/routing/dish.rb +0 -78
  45. data/lib/omq/routing/gather.rb +0 -46
  46. data/lib/omq/routing/peer.rb +0 -101
  47. data/lib/omq/routing/radio.rb +0 -150
  48. data/lib/omq/routing/scatter.rb +0 -82
  49. data/lib/omq/routing/server.rb +0 -101
  50. data/lib/omq/scatter_gather.rb +0 -23
  51. data/lib/omq/single_frame.rb +0 -18
  52. data/lib/omq/transport/tls.rb +0 -146
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b736b6b715f5c4f330d21be278450600b2642239b8bea9e85957f516e89211ed
4
- data.tar.gz: 34c057a32055e7333e4299e85c0fee9e798b43ea34d021f237ba6778338b65f1
3
+ metadata.gz: 91e6db2b4fd881530030f63c0c47a534c3f6361497d0f55ad1d28c7dbb85a669
4
+ data.tar.gz: 1af1d5692333586b650f7d0f14118ae04f64a65ab3d127d8ea3a3781e0eaae34
5
5
  SHA512:
6
- metadata.gz: e95a6df6d4eb56ac1fc0586f61cfdb77d0afe9de620d3e540ffbae1256c2fe6526556485215eda3de20c8f34a332f51a68e5bb075087665049c0219bb07f6960
7
- data.tar.gz: 8b5c8d38b7523e9c6ab30e17c21b0cf4adb2086e1bdde90298f62e42f4b8f2336cdd95c47293c3fb24fa41f376aed8a2ec6d1121fc56d41aa83d4afdc289d707
6
+ metadata.gz: 4a724ddc29600b8d101046ca82869e4598f733564be4022fc8eb287084a80319a1586d5e0ced449f3eea3a26127add8f962fc3914177cc95f4f495e98f87e7d4
7
+ data.tar.gz: '08935cd0ad4548af6a526984d84514b242ab1d29bfd14fb8933f7b84b9f906f1197588f6bc0fec95332d2dbac6e016308cbdb5bf5efe9e2cba7b2bbf7d695d58'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,148 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Changed
6
+
7
+ - **`Engine` internals: `ConnectionRecord` + lifecycle state** — three parallel
8
+ per-connection ivars (`@connections` Array, `@connection_endpoints`,
9
+ `@connection_promises`) replaced by a single `@connections` Hash keyed by
10
+ connection, with values `ConnectionRecord = Data.define(:endpoint, :done)`.
11
+ `@connected_endpoints` renamed to `@dialed` (`Set`). `@closed`/`@closing`
12
+ booleans replaced by a `@state` symbol (`:open`/`:closing`/`:closed`).
13
+ Net: −4 instance variables.
14
+ - **`@connections` in `FanOut`, `Sub`, `XSub` routing strategies changed from
15
+ `Array` to `Set`** — O(1) `#delete` on peer disconnect; semantics already
16
+ required uniqueness.
17
+
18
+ ### Fixed
19
+
20
+ - **FanOut send queues no longer drop messages** — per-connection send queues in
21
+ `FanOut` (PUB/XPUB/RADIO) used `DropQueue` (`Thread::SizedQueue`) which never
22
+ blocked the publisher fiber. When burst-sending beyond `send_hwm`, the sender
23
+ ran without yielding and messages were silently dropped. Switched to
24
+ `Async::LimitedQueue` (`:block`) so the publisher yields when a per-connection
25
+ queue is full, giving the send pump fiber a chance to drain it.
26
+
27
+ ### Changed
28
+
29
+ - **Benchmark suite redesign** — replaced ASCII plots (unicode_plot) with JSONL
30
+ result storage and a colored terminal regression report. Results are appended
31
+ to `bench/results.jsonl` (gitignored, machine-local). New commands:
32
+ `ruby bench/run_all.rb` (run all patterns), `ruby bench/report.rb` (compare
33
+ last runs, highlight regressions/improvements).
34
+
35
+ ### Added
36
+
37
+ - **Per-peer HWM** — send and receive high-water marks now apply per connected
38
+ peer (RFC 28/29/30). Each peer gets its own bounded send queue and its own
39
+ bounded recv queue. A slow or muted peer no longer steals capacity from
40
+ other peers. `FairQueue` + `SignalingQueue` aggregate per-connection recv
41
+ queues with fair round-robin delivery; `RoundRobin` and `FanOut` mixins
42
+ maintain per-connection send queues with dedicated send pump fibers.
43
+ `PUSH`/`DEALER`/`PAIR` buffer messages in a staging queue when no peers are
44
+ connected yet, draining into the first peer's queue on connect.
45
+ - **`FairQueue`** — new aggregator class (`lib/omq/routing/fair_queue.rb`)
46
+ that fair-queues across per-connection bounded queues. Pending messages from
47
+ a disconnected peer are drained before the queue is discarded.
48
+ - **`Socket.bind` / `Socket.connect` class-method fix** — now pass the
49
+ endpoint via `@`/`>` prefix into the constructor so any post-attach
50
+ initialization in subclasses (e.g. XSUB's `subscribe:` kwarg) runs after
51
+ the connection is established.
52
+
53
+
54
+
55
+ - **QoS infrastructure** — `Options#qos` attribute (default 0) and inproc
56
+ command queue support for QoS-enabled connections. The
57
+ [omq-qos](https://github.com/paddor/omq-qos) gem activates delivery
58
+ guarantees via prepends.
59
+ - **REQ send/recv ordering** — REQ sockets now enforce strict
60
+ send/recv/send/recv alternation. Calling `#send` twice without a
61
+ `#receive` in between raises `SocketError`.
62
+ - **DirectPipe command frame support** — `DirectPipe#receive_message`
63
+ accepts a block for command frames, matching the `Protocol::ZMTP::Connection`
64
+ interface. Enables inproc transports to handle ACK/NACK and other
65
+ command-level protocols.
66
+
67
+ ### Fixed
68
+
69
+ - **`send_pump_idle?` visibility** — moved above `private` in `RoundRobin`
70
+ and `FanOut` so `Engine#drain_send_queues` can call it during socket close.
71
+
72
+ - **`Socket#monitor`** — observe connection lifecycle events via a
73
+ block-based API. Returns an `Async::Task` that yields `MonitorEvent`
74
+ (Data.define) instances for `:listening`, `:accepted`, `:connected`,
75
+ `:connect_delayed`, `:connect_retried`, `:handshake_succeeded`,
76
+ `:handshake_failed`, `:accept_failed`, `:bind_failed`, `:disconnected`,
77
+ `:closed`, and `:monitor_stopped`. Event types align with libzmq's
78
+ `zmq_socket_monitor` where applicable. Pattern-matchable, zero overhead
79
+ when no monitor is attached.
80
+ - **Pluggable transport registry** — `Engine.transports` is a scheme →
81
+ module hash. Built-in transports (`tcp`, `ipc`, `inproc`) are registered
82
+ at load time. External gems register via
83
+ `OMQ::Engine.transports["scheme"] = MyTransport`. Each transport
84
+ implements `.bind(endpoint, engine)` → Listener, `.connect(endpoint,
85
+ engine)`, and optionally `.validate_endpoint!(endpoint)`. Listeners
86
+ implement `#start_accept_loops(parent_task, &on_accepted)`, `#stop`,
87
+ `#endpoint`, and optionally `#port`.
88
+ - **Mutable error lists** — `CONNECTION_LOST` and `CONNECTION_FAILED` are
89
+ no longer frozen at load time. Transport plugins can append error classes
90
+ (e.g. `OpenSSL::SSL::SSLError`) before the first `#bind`/`#connect`,
91
+ which freezes both arrays.
92
+
93
+ - **`on_mute` option** — controls behavior when a socket enters the mute state
94
+ (HWM full). PUB, XPUB, and RADIO default to `on_mute: :drop_newest` — slow
95
+ subscribers are skipped in the fan-out rather than blocking the publisher.
96
+ SUB, XSUB, and DISH accept `on_mute: :drop_newest` or `:drop_oldest` to
97
+ drop messages on the receive side instead of applying backpressure. All other
98
+ socket types default to `:block` (existing behavior).
99
+ - **`DropQueue`** — bounded queue with `:drop_newest` (tail drop) and
100
+ `:drop_oldest` (head drop) strategies. Used by recv queues when `on_mute`
101
+ is a drop strategy.
102
+ - **`Routing.build_queue`** — factory method for building send/recv queues
103
+ based on HWM and mute strategy. Supports HWM of `0` or `nil` for unbounded
104
+ queues.
105
+
106
+ ### Changed
107
+
108
+ - **`max_message_size` defaults to 1 MiB** — frames exceeding this limit cause
109
+ the connection to be dropped before the body is read from the wire, preventing
110
+ a malicious peer from causing arbitrary memory allocation. Set `socket.max_message_size = nil`
111
+ to restore the previous unlimited behavior.
112
+ - **Accept loops moved into Listeners** — `TCP::Listener` and
113
+ `IPC::Listener` now own their accept loop logic via
114
+ `#start_accept_loops(parent_task, &on_accepted)`. Engine delegates
115
+ via duck-type check. This enables external transports to define
116
+ custom accept behavior without modifying Engine.
117
+ - `Engine#transport_for` uses registry lookup instead of `case/when`.
118
+ - `Engine#validate_endpoint!` delegates to transport module.
119
+ - `Engine#bind` reads `listener.port` instead of parsing the endpoint
120
+ string.
121
+
122
+ ### Removed
123
+
124
+ - **Draft socket types extracted** — `RADIO`, `DISH`, `CLIENT`, `SERVER`,
125
+ `SCATTER`, `GATHER`, `CHANNEL`, and `PEER` are no longer bundled with `omq`.
126
+ Use the [omq-draft](https://github.com/paddor/omq-draft) gem and require
127
+ the relevant entry point (`omq/draft/radiodish`, `omq/draft/clientserver`,
128
+ etc.).
129
+ - **UDP transport extracted** — `udp://` endpoints are provided by
130
+ `omq-draft` (via `require "omq/draft/radiodish"`). No longer registered by
131
+ default.
132
+ - **`Routing.for` plugin registry** — draft socket type removal added
133
+ `Routing.register(socket_type, strategy_class)` for external gems to
134
+ register routing strategies. Unknown types fall through the built-in
135
+ `case` to this registry before raising `ArgumentError`.
136
+
137
+ - **TLS transport** — extracted to the
138
+ [omq-transport-tls](https://github.com/paddor/omq-transport-tls) gem.
139
+ (Experimental) `require "omq/transport/tls"` to restore `tls+tcp://` support.
140
+ - `tls_context` / `tls_context=` removed from `Options` and `Socket`
141
+ (provided by omq-transport-tls).
142
+ - `OpenSSL::SSL::SSLError` removed from `CONNECTION_LOST` (added back
143
+ by omq-transport-tls).
144
+ - TLS benchmark transport removed from `bench_helper.rb` and `plot.rb`.
145
+
3
146
  ## 0.11.0
4
147
 
5
148
  ### Added
data/README.md CHANGED
@@ -147,7 +147,7 @@ end
147
147
 
148
148
  ## Socket Types
149
149
 
150
- All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes live under `OMQ::` (alias: `ØMQ`).
150
+ All sockets are thread-safe. Default HWM is 1000 messages per socket. Default `max_message_size` is **1 MiB** — frames larger than this cause the connection to be dropped before the body is read from the wire. Set `socket.max_message_size = nil` to disable the limit or raise it as needed. Classes live under `OMQ::` (alias: `ØMQ`).
151
151
 
152
152
  #### Standard (multipart messages)
153
153
 
@@ -162,6 +162,8 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes li
162
162
 
163
163
  #### Draft (single-frame only)
164
164
 
165
+ These require the `omq-draft` gem.
166
+
165
167
  | Pattern | Send | Receive | When HWM full |
166
168
  |---------|------|---------|---------------|
167
169
  | **CLIENT** / **SERVER** | Round-robin / routing-ID | Fair-queue | Block |
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ # A bounded queue that drops messages when full instead of blocking.
5
+ #
6
+ # Two drop strategies:
7
+ # :drop_newest — discard the incoming message (tail drop)
8
+ # :drop_oldest — discard the head, then enqueue the new message
9
+ #
10
+ # Used by SUB/XSUB/DISH recv queues when on_mute is a drop strategy.
11
+ #
12
+ class DropQueue
13
+ # @param limit [Integer] maximum number of items
14
+ # @param strategy [Symbol] :drop_newest or :drop_oldest
15
+ #
16
+ def initialize(limit, strategy: :drop_newest)
17
+ @queue = Thread::SizedQueue.new(limit)
18
+ @strategy = strategy
19
+ end
20
+
21
+ # Enqueues an item. Drops according to the configured strategy if full.
22
+ #
23
+ # @param item [Object]
24
+ # @return [void]
25
+ #
26
+ def enqueue(item)
27
+ @queue.push(item, true)
28
+ rescue ThreadError
29
+ return if @strategy == :drop_newest
30
+
31
+ # :drop_oldest — discard head, enqueue new
32
+ @queue.pop(true) rescue nil
33
+ retry
34
+ end
35
+
36
+ # Removes and returns the next item, blocking if empty.
37
+ #
38
+ # @return [Object]
39
+ #
40
+ def dequeue(timeout: nil)
41
+ if timeout
42
+ @queue.pop(timeout: timeout)
43
+ else
44
+ @queue.pop
45
+ end
46
+ end
47
+
48
+ # @return [Boolean]
49
+ #
50
+ def empty?
51
+ @queue.empty?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,47 @@
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
+ conn = build_connection(io, engine, as_server)
17
+ 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, engine, endpoint, done)
21
+ engine.emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
22
+ conn
23
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
24
+ engine.emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
25
+ conn&.close
26
+ raise
27
+ end
28
+
29
+ def self.build_connection(io, engine, as_server)
30
+ Protocol::ZMTP::Connection.new(
31
+ io,
32
+ socket_type: engine.socket_type.to_s,
33
+ identity: engine.options.identity,
34
+ as_server: as_server,
35
+ mechanism: engine.options.mechanism&.dup,
36
+ max_message_size: engine.options.max_message_size,
37
+ )
38
+ end
39
+
40
+ def self.register(conn, engine, endpoint, done)
41
+ engine.connections[conn] = Engine::ConnectionRecord.new(endpoint: endpoint, done: done)
42
+ engine.routing.connection_added(conn)
43
+ engine.peer_connected.resolve(conn)
44
+ end
45
+ end
46
+ end
47
+ 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
+ class 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,56 @@
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] for transport_for / emit_monitor_event / signal_fatal_error / closed?
15
+ # @param delay [Numeric, nil] initial delay (defaults to reconnect_interval)
16
+ #
17
+ def self.schedule(endpoint, options, parent_task, engine, delay: nil)
18
+ ri = options.reconnect_interval
19
+ delay, max_delay = init_delay(ri, delay)
20
+
21
+ engine.tasks << parent_task.async(transient: true, annotation: "reconnect #{endpoint}") do
22
+ loop do
23
+ break if engine.closed?
24
+ sleep delay if delay > 0
25
+ break if engine.closed?
26
+ begin
27
+ engine.transport_for(endpoint).connect(endpoint, engine)
28
+ break
29
+ rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
30
+ delay = next_delay(delay, max_delay, ri)
31
+ engine.emit_monitor_event(:connect_retried, endpoint: endpoint, detail: { interval: delay })
32
+ end
33
+ end
34
+ rescue Async::Stop
35
+ rescue => error
36
+ engine.signal_fatal_error(error)
37
+ end
38
+ end
39
+
40
+ def self.init_delay(ri, delay)
41
+ if ri.is_a?(Range)
42
+ [delay || ri.begin, ri.end]
43
+ else
44
+ [delay || ri, nil]
45
+ end
46
+ end
47
+
48
+ def self.next_delay(delay, max_delay, ri)
49
+ delay = delay * 2
50
+ delay = [delay, max_delay].min if max_delay
51
+ delay = (ri.is_a?(Range) ? ri.begin : ri) if delay == 0
52
+ delay
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ class Engine
5
+ # Starts a 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-branch 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
+ # @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
+ class RecvPump
23
+ FAIRNESS_MESSAGES = 64
24
+ FAIRNESS_BYTES = 1 << 20 # 1 MB
25
+
26
+ def self.start(parent_task, conn, recv_queue, engine, transform)
27
+ if conn.is_a?(Transport::Inproc::DirectPipe) && conn.peer
28
+ conn.peer.direct_recv_queue = recv_queue
29
+ conn.peer.direct_recv_transform = transform
30
+ return nil
31
+ end
32
+
33
+ if transform
34
+ parent_task.async(transient: true, annotation: "recv pump") do |task|
35
+ loop do
36
+ count = 0
37
+ bytes = 0
38
+ while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
39
+ msg = conn.receive_message
40
+ msg = transform.call(msg).freeze
41
+ recv_queue.enqueue(msg)
42
+ count += 1
43
+ bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
44
+ end
45
+ task.yield
46
+ end
47
+ rescue Async::Stop
48
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
49
+ engine.connection_lost(conn)
50
+ rescue => error
51
+ engine.signal_fatal_error(error)
52
+ end
53
+ else
54
+ parent_task.async(transient: true, annotation: "recv pump") do |task|
55
+ loop do
56
+ count = 0
57
+ bytes = 0
58
+ while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
59
+ msg = conn.receive_message
60
+ recv_queue.enqueue(msg)
61
+ count += 1
62
+ bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
63
+ end
64
+ task.yield
65
+ end
66
+ rescue Async::Stop
67
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
68
+ engine.connection_lost(conn)
69
+ rescue => error
70
+ engine.signal_fatal_error(error)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end