omq 0.17.9 → 0.19.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.
data/lib/omq/reactor.rb CHANGED
@@ -15,12 +15,15 @@ module OMQ
15
15
  # thread are dispatched to the IO thread via {.run}.
16
16
  #
17
17
  module Reactor
18
+ THREAD_NAME = 'omq-io'
19
+
18
20
  @mutex = Mutex.new
19
21
  @thread = nil
20
22
  @root_task = nil
21
23
  @work_queue = nil
22
24
  @lingers = Hash.new(0) # linger value → count of active sockets
23
25
 
26
+
24
27
  class << self
25
28
  # Returns the root Async task inside the shared IO thread.
26
29
  # Starts the thread exactly once (double-checked lock).
@@ -29,15 +32,19 @@ module OMQ
29
32
  #
30
33
  def root_task
31
34
  return @root_task if @root_task
35
+
32
36
  @mutex.synchronize do
33
37
  return @root_task if @root_task
34
- ready = Thread::Queue.new
35
- @work_queue = Async::Queue.new
36
- @thread = Thread.new { run_reactor(ready) }
37
- @thread.name = "omq-io"
38
- @root_task = ready.pop
38
+
39
+ ready = Thread::Queue.new
40
+ @work_queue = Async::Queue.new
41
+ @thread = Thread.new { run_reactor(ready) }
42
+ @thread.name = THREAD_NAME
43
+ @root_task = ready.pop
44
+
39
45
  at_exit { stop! }
40
46
  end
47
+
41
48
  @root_task
42
49
  end
43
50
 
@@ -50,16 +57,20 @@ module OMQ
50
57
  #
51
58
  # @return [Object] the block's return value
52
59
  #
53
- def run(&block)
54
- if Async::Task.current?
55
- yield
60
+ def run(timeout: nil, &block)
61
+ task = Async::Task.current?
62
+
63
+ if task
64
+ if timeout
65
+ task.with_timeout(timeout, IO::TimeoutError) { yield }
66
+ else
67
+ yield
68
+ end
56
69
  else
57
- result = Thread::Queue.new
70
+ result = Async::Promise.new
58
71
  root_task # ensure started
59
- @work_queue.push([block, result])
60
- status, value = result.pop
61
- raise value if status == :error
62
- value
72
+ @work_queue.push([block, result, timeout])
73
+ result.wait
63
74
  end
64
75
  end
65
76
 
@@ -90,17 +101,21 @@ module OMQ
90
101
  #
91
102
  def stop!
92
103
  return unless @thread&.alive?
104
+
93
105
  max_linger = @lingers.empty? ? 0 : @lingers.keys.max
94
106
  @work_queue&.push(nil)
95
107
  @thread&.join(max_linger + 1)
108
+
96
109
  @thread = nil
97
110
  @root_task = nil
98
111
  @work_queue = nil
99
112
  @lingers = Hash.new(0)
100
113
  end
101
114
 
115
+
102
116
  private
103
117
 
118
+
104
119
  # Runs the shared Async reactor.
105
120
  #
106
121
  # Processes work items dispatched via {.run} while engine
@@ -111,18 +126,24 @@ module OMQ
111
126
  def run_reactor(ready)
112
127
  Async do |task|
113
128
  ready.push(task)
129
+
114
130
  loop do
115
- item = @work_queue.dequeue
116
- break if item.nil?
117
- block, result = item
118
- task.async do
119
- result.push([:ok, block.call])
120
- rescue => e
121
- result.push([:error, e])
131
+ item = @work_queue.dequeue or break
132
+ block, result, timeout = item
133
+
134
+ task.async do |t|
135
+ if timeout
136
+ result.fulfill do
137
+ t.with_timeout(timeout, IO::TimeoutError) { block.call }
138
+ end
139
+ else
140
+ result.fulfill { block.call }
141
+ end
122
142
  end
123
143
  end
124
144
  end
125
145
  end
146
+
126
147
  end
127
148
  end
128
149
  end
data/lib/omq/readable.rb CHANGED
@@ -14,7 +14,9 @@ module OMQ
14
14
  # @raise [IO::TimeoutError] if read_timeout exceeded
15
15
  #
16
16
  def receive
17
- Reactor.run { with_timeout(@options.read_timeout) { @engine.dequeue_recv } }
17
+ Reactor.run timeout: @options.read_timeout do |task|
18
+ @engine.dequeue_recv
19
+ end
18
20
  end
19
21
 
20
22
 
data/lib/omq/req_rep.rb CHANGED
@@ -12,8 +12,8 @@ module OMQ
12
12
  # @param backend [Symbol, nil] :ruby (default) or :ffi
13
13
  #
14
14
  def initialize(endpoints = nil, linger: 0, backend: nil)
15
- _init_engine(:REQ, linger: linger, backend: backend)
16
- _attach(endpoints, default: :connect)
15
+ init_engine(:REQ, linger: linger, backend: backend)
16
+ attach_endpoints(endpoints, default: :connect)
17
17
  end
18
18
  end
19
19
 
@@ -29,8 +29,8 @@ module OMQ
29
29
  # @param backend [Symbol, nil] :ruby (default) or :ffi
30
30
  #
31
31
  def initialize(endpoints = nil, linger: 0, backend: nil)
32
- _init_engine(:REP, linger: linger, backend: backend)
33
- _attach(endpoints, default: :bind)
32
+ init_engine(:REP, linger: linger, backend: backend)
33
+ attach_endpoints(endpoints, default: :bind)
34
34
  end
35
35
  end
36
36
  end
@@ -12,8 +12,8 @@ module OMQ
12
12
  # @param backend [Symbol, nil] :ruby (default) or :ffi
13
13
  #
14
14
  def initialize(endpoints = nil, linger: 0, backend: nil)
15
- _init_engine(:DEALER, linger: linger, backend: backend)
16
- _attach(endpoints, default: :connect)
15
+ init_engine(:DEALER, linger: linger, backend: backend)
16
+ attach_endpoints(endpoints, default: :connect)
17
17
  end
18
18
  end
19
19
 
@@ -29,8 +29,8 @@ module OMQ
29
29
  # @param backend [Symbol, nil] :ruby (default) or :ffi
30
30
  #
31
31
  def initialize(endpoints = nil, linger: 0, backend: nil)
32
- _init_engine(:ROUTER, linger: linger, backend: backend)
33
- _attach(endpoints, default: :bind)
32
+ init_engine(:ROUTER, linger: linger, backend: backend)
33
+ attach_endpoints(endpoints, default: :bind)
34
34
  end
35
35
 
36
36
 
@@ -21,15 +21,19 @@ module OMQ
21
21
  loop do
22
22
  batch = [q.dequeue]
23
23
  Routing.drain_send_queue(q, batch)
24
+
24
25
  if batch.size == 1
25
- conn.write_message(batch[0])
26
+ conn.write_message batch.first
26
27
  else
27
- conn.write_messages(batch)
28
+ conn.write_messages batch
28
29
  end
30
+
29
31
  conn.flush
30
- batch.each { |parts| engine.emit_verbose_monitor_event(:message_sent, parts: parts) }
32
+
33
+ batch.each { |parts| engine.emit_verbose_msg_sent(conn, parts) }
31
34
  end
32
35
  end
36
+
33
37
  tasks << task
34
38
  task
35
39
  end
@@ -10,6 +10,7 @@ module OMQ
10
10
  include RoundRobin
11
11
  include FairRecv
12
12
 
13
+
13
14
  # @param engine [Engine]
14
15
  #
15
16
  def initialize(engine)
@@ -24,6 +25,7 @@ module OMQ
24
25
  #
25
26
  attr_reader :recv_queue
26
27
 
28
+
27
29
  # @param connection [Connection]
28
30
  #
29
31
  def connection_added(connection)
@@ -104,8 +104,10 @@ module OMQ
104
104
  @drain.empty? && @queues.all?(&:empty?)
105
105
  end
106
106
 
107
+
107
108
  private
108
109
 
110
+
109
111
  # Drains orphaned queues first (preserves FIFO for disconnected
110
112
  # peers), then tries each active queue once in round-robin order.
111
113
  #
@@ -162,16 +164,25 @@ module OMQ
162
164
  # @param timeout [Numeric, nil] dequeue timeout
163
165
  # @return [Array<String>, nil]
164
166
  #
165
- def dequeue(timeout: nil) = @inner.dequeue(timeout: timeout)
167
+ def dequeue(timeout: nil)
168
+ @inner.dequeue(timeout: timeout)
169
+ end
170
+
166
171
 
167
172
  # @return [Boolean]
168
173
  #
169
- def empty? = @inner.empty?
174
+ def empty?
175
+ @inner.empty?
176
+ end
177
+
170
178
 
171
179
  # @param item [Object, nil]
172
180
  # @return [void]
173
181
  #
174
- def push(item) = @inner.push(item)
182
+ def push(item)
183
+ @inner.push(item)
184
+ end
185
+
175
186
  end
176
187
  end
177
188
  end
@@ -17,18 +17,25 @@ module OMQ
17
17
  # their #initialize.
18
18
  #
19
19
  module FanOut
20
+ # Shared frozen empty binary string to avoid repeated allocations.
21
+ EMPTY_BINARY = ::Protocol::ZMTP::Codec::EMPTY_BINARY
22
+
23
+
20
24
  # @return [Async::Promise] resolves when the first subscriber joins
21
25
  #
22
26
  attr_reader :subscriber_joined
23
27
 
28
+
24
29
  # @return [Boolean] true when all per-connection send queues are empty
25
30
  #
26
31
  def send_queues_drained?
27
32
  @conn_queues.values.all?(&:empty?)
28
33
  end
29
34
 
35
+
30
36
  private
31
37
 
38
+
32
39
  def init_fan_out(engine)
33
40
  @connections = Set.new
34
41
  @subscriptions = {} # connection => Set of prefixes
@@ -156,6 +163,15 @@ module OMQ
156
163
  end
157
164
 
158
165
 
166
+ # Send pump variant for non-conflate fan-out: dequeues, batch-drains,
167
+ # writes each subscribed message, then flushes once.
168
+ #
169
+ # @param conn [Connection]
170
+ # @param q [Async::LimitedQueue, DropQueue]
171
+ # @param use_wire [Boolean] true iff the encoded wire bytes can
172
+ # be shared across peers (unencrypted ZMTP)
173
+ # @return [Async::Task]
174
+ #
159
175
  def start_conn_send_pump_normal(conn, q, use_wire)
160
176
  @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
161
177
  loop do
@@ -163,38 +179,59 @@ module OMQ
163
179
  Routing.drain_send_queue(q, batch)
164
180
  if write_matching_batch(conn, batch, use_wire)
165
181
  conn.flush
166
- batch.each { |parts| @engine.emit_verbose_monitor_event(:message_sent, parts: parts) }
182
+ batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
167
183
  end
168
184
  end
169
185
  end
170
186
  end
171
187
 
172
188
 
189
+ # Writes every batch entry whose topic matches a subscription on
190
+ # +conn+ to +conn+'s write buffer. Does not flush.
191
+ #
192
+ # @return [Boolean] true iff at least one message was written
193
+ #
173
194
  def write_matching_batch(conn, batch, use_wire)
174
195
  sent = false
175
196
  batch.each do |parts|
176
197
  next unless subscribed?(conn, parts.first || EMPTY_BINARY)
177
- use_wire ? conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts)) : conn.write_message(parts)
198
+ if use_wire
199
+ conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts))
200
+ else
201
+ conn.write_message(parts)
202
+ end
178
203
  sent = true
179
204
  end
180
205
  sent
181
206
  end
182
207
 
183
208
 
209
+ # Send pump variant for conflate mode: keeps only the latest
210
+ # subscribed message per batch. Stale duplicates are dropped.
211
+ #
212
+ # @param conn [Connection]
213
+ # @param q [Async::LimitedQueue, DropQueue]
214
+ # @return [Async::Task]
215
+ #
184
216
  def start_conn_send_pump_conflate(conn, q)
185
217
  @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
186
218
  loop do
187
219
  batch = [q.dequeue]
188
220
  Routing.drain_send_queue(q, batch)
221
+
189
222
  # Keep only the latest message that matches the subscription.
190
- latest = batch.reverse.find { |parts| subscribed?(conn, parts.first || EMPTY_BINARY) }
223
+ latest = batch.reverse.find do |parts|
224
+ subscribed?(conn, parts.first || EMPTY_BINARY)
225
+ end
191
226
  next unless latest
227
+
192
228
  conn.write_message(latest)
193
229
  conn.flush
194
- @engine.emit_verbose_monitor_event(:message_sent, parts: latest)
230
+ @engine.emit_verbose_msg_sent(conn, latest)
195
231
  end
196
232
  end
197
233
  end
234
+
198
235
  end
199
236
  end
200
237
  end
@@ -96,7 +96,7 @@ module OMQ
96
96
  conn.write_messages(batch)
97
97
  end
98
98
  conn.flush
99
- batch.each { |parts| @engine.emit_verbose_monitor_event(:message_sent, parts: parts) }
99
+ batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
100
100
  end
101
101
  end
102
102
  @tasks << @send_pump
@@ -10,6 +10,10 @@ module OMQ
10
10
  include RoundRobin
11
11
  include FairRecv
12
12
 
13
+ # Shared frozen empty binary string to avoid repeated allocations.
14
+ EMPTY_BINARY = ::Protocol::ZMTP::Codec::EMPTY_BINARY
15
+
16
+
13
17
  # @param engine [Engine]
14
18
  #
15
19
  def initialize(engine)
@@ -64,11 +68,16 @@ module OMQ
64
68
  @tasks.clear
65
69
  end
66
70
 
71
+
67
72
  private
68
73
 
74
+
69
75
  # REQ prepends empty delimiter frame on the wire.
70
76
  #
71
- def transform_send(parts) = [EMPTY_BINARY, *parts]
77
+ def transform_send(parts)
78
+ [EMPTY_BINARY, *parts]
79
+ end
80
+
72
81
  end
73
82
  end
74
83
  end
@@ -134,7 +134,7 @@ module OMQ
134
134
  ensure
135
135
  @in_flight -= batch.size
136
136
  end
137
- batch.each { |parts| @engine.emit_verbose_monitor_event(:message_sent, parts: parts) }
137
+ batch.each { |parts| @engine.emit_verbose_msg_sent(conn, parts) }
138
138
  Async::Task.current.yield
139
139
  end
140
140
  end
data/lib/omq/routing.rb CHANGED
@@ -15,16 +15,17 @@ module OMQ
15
15
  # the socket's send/recv queues.
16
16
  #
17
17
  module Routing
18
- # Shared frozen empty binary string to avoid repeated allocations.
19
- EMPTY_BINARY = "".b.freeze
20
-
21
-
22
18
  # Plugin registry for socket types not built into omq.
23
19
  # Populated by sister gems via +Routing.register+.
24
20
  #
25
21
  @registry = {}
26
22
 
23
+
27
24
  class << self
25
+ # @return [Hash{Symbol => Class}] plugin registry
26
+ attr_reader :registry
27
+
28
+
28
29
  # Registers a routing strategy class for a socket type.
29
30
  # Called by omq-draft (and other plugins) at require time.
30
31
  #
data/lib/omq/socket.rb CHANGED
@@ -88,7 +88,7 @@ module OMQ
88
88
  def bind(endpoint, parent: nil)
89
89
  ensure_parent_task(parent: parent)
90
90
  Reactor.run do
91
- @engine.bind(endpoint)
91
+ @engine.bind(endpoint) # TODO: use timeout?
92
92
  @last_tcp_port = @engine.last_tcp_port
93
93
  end
94
94
  end
@@ -102,7 +102,7 @@ module OMQ
102
102
  #
103
103
  def connect(endpoint, parent: nil)
104
104
  ensure_parent_task(parent: parent)
105
- Reactor.run { @engine.connect(endpoint) }
105
+ Reactor.run { @engine.connect(endpoint) } # TODO: use timeout?
106
106
  end
107
107
 
108
108
 
@@ -112,7 +112,7 @@ module OMQ
112
112
  # @return [void]
113
113
  #
114
114
  def disconnect(endpoint)
115
- Reactor.run { @engine.disconnect(endpoint) }
115
+ Reactor.run { @engine.disconnect(endpoint) } # TODO: use timeout?
116
116
  end
117
117
 
118
118
 
@@ -122,7 +122,7 @@ module OMQ
122
122
  # @return [void]
123
123
  #
124
124
  def unbind(endpoint)
125
- Reactor.run { @engine.unbind(endpoint) }
125
+ Reactor.run { @engine.unbind(endpoint) } # TODO: use timeout?
126
126
  end
127
127
 
128
128
 
@@ -183,9 +183,11 @@ module OMQ
183
183
  #
184
184
  def monitor(verbose: false, &block)
185
185
  ensure_parent_task
186
- queue = Async::LimitedQueue.new(64)
187
- @engine.monitor_queue = queue
186
+
187
+ queue = Async::LimitedQueue.new(64)
188
+ @engine.monitor_queue = queue
188
189
  @engine.verbose_monitor = verbose
190
+
189
191
  Reactor.run do
190
192
  @engine.parent_task.async(transient: true, annotation: "monitor") do
191
193
  while (event = queue.dequeue)
@@ -218,7 +220,7 @@ module OMQ
218
220
  # @return [nil]
219
221
  #
220
222
  def close
221
- Reactor.run { @engine.close }
223
+ Reactor.run { @engine.close } # TODO: use timeout?
222
224
  nil
223
225
  end
224
226
 
@@ -230,7 +232,7 @@ module OMQ
230
232
  # @return [nil]
231
233
  #
232
234
  def stop
233
- Reactor.run { @engine.stop }
235
+ Reactor.run { @engine.stop } # TODO: use timeout?
234
236
  nil
235
237
  end
236
238
 
@@ -253,42 +255,13 @@ module OMQ
253
255
  end
254
256
 
255
257
 
256
- private
257
-
258
-
259
- # Runs a block with a timeout. Uses Async's with_timeout if inside
260
- # a reactor, otherwise falls back to Timeout.timeout.
261
- #
262
- # @param seconds [Numeric]
263
- # @raise [IO::TimeoutError]
264
- #
265
- def with_timeout(seconds, &block)
266
- return yield if seconds.nil?
267
- if Async::Task.current?
268
- Async::Task.current.with_timeout(seconds, &block)
269
- else
270
- Timeout.timeout(seconds, &block)
271
- end
272
- rescue Async::TimeoutError, Timeout::Error
273
- raise IO::TimeoutError, "timed out"
274
- end
275
-
276
-
277
- # Sets the engine's parent task before the first bind or connect.
278
- # Must be called OUTSIDE Reactor.run so that non-Async callers
279
- # get the IO thread's root task, not an ephemeral work task.
280
- #
281
- def ensure_parent_task(parent: nil)
282
- @engine.capture_parent_task(parent: parent)
283
- end
284
-
285
-
286
- # Connects or binds based on endpoint prefix convention.
258
+ # Connects or binds based on endpoint prefix convention. Called
259
+ # from subclass initializers (including out-of-tree socket types).
287
260
  #
288
261
  # @param endpoints [String, nil]
289
262
  # @param default [Symbol] :connect or :bind
290
263
  #
291
- def _attach(endpoints, default:)
264
+ def attach_endpoints(endpoints, default:)
292
265
  return unless endpoints
293
266
  case endpoints
294
267
  when /\A@(.+)\z/
@@ -301,29 +274,42 @@ module OMQ
301
274
  end
302
275
 
303
276
 
304
- # Initializes engine and options for a socket type.
277
+ # Initializes engine and options for a socket type. Called from
278
+ # subclass initializers (including out-of-tree socket types).
305
279
  #
306
280
  # @param socket_type [Symbol]
307
281
  # @param linger [Integer]
308
282
  #
309
- def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
310
- send_timeout: nil, recv_timeout: nil, conflate: false,
311
- on_mute: nil, backend: nil)
283
+ def init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
284
+ send_timeout: nil, recv_timeout: nil, conflate: false,
285
+ on_mute: nil, backend: nil)
312
286
  @options = Options.new(linger: linger)
313
- @options.send_hwm = send_hwm if send_hwm
314
- @options.recv_hwm = recv_hwm if recv_hwm
315
- @options.send_timeout = send_timeout if send_timeout
316
- @options.recv_timeout = recv_timeout if recv_timeout
317
- @options.conflate = conflate
318
- @options.on_mute = on_mute if on_mute
319
- @engine = case backend
320
- when nil, :ruby
321
- Engine.new(socket_type, @options)
322
- when :ffi
323
- FFI::Engine.new(socket_type, @options)
324
- else
325
- raise ArgumentError, "unknown backend: #{backend}"
326
- end
287
+ @options.send_hwm = send_hwm if send_hwm
288
+ @options.recv_hwm = recv_hwm if recv_hwm
289
+ @options.send_timeout = send_timeout if send_timeout
290
+ @options.recv_timeout = recv_timeout if recv_timeout
291
+ @options.conflate = conflate
292
+ @options.on_mute = on_mute if on_mute
293
+ @engine = case backend
294
+ when nil, :ruby
295
+ Engine.new(socket_type, @options)
296
+ when :ffi
297
+ FFI::Engine.new(socket_type, @options)
298
+ else
299
+ raise ArgumentError, "unknown backend: #{backend}"
300
+ end
301
+ end
302
+
303
+
304
+ private
305
+
306
+
307
+ # Sets the engine's parent task before the first bind or connect.
308
+ # Must be called OUTSIDE Reactor.run so that non-Async callers
309
+ # get the IO thread's root task, not an ephemeral work task.
310
+ #
311
+ def ensure_parent_task(parent: nil)
312
+ @engine.capture_parent_task(parent: parent)
327
313
  end
328
314
  end
329
315
  end
@@ -112,6 +112,7 @@ module OMQ
112
112
  #
113
113
  def encrypted? = false
114
114
 
115
+
115
116
  # No-op — inproc has no IO buffer to flush.
116
117
  #
117
118
  # @return [nil]
@@ -127,11 +128,16 @@ module OMQ
127
128
  def receive_message
128
129
  loop do
129
130
  item = @receive_queue.dequeue
131
+
130
132
  raise EOFError, "connection closed" if item.nil?
133
+
131
134
  if item.is_a?(Array) && item.first == :command
132
- yield Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true) if block_given?
135
+ if block_given?
136
+ yield Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
137
+ end
133
138
  next
134
139
  end
140
+
135
141
  return item
136
142
  end
137
143
  end
@@ -157,6 +163,7 @@ module OMQ
157
163
  loop do
158
164
  item = @receive_queue.dequeue
159
165
  raise EOFError, "connection closed" if item.nil?
166
+
160
167
  if item.is_a?(Array) && item.first == :command
161
168
  return Protocol::ZMTP::Codec::Frame.new(item[1].to_body, command: true)
162
169
  end
@@ -174,11 +181,18 @@ module OMQ
174
181
  @send_queue&.enqueue(nil) # close sentinel
175
182
  end
176
183
 
184
+
177
185
  private
178
186
 
187
+
179
188
  def apply_transform(parts)
180
- @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
189
+ if @direct_recv_transform
190
+ @direct_recv_transform.call(parts).freeze
191
+ else
192
+ parts
193
+ end
181
194
  end
195
+
182
196
  end
183
197
  end
184
198
  end