omq 0.12.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.
@@ -5,45 +5,57 @@ module OMQ
5
5
  # XSUB socket routing: like SUB but subscriptions sent as data messages.
6
6
  #
7
7
  # Subscriptions are sent as data frames: \x01 + prefix for subscribe,
8
- # \x00 + prefix for unsubscribe.
8
+ # \x00 + prefix for unsubscribe. Each connected PUB gets its own send
9
+ # queue so subscription commands are delivered independently per peer.
9
10
  #
10
11
  class XSub
11
12
 
12
13
  # @param engine [Engine]
13
14
  #
14
15
  def initialize(engine)
15
- @engine = engine
16
- @connections = []
17
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, engine.options.on_mute)
18
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
19
- @tasks = []
20
- @send_pump_started = false
21
- @send_pump_idle = true
16
+ @engine = engine
17
+ @connections = Set.new
18
+ @recv_queue = FairQueue.new
19
+ @conn_queues = {} # connection => per-connection send queue
20
+ @conn_send_tasks = {} # connection => send pump task
21
+ @tasks = []
22
22
  end
23
23
 
24
- # @return [Async::LimitedQueue]
24
+ # @return [FairQueue]
25
25
  #
26
- attr_reader :recv_queue, :send_queue
26
+ attr_reader :recv_queue
27
27
 
28
28
  # @param connection [Connection]
29
29
  #
30
30
  def connection_added(connection)
31
31
  @connections << connection
32
- task = @engine.start_recv_pump(connection, @recv_queue)
32
+
33
+ conn_q = Routing.build_queue(@engine.options.recv_hwm, @engine.options.on_mute)
34
+ signaling = SignalingQueue.new(conn_q, @recv_queue)
35
+ @recv_queue.add_queue(connection, conn_q)
36
+ task = @engine.start_recv_pump(connection, signaling)
33
37
  @tasks << task if task
34
- start_send_pump unless @send_pump_started
38
+
39
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
40
+ @conn_queues[connection] = q
41
+ start_conn_send_pump(connection, q)
35
42
  end
36
43
 
37
44
  # @param connection [Connection]
38
45
  #
39
46
  def connection_removed(connection)
40
47
  @connections.delete(connection)
48
+ @recv_queue.remove_queue(connection)
49
+ @conn_queues.delete(connection)
50
+ @conn_send_tasks.delete(connection)&.stop
41
51
  end
42
52
 
53
+ # Enqueues a subscription command (fan-out to all connected PUBs).
54
+ #
43
55
  # @param parts [Array<String>]
44
56
  #
45
57
  def enqueue(parts)
46
- @send_queue.enqueue(parts)
58
+ @connections.each { |conn| @conn_queues[conn]&.enqueue(parts) }
47
59
  end
48
60
 
49
61
  #
@@ -52,31 +64,35 @@ module OMQ
52
64
  @tasks.clear
53
65
  end
54
66
 
55
- def send_pump_idle? = @send_pump_idle
67
+ # True when all per-connection send queues are empty.
68
+ #
69
+ def send_queues_drained?
70
+ @conn_queues.values.all?(&:empty?)
71
+ end
56
72
 
57
73
  private
58
74
 
59
- def start_send_pump
60
- @send_pump_started = true
61
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
75
+ def start_conn_send_pump(conn, q)
76
+ task = @engine.spawn_pump_task(annotation: "send pump") do
62
77
  loop do
63
- @send_pump_idle = true
64
- parts = @send_queue.dequeue
65
- @send_pump_idle = false
78
+ parts = q.dequeue
66
79
  frame = parts.first&.b
67
80
  next if frame.nil? || frame.empty?
68
-
69
81
  flag = frame.getbyte(0)
70
82
  prefix = frame.byteslice(1..) || "".b
71
-
72
- case flag
73
- when 0x01
74
- @connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix)) }
75
- when 0x00
76
- @connections.each { |c| c.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix)) }
83
+ begin
84
+ case flag
85
+ when 0x01 then conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
86
+ when 0x00 then conn.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix))
87
+ end
88
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
89
+ @engine.connection_lost(conn)
90
+ break
77
91
  end
78
92
  end
79
93
  end
94
+ @conn_send_tasks[conn] = task
95
+ @tasks << task
80
96
  end
81
97
  end
82
98
  end
data/lib/omq/routing.rb CHANGED
@@ -4,6 +4,9 @@ require "async"
4
4
  require "async/queue"
5
5
  require "async/limited_queue"
6
6
  require_relative "drop_queue"
7
+ require_relative "routing/fair_queue"
8
+ require_relative "routing/fair_recv"
9
+ require_relative "routing/conn_send_pump"
7
10
 
8
11
  module OMQ
9
12
  # Routing strategies for each ZMQ socket type.
data/lib/omq/socket.rb CHANGED
@@ -47,7 +47,7 @@ module OMQ
47
47
  # @return [Socket]
48
48
  #
49
49
  def self.bind(endpoint, **opts)
50
- new(nil, **opts).tap { |s| s.bind(endpoint) }
50
+ new("@#{endpoint}", **opts)
51
51
  end
52
52
 
53
53
 
@@ -58,7 +58,7 @@ module OMQ
58
58
  # @return [Socket]
59
59
  #
60
60
  def self.connect(endpoint, **opts)
61
- new(nil, **opts).tap { |s| s.connect(endpoint) }
61
+ new(">#{endpoint}", **opts)
62
62
  end
63
63
 
64
64
 
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Transport
5
+ module Inproc
6
+ # A direct in-process pipe that transfers Ruby arrays through queues.
7
+ #
8
+ # Implements the same interface as Connection so routing strategies
9
+ # can use it transparently.
10
+ #
11
+ # When a routing strategy sets {#direct_recv_queue} on a pipe,
12
+ # {#send_message} enqueues directly into the peer's recv queue,
13
+ # bypassing the intermediate pipe queues and the recv pump task.
14
+ # This reduces inproc from 3 queue hops to 2 (send_queue →
15
+ # recv_queue), eliminating the internal pipe queue in between.
16
+ #
17
+ class DirectPipe
18
+ # @return [String] peer's socket type
19
+ #
20
+ attr_reader :peer_socket_type
21
+
22
+
23
+ # @return [String] peer's identity
24
+ #
25
+ attr_reader :peer_identity
26
+
27
+
28
+ # @return [DirectPipe, nil] the other end of this pipe pair
29
+ #
30
+ attr_accessor :peer
31
+
32
+
33
+ # @return [Async::LimitedQueue, nil] when set, {#send_message}
34
+ # enqueues directly here instead of using the internal queue
35
+ #
36
+ attr_reader :direct_recv_queue
37
+
38
+
39
+ # @return [Proc, nil] optional transform applied before
40
+ # enqueuing into {#direct_recv_queue}
41
+ #
42
+ attr_accessor :direct_recv_transform
43
+
44
+
45
+ # @param send_queue [Async::Queue, nil] outgoing command queue
46
+ # (nil for non-PUB/SUB types that don't exchange commands)
47
+ # @param receive_queue [Async::Queue, nil] incoming command queue
48
+ # @param peer_identity [String]
49
+ # @param peer_type [String]
50
+ #
51
+ def initialize(send_queue: nil, receive_queue: nil, peer_identity:, peer_type:)
52
+ @send_queue = send_queue
53
+ @receive_queue = receive_queue
54
+ @peer_identity = peer_identity || "".b
55
+ @peer_socket_type = peer_type
56
+ @closed = false
57
+ @peer = nil
58
+ @direct_recv_queue = nil
59
+ @direct_recv_transform = nil
60
+ @pending_direct = nil
61
+ end
62
+
63
+
64
+ # Sets the direct recv queue. Drains any messages that were
65
+ # buffered before the queue was available.
66
+ #
67
+ def direct_recv_queue=(queue)
68
+ @direct_recv_queue = queue
69
+ if queue && @pending_direct
70
+ @pending_direct.each { |msg| queue.enqueue(msg) }
71
+ @pending_direct = nil
72
+ end
73
+ end
74
+
75
+
76
+ # Sends a multi-frame message.
77
+ #
78
+ # @param parts [Array<String>]
79
+ # @return [void]
80
+ #
81
+ def send_message(parts)
82
+ raise IOError, "closed" if @closed
83
+ if @direct_recv_queue
84
+ @direct_recv_queue.enqueue(apply_transform(parts))
85
+ elsif @send_queue
86
+ @send_queue.enqueue(parts)
87
+ else
88
+ (@pending_direct ||= []) << apply_transform(parts)
89
+ end
90
+ end
91
+
92
+
93
+ alias write_message send_message
94
+
95
+
96
+ # No-op — inproc has no IO buffer to flush.
97
+ #
98
+ def flush = nil
99
+
100
+
101
+ # Receives a multi-frame message.
102
+ #
103
+ # @return [Array<String>]
104
+ # @raise [EOFError] if closed
105
+ #
106
+ def receive_message
107
+ loop do
108
+ item = @receive_queue.dequeue
109
+ raise EOFError, "connection closed" if item.nil?
110
+ if item.is_a?(Array) && item.first == :command
111
+ yield Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true) if block_given?
112
+ next
113
+ end
114
+ return item
115
+ end
116
+ end
117
+
118
+
119
+ # Sends a command via the internal command queue.
120
+ # Only available for PUB/SUB-family pipes.
121
+ #
122
+ # @param command [Protocol::ZMTP::Codec::Command]
123
+ #
124
+ def send_command(command)
125
+ raise IOError, "closed" if @closed
126
+ @send_queue.enqueue([:command, command])
127
+ end
128
+
129
+
130
+ # Reads one command frame from the internal command queue.
131
+ # Used by PUB/XPUB subscription listeners.
132
+ #
133
+ # @return [Protocol::ZMTP::Codec::Frame]
134
+ #
135
+ def read_frame
136
+ loop do
137
+ item = @receive_queue.dequeue
138
+ raise EOFError, "connection closed" if item.nil?
139
+ if item.is_a?(Array) && item.first == :command
140
+ return Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
141
+ end
142
+ end
143
+ end
144
+
145
+
146
+ # Closes this pipe end.
147
+ #
148
+ def close
149
+ return if @closed
150
+ @closed = true
151
+ @send_queue&.enqueue(nil) # close sentinel
152
+ end
153
+
154
+ private
155
+
156
+ def apply_transform(parts)
157
+ @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "async"
4
4
  require "async/queue"
5
+ require_relative "inproc/direct_pipe"
5
6
 
6
7
  module OMQ
7
8
  module Transport
@@ -54,25 +55,7 @@ module OMQ
54
55
  #
55
56
  def connect(endpoint, engine)
56
57
  bound_engine = @mutex.synchronize { @registry[endpoint] }
57
-
58
- unless bound_engine
59
- # Endpoint not bound yet. Wait with timeout derived from
60
- # reconnect_interval. If it doesn't appear, silently return —
61
- # matching ZMQ 4.x behavior where inproc connect to an
62
- # unbound endpoint succeeds but messages go nowhere.
63
- # A background task retries periodically.
64
- ri = engine.options.reconnect_interval
65
- timeout = ri.is_a?(Range) ? ri.begin : ri
66
- promise = Async::Promise.new
67
- @mutex.synchronize { @waiters[endpoint] << promise }
68
- unless promise.wait?(timeout: timeout)
69
- @mutex.synchronize { @waiters[endpoint].delete(promise) }
70
- start_connect_retry(endpoint, engine)
71
- return
72
- end
73
- bound_engine = @mutex.synchronize { @registry[endpoint] }
74
- end
75
-
58
+ bound_engine ||= await_bind(endpoint, engine) or return
76
59
  establish_link(engine, bound_engine, endpoint)
77
60
  end
78
61
 
@@ -112,42 +95,51 @@ module OMQ
112
95
  def establish_link(client_engine, server_engine, endpoint)
113
96
  client_type = client_engine.socket_type
114
97
  server_type = server_engine.socket_type
115
-
116
98
  unless Protocol::ZMTP::VALID_PEERS[client_type]&.include?(server_type)
117
99
  raise Protocol::ZMTP::Error,
118
100
  "incompatible socket types: #{client_type} cannot connect to #{server_type}"
119
101
  end
102
+ needs_cmds = needs_commands?(client_engine, server_engine, client_type, server_type)
103
+ client_pipe, server_pipe = make_pipe_pair(client_engine, server_engine, client_type, server_type, needs_cmds)
104
+ client_engine.connection_ready(client_pipe, endpoint: endpoint)
105
+ server_engine.connection_ready(server_pipe, endpoint: endpoint)
106
+ end
120
107
 
121
- # PUB/SUB-family types exchange commands (SUBSCRIBE/CANCEL)
122
- # over inproc. QoS >= 1 needs command queues for ACK/NACK.
123
- needs_commands = COMMAND_TYPES.include?(client_type) ||
124
- COMMAND_TYPES.include?(server_type) ||
125
- client_engine.options.qos >= 1 ||
126
- server_engine.options.qos >= 1
108
+ def needs_commands?(ce, se, ct, st)
109
+ COMMAND_TYPES.include?(ct) || COMMAND_TYPES.include?(st) ||
110
+ ce.options.qos >= 1 || se.options.qos >= 1
111
+ end
127
112
 
128
- if needs_commands
113
+ def make_pipe_pair(ce, se, ct, st, needs_cmds)
114
+ if needs_cmds
129
115
  a_to_b = Async::Queue.new
130
116
  b_to_a = Async::Queue.new
131
117
  end
118
+ client = DirectPipe.new(send_queue: needs_cmds ? a_to_b : nil,
119
+ receive_queue: needs_cmds ? b_to_a : nil,
120
+ peer_identity: se.options.identity, peer_type: st.to_s)
121
+ server = DirectPipe.new(send_queue: needs_cmds ? b_to_a : nil,
122
+ receive_queue: needs_cmds ? a_to_b : nil,
123
+ peer_identity: ce.options.identity, peer_type: ct.to_s)
124
+ client.peer = server
125
+ server.peer = client
126
+ [client, server]
127
+ end
132
128
 
133
- client_pipe = DirectPipe.new(
134
- send_queue: needs_commands ? a_to_b : nil,
135
- receive_queue: needs_commands ? b_to_a : nil,
136
- peer_identity: server_engine.options.identity,
137
- peer_type: server_type.to_s,
138
- )
139
- server_pipe = DirectPipe.new(
140
- send_queue: needs_commands ? b_to_a : nil,
141
- receive_queue: needs_commands ? a_to_b : nil,
142
- peer_identity: client_engine.options.identity,
143
- peer_type: client_type.to_s,
144
- )
145
-
146
- client_pipe.peer = server_pipe
147
- server_pipe.peer = client_pipe
148
-
149
- client_engine.connection_ready(client_pipe, endpoint: endpoint)
150
- server_engine.connection_ready(server_pipe, endpoint: endpoint)
129
+ def await_bind(endpoint, engine)
130
+ # Endpoint not bound yet — wait briefly then start background retry.
131
+ # Matches ZMQ 4.x: connect to unbound inproc succeeds silently.
132
+ ri = engine.options.reconnect_interval
133
+ timeout = ri.is_a?(Range) ? ri.begin : ri
134
+ promise = Async::Promise.new
135
+ @mutex.synchronize { @waiters[endpoint] << promise }
136
+ if promise.wait?(timeout: timeout)
137
+ @mutex.synchronize { @registry[endpoint] }
138
+ else
139
+ @mutex.synchronize { @waiters[endpoint].delete(promise) }
140
+ start_connect_retry(endpoint, engine)
141
+ nil
142
+ end
151
143
  end
152
144
 
153
145
 
@@ -195,179 +187,6 @@ module OMQ
195
187
  end
196
188
  end
197
189
 
198
- # A direct in-process pipe that transfers Ruby arrays through queues.
199
- #
200
- # Implements the same interface as Connection so routing strategies
201
- # can use it transparently.
202
- #
203
- # When a routing strategy sets {#direct_recv_queue} on a pipe,
204
- # {#send_message} enqueues directly into the peer's recv queue,
205
- # bypassing the intermediate pipe queues and the recv pump task.
206
- # This reduces inproc from 3 queue hops to 2 (send_queue →
207
- # recv_queue), eliminating the internal pipe queue in between.
208
- #
209
- class DirectPipe
210
- # @return [String] peer's socket type
211
- #
212
- attr_reader :peer_socket_type
213
-
214
-
215
- # @return [String] peer's identity
216
- #
217
- attr_reader :peer_identity
218
-
219
-
220
- # @return [DirectPipe, nil] the other end of this pipe pair
221
- #
222
- attr_accessor :peer
223
-
224
-
225
- # @return [Async::LimitedQueue, nil] when set, {#send_message}
226
- # enqueues directly here instead of using the internal queue
227
- #
228
- attr_reader :direct_recv_queue
229
-
230
-
231
- # @return [Proc, nil] optional transform applied before
232
- # enqueuing into {#direct_recv_queue}
233
- #
234
- attr_accessor :direct_recv_transform
235
-
236
-
237
- # @param send_queue [Async::Queue, nil] outgoing command queue
238
- # (nil for non-PUB/SUB types that don't exchange commands)
239
- # @param receive_queue [Async::Queue, nil] incoming command queue
240
- # @param peer_identity [String]
241
- # @param peer_type [String]
242
- #
243
- def initialize(send_queue: nil, receive_queue: nil, peer_identity:, peer_type:)
244
- @send_queue = send_queue
245
- @receive_queue = receive_queue
246
- @peer_identity = peer_identity || "".b
247
- @peer_socket_type = peer_type
248
- @closed = false
249
- @peer = nil
250
- @direct_recv_queue = nil
251
- @direct_recv_transform = nil
252
- @pending_direct = nil
253
- end
254
-
255
-
256
- # Sets the direct recv queue. Drains any messages that were
257
- # buffered before the queue was available.
258
- #
259
- def direct_recv_queue=(queue)
260
- @direct_recv_queue = queue
261
- if queue && @pending_direct
262
- @pending_direct.each do |msg|
263
- queue.enqueue(msg)
264
- end
265
- @pending_direct = nil
266
- end
267
- end
268
-
269
-
270
- # Sends a multi-frame message.
271
- #
272
- # When {#direct_recv_queue} is set (inproc fast path), the
273
- # message is delivered directly to the peer's recv queue,
274
- # skipping the internal pipe queues and the recv pump.
275
- #
276
- # @param parts [Array<String>]
277
- # @return [void]
278
- #
279
- def send_message(parts)
280
- raise IOError, "closed" if @closed
281
- if @direct_recv_queue
282
- msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
283
- @direct_recv_queue.enqueue(msg)
284
- elsif @send_queue
285
- @send_queue.enqueue(parts)
286
- else
287
- msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
288
- (@pending_direct ||= []) << msg
289
- end
290
- end
291
-
292
-
293
- alias write_message send_message
294
-
295
-
296
- # No-op — inproc has no IO buffer to flush.
297
- #
298
- # @return [void]
299
- #
300
- def flush = nil
301
-
302
-
303
- # Receives a multi-frame message.
304
- #
305
- # When a block is given, command items ([:command, cmd]) are
306
- # yielded as command frames — matching the Protocol::ZMTP::Connection
307
- # interface. Without a block, commands are silently skipped if
308
- # the pipe has command queues.
309
- #
310
- # @return [Array<String>]
311
- # @raise [EOFError] if closed
312
- #
313
- def receive_message
314
- loop do
315
- item = @receive_queue.dequeue
316
- raise EOFError, "connection closed" if item.nil?
317
-
318
- if item.is_a?(Array) && item.first == :command
319
- if block_given?
320
- cmd = item[1]
321
- frame = Protocol::ZMTP::Codec::Frame.new(cmd.to_body, command: true)
322
- yield frame
323
- end
324
- next
325
- end
326
-
327
- return item
328
- end
329
- end
330
-
331
-
332
- # Sends a command via the internal command queue.
333
- # Only available for PUB/SUB-family pipes.
334
- #
335
- # @param command [Protocol::ZMTP::Codec::Command]
336
- # @return [void]
337
- #
338
- def send_command(command)
339
- raise IOError, "closed" if @closed
340
- @send_queue.enqueue([:command, command])
341
- end
342
-
343
-
344
- # Reads one command frame from the internal command queue.
345
- # Used by PUB/XPUB subscription listeners.
346
- #
347
- # @return [Protocol::ZMTP::Codec::Frame]
348
- #
349
- def read_frame
350
- loop do
351
- item = @receive_queue.dequeue
352
- raise EOFError, "connection closed" if item.nil?
353
- if item.is_a?(Array) && item.first == :command
354
- cmd = item[1]
355
- return Protocol::ZMTP::Codec::Frame.new(cmd.to_body, command: true)
356
- end
357
- end
358
- end
359
-
360
-
361
- # Closes this pipe end.
362
- #
363
- # @return [void]
364
- #
365
- def close
366
- return if @closed
367
- @closed = true
368
- @send_queue&.enqueue(nil) # close sentinel
369
- end
370
- end
371
190
  end
372
191
  end
373
192
  end
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.12.0"
4
+ VERSION = "0.13.0"
5
5
  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.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -67,6 +67,10 @@ files:
67
67
  - lib/omq.rb
68
68
  - lib/omq/drop_queue.rb
69
69
  - lib/omq/engine.rb
70
+ - lib/omq/engine/connection_setup.rb
71
+ - lib/omq/engine/heartbeat.rb
72
+ - lib/omq/engine/reconnect.rb
73
+ - lib/omq/engine/recv_pump.rb
70
74
  - lib/omq/monitor_event.rb
71
75
  - lib/omq/options.rb
72
76
  - lib/omq/pair.rb
@@ -78,7 +82,10 @@ files:
78
82
  - lib/omq/req_rep.rb
79
83
  - lib/omq/router_dealer.rb
80
84
  - lib/omq/routing.rb
85
+ - lib/omq/routing/conn_send_pump.rb
81
86
  - lib/omq/routing/dealer.rb
87
+ - lib/omq/routing/fair_queue.rb
88
+ - lib/omq/routing/fair_recv.rb
82
89
  - lib/omq/routing/fan_out.rb
83
90
  - lib/omq/routing/pair.rb
84
91
  - lib/omq/routing/pub.rb
@@ -93,6 +100,7 @@ files:
93
100
  - lib/omq/routing/xsub.rb
94
101
  - lib/omq/socket.rb
95
102
  - lib/omq/transport/inproc.rb
103
+ - lib/omq/transport/inproc/direct_pipe.rb
96
104
  - lib/omq/transport/ipc.rb
97
105
  - lib/omq/transport/tcp.rb
98
106
  - lib/omq/version.rb