omq 0.5.0 → 0.6.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +195 -0
  3. data/README.md +21 -19
  4. data/exe/omq +6 -0
  5. data/lib/omq/cli/base_runner.rb +423 -0
  6. data/lib/omq/cli/channel.rb +8 -0
  7. data/lib/omq/cli/client_server.rb +106 -0
  8. data/lib/omq/cli/config.rb +51 -0
  9. data/lib/omq/cli/formatter.rb +75 -0
  10. data/lib/omq/cli/pair.rb +31 -0
  11. data/lib/omq/cli/peer.rb +8 -0
  12. data/lib/omq/cli/pipe.rb +249 -0
  13. data/lib/omq/cli/pub_sub.rb +14 -0
  14. data/lib/omq/cli/push_pull.rb +14 -0
  15. data/lib/omq/cli/radio_dish.rb +27 -0
  16. data/lib/omq/cli/req_rep.rb +77 -0
  17. data/lib/omq/cli/router_dealer.rb +70 -0
  18. data/lib/omq/cli/scatter_gather.rb +14 -0
  19. data/lib/omq/cli.rb +444 -0
  20. data/lib/omq/pub_sub.rb +2 -2
  21. data/lib/omq/radio_dish.rb +2 -2
  22. data/lib/omq/socket.rb +74 -27
  23. data/lib/omq/version.rb +1 -1
  24. data/lib/omq/zmtp/connection.rb +59 -12
  25. data/lib/omq/zmtp/engine.rb +179 -17
  26. data/lib/omq/zmtp/options.rb +4 -3
  27. data/lib/omq/zmtp/reactor.rb +25 -36
  28. data/lib/omq/zmtp/routing/channel.rb +14 -3
  29. data/lib/omq/zmtp/routing/fan_out.rb +52 -10
  30. data/lib/omq/zmtp/routing/pair.rb +14 -3
  31. data/lib/omq/zmtp/routing/peer.rb +28 -6
  32. data/lib/omq/zmtp/routing/push.rb +14 -7
  33. data/lib/omq/zmtp/routing/radio.rb +45 -12
  34. data/lib/omq/zmtp/routing/rep.rb +32 -13
  35. data/lib/omq/zmtp/routing/req.rb +1 -2
  36. data/lib/omq/zmtp/routing/round_robin.rb +72 -3
  37. data/lib/omq/zmtp/routing/router.rb +30 -10
  38. data/lib/omq/zmtp/routing/scatter.rb +16 -3
  39. data/lib/omq/zmtp/routing/server.rb +28 -6
  40. data/lib/omq/zmtp/routing/xsub.rb +7 -1
  41. data/lib/omq/zmtp/routing.rb +19 -0
  42. data/lib/omq/zmtp/transport/inproc.rb +48 -5
  43. data/lib/omq/zmtp/transport/ipc.rb +9 -7
  44. data/lib/omq/zmtp/transport/tcp.rb +14 -7
  45. data/lib/omq/zmtp/writable.rb +21 -4
  46. data/lib/omq.rb +7 -0
  47. metadata +18 -3
  48. data/exe/omqcat +0 -532
@@ -16,37 +16,44 @@ module OMQ
16
16
  init_round_robin(engine)
17
17
  end
18
18
 
19
+
19
20
  # @return [Async::LimitedQueue]
20
21
  #
21
22
  attr_reader :send_queue
22
23
 
24
+
23
25
  # SCATTER is write-only.
24
26
  #
25
27
  def recv_queue
26
28
  raise "SCATTER sockets cannot receive"
27
29
  end
28
30
 
31
+
29
32
  # @param connection [Connection]
30
33
  #
31
34
  def connection_added(connection)
32
35
  @connections << connection
33
36
  signal_connection_available
34
37
  start_send_pump unless @send_pump_started
35
- start_monitor(connection)
38
+ start_reaper(connection)
36
39
  end
37
40
 
41
+
38
42
  # @param connection [Connection]
39
43
  #
40
44
  def connection_removed(connection)
41
45
  @connections.delete(connection)
42
46
  end
43
47
 
48
+
44
49
  # @param parts [Array<String>]
45
50
  #
46
51
  def enqueue(parts)
47
52
  @send_queue.enqueue(parts)
48
53
  end
49
54
 
55
+
56
+ # Stops all background tasks (send pump, reapers).
50
57
  #
51
58
  def stop
52
59
  @tasks.each(&:stop)
@@ -55,8 +62,14 @@ module OMQ
55
62
 
56
63
  private
57
64
 
58
- def start_monitor(conn)
59
- @tasks << Reactor.spawn_pump do
65
+
66
+ # Detects peer disconnection on write-only sockets by
67
+ # blocking on a receive that only returns on disconnect.
68
+ #
69
+ # @param conn [Connection]
70
+ #
71
+ def start_reaper(conn)
72
+ @tasks << Reactor.spawn_pump(annotation: "reaper") do
60
73
  conn.receive_message
61
74
  rescue *ZMTP::CONNECTION_LOST
62
75
  @engine.connection_lost(conn)
@@ -21,6 +21,7 @@ module OMQ
21
21
  @connections_by_routing_id = {}
22
22
  @tasks = []
23
23
  @send_pump_started = false
24
+ @send_pump_idle = true
24
25
  end
25
26
 
26
27
  # @return [Async::LimitedQueue]
@@ -59,15 +60,36 @@ module OMQ
59
60
 
60
61
  private
61
62
 
63
+ def send_pump_idle? = @send_pump_idle
64
+
65
+
62
66
  def start_send_pump
63
67
  @send_pump_started = true
64
- @tasks << Reactor.spawn_pump do
68
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
65
69
  loop do
66
- parts = @send_queue.dequeue
67
- routing_id = parts.first
68
- conn = @connections_by_routing_id[routing_id]
69
- next unless conn # silently drop if peer gone
70
- conn.send_message(parts[1..])
70
+ @send_pump_idle = true
71
+ batch = [@send_queue.dequeue]
72
+ @send_pump_idle = false
73
+ Routing.drain_send_queue(@send_queue, batch)
74
+
75
+ written = Set.new
76
+ batch.each do |parts|
77
+ routing_id = parts.first
78
+ conn = @connections_by_routing_id[routing_id]
79
+ next unless conn # silently drop if peer gone
80
+ begin
81
+ conn.write_message(parts[1..])
82
+ written << conn
83
+ rescue *ZMTP::CONNECTION_LOST
84
+ # will be cleaned up
85
+ end
86
+ end
87
+
88
+ written.each do |conn|
89
+ conn.flush
90
+ rescue *ZMTP::CONNECTION_LOST
91
+ # will be cleaned up
92
+ end
71
93
  end
72
94
  end
73
95
  end
@@ -19,6 +19,7 @@ module OMQ
19
19
  @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
20
20
  @tasks = []
21
21
  @send_pump_started = false
22
+ @send_pump_idle = true
22
23
  end
23
24
 
24
25
  # @return [Async::LimitedQueue]
@@ -54,11 +55,16 @@ module OMQ
54
55
 
55
56
  private
56
57
 
58
+ def send_pump_idle? = @send_pump_idle
59
+
60
+
57
61
  def start_send_pump
58
62
  @send_pump_started = true
59
- @tasks << Reactor.spawn_pump do
63
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
60
64
  loop do
65
+ @send_pump_idle = true
61
66
  parts = @send_queue.dequeue
67
+ @send_pump_idle = false
62
68
  frame = parts.first&.b
63
69
  next if frame.nil? || frame.empty?
64
70
 
@@ -12,6 +12,25 @@ module OMQ
12
12
  # the socket's send/recv queues.
13
13
  #
14
14
  module Routing
15
+ # Maximum messages to drain from the send queue per flush cycle.
16
+ MAX_SEND_BATCH = 64
17
+
18
+ # Drains up to +max+ additional messages from +queue+ into +batch+
19
+ # without blocking. Call after the initial blocking dequeue.
20
+ #
21
+ # @param queue [Async::LimitedQueue]
22
+ # @param batch [Array]
23
+ # @param max [Integer]
24
+ # @return [void]
25
+ #
26
+ def self.drain_send_queue(queue, batch, max = MAX_SEND_BATCH)
27
+ while batch.size < max
28
+ msg = queue.dequeue(timeout: 0)
29
+ break unless msg
30
+ batch << msg
31
+ end
32
+ end
33
+
15
34
  # Returns the routing strategy class for a socket type.
16
35
  #
17
36
  # @param socket_type [Symbol] e.g. :PAIR, :REQ
@@ -46,6 +46,7 @@ module OMQ
46
46
  Listener.new(endpoint)
47
47
  end
48
48
 
49
+
49
50
  # Connects to a bound inproc endpoint.
50
51
  #
51
52
  # @param endpoint [String] e.g. "inproc://my-endpoint"
@@ -76,6 +77,7 @@ module OMQ
76
77
  establish_link(engine, bound_engine, endpoint)
77
78
  end
78
79
 
80
+
79
81
  # Removes a bound endpoint from the registry.
80
82
  #
81
83
  # @param endpoint [String]
@@ -85,6 +87,7 @@ module OMQ
85
87
  @mutex.synchronize { @registry.delete(endpoint) }
86
88
  end
87
89
 
90
+
88
91
  # Resets the registry. Used in tests.
89
92
  #
90
93
  # @return [void]
@@ -96,8 +99,17 @@ module OMQ
96
99
  end
97
100
  end
98
101
 
102
+
99
103
  private
100
104
 
105
+
106
+ # Wires up a client-server inproc pipe pair after validating
107
+ # that the two socket types are compatible.
108
+ #
109
+ # @param client_engine [Engine] the connecting engine
110
+ # @param server_engine [Engine] the bound engine
111
+ # @param endpoint [String] the inproc endpoint name
112
+ #
101
113
  def establish_link(client_engine, server_engine, endpoint)
102
114
  client_type = client_engine.socket_type
103
115
  server_type = server_engine.socket_type
@@ -138,8 +150,15 @@ module OMQ
138
150
  server_engine.connection_ready(server_pipe, endpoint: endpoint)
139
151
  end
140
152
 
153
+
154
+ # Spawns a background task that periodically retries
155
+ # #establish_link until the endpoint appears in the registry.
156
+ #
157
+ # @param endpoint [String] the inproc endpoint name
158
+ # @param engine [Engine] the connecting engine
159
+ #
141
160
  def start_connect_retry(endpoint, engine)
142
- Reactor.spawn_pump do
161
+ Reactor.spawn_pump(annotation: "reconnect") do
143
162
  ri = engine.options.reconnect_interval
144
163
  ivl = ri.is_a?(Range) ? ri.begin : ri
145
164
  loop do
@@ -161,12 +180,14 @@ module OMQ
161
180
  #
162
181
  attr_reader :endpoint
163
182
 
183
+
164
184
  # @param endpoint [String]
165
185
  #
166
186
  def initialize(endpoint)
167
187
  @endpoint = endpoint
168
188
  end
169
189
 
190
+
170
191
  # Stops the listener by removing it from the registry.
171
192
  #
172
193
  # @return [void]
@@ -183,32 +204,38 @@ module OMQ
183
204
  #
184
205
  # When a routing strategy sets {#direct_recv_queue} on a pipe,
185
206
  # {#send_message} enqueues directly into the peer's recv queue,
186
- # bypassing the intermediate internal queues and the recv pump
187
- # task entirely. This reduces inproc from 3 queue hops to 1.
207
+ # bypassing the intermediate pipe queues and the recv pump task.
208
+ # This reduces inproc from 3 queue hops to 2 (send_queue →
209
+ # recv_queue), eliminating the internal pipe queue in between.
188
210
  #
189
211
  class DirectPipe
190
212
  # @return [String] peer's socket type
191
213
  #
192
214
  attr_reader :peer_socket_type
193
215
 
216
+
194
217
  # @return [String] peer's identity
195
218
  #
196
219
  attr_reader :peer_identity
197
220
 
221
+
198
222
  # @return [DirectPipe, nil] the other end of this pipe pair
199
223
  #
200
224
  attr_accessor :peer
201
225
 
226
+
202
227
  # @return [Async::LimitedQueue, nil] when set, {#send_message}
203
228
  # enqueues directly here instead of using the internal queue
204
229
  #
205
230
  attr_reader :direct_recv_queue
206
231
 
232
+
207
233
  # @return [Proc, nil] optional transform applied before
208
234
  # enqueuing into {#direct_recv_queue}
209
235
  #
210
236
  attr_accessor :direct_recv_transform
211
237
 
238
+
212
239
  # @param send_queue [Async::Queue, nil] outgoing command queue
213
240
  # (nil for non-PUB/SUB types that don't exchange commands)
214
241
  # @param receive_queue [Async::Queue, nil] incoming command queue
@@ -227,6 +254,7 @@ module OMQ
227
254
  @pending_direct = nil
228
255
  end
229
256
 
257
+
230
258
  # Sets the direct recv queue. Drains any messages that were
231
259
  # buffered before the queue was available.
232
260
  #
@@ -240,6 +268,7 @@ module OMQ
240
268
  end
241
269
  end
242
270
 
271
+
243
272
  # Sends a multi-frame message.
244
273
  #
245
274
  # When {#direct_recv_queue} is set (inproc fast path), the
@@ -252,16 +281,27 @@ module OMQ
252
281
  def send_message(parts)
253
282
  raise IOError, "closed" if @closed
254
283
  if @direct_recv_queue
255
- msg = @direct_recv_transform ? @direct_recv_transform.call(parts) : parts
284
+ msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
256
285
  @direct_recv_queue.enqueue(msg)
257
286
  elsif @send_queue
258
287
  @send_queue.enqueue(parts)
259
288
  else
260
- msg = @direct_recv_transform ? @direct_recv_transform.call(parts) : parts
289
+ msg = @direct_recv_transform ? @direct_recv_transform.call(parts).freeze : parts
261
290
  (@pending_direct ||= []) << msg
262
291
  end
263
292
  end
264
293
 
294
+
295
+ alias write_message send_message
296
+
297
+
298
+ # No-op — inproc has no IO buffer to flush.
299
+ #
300
+ # @return [void]
301
+ #
302
+ def flush = nil
303
+
304
+
265
305
  # Receives a multi-frame message.
266
306
  #
267
307
  # @return [Array<String>]
@@ -273,6 +313,7 @@ module OMQ
273
313
  msg
274
314
  end
275
315
 
316
+
276
317
  # Sends a command via the internal command queue.
277
318
  # Only available for PUB/SUB-family pipes.
278
319
  #
@@ -284,6 +325,7 @@ module OMQ
284
325
  @send_queue.enqueue([:command, command])
285
326
  end
286
327
 
328
+
287
329
  # Reads one command frame from the internal command queue.
288
330
  # Used by PUB/XPUB subscription listeners.
289
331
  #
@@ -300,6 +342,7 @@ module OMQ
300
342
  end
301
343
  end
302
344
 
345
+
303
346
  # Closes this pipe end.
304
347
  #
305
348
  # @return [void]
@@ -28,16 +28,11 @@ module OMQ
28
28
 
29
29
  server = UNIXServer.new(sock_path)
30
30
 
31
- accept_task = Reactor.spawn_pump do
31
+ accept_task = Reactor.spawn_pump(annotation: "ipc accept #{endpoint}") do
32
32
  loop do
33
33
  client = server.accept
34
- Reactor.run do
34
+ Async::Task.current.defer_stop do
35
35
  engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: endpoint)
36
- rescue ProtocolError, *ZMTP::CONNECTION_LOST
37
- # peer disconnected during handshake
38
- rescue
39
- client&.close rescue nil
40
- raise
41
36
  end
42
37
  end
43
38
  rescue IOError
@@ -92,6 +87,12 @@ module OMQ
92
87
  #
93
88
  attr_reader :endpoint
94
89
 
90
+
91
+ # @param endpoint [String] the IPC endpoint URI
92
+ # @param server [UNIXServer]
93
+ # @param accept_task [#stop] the accept loop handle
94
+ # @param path [String] filesystem or abstract namespace path
95
+ #
95
96
  def initialize(endpoint, server, accept_task, path)
96
97
  @endpoint = endpoint
97
98
  @server = server
@@ -99,6 +100,7 @@ module OMQ
99
100
  @path = path
100
101
  end
101
102
 
103
+
102
104
  # Stops the listener.
103
105
  #
104
106
  def stop
@@ -25,16 +25,11 @@ module OMQ
25
25
  host_part = host.include?(":") ? "[#{host}]" : host
26
26
  resolved = "tcp://#{host_part}:#{actual_port}"
27
27
 
28
- accept_task = Reactor.spawn_pump do
28
+ accept_task = Reactor.spawn_pump(annotation: "tcp accept #{resolved}") do
29
29
  loop do
30
30
  client = server.accept
31
- Reactor.run do
31
+ Async::Task.current.defer_stop do
32
32
  engine.handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: resolved)
33
- rescue ProtocolError, *ZMTP::CONNECTION_LOST
34
- # peer disconnected during handshake
35
- rescue
36
- client&.close rescue nil
37
- raise
38
33
  end
39
34
  end
40
35
  rescue IOError
@@ -58,6 +53,11 @@ module OMQ
58
53
 
59
54
  private
60
55
 
56
+ # Parses a TCP endpoint URI into host and port.
57
+ #
58
+ # @param endpoint [String]
59
+ # @return [Array(String, Integer)]
60
+ #
61
61
  def parse_endpoint(endpoint)
62
62
  uri = URI.parse(endpoint)
63
63
  [uri.hostname, uri.port]
@@ -75,6 +75,12 @@ module OMQ
75
75
  #
76
76
  attr_reader :port
77
77
 
78
+
79
+ # @param endpoint [String] resolved endpoint URI
80
+ # @param server [TCPServer]
81
+ # @param accept_task [#stop] the accept loop handle
82
+ # @param port [Integer] bound port number
83
+ #
78
84
  def initialize(endpoint, server, accept_task, port)
79
85
  @endpoint = endpoint
80
86
  @server = server
@@ -82,6 +88,7 @@ module OMQ
82
88
  @port = port
83
89
  end
84
90
 
91
+
85
92
  # Stops the listener.
86
93
  #
87
94
  def stop
@@ -14,10 +14,7 @@ module OMQ
14
14
  # @raise [IO::TimeoutError] if write_timeout exceeded
15
15
  #
16
16
  def send(message)
17
- parts = message.is_a?(Array) ? message : [message]
18
- raise ArgumentError, "message has no parts" if parts.empty?
19
- parts = parts.map { |p| p.b.freeze }
20
-
17
+ parts = freeze_message(message)
21
18
  with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
22
19
  self
23
20
  end
@@ -31,6 +28,26 @@ module OMQ
31
28
  send(message)
32
29
  end
33
30
 
31
+ private
32
+
33
+ # Converts a message into a frozen array of frozen binary strings.
34
+ #
35
+ # @param message [String, Array<String>]
36
+ # @return [Array<String>] frozen array of frozen binary strings
37
+ #
38
+ def freeze_message(message)
39
+ parts = message.is_a?(Array) ? message : [message]
40
+ raise ArgumentError, "message has no parts" if parts.empty?
41
+ if parts.frozen?
42
+ parts = parts.map { |p| p.to_str.b.freeze }
43
+ else
44
+ parts.map! { |p| p.to_str.b.freeze }
45
+ end
46
+ parts.freeze
47
+ end
48
+
49
+ public
50
+
34
51
  # Waits until the socket is writable.
35
52
  #
36
53
  # @param timeout [Numeric, nil] timeout in seconds
data/lib/omq.rb CHANGED
@@ -10,6 +10,13 @@
10
10
  #
11
11
 
12
12
  require_relative "omq/version"
13
+
14
+ module OMQ
15
+ # Raised when an internal pump task crashes unexpectedly.
16
+ # The socket is no longer usable; the original error is available via #cause.
17
+ #
18
+ class SocketDeadError < RuntimeError; end
19
+ end
13
20
  require_relative "omq/zmtp"
14
21
  require_relative "omq/socket"
15
22
  require_relative "omq/req_rep"
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -42,16 +42,31 @@ description: Pure Ruby implementation of the ZMTP 3.1 wire protocol (ZeroMQ) usi
42
42
  email:
43
43
  - paddor@gmail.com
44
44
  executables:
45
- - omqcat
45
+ - omq
46
46
  extensions: []
47
47
  extra_rdoc_files: []
48
48
  files:
49
49
  - CHANGELOG.md
50
50
  - LICENSE
51
51
  - README.md
52
- - exe/omqcat
52
+ - exe/omq
53
53
  - lib/omq.rb
54
54
  - lib/omq/channel.rb
55
+ - lib/omq/cli.rb
56
+ - lib/omq/cli/base_runner.rb
57
+ - lib/omq/cli/channel.rb
58
+ - lib/omq/cli/client_server.rb
59
+ - lib/omq/cli/config.rb
60
+ - lib/omq/cli/formatter.rb
61
+ - lib/omq/cli/pair.rb
62
+ - lib/omq/cli/peer.rb
63
+ - lib/omq/cli/pipe.rb
64
+ - lib/omq/cli/pub_sub.rb
65
+ - lib/omq/cli/push_pull.rb
66
+ - lib/omq/cli/radio_dish.rb
67
+ - lib/omq/cli/req_rep.rb
68
+ - lib/omq/cli/router_dealer.rb
69
+ - lib/omq/cli/scatter_gather.rb
55
70
  - lib/omq/client_server.rb
56
71
  - lib/omq/pair.rb
57
72
  - lib/omq/peer.rb