omq 0.23.0 → 0.26.2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +144 -3
  3. data/README.md +23 -7
  4. data/lib/omq/client_server.rb +1 -1
  5. data/lib/omq/engine/connection_lifecycle.rb +12 -6
  6. data/lib/omq/engine/heartbeat.rb +1 -1
  7. data/lib/omq/engine/recv_pump.rb +29 -16
  8. data/lib/omq/engine.rb +6 -5
  9. data/lib/omq/ffi/engine.rb +646 -0
  10. data/lib/omq/ffi/libzmq.rb +134 -0
  11. data/lib/omq/ffi.rb +12 -0
  12. data/lib/omq/peer.rb +1 -1
  13. data/lib/omq/radio_dish.rb +1 -1
  14. data/lib/omq/readable.rb +5 -1
  15. data/lib/omq/routing/channel.rb +4 -4
  16. data/lib/omq/routing/client.rb +2 -2
  17. data/lib/omq/routing/conn_send_pump.rb +1 -1
  18. data/lib/omq/routing/dealer.rb +2 -2
  19. data/lib/omq/routing/dish.rb +2 -2
  20. data/lib/omq/routing/fan_out.rb +7 -7
  21. data/lib/omq/routing/gather.rb +2 -2
  22. data/lib/omq/routing/pair.rb +4 -4
  23. data/lib/omq/routing/peer.rb +2 -2
  24. data/lib/omq/routing/pub.rb +2 -2
  25. data/lib/omq/routing/pull.rb +2 -2
  26. data/lib/omq/routing/push.rb +3 -3
  27. data/lib/omq/routing/radio.rb +2 -2
  28. data/lib/omq/routing/rep.rb +2 -2
  29. data/lib/omq/routing/req.rb +2 -2
  30. data/lib/omq/routing/round_robin.rb +4 -4
  31. data/lib/omq/routing/router.rb +2 -2
  32. data/lib/omq/routing/scatter.rb +4 -5
  33. data/lib/omq/routing/server.rb +2 -2
  34. data/lib/omq/routing/sub.rb +2 -2
  35. data/lib/omq/routing/xpub.rb +2 -2
  36. data/lib/omq/routing/xsub.rb +2 -2
  37. data/lib/omq/socket.rb +2 -1
  38. data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +22 -9
  39. data/lib/omq/transport/inproc.rb +26 -14
  40. data/lib/omq/transport/udp.rb +1 -1
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +32 -43
  43. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a0e72756aae2179b8c790d5b65c54a82b516a17d11f760187a6fefecf6bf2c1
4
- data.tar.gz: d6ce860b558977939b28c3d1f23d7bfc0740cf602fb63a519107a59cdca3c8c3
3
+ metadata.gz: c2c1793056ec0f8ed585034cf19a939a7bb22278d865dc9f570a2117e1bd45d4
4
+ data.tar.gz: b8502a9780715a9e565b52b987ad1a698c07ff76c08c1a6e38f01b833b724f71
5
5
  SHA512:
6
- metadata.gz: fa8a7c98147aa4f0fea5fad3004e8a964105a2005ed645d03b0fd1f0fba67bfc4952a3f4d14926fbaf4293efdcada93235b2ba59956737ef0ced5c1b97d35ea2
7
- data.tar.gz: b5878650300c7135e04ec8fffcadef6d5101f77c8f2a7e3e6c1407faebe57714b613398c3fecf71b561cb8bdb6c25a70646d812c395db46f78827e97f796c8e7
6
+ metadata.gz: f3cbc43a10f74f0c1a872071871b9d5daddae41f2304823f8b57388a5c0c99cfb52e9a4118d9504c462d609af759c86a290d3235867fa5d22d46b9ffda63203c
7
+ data.tar.gz: 44ca650e5c321a56fd4649cbebe0a4bd60efc3127547a5da0010582ed0fbb42fe4bcbf705ddfbf44596266a1d37869d49d4bef03df08551b703091d733b4e10f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,145 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.2 — 2026-04-20
4
+
5
+ ### Fixed
6
+
7
+ - **Ruby 3.3 compatibility.** Replaced bare `it` block references
8
+ with explicit block parameters in `engine/recv_pump.rb` and
9
+ `writable.rb`. Ruby 3.3 warned that `it` would change meaning in
10
+ 3.4; the explicit params work on both.
11
+
12
+ ## 0.26.1 — 2026-04-20
13
+
14
+ ### Fixed
15
+
16
+ - **Inproc Pipe: tolerate non-String parts.** The BINARY-encoding
17
+ upgrade introduced in 0.25 called `.encoding` on every frame,
18
+ crashing when plugins (e.g. omq-ractor's `ShareableConnection`)
19
+ carried arbitrary Ruby objects through inproc. Non-String parts
20
+ now pass through untouched; String parts still get the
21
+ frozen-string-literal → BINARY upgrade.
22
+
23
+ ## 0.26.0 — 2026-04-20
24
+
25
+ ### Added
26
+
27
+ - **FFI backend absorbed in-tree.** The libzmq-backed `OMQ::FFI::Engine`
28
+ (previously shipped as the separate `omq-ffi` gem) now lives in
29
+ `lib/omq/ffi/`. Load with `require "omq/ffi"` and select per socket
30
+ via `OMQ::PUSH.new(backend: :ffi)`. `lib/omq/socket.rb` also
31
+ lazy-requires `omq/ffi` on first `:ffi` use, so the explicit require
32
+ is optional.
33
+ - **Auto-running FFI interop tests.** `test/omq/ffi_test.rb` (FFI
34
+ backend) and `test/omq/interop_test.rb` (FFI ↔ pure Ruby wire
35
+ compatibility) now run as part of `rake test` whenever the `ffi` gem
36
+ and system libzmq are both available. They self-skip otherwise —
37
+ detection runs once in `test/test_helper.rb` as `OMQ_FFI_AVAILABLE`.
38
+
39
+ ### Notes
40
+
41
+ - `ffi` remains optional and is NOT a runtime dependency of `omq`.
42
+ Install it explicitly (`gem install ffi` + system libzmq 4.x) to use
43
+ the `:ffi` backend. The omq-ffi gem is superseded; existing pins to
44
+ `omq-ffi ~> 0.3` keep working via its own dependency on `omq ~> 0.23`.
45
+
46
+ ## 0.25.0 — 2026-04-20
47
+
48
+ ### Added
49
+
50
+ - **Recv-pump transforms can drop messages.** A `transform` block passed
51
+ to `Engine#start_recv_pump` may now return `nil` to discard the
52
+ message instead of enqueueing it to the application's recv queue.
53
+ The pump still counts the dropped message toward its per-connection
54
+ fairness caps (64 msgs / 1 MiB), so a duplicate flood can't starve
55
+ siblings. omq-qos 0.3.0 uses this at QoS >= 2 for dedup-set hits:
56
+ the transform ACKs the sender and returns `nil`.
57
+
58
+ ### Changed
59
+
60
+ - **Uniform frozen + BINARY contract on both sides of the wire —
61
+ restoring pre-0.24 behavior.** 0.24 dropped freezing from the
62
+ send/receive paths to chase throughput numbers, which left inproc
63
+ with an unsafe shared-reference contract (sender and receiver share
64
+ the same array and strings) and made the contract differ by
65
+ transport. Safety is back, minus the `.b` copy that was the
66
+ actually-expensive part of the old path. Invariants:
67
+
68
+ - `Writable#send` freezes every part (and the parts array, if one
69
+ was passed). Unfrozen non-BINARY parts are re-tagged to
70
+ `Encoding::BINARY` in place — a flag flip, no allocation.
71
+ - Receivers always get frozen `BINARY`-tagged parts. TCP/IPC get
72
+ this via byteslice on the wire + recv-pump freeze. Inproc gets
73
+ it via `Pipe#send_message`, which only allocates (one `.b` copy
74
+ per part) in the pathological case of a frozen non-BINARY part
75
+ — the typical `# frozen_string_literal: true` UTF-8 literal.
76
+
77
+ Mutation bugs surface as `FrozenError` instead of silently
78
+ corrupting a shared reference on inproc. Cost on inproc is ~20-30%
79
+ throughput; TCP/IPC unaffected.
80
+
81
+ - **String-like part coercion via `#to_str`.** Non-String parts are
82
+ coerced via `#to_str` (not `#to_s`) — an object must be explicitly
83
+ string-like to serialize. Passing `42`, `:foo`, or `nil` raises
84
+ `NoMethodError` instead of silently accepting a `#to_s`
85
+ representation or producing a zero-byte frame from a `nil`. Use
86
+ `""` to send an empty frame.
87
+
88
+ - **Inproc `needs_commands?` accepts nilable `options.qos`.** Core
89
+ `Options#qos` is still an Integer (default `0`), but omq-qos 0.3
90
+ stores either `nil` (QoS 0) or an `OMQ::QoS` instance (levels 1–3)
91
+ in that slot. The inproc transport's command-queue decision now
92
+ treats both Integer `0` and `nil` as disabled; any non-zero
93
+ Integer or non-nil object forces the command-queue path.
94
+
95
+
96
+ ## 0.24.0 — 2026-04-18
97
+
98
+ ### Changed
99
+
100
+ - **Caller owns message parts.** `Writable#send` no longer deep-freezes or
101
+ binary-coerces the caller's input. The contract is now libzmq-style:
102
+ don't mutate parts after sending. `#receive` likewise returns mutable
103
+ arrays of mutable strings. This removes a full-payload allocation per
104
+ message (`.b.freeze`) on the send path and a per-frame freeze on the
105
+ receive path.
106
+
107
+ - **No more implicit `#to_s` / nil coercion.** Passing a non-string part
108
+ (e.g. Integer, Symbol, nil) will raise `NoMethodError` at the wire layer
109
+ instead of being silently converted. The `EMPTY_PART` constant is gone.
110
+
111
+ - **Reactor fast path for `#send` / `#receive`.** When the socket was
112
+ bound/connected from an Async fiber, hot-path I/O skips `Reactor.run`
113
+ entirely and calls the engine directly (with an `Async::Task#with_timeout`
114
+ wrapper only when a timeout is configured). The shared IO thread is used
115
+ only when the socket was created from a non-Async thread.
116
+
117
+ ### Performance
118
+
119
+ Combined effect of caller-owns-data + Reactor fast path on inproc:
120
+
121
+ - PUSH/PULL inproc 1-peer: **+105% to +128%** msg/s across payload sizes
122
+ - PUSH/PULL inproc 3-peer: **+63% to +111%** msg/s
123
+ - PUSH/PULL ipc: +5% to +17%
124
+ - TCP numbers unchanged (OS/syscall-dominated)
125
+
126
+ ### Removed
127
+
128
+ - `Writable#freeze_message` and `#frozen_binary` private helpers.
129
+ - `Writable::EMPTY_PART` constant.
130
+
131
+
132
+ ## 0.23.1 — 2026-04-18
133
+
134
+ ### Fixed
135
+
136
+ - **SCATTER double-tracked each peer.** `Routing::Scatter#connection_added`
137
+ appended to `@connections` and then called `add_round_robin_send_connection`,
138
+ which appends again — so every connected peer had two entries in the list.
139
+ `#connection_removed` deleted only one on disconnect, leaving a stale entry
140
+ behind. Fixed by dropping the duplicate append.
141
+
142
+
3
143
  ## 0.23.0 — 2026-04-17
4
144
 
5
145
  ### Added
@@ -997,9 +1137,10 @@
997
1137
  ### Added
998
1138
 
999
1139
  - **`backend:` kwarg** — all socket types accept `backend: :ffi` to use
1000
- the libzmq FFI backend (via the [omq-ffi](https://github.com/paddor/omq-ffi)
1001
- gem). Default is `:ruby` (pure Ruby ZMTP). Enables interop testing and
1002
- access to libzmq-specific features without changing the socket API.
1140
+ the libzmq FFI backend (then shipped separately as the `omq-ffi` gem;
1141
+ absorbed in-tree in 0.26.0). Default is `:ruby` (pure Ruby ZMTP).
1142
+ Enables interop testing and access to libzmq-specific features without
1143
+ changing the socket API.
1003
1144
  - **TLS transport (`tls+tcp://`)** — TLS v1.3 on top of TCP using Ruby's
1004
1145
  stdlib `openssl`. Set `socket.tls_context` to an `OpenSSL::SSL::SSLContext`
1005
1146
  before bind/connect. Per-socket (not per-endpoint), frozen on first use.
data/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
6
6
  [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D?logo=ruby&logoColor=white)](https://www.ruby-lang.org)
7
7
 
8
- > **1.23M msg/s** inproc | **361k msg/s** ipc | **358k msg/s** tcp
8
+ > **1.64M msg/s** inproc | **294k msg/s** ipc | **308k msg/s** tcp
9
9
  >
10
- > **9.1 µs** inproc latency | **49 µs** ipc | **64 µs** tcp
10
+ > **8.7 µs** inproc latency | **51 µs** ipc | **64 µs** tcp
11
11
  >
12
12
  > Ruby 4.0 + YJIT on a Linux VM — see [`bench/`](bench/) for full results
13
13
 
@@ -162,6 +162,12 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket.
162
162
  frames cause the connection to be dropped before the body is read from the
163
163
  wire. Classes live under `OMQ::` (alias: `ØMQ`).
164
164
 
165
+ **Received messages are frozen** across all transports (inproc, ipc, tcp).
166
+ The array returned by `#receive` and every part inside it is frozen —
167
+ mutating a received part raises `FrozenError` rather than silently
168
+ corrupting a shared reference on the inproc fast path. `dup` a part if
169
+ you need to mutate it.
170
+
165
171
  #### Standard (multipart messages)
166
172
 
167
173
  | Pattern | Send | Receive | When HWM full |
@@ -207,12 +213,23 @@ echo "hello" | omq req -c tcp://localhost:5555
207
213
 
208
214
  See the [omq-cli README](https://github.com/paddor/omq-cli) for full documentation.
209
215
 
216
+ ## Optional libzmq backend
217
+
218
+ OMQ ships with an optional libzmq FFI backend. Same socket API, but
219
+ backed by libzmq instead of the pure Ruby ZMTP stack. Useful when you
220
+ need libzmq-specific features or for verifying wire compatibility.
221
+
222
+ ```ruby
223
+ require "omq/ffi"
224
+ push = OMQ::PUSH.new(backend: :ffi)
225
+ ```
226
+
227
+ Requires the `ffi` gem and a system libzmq 4.x. `ffi` is not a runtime
228
+ dependency of `omq` — install it explicitly (`gem install ffi`) if you
229
+ want the `:ffi` backend.
230
+
210
231
  ## Companion Gems
211
232
 
212
- - **[omq-ffi](https://github.com/paddor/omq-ffi)** — libzmq FFI backend. Same
213
- OMQ socket API, but backed by libzmq instead of the pure Ruby ZMTP stack.
214
- Useful for interop testing and when you need libzmq-specific features.
215
- Requires libzmq installed.
216
233
  - **[omq-ractor](https://github.com/paddor/omq-ractor)** — bridge OMQ sockets
217
234
  into Ruby Ractors for true parallel processing across cores. I/O stays on the
218
235
  main Ractor, worker Ractors do pure computation.
@@ -244,7 +261,6 @@ the stack.
244
261
  git clone https://github.com/paddor/omq.git
245
262
  git clone https://github.com/paddor/protocol-zmtp.git
246
263
  git clone https://github.com/paddor/omq-zstd.git
247
- git clone https://github.com/paddor/omq-ffi.git
248
264
  git clone https://github.com/paddor/omq-ractor.git
249
265
  git clone https://github.com/paddor/nuckle.git
250
266
 
@@ -60,7 +60,7 @@ module OMQ
60
60
  # @return [self]
61
61
  #
62
62
  def send_to(routing_id, message)
63
- parts = [routing_id.b.freeze, message.b.freeze]
63
+ parts = [routing_id, message]
64
64
  Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
65
65
  self
66
66
  end
@@ -5,7 +5,7 @@ module OMQ
5
5
  # Owns the full arc of *one* connection: handshake → ready → closed.
6
6
  #
7
7
  # Scope boundary: ConnectionLifecycle tracks a single peer link
8
- # (one ZMTP connection or one inproc DirectPipe). SocketLifecycle
8
+ # (one ZMTP connection or one inproc Pipe). SocketLifecycle
9
9
  # owns the socket-wide state above it — first-peer/last-peer
10
10
  # signaling, reconnect enable flag, the parent task tree, and the
11
11
  # open → closing → closed transitions that gate close-time drain.
@@ -43,7 +43,7 @@ module OMQ
43
43
  }.freeze
44
44
 
45
45
 
46
- # @return [Protocol::ZMTP::Connection, Transport::Inproc::DirectPipe, nil]
46
+ # @return [Protocol::ZMTP::Connection, Transport::Inproc::Pipe, nil]
47
47
  attr_reader :conn
48
48
 
49
49
 
@@ -120,9 +120,9 @@ module OMQ
120
120
 
121
121
 
122
122
  # Registers an already-connected inproc pipe as :ready.
123
- # No handshake — inproc DirectPipe bypasses ZMTP entirely.
123
+ # No handshake — inproc Pipe bypasses ZMTP entirely.
124
124
  #
125
- # @param pipe [Transport::Inproc::DirectPipe]
125
+ # @param pipe [Transport::Inproc::Pipe]
126
126
  #
127
127
  def ready_direct!(pipe)
128
128
  ready!(pipe)
@@ -161,7 +161,9 @@ module OMQ
161
161
 
162
162
 
163
163
  def ready!(conn)
164
- conn = @engine.connection_wrapper.call(conn) if @engine.connection_wrapper
164
+ if @engine.connection_wrapper
165
+ conn = @engine.connection_wrapper.call(conn)
166
+ end
165
167
 
166
168
  if @endpoint
167
169
  transport_obj = @engine.transport_object_for(@endpoint)
@@ -177,7 +179,7 @@ module OMQ
177
179
  @engine.peer_connected.resolve(@conn)
178
180
  transition!(:ready)
179
181
 
180
- # No supervisor if nothing to supervise: inproc DirectPipes
182
+ # No supervisor if nothing to supervise: inproc Pipes
181
183
  # wire the recv/send paths synchronously (no task-based pumps),
182
184
  # and isolated unit tests use a FakeEngine without pumps at all.
183
185
  # Waiting on an empty barrier returns immediately and would
@@ -213,6 +215,7 @@ module OMQ
213
215
 
214
216
  def tear_down!(reconnect:, reason: nil)
215
217
  return if @state == :closed
218
+
216
219
  transition!(:closed)
217
220
  @engine.connections.delete(@conn)
218
221
  @engine.routing.connection_removed(@conn) if @conn
@@ -244,11 +247,14 @@ module OMQ
244
247
 
245
248
  def transition!(new_state)
246
249
  allowed = TRANSITIONS[@state]
250
+
247
251
  unless allowed&.include?(new_state)
248
252
  raise InvalidTransition, "#{@state} → #{new_state}"
249
253
  end
254
+
250
255
  @state = new_state
251
256
  end
257
+
252
258
  end
253
259
  end
254
260
  end
@@ -9,7 +9,7 @@ module OMQ
9
9
  #
10
10
  module Heartbeat
11
11
  # @param parent [Async::Task, Async::Barrier] parent to spawn under
12
- # @param conn [Connection]
12
+ # @param conn [Protocol::ZMTP::Connection]
13
13
  # @param options [Options]
14
14
  #
15
15
  def self.start(parent, conn, options)
@@ -4,7 +4,7 @@ module OMQ
4
4
  class Engine
5
5
  # Recv pump for a connection.
6
6
  #
7
- # For inproc DirectPipe: wires the direct recv path (no fiber spawned).
7
+ # For inproc Pipe: 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
  #
@@ -29,7 +29,7 @@ module OMQ
29
29
  # Public entry point — callers use the class method.
30
30
  #
31
31
  # @param parent [Async::Task, Async::Barrier] parent to spawn under
32
- # @param conn [Connection, Transport::Inproc::DirectPipe]
32
+ # @param conn [Protocol::ZMTP::Connection, Transport::Inproc::Pipe]
33
33
  # @param recv_queue [Async::LimitedQueue]
34
34
  # @param engine [Engine]
35
35
  # @param transform [Proc, nil]
@@ -40,7 +40,7 @@ module OMQ
40
40
  end
41
41
 
42
42
 
43
- # @param conn [Connection, Transport::Inproc::DirectPipe]
43
+ # @param conn [Protocol::ZMTP::Connection, Transport::Inproc::Pipe]
44
44
  # @param recv_queue [Async::LimitedQueue]
45
45
  # @param engine [Engine]
46
46
  #
@@ -52,7 +52,7 @@ module OMQ
52
52
  end
53
53
 
54
54
 
55
- # Starts the recv pump. For inproc DirectPipe, wires the direct path
55
+ # Starts the recv pump. For inproc Pipe, wires the direct path
56
56
  # (no task spawned). For TCP/IPC, spawns a fiber that reads messages.
57
57
  #
58
58
  # @param parent_task [Async::Task]
@@ -60,7 +60,7 @@ module OMQ
60
60
  # @return [Async::Task, nil]
61
61
  #
62
62
  def start(parent_task, transform)
63
- if @conn.is_a?(Transport::Inproc::DirectPipe) && @conn.peer
63
+ if @conn.is_a?(Transport::Inproc::Pipe) && @conn.peer
64
64
  @conn.peer.wire_direct_recv(@recv_queue, transform)
65
65
  return nil
66
66
  end
@@ -72,6 +72,7 @@ module OMQ
72
72
  end
73
73
  end
74
74
 
75
+
75
76
  private
76
77
 
77
78
 
@@ -79,6 +80,12 @@ module OMQ
79
80
  # cross-Ractor transport). Kept separate from {#start_direct} so
80
81
  # YJIT sees a monomorphic transform.call site.
81
82
  #
83
+ # A transform that returns +nil+ drops the message — the recv pump
84
+ # still counts it toward fairness (so dup-floods can't starve
85
+ # siblings) but it is neither emitted nor enqueued to the
86
+ # application. omq-qos uses this at QoS >= 2 for dedup-set hits:
87
+ # the transform ACKs the sender and returns nil.
88
+ #
82
89
  # @param parent [Async::Task, Async::Barrier]
83
90
  # @param transform [Proc]
84
91
  # @return [Async::Task]
@@ -93,18 +100,11 @@ module OMQ
93
100
 
94
101
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
95
102
  msg = conn.receive_message
96
- msg = transform.call(msg).freeze
97
-
98
- # Emit the verbose trace BEFORE enqueueing so the monitor
99
- # fiber is woken before the application fiber -- the
100
- # async scheduler is FIFO on the ready list, so this
101
- # preserves log-before-stdout ordering for -vvv traces.
102
- engine.emit_verbose_msg_received(conn, msg)
103
- recv_queue.enqueue(msg)
103
+ msg.each { |part| part.freeze }
104
+ msg.freeze
104
105
 
105
- count += 1
106
-
107
- # hot path
106
+ # hot path bytes — count before transform so dropped
107
+ # messages still contribute to the fairness cap.
108
108
  if count_bytes
109
109
  if msg.size == 1
110
110
  bytes += msg.first.bytesize
@@ -116,6 +116,17 @@ module OMQ
116
116
  end
117
117
  end
118
118
  end
119
+
120
+ count += 1
121
+ transformed = transform.call(msg)
122
+ next unless transformed
123
+
124
+ # Emit the verbose trace BEFORE enqueueing so the monitor
125
+ # fiber is woken before the application fiber -- the
126
+ # async scheduler is FIFO on the ready list, so this
127
+ # preserves log-before-stdout ordering for -vvv traces.
128
+ engine.emit_verbose_msg_received(conn, transformed)
129
+ recv_queue.enqueue(transformed)
119
130
  end
120
131
 
121
132
  task.yield
@@ -146,6 +157,8 @@ module OMQ
146
157
 
147
158
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
148
159
  msg = conn.receive_message
160
+ msg.each { |part| part.freeze }
161
+ msg.freeze
149
162
  engine.emit_verbose_msg_received(conn, msg)
150
163
  recv_queue.enqueue(msg)
151
164
 
data/lib/omq/engine.rb CHANGED
@@ -100,6 +100,7 @@ module OMQ
100
100
  def parent_task = @lifecycle.parent_task
101
101
  def barrier = @lifecycle.barrier
102
102
  def closed? = @lifecycle.closed?
103
+ def on_io_thread? = @lifecycle.on_io_thread
103
104
 
104
105
 
105
106
  # Enables or disables auto-reconnect for dropped connections.
@@ -280,10 +281,10 @@ module OMQ
280
281
  end
281
282
 
282
283
 
283
- # Called by inproc transport with a pre-validated DirectPipe.
284
+ # Called by inproc transport with a pre-validated Pipe.
284
285
  # Skips ZMTP handshake — just registers with routing strategy.
285
286
  #
286
- # @param pipe [Transport::Inproc::DirectPipe]
287
+ # @param pipe [Transport::Inproc::Pipe]
287
288
  # @return [void]
288
289
  #
289
290
  def connection_ready(pipe, endpoint: nil)
@@ -328,7 +329,7 @@ module OMQ
328
329
 
329
330
  # Starts a recv pump for a connection, or wires the inproc fast path.
330
331
  #
331
- # @param conn [Connection, Transport::Inproc::DirectPipe]
332
+ # @param conn [Protocol::ZMTP::Connection, Transport::Inproc::Pipe]
332
333
  # @param recv_queue [Async::LimitedQueue]
333
334
  # @yield [msg] optional per-message transform
334
335
  # @return [Async::Task, nil]
@@ -345,7 +346,7 @@ module OMQ
345
346
 
346
347
  # Called when a connection is lost.
347
348
  #
348
- # @param connection [Connection]
349
+ # @param connection [Protocol::ZMTP::Connection]
349
350
  # @return [void]
350
351
  #
351
352
  def connection_lost(connection)
@@ -460,7 +461,7 @@ module OMQ
460
461
  # pumps blocked on `dequeue` waiting for messages that will never
461
462
  # be written.
462
463
  #
463
- # @param conn [Connection, Transport::Inproc::DirectPipe]
464
+ # @param conn [Protocol::ZMTP::Connection, Transport::Inproc::Pipe]
464
465
  # @param annotation [String]
465
466
  #
466
467
  def spawn_conn_pump_task(conn, annotation:, &block)