omq 0.23.0 → 0.24.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a0e72756aae2179b8c790d5b65c54a82b516a17d11f760187a6fefecf6bf2c1
4
- data.tar.gz: d6ce860b558977939b28c3d1f23d7bfc0740cf602fb63a519107a59cdca3c8c3
3
+ metadata.gz: ed87cc3a3243100b7977fd58f102b87918de8568cf8739642afc6377bb3f76e5
4
+ data.tar.gz: f01b30844ae48ffe26ec3934fef780521937a7611e1d00c87b333136fa5641ac
5
5
  SHA512:
6
- metadata.gz: fa8a7c98147aa4f0fea5fad3004e8a964105a2005ed645d03b0fd1f0fba67bfc4952a3f4d14926fbaf4293efdcada93235b2ba59956737ef0ced5c1b97d35ea2
7
- data.tar.gz: b5878650300c7135e04ec8fffcadef6d5101f77c8f2a7e3e6c1407faebe57714b613398c3fecf71b561cb8bdb6c25a70646d812c395db46f78827e97f796c8e7
6
+ metadata.gz: 853d3171298de868ad3de28fe284c7970ddf196203bbb217fc91fa62cebb9d39c1d278b53d4259d0dc505b09457624ea68f4a9a8a0e654850c9266139a1fa05e
7
+ data.tar.gz: ad8a2cec518ceebac32055aeab7fd159d982421e9983670ec28182094f4fe2338f89cd7a1609d02cd8720f01c121159fec7e2489b8c816f9f16adf9d950c34c4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.24.0 — 2026-04-18
4
+
5
+ ### Changed
6
+
7
+ - **Caller owns message parts.** `Writable#send` no longer deep-freezes or
8
+ binary-coerces the caller's input. The contract is now libzmq-style:
9
+ don't mutate parts after sending. `#receive` likewise returns mutable
10
+ arrays of mutable strings. This removes a full-payload allocation per
11
+ message (`.b.freeze`) on the send path and a per-frame freeze on the
12
+ receive path.
13
+
14
+ - **No more implicit `#to_s` / nil coercion.** Passing a non-string part
15
+ (e.g. Integer, Symbol, nil) will raise `NoMethodError` at the wire layer
16
+ instead of being silently converted. The `EMPTY_PART` constant is gone.
17
+
18
+ - **Reactor fast path for `#send` / `#receive`.** When the socket was
19
+ bound/connected from an Async fiber, hot-path I/O skips `Reactor.run`
20
+ entirely and calls the engine directly (with an `Async::Task#with_timeout`
21
+ wrapper only when a timeout is configured). The shared IO thread is used
22
+ only when the socket was created from a non-Async thread.
23
+
24
+ ### Performance
25
+
26
+ Combined effect of caller-owns-data + Reactor fast path on inproc:
27
+
28
+ - PUSH/PULL inproc 1-peer: **+105% to +128%** msg/s across payload sizes
29
+ - PUSH/PULL inproc 3-peer: **+63% to +111%** msg/s
30
+ - PUSH/PULL ipc: +5% to +17%
31
+ - TCP numbers unchanged (OS/syscall-dominated)
32
+
33
+ ### Removed
34
+
35
+ - `Writable#freeze_message` and `#frozen_binary` private helpers.
36
+ - `Writable::EMPTY_PART` constant.
37
+
38
+
39
+ ## 0.23.1 — 2026-04-18
40
+
41
+ ### Fixed
42
+
43
+ - **SCATTER double-tracked each peer.** `Routing::Scatter#connection_added`
44
+ appended to `@connections` and then called `add_round_robin_send_connection`,
45
+ which appends again — so every connected peer had two entries in the list.
46
+ `#connection_removed` deleted only one on disconnect, leaving a stale entry
47
+ behind. Fixed by dropping the duplicate append.
48
+
49
+
3
50
  ## 0.23.0 — 2026-04-17
4
51
 
5
52
  ### Added
@@ -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
 
@@ -93,7 +94,7 @@ module OMQ
93
94
 
94
95
  while count < FAIRNESS_MESSAGES && bytes < FAIRNESS_BYTES
95
96
  msg = conn.receive_message
96
- msg = transform.call(msg).freeze
97
+ msg = transform.call(msg)
97
98
 
98
99
  # Emit the verbose trace BEFORE enqueueing so the monitor
99
100
  # fiber is woken before the application fiber -- the
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)
data/lib/omq/peer.rb CHANGED
@@ -38,7 +38,7 @@ module OMQ
38
38
  # @return [self]
39
39
  #
40
40
  def send_to(routing_id, message)
41
- parts = [routing_id.b.freeze, message.b.freeze]
41
+ parts = [routing_id, message]
42
42
  Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
43
43
  self
44
44
  end
@@ -42,7 +42,7 @@ module OMQ
42
42
  # @return [self]
43
43
  #
44
44
  def publish(group, body)
45
- parts = [group.b.freeze, body.b.freeze]
45
+ parts = [group, body]
46
46
  Reactor.run timeout: @options.write_timeout do
47
47
  @engine.enqueue_send(parts)
48
48
  end
data/lib/omq/readable.rb CHANGED
@@ -14,7 +14,11 @@ module OMQ
14
14
  # @raise [IO::TimeoutError] if read_timeout exceeded
15
15
  #
16
16
  def receive
17
- Reactor.run timeout: @options.read_timeout do |task|
17
+ if @engine.on_io_thread?
18
+ Reactor.run(timeout: @options.read_timeout) { @engine.dequeue_recv }
19
+ elsif (timeout = @options.read_timeout)
20
+ Async::Task.current.with_timeout(timeout, IO::TimeoutError) { @engine.dequeue_recv }
21
+ else
18
22
  @engine.dequeue_recv
19
23
  end
20
24
  end
@@ -39,7 +39,7 @@ module OMQ
39
39
  end
40
40
 
41
41
 
42
- # @param connection [Connection]
42
+ # @param connection [Protocol::ZMTP::Connection]
43
43
  # @raise [RuntimeError] if a connection already exists
44
44
  #
45
45
  def connection_added(connection)
@@ -48,7 +48,7 @@ module OMQ
48
48
 
49
49
  @engine.start_recv_pump(connection, @recv_queue)
50
50
 
51
- unless connection.is_a?(Transport::Inproc::DirectPipe)
51
+ unless connection.is_a?(Transport::Inproc::Pipe)
52
52
  @send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
53
53
  while (msg = @staging_queue.dequeue(timeout: 0))
54
54
  @send_queue.enqueue(msg)
@@ -58,7 +58,7 @@ module OMQ
58
58
  end
59
59
 
60
60
 
61
- # @param connection [Connection]
61
+ # @param connection [Protocol::ZMTP::Connection]
62
62
  #
63
63
  def connection_removed(connection)
64
64
  if @connection == connection
@@ -72,7 +72,7 @@ module OMQ
72
72
  #
73
73
  def enqueue(parts)
74
74
  conn = @connection
75
- if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
75
+ if conn.is_a?(Transport::Inproc::Pipe) && conn.direct_recv_queue
76
76
  conn.send_message(parts)
77
77
  elsif @send_queue
78
78
  @send_queue.enqueue(parts)
@@ -42,7 +42,7 @@ module OMQ
42
42
  end
43
43
 
44
44
 
45
- # @param connection [Connection]
45
+ # @param connection [Protocol::ZMTP::Connection]
46
46
  #
47
47
  def connection_added(connection)
48
48
  @connections << connection
@@ -51,7 +51,7 @@ module OMQ
51
51
  end
52
52
 
53
53
 
54
- # @param connection [Connection]
54
+ # @param connection [Protocol::ZMTP::Connection]
55
55
  #
56
56
  def connection_removed(connection)
57
57
  @connections.delete(connection)
@@ -12,7 +12,7 @@ module OMQ
12
12
  # is torn down with the rest of the connection's pumps.
13
13
  #
14
14
  # @param engine [Engine]
15
- # @param conn [Connection]
15
+ # @param conn [Protocol::ZMTP::Connection]
16
16
  # @param q [Async::LimitedQueue]
17
17
  # @return [Async::Task]
18
18
  #
@@ -42,7 +42,7 @@ module OMQ
42
42
  end
43
43
 
44
44
 
45
- # @param connection [Connection]
45
+ # @param connection [Protocol::ZMTP::Connection]
46
46
  #
47
47
  def connection_added(connection)
48
48
  @engine.start_recv_pump(connection, @recv_queue)
@@ -50,7 +50,7 @@ module OMQ
50
50
  end
51
51
 
52
52
 
53
- # @param connection [Connection]
53
+ # @param connection [Protocol::ZMTP::Connection]
54
54
  #
55
55
  def connection_removed(connection)
56
56
  @connections.delete(connection)
@@ -41,7 +41,7 @@ module OMQ
41
41
  end
42
42
 
43
43
 
44
- # @param connection [Connection]
44
+ # @param connection [Protocol::ZMTP::Connection]
45
45
  #
46
46
  def connection_added(connection)
47
47
  @connections << connection
@@ -52,7 +52,7 @@ module OMQ
52
52
  end
53
53
 
54
54
 
55
- # @param connection [Connection]
55
+ # @param connection [Protocol::ZMTP::Connection]
56
56
  #
57
57
  def connection_removed(connection)
58
58
  @connections.delete(connection)
@@ -61,7 +61,7 @@ module OMQ
61
61
  # Override in subclasses to expose subscriptions to the
62
62
  # application (e.g. XPUB enqueues to recv_queue).
63
63
  #
64
- # @param conn [Connection]
64
+ # @param conn [Protocol::ZMTP::Connection]
65
65
  # @param prefix [String]
66
66
  #
67
67
  def on_subscribe(conn, prefix)
@@ -74,7 +74,7 @@ module OMQ
74
74
  # Called when a cancel command is received from a peer.
75
75
  # Override in subclasses (e.g. XPUB enqueues to recv_queue).
76
76
  #
77
- # @param conn [Connection]
77
+ # @param conn [Protocol::ZMTP::Connection]
78
78
  # @param prefix [String]
79
79
  #
80
80
  def on_cancel(conn, prefix)
@@ -86,7 +86,7 @@ module OMQ
86
86
  # Creates a per-connection send queue and starts its send pump.
87
87
  # Call from #connection_added.
88
88
  #
89
- # @param conn [Connection]
89
+ # @param conn [Protocol::ZMTP::Connection]
90
90
  #
91
91
  def add_fan_out_send_connection(conn)
92
92
  q = Routing.build_queue(@engine.options.send_hwm, @engine.options.on_mute)
@@ -99,7 +99,7 @@ module OMQ
99
99
  # down by the per-connection lifecycle barrier.
100
100
  # Call from #connection_removed.
101
101
  #
102
- # @param conn [Connection]
102
+ # @param conn [Protocol::ZMTP::Connection]
103
103
  #
104
104
  def remove_fan_out_send_connection(conn)
105
105
  @subscribe_all.delete(conn)
@@ -152,7 +152,7 @@ module OMQ
152
152
  # In conflate mode, drains the batch and keeps only the latest
153
153
  # message per topic before writing.
154
154
  #
155
- # @param conn [Connection]
155
+ # @param conn [Protocol::ZMTP::Connection]
156
156
  # @param q [Async::LimitedQueue, DropQueue]
157
157
  #
158
158
  def start_conn_send_pump(conn, q)
@@ -169,7 +169,7 @@ module OMQ
169
169
  # Send pump variant for non-conflate fan-out: dequeues, batch-drains,
170
170
  # writes each subscribed message, then flushes once.
171
171
  #
172
- # @param conn [Connection]
172
+ # @param conn [Protocol::ZMTP::Connection]
173
173
  # @param q [Async::LimitedQueue, DropQueue]
174
174
  # @param use_wire [Boolean] true iff the encoded wire bytes can
175
175
  # be shared across peers (unencrypted ZMTP)
@@ -219,7 +219,7 @@ module OMQ
219
219
  # Send pump variant for conflate mode: keeps only the latest
220
220
  # subscribed message per batch. Stale duplicates are dropped.
221
221
  #
222
- # @param conn [Connection]
222
+ # @param conn [Protocol::ZMTP::Connection]
223
223
  # @param q [Async::LimitedQueue, DropQueue]
224
224
  # @return [Async::Task]
225
225
  #
@@ -36,14 +36,14 @@ module OMQ
36
36
  end
37
37
 
38
38
 
39
- # @param connection [Connection]
39
+ # @param connection [Protocol::ZMTP::Connection]
40
40
  #
41
41
  def connection_added(connection)
42
42
  @engine.start_recv_pump(connection, @recv_queue)
43
43
  end
44
44
 
45
45
 
46
- # @param connection [Connection]
46
+ # @param connection [Protocol::ZMTP::Connection]
47
47
  #
48
48
  def connection_removed(connection)
49
49
  end
@@ -43,7 +43,7 @@ module OMQ
43
43
  end
44
44
 
45
45
 
46
- # @param connection [Connection]
46
+ # @param connection [Protocol::ZMTP::Connection]
47
47
  # @raise [RuntimeError] if a connection already exists
48
48
  #
49
49
  def connection_added(connection)
@@ -52,13 +52,13 @@ module OMQ
52
52
 
53
53
  @engine.start_recv_pump(connection, @recv_queue)
54
54
 
55
- unless connection.is_a?(Transport::Inproc::DirectPipe)
55
+ unless connection.is_a?(Transport::Inproc::Pipe)
56
56
  start_send_pump(connection)
57
57
  end
58
58
  end
59
59
 
60
60
 
61
- # @param connection [Connection]
61
+ # @param connection [Protocol::ZMTP::Connection]
62
62
  #
63
63
  def connection_removed(connection)
64
64
  @connection = nil if @connection == connection
@@ -69,7 +69,7 @@ module OMQ
69
69
  #
70
70
  def enqueue(parts)
71
71
  conn = @connection
72
- if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
72
+ if conn.is_a?(Transport::Inproc::Pipe) && conn.direct_recv_queue
73
73
  conn.send_message(parts)
74
74
  else
75
75
  @send_queue.enqueue(parts)
@@ -50,7 +50,7 @@ module OMQ
50
50
  end
51
51
 
52
52
 
53
- # @param connection [Connection]
53
+ # @param connection [Protocol::ZMTP::Connection]
54
54
  #
55
55
  def connection_added(connection)
56
56
  routing_id = SecureRandom.bytes(4)
@@ -65,7 +65,7 @@ module OMQ
65
65
  end
66
66
 
67
67
 
68
- # @param connection [Connection]
68
+ # @param connection [Protocol::ZMTP::Connection]
69
69
  #
70
70
  def connection_removed(connection)
71
71
  routing_id = @routing_id_by_connection.delete(connection)
@@ -36,7 +36,7 @@ module OMQ
36
36
  end
37
37
 
38
38
 
39
- # @param connection [Connection]
39
+ # @param connection [Protocol::ZMTP::Connection]
40
40
  #
41
41
  def connection_added(connection)
42
42
  @connections << connection
@@ -46,7 +46,7 @@ module OMQ
46
46
  end
47
47
 
48
48
 
49
- # @param connection [Connection]
49
+ # @param connection [Protocol::ZMTP::Connection]
50
50
  #
51
51
  def connection_removed(connection)
52
52
  @connections.delete(connection)
@@ -38,14 +38,14 @@ module OMQ
38
38
  end
39
39
 
40
40
 
41
- # @param connection [Connection]
41
+ # @param connection [Protocol::ZMTP::Connection]
42
42
  #
43
43
  def connection_added(connection)
44
44
  @engine.start_recv_pump(connection, @recv_queue)
45
45
  end
46
46
 
47
47
 
48
- # @param connection [Connection]
48
+ # @param connection [Protocol::ZMTP::Connection]
49
49
  #
50
50
  def connection_removed(connection)
51
51
  # recv pump stops on EOFError via its connection barrier
@@ -33,7 +33,7 @@ module OMQ
33
33
  end
34
34
 
35
35
 
36
- # @param connection [Connection]
36
+ # @param connection [Protocol::ZMTP::Connection]
37
37
  #
38
38
  def connection_added(connection)
39
39
  add_round_robin_send_connection(connection)
@@ -41,7 +41,7 @@ module OMQ
41
41
  end
42
42
 
43
43
 
44
- # @param connection [Connection]
44
+ # @param connection [Protocol::ZMTP::Connection]
45
45
  #
46
46
  def connection_removed(connection)
47
47
  @connections.delete(connection)
@@ -64,7 +64,7 @@ module OMQ
64
64
  # may succeed if the kernel send buffer absorbs the data.
65
65
  #
66
66
  def start_reaper(conn)
67
- return if conn.is_a?(Transport::Inproc::DirectPipe)
67
+ return if conn.is_a?(Transport::Inproc::Pipe)
68
68
  @engine.spawn_conn_pump_task(conn, annotation: "reaper") do
69
69
  conn.receive_message # blocks until peer disconnects; then exits
70
70
  end
@@ -50,7 +50,7 @@ module OMQ
50
50
  end
51
51
 
52
52
 
53
- # @param connection [Connection]
53
+ # @param connection [Protocol::ZMTP::Connection]
54
54
  #
55
55
  def connection_added(connection)
56
56
  @connections << connection
@@ -64,7 +64,7 @@ module OMQ
64
64
  end
65
65
 
66
66
 
67
- # @param connection [Connection]
67
+ # @param connection [Protocol::ZMTP::Connection]
68
68
  #
69
69
  def connection_removed(connection)
70
70
  @connections.delete(connection)
@@ -45,7 +45,7 @@ module OMQ
45
45
  end
46
46
 
47
47
 
48
- # @param connection [Connection]
48
+ # @param connection [Protocol::ZMTP::Connection]
49
49
  #
50
50
  def connection_added(connection)
51
51
  @engine.start_recv_pump(connection, @recv_queue) do |msg|
@@ -63,7 +63,7 @@ module OMQ
63
63
  end
64
64
 
65
65
 
66
- # @param connection [Connection]
66
+ # @param connection [Protocol::ZMTP::Connection]
67
67
  #
68
68
  def connection_removed(connection)
69
69
  @pending_replies.reject! { |r| r[0] == connection }
@@ -46,7 +46,7 @@ module OMQ
46
46
  end
47
47
 
48
48
 
49
- # @param connection [Connection]
49
+ # @param connection [Protocol::ZMTP::Connection]
50
50
  #
51
51
  def connection_added(connection)
52
52
  @engine.start_recv_pump(connection, @recv_queue) do |msg|
@@ -58,7 +58,7 @@ module OMQ
58
58
  end
59
59
 
60
60
 
61
- # @param connection [Connection]
61
+ # @param connection [Protocol::ZMTP::Connection]
62
62
  #
63
63
  def connection_removed(connection)
64
64
  @connections.delete(connection)
@@ -48,7 +48,7 @@ module OMQ
48
48
  # Registers a connection and starts its send pump.
49
49
  # Call from #connection_added.
50
50
  #
51
- # @param conn [Connection]
51
+ # @param conn [Protocol::ZMTP::Connection]
52
52
  #
53
53
  def add_round_robin_send_connection(conn)
54
54
  @connections << conn
@@ -63,7 +63,7 @@ module OMQ
63
63
  # guarantee, so this is safe. The pump itself is torn down by
64
64
  # the per-connection lifecycle barrier.
65
65
  #
66
- # @param conn [Connection]
66
+ # @param conn [Protocol::ZMTP::Connection]
67
67
  #
68
68
  def remove_round_robin_send_connection(conn)
69
69
  update_direct_pipe
@@ -73,7 +73,7 @@ module OMQ
73
73
  # Updates the direct-pipe shortcut for inproc single-peer bypass.
74
74
  #
75
75
  def update_direct_pipe
76
- if @connections.size == 1 && @connections.first.is_a?(Transport::Inproc::DirectPipe)
76
+ if @connections.size == 1 && @connections.first.is_a?(Transport::Inproc::Pipe)
77
77
  @direct_pipe = @connections.first
78
78
  else
79
79
  @direct_pipe = nil
@@ -128,7 +128,7 @@ module OMQ
128
128
  # run. The yield is effectively free when the scheduler has no
129
129
  # other work.
130
130
  #
131
- # @param conn [Connection]
131
+ # @param conn [Protocol::ZMTP::Connection]
132
132
  #
133
133
  def start_conn_send_pump(conn)
134
134
  @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
@@ -45,7 +45,7 @@ module OMQ
45
45
  end
46
46
 
47
47
 
48
- # @param connection [Connection]
48
+ # @param connection [Protocol::ZMTP::Connection]
49
49
  #
50
50
  def connection_added(connection)
51
51
  identity = connection.peer_identity
@@ -61,7 +61,7 @@ module OMQ
61
61
  end
62
62
 
63
63
 
64
- # @param connection [Connection]
64
+ # @param connection [Protocol::ZMTP::Connection]
65
65
  #
66
66
  def connection_removed(connection)
67
67
  identity = @identity_by_connection.delete(connection)
@@ -33,16 +33,15 @@ module OMQ
33
33
  end
34
34
 
35
35
 
36
- # @param connection [Connection]
36
+ # @param connection [Protocol::ZMTP::Connection]
37
37
  #
38
38
  def connection_added(connection)
39
- @connections << connection
40
39
  add_round_robin_send_connection(connection)
41
40
  start_reaper(connection)
42
41
  end
43
42
 
44
43
 
45
- # @param connection [Connection]
44
+ # @param connection [Protocol::ZMTP::Connection]
46
45
  #
47
46
  def connection_removed(connection)
48
47
  @connections.delete(connection)
@@ -63,10 +62,10 @@ module OMQ
63
62
  # Detects peer disconnection on write-only sockets by
64
63
  # blocking on a receive that only returns on disconnect.
65
64
  #
66
- # @param conn [Connection]
65
+ # @param conn [Protocol::ZMTP::Connection]
67
66
  #
68
67
  def start_reaper(conn)
69
- return if conn.is_a?(Transport::Inproc::DirectPipe)
68
+ return if conn.is_a?(Transport::Inproc::Pipe)
70
69
  @engine.spawn_conn_pump_task(conn, annotation: "reaper") do
71
70
  conn.receive_message # blocks until peer disconnects; then exits
72
71
  end
@@ -45,7 +45,7 @@ module OMQ
45
45
  end
46
46
 
47
47
 
48
- # @param connection [Connection]
48
+ # @param connection [Protocol::ZMTP::Connection]
49
49
  #
50
50
  def connection_added(connection)
51
51
  routing_id = SecureRandom.bytes(4)
@@ -60,7 +60,7 @@ module OMQ
60
60
  end
61
61
 
62
62
 
63
- # @param connection [Connection]
63
+ # @param connection [Protocol::ZMTP::Connection]
64
64
  #
65
65
  def connection_removed(connection)
66
66
  routing_id = @routing_id_by_connection.delete(connection)
@@ -41,7 +41,7 @@ module OMQ
41
41
  end
42
42
 
43
43
 
44
- # @param connection [Connection]
44
+ # @param connection [Protocol::ZMTP::Connection]
45
45
  #
46
46
  def connection_added(connection)
47
47
  @connections << connection
@@ -54,7 +54,7 @@ module OMQ
54
54
  end
55
55
 
56
56
 
57
- # @param connection [Connection]
57
+ # @param connection [Protocol::ZMTP::Connection]
58
58
  #
59
59
  def connection_removed(connection)
60
60
  @connections.delete(connection)
@@ -41,7 +41,7 @@ module OMQ
41
41
  end
42
42
 
43
43
 
44
- # @param connection [Connection]
44
+ # @param connection [Protocol::ZMTP::Connection]
45
45
  #
46
46
  def connection_added(connection)
47
47
  @connections << connection
@@ -51,7 +51,7 @@ module OMQ
51
51
  end
52
52
 
53
53
 
54
- # @param connection [Connection]
54
+ # @param connection [Protocol::ZMTP::Connection]
55
55
  #
56
56
  def connection_removed(connection)
57
57
  @connections.delete(connection)
@@ -43,7 +43,7 @@ module OMQ
43
43
  end
44
44
 
45
45
 
46
- # @param connection [Connection]
46
+ # @param connection [Protocol::ZMTP::Connection]
47
47
  #
48
48
  def connection_added(connection)
49
49
  @connections << connection
@@ -56,7 +56,7 @@ module OMQ
56
56
  end
57
57
 
58
58
 
59
- # @param connection [Connection]
59
+ # @param connection [Protocol::ZMTP::Connection]
60
60
  #
61
61
  def connection_removed(connection)
62
62
  @connections.delete(connection)
@@ -14,7 +14,7 @@ module OMQ
14
14
  # This reduces inproc from 3 queue hops to 2 (send_queue →
15
15
  # recv_queue), eliminating the internal pipe queue in between.
16
16
  #
17
- class DirectPipe
17
+ class Pipe
18
18
  # @return [String] peer's socket type
19
19
  #
20
20
  attr_reader :peer_socket_type
@@ -39,7 +39,7 @@ module OMQ
39
39
  attr_reader :peer_identity
40
40
 
41
41
 
42
- # @return [DirectPipe, nil] the other end of this pipe pair
42
+ # @return [Pipe, nil] the other end of this pipe pair
43
43
  #
44
44
  attr_accessor :peer
45
45
 
@@ -85,6 +85,7 @@ module OMQ
85
85
  def wire_direct_recv(queue, transform)
86
86
  @direct_recv_transform = transform
87
87
  @direct_recv_queue = queue
88
+
88
89
  return unless @pending_direct
89
90
 
90
91
  @pending_direct.each { |msg| queue.enqueue(msg) }
@@ -99,6 +100,7 @@ module OMQ
99
100
  #
100
101
  def send_message(parts)
101
102
  raise IOError, "closed" if @closed
103
+
102
104
  if @direct_recv_queue
103
105
  @direct_recv_queue.enqueue(apply_transform(parts))
104
106
  elsif @send_queue
@@ -114,7 +116,7 @@ module OMQ
114
116
 
115
117
  # Batched form, for parity with Protocol::ZMTP::Connection. The
116
118
  # work-stealing pumps call this when they dequeue more than one
117
- # message at once; DirectPipe just loops — no mutex to amortize.
119
+ # message at once; Pipe just loops — no mutex to amortize.
118
120
  #
119
121
  # @param messages [Array<Array<String>>]
120
122
  # @return [void]
@@ -147,14 +149,13 @@ module OMQ
147
149
  #
148
150
  def receive_message
149
151
  loop do
150
- item = @receive_queue.dequeue
151
-
152
- raise EOFError, "connection closed" if item.nil?
152
+ item = @receive_queue.dequeue or raise EOFError, "connection closed"
153
153
 
154
154
  if item.is_a?(Array) && item.first == :command
155
155
  if block_given?
156
156
  yield Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
157
157
  end
158
+
158
159
  next
159
160
  end
160
161
 
@@ -181,8 +182,7 @@ module OMQ
181
182
  # @return [Protocol::ZMTP::Codec::Frame]
182
183
  #
183
184
  def read_frame
184
- item = @receive_queue.dequeue
185
- raise EOFError, "connection closed" if item.nil?
185
+ item = @receive_queue.dequeue or raise EOFError, "connection closed"
186
186
 
187
187
  if item.is_a?(Array) && item.first == :command
188
188
  Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
@@ -208,7 +208,7 @@ module OMQ
208
208
 
209
209
  def apply_transform(parts)
210
210
  if @direct_recv_transform
211
- @direct_recv_transform.call(parts).freeze
211
+ @direct_recv_transform.call(parts)
212
212
  else
213
213
  parts
214
214
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "async"
4
4
  require "async/queue"
5
- require_relative "inproc/direct_pipe"
5
+ require_relative "inproc/pipe"
6
6
 
7
7
  module OMQ
8
8
  module Transport
@@ -122,8 +122,8 @@ module OMQ
122
122
  end
123
123
 
124
124
 
125
- # Decides whether a DirectPipe pair needs command queues.
126
- # DirectPipe's fast path skips queues entirely; command queues
125
+ # Decides whether a Pipe pair needs command queues.
126
+ # Pipe's fast path skips queues entirely; command queues
127
127
  # are only needed for socket types that exchange ZMTP commands
128
128
  # (e.g. ROUTER/DEALER identity, PUB/SUB subscriptions) or when
129
129
  # either side enables QoS ≥ 1.
@@ -137,11 +137,11 @@ module OMQ
137
137
  end
138
138
 
139
139
 
140
- # Builds a bidirectional {DirectPipe} pair for client + server.
140
+ # Builds a bidirectional {Pipe} pair for client + server.
141
141
  # When +needs_cmds+ is false the pipes have no command queues
142
142
  # (fast path — all traffic bypasses Async::Queue entirely).
143
143
  #
144
- # @return [Array(DirectPipe, DirectPipe)] client, server
144
+ # @return [Array(Pipe, Pipe)] client, server
145
145
  #
146
146
  def make_pipe_pair(ce, se, ct, st, needs_cmds)
147
147
  if needs_cmds
@@ -149,12 +149,12 @@ module OMQ
149
149
  b_to_a = Async::Queue.new
150
150
  end
151
151
 
152
- client = DirectPipe.new(send_queue: needs_cmds ? a_to_b : nil,
153
- receive_queue: needs_cmds ? b_to_a : nil,
154
- peer_identity: se.options.identity, peer_type: st.to_s)
155
- server = DirectPipe.new(send_queue: needs_cmds ? b_to_a : nil,
156
- receive_queue: needs_cmds ? a_to_b : nil,
157
- peer_identity: ce.options.identity, peer_type: ct.to_s)
152
+ client = Pipe.new(send_queue: needs_cmds ? a_to_b : nil,
153
+ receive_queue: needs_cmds ? b_to_a : nil,
154
+ peer_identity: se.options.identity, peer_type: st.to_s)
155
+ server = Pipe.new(send_queue: needs_cmds ? b_to_a : nil,
156
+ receive_queue: needs_cmds ? a_to_b : nil,
157
+ peer_identity: ce.options.identity, peer_type: ct.to_s)
158
158
 
159
159
  client.peer = server
160
160
  server.peer = client
@@ -200,7 +200,7 @@ module OMQ
200
200
  next unless parts
201
201
  group, body = parts
202
202
  next unless @groups.include?(group.b)
203
- return [group.b.freeze, body.b.freeze]
203
+ return [group, body]
204
204
  rescue IO::WaitReadable
205
205
  @socket.wait_readable
206
206
  retry
data/lib/omq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
- VERSION = "0.23.0"
4
+ VERSION = "0.24.0"
5
5
  end
data/lib/omq/writable.rb CHANGED
@@ -9,19 +9,25 @@ module OMQ
9
9
  include QueueWritable
10
10
 
11
11
 
12
- EMPTY_PART = "".b.freeze
13
-
14
-
15
12
  # Sends a message.
16
13
  #
14
+ # Caller owns the message parts. Don't mutate them after sending — especially
15
+ # with inproc transport or PUB fan-out, where a single reference can be shared
16
+ # across peers and read later by the send pump.
17
+ #
17
18
  # @param message [String, Array<String>] message parts
18
19
  # @return [self]
19
20
  # @raise [IO::TimeoutError] if write_timeout exceeded
20
21
  #
21
22
  def send(message)
22
- parts = freeze_message(message)
23
+ parts = message.is_a?(Array) ? message : [message]
24
+ raise ArgumentError, "message has no parts" if parts.empty?
23
25
 
24
- Reactor.run timeout: @options.write_timeout do |task|
26
+ if @engine.on_io_thread?
27
+ Reactor.run(timeout: @options.write_timeout) { @engine.enqueue_send(parts) }
28
+ elsif (timeout = @options.write_timeout)
29
+ Async::Task.current.with_timeout(timeout, IO::TimeoutError) { @engine.enqueue_send(parts) }
30
+ else
25
31
  @engine.enqueue_send(parts)
26
32
  end
27
33
 
@@ -48,42 +54,5 @@ module OMQ
48
54
  true
49
55
  end
50
56
 
51
-
52
- private
53
-
54
-
55
- # Converts a message into a frozen array of frozen binary strings.
56
- #
57
- # @param message [String, Array<String>]
58
- # @return [Array<String>] frozen array of frozen binary strings
59
- #
60
- def freeze_message(message)
61
- parts = message.is_a?(Array) ? message : [message]
62
- raise ArgumentError, "message has no parts" if parts.empty?
63
-
64
- all_ready = parts.all? { |p| p.is_a?(String) && p.frozen? && p.encoding == Encoding::BINARY }
65
-
66
- # Already a frozen array of frozen binary strings → return as-is.
67
- return parts if all_ready && parts.frozen?
68
-
69
- # Items are ready; just freeze the outer array.
70
- return parts.freeze if all_ready
71
-
72
- # Items need conversion. Mutate in place when we can.
73
- if parts.frozen?
74
- parts.map { |p| frozen_binary(p) }.freeze
75
- else
76
- parts.map! { |p| frozen_binary(p) }.freeze
77
- end
78
- end
79
-
80
-
81
- def frozen_binary(obj)
82
- return EMPTY_PART if obj.nil?
83
- s = obj.to_s
84
- return s if s.frozen? && s.encoding == Encoding::BINARY
85
- s.b.freeze
86
- end
87
-
88
57
  end
89
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -114,7 +114,7 @@ files:
114
114
  - lib/omq/single_frame.rb
115
115
  - lib/omq/socket.rb
116
116
  - lib/omq/transport/inproc.rb
117
- - lib/omq/transport/inproc/direct_pipe.rb
117
+ - lib/omq/transport/inproc/pipe.rb
118
118
  - lib/omq/transport/ipc.rb
119
119
  - lib/omq/transport/tcp.rb
120
120
  - lib/omq/transport/udp.rb