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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/README.md +27 -0
  4. data/lib/omq/drop_queue.rb +3 -0
  5. data/lib/omq/engine/connection_setup.rb +70 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/maintenance.rb +35 -0
  8. data/lib/omq/engine/reconnect.rb +82 -0
  9. data/lib/omq/engine/recv_pump.rb +119 -0
  10. data/lib/omq/engine.rb +139 -304
  11. data/lib/omq/options.rb +44 -0
  12. data/lib/omq/pair.rb +6 -0
  13. data/lib/omq/pub_sub.rb +25 -0
  14. data/lib/omq/push_pull.rb +17 -0
  15. data/lib/omq/queue_interface.rb +1 -0
  16. data/lib/omq/readable.rb +2 -0
  17. data/lib/omq/req_rep.rb +13 -0
  18. data/lib/omq/router_dealer.rb +12 -0
  19. data/lib/omq/routing/conn_send_pump.rb +36 -0
  20. data/lib/omq/routing/dealer.rb +15 -10
  21. data/lib/omq/routing/fair_queue.rb +172 -0
  22. data/lib/omq/routing/fair_recv.rb +27 -0
  23. data/lib/omq/routing/fan_out.rb +127 -74
  24. data/lib/omq/routing/pair.rb +47 -20
  25. data/lib/omq/routing/pub.rb +12 -6
  26. data/lib/omq/routing/pull.rb +12 -4
  27. data/lib/omq/routing/push.rb +3 -12
  28. data/lib/omq/routing/rep.rb +41 -51
  29. data/lib/omq/routing/req.rb +15 -10
  30. data/lib/omq/routing/round_robin.rb +82 -63
  31. data/lib/omq/routing/router.rb +32 -48
  32. data/lib/omq/routing/sub.rb +18 -5
  33. data/lib/omq/routing/xpub.rb +15 -3
  34. data/lib/omq/routing/xsub.rb +53 -27
  35. data/lib/omq/routing.rb +29 -11
  36. data/lib/omq/socket.rb +25 -7
  37. data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
  38. data/lib/omq/transport/inproc.rb +41 -217
  39. data/lib/omq/transport/ipc.rb +7 -1
  40. data/lib/omq/transport/tcp.rb +12 -7
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +2 -0
  43. data/lib/omq.rb +4 -1
  44. metadata +14 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5ea3ce51d24a9181cfe7a4c1f2680ed57c5dbba078010a54de5b1c13d27d484
4
- data.tar.gz: 97f7e7fcb27a250e71af4732ff06298e7311af052a9d9a6b3868d73059373174
3
+ metadata.gz: 997f3d83d6ddc56d44341f69743f4f30b9e122f046718246de78da75969ec6fa
4
+ data.tar.gz: 65f4f071d952c477630bfae4c25f136abfa79350099b58b1be4ca79ec500f2e3
5
5
  SHA512:
6
- metadata.gz: 9f60b7d45d1b3783b2bde482a6f826ec3b89bfff6a9532836dbd2a8a22622709e7c298cc7c2abae6e596641973636b8972efa92de7258bd1e4164ff62d782237
7
- data.tar.gz: 7024b97bb46c9005b73bc7959cc4138bd5498a514af26253441613aa05206d357d1fbd002afff5820c9a3baff0c4351d66bcf1aa738955783272a2c2b459a697
6
+ metadata.gz: d92503d56e3106987bda522497f42630d7999bca8ba1ddd5190129d0dc567c5dac1714a8f8566601ac1fa309d0e142f87936342667b36588f81e39f785a2b502
7
+ data.tar.gz: bed4e86b9404a7ecc175c50ba85a2b7ad8daaea1490c8f39a449c8f92c197b039326d73708279838c1678a29235c7a5ff30c1bf36903306d452dccd743059b76
data/CHANGELOG.md CHANGED
@@ -1,9 +1,92 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
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)
@@ -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