omq 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +3 -1
  4. data/lib/omq/drop_queue.rb +54 -0
  5. data/lib/omq/engine/connection_setup.rb +47 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/reconnect.rb +56 -0
  8. data/lib/omq/engine/recv_pump.rb +76 -0
  9. data/lib/omq/engine.rb +145 -371
  10. data/lib/omq/monitor_event.rb +16 -0
  11. data/lib/omq/options.rb +5 -3
  12. data/lib/omq/pub_sub.rb +9 -8
  13. data/lib/omq/routing/conn_send_pump.rb +36 -0
  14. data/lib/omq/routing/dealer.rb +8 -10
  15. data/lib/omq/routing/fair_queue.rb +144 -0
  16. data/lib/omq/routing/fair_recv.rb +27 -0
  17. data/lib/omq/routing/fan_out.rb +116 -63
  18. data/lib/omq/routing/pair.rb +39 -20
  19. data/lib/omq/routing/pub.rb +5 -7
  20. data/lib/omq/routing/pull.rb +5 -4
  21. data/lib/omq/routing/push.rb +3 -10
  22. data/lib/omq/routing/rep.rb +31 -51
  23. data/lib/omq/routing/req.rb +15 -12
  24. data/lib/omq/routing/round_robin.rb +82 -72
  25. data/lib/omq/routing/router.rb +23 -48
  26. data/lib/omq/routing/sub.rb +8 -6
  27. data/lib/omq/routing/xpub.rb +8 -4
  28. data/lib/omq/routing/xsub.rb +43 -27
  29. data/lib/omq/routing.rb +44 -11
  30. data/lib/omq/socket.rb +46 -5
  31. data/lib/omq/transport/inproc/direct_pipe.rb +162 -0
  32. data/lib/omq/transport/inproc.rb +37 -200
  33. data/lib/omq/transport/ipc.rb +16 -4
  34. data/lib/omq/transport/tcp.rb +31 -8
  35. data/lib/omq/version.rb +1 -1
  36. data/lib/omq.rb +5 -19
  37. metadata +11 -16
  38. data/lib/omq/channel.rb +0 -14
  39. data/lib/omq/client_server.rb +0 -37
  40. data/lib/omq/peer.rb +0 -26
  41. data/lib/omq/radio_dish.rb +0 -74
  42. data/lib/omq/routing/channel.rb +0 -83
  43. data/lib/omq/routing/client.rb +0 -56
  44. data/lib/omq/routing/dish.rb +0 -78
  45. data/lib/omq/routing/gather.rb +0 -46
  46. data/lib/omq/routing/peer.rb +0 -101
  47. data/lib/omq/routing/radio.rb +0 -150
  48. data/lib/omq/routing/scatter.rb +0 -82
  49. data/lib/omq/routing/server.rb +0 -101
  50. data/lib/omq/scatter_gather.rb +0 -23
  51. data/lib/omq/single_frame.rb +0 -18
  52. data/lib/omq/transport/tls.rb +0 -146
@@ -8,6 +8,9 @@ module OMQ
8
8
  # the application as data frames: \x01 + prefix for subscribe,
9
9
  # \x00 + prefix for unsubscribe.
10
10
  #
11
+ # The recv_queue is a simple bounded queue (not a FairQueue) because
12
+ # messages come from subscription commands, not from peer data pumps.
13
+ #
11
14
  class XPub
12
15
  include FanOut
13
16
 
@@ -15,14 +18,14 @@ module OMQ
15
18
  #
16
19
  def initialize(engine)
17
20
  @engine = engine
18
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
21
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
22
  @tasks = []
20
23
  init_fan_out(engine)
21
24
  end
22
25
 
23
26
  # @return [Async::LimitedQueue]
24
27
  #
25
- attr_reader :recv_queue, :send_queue
28
+ attr_reader :recv_queue
26
29
 
27
30
  # @param connection [Connection]
28
31
  #
@@ -30,7 +33,7 @@ module OMQ
30
33
  @connections << connection
31
34
  @subscriptions[connection] = Set.new
32
35
  start_subscription_listener(connection)
33
- start_send_pump unless @send_pump_started
36
+ add_fan_out_send_connection(connection)
34
37
  end
35
38
 
36
39
  # @param connection [Connection]
@@ -38,12 +41,13 @@ module OMQ
38
41
  def connection_removed(connection)
39
42
  @connections.delete(connection)
40
43
  @subscriptions.delete(connection)
44
+ remove_fan_out_send_connection(connection)
41
45
  end
42
46
 
43
47
  # @param parts [Array<String>]
44
48
  #
45
49
  def enqueue(parts)
46
- @send_queue.enqueue(parts)
50
+ fan_out_enqueue(parts)
47
51
  end
48
52
 
49
53
  #
@@ -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 = Async::LimitedQueue.new(engine.options.recv_hwm)
18
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
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
@@ -3,6 +3,10 @@
3
3
  require "async"
4
4
  require "async/queue"
5
5
  require "async/limited_queue"
6
+ require_relative "drop_queue"
7
+ require_relative "routing/fair_queue"
8
+ require_relative "routing/fair_recv"
9
+ require_relative "routing/conn_send_pump"
6
10
 
7
11
  module OMQ
8
12
  # Routing strategies for each ZMQ socket type.
@@ -14,6 +18,42 @@ module OMQ
14
18
  # Shared frozen empty binary string to avoid repeated allocations.
15
19
  EMPTY_BINARY = "".b.freeze
16
20
 
21
+ # Plugin registry for socket types not built into omq.
22
+ # Populated by sister gems via +Routing.register+.
23
+ #
24
+ @registry = {}
25
+
26
+ class << self
27
+ # Registers a routing strategy class for a socket type.
28
+ # Called by omq-draft (and other plugins) at require time.
29
+ #
30
+ # @param socket_type [Symbol] e.g. :RADIO, :CLIENT
31
+ # @param strategy_class [Class]
32
+ #
33
+ def register(socket_type, strategy_class)
34
+ @registry[socket_type] = strategy_class
35
+ end
36
+ end
37
+
38
+ # Builds a send or recv queue based on the mute strategy.
39
+ #
40
+ # @param hwm [Integer] high water mark
41
+ # @param on_mute [Symbol] :block, :drop_newest, or :drop_oldest
42
+ # @return [Async::LimitedQueue, DropQueue]
43
+ #
44
+ def self.build_queue(hwm, on_mute)
45
+ return Async::Queue.new if hwm.nil? || hwm == 0
46
+
47
+ case on_mute
48
+ when :block
49
+ Async::LimitedQueue.new(hwm)
50
+ when :drop_newest, :drop_oldest
51
+ DropQueue.new(hwm, strategy: on_mute)
52
+ else
53
+ raise ArgumentError, "unknown on_mute strategy: #{on_mute.inspect}"
54
+ end
55
+ end
56
+
17
57
  # Drains all available messages from +queue+ into +batch+ without
18
58
  # blocking. Call after the initial blocking dequeue.
19
59
  #
@@ -49,17 +89,10 @@ module OMQ
49
89
  when :SUB then Sub
50
90
  when :XPUB then XPub
51
91
  when :XSUB then XSub
52
- when :PUSH then Push
53
- when :PULL then Pull
54
- when :CLIENT then Client
55
- when :SERVER then Server
56
- when :RADIO then Radio
57
- when :DISH then Dish
58
- when :SCATTER then Scatter
59
- when :GATHER then Gather
60
- when :PEER then Peer
61
- when :CHANNEL then Channel
62
- else raise ArgumentError, "unknown socket type: #{socket_type}"
92
+ when :PUSH then Push
93
+ when :PULL then Pull
94
+ else
95
+ @registry[socket_type] or raise ArgumentError, "unknown socket type: #{socket_type.inspect}"
63
96
  end
64
97
  end
65
98
  end
data/lib/omq/socket.rb CHANGED
@@ -36,8 +36,8 @@ module OMQ
36
36
  :heartbeat_ttl, :heartbeat_ttl=,
37
37
  :heartbeat_timeout, :heartbeat_timeout=,
38
38
  :max_message_size, :max_message_size=,
39
- :mechanism, :mechanism=,
40
- :tls_context, :tls_context=
39
+ :on_mute, :on_mute=,
40
+ :mechanism, :mechanism=
41
41
 
42
42
 
43
43
  # Creates a new socket and binds it to the given endpoint.
@@ -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
 
@@ -141,6 +141,46 @@ module OMQ
141
141
  end
142
142
 
143
143
 
144
+ # Yields lifecycle events for this socket.
145
+ #
146
+ # Spawns a background fiber that reads from an internal event queue.
147
+ # The block receives {MonitorEvent} instances until the socket is
148
+ # closed or the returned task is stopped.
149
+ #
150
+ # @yield [event] called for each lifecycle event
151
+ # @yieldparam event [MonitorEvent]
152
+ # @return [Async::Task] the monitor task (call +#stop+ to end early)
153
+ #
154
+ # @example
155
+ # task = socket.monitor do |event|
156
+ # case event
157
+ # in type: :connected, endpoint:
158
+ # puts "peer up: #{endpoint}"
159
+ # in type: :disconnected, endpoint:
160
+ # puts "peer down: #{endpoint}"
161
+ # end
162
+ # end
163
+ # # later:
164
+ # task.stop
165
+ #
166
+ def monitor(&block)
167
+ ensure_parent_task
168
+ queue = Async::Queue.new
169
+ @engine.monitor_queue = queue
170
+ Reactor.run do
171
+ @engine.parent_task.async(transient: true, annotation: "monitor") do
172
+ while (event = queue.dequeue)
173
+ block.call(event)
174
+ end
175
+ rescue Async::Stop
176
+ ensure
177
+ @engine.monitor_queue = nil
178
+ block.call(MonitorEvent.new(type: :monitor_stopped))
179
+ end
180
+ end
181
+ end
182
+
183
+
144
184
  # Disable auto-reconnect for connected endpoints.
145
185
  def reconnect_enabled=(val)
146
186
  @engine.reconnect_enabled = val
@@ -226,13 +266,14 @@ module OMQ
226
266
  #
227
267
  def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
228
268
  send_timeout: nil, recv_timeout: nil, conflate: false,
229
- backend: nil)
269
+ on_mute: nil, backend: nil)
230
270
  @options = Options.new(linger: linger)
231
271
  @options.send_hwm = send_hwm if send_hwm
232
272
  @options.recv_hwm = recv_hwm if recv_hwm
233
273
  @options.send_timeout = send_timeout if send_timeout
234
274
  @options.recv_timeout = recv_timeout if recv_timeout
235
275
  @options.conflate = conflate
276
+ @options.on_mute = on_mute if on_mute
236
277
  @recv_buffer = []
237
278
  @recv_mutex = Mutex.new
238
279
  @engine = case backend
@@ -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