omq 0.9.0 → 0.11.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -0
  3. data/README.md +28 -3
  4. data/lib/omq/channel.rb +5 -5
  5. data/lib/omq/client_server.rb +10 -10
  6. data/lib/omq/engine.rb +702 -0
  7. data/lib/omq/options.rb +48 -0
  8. data/lib/omq/pair.rb +4 -4
  9. data/lib/omq/peer.rb +5 -5
  10. data/lib/omq/pub_sub.rb +18 -18
  11. data/lib/omq/push_pull.rb +6 -6
  12. data/lib/omq/queue_interface.rb +73 -0
  13. data/lib/omq/radio_dish.rb +6 -6
  14. data/lib/omq/reactor.rb +128 -0
  15. data/lib/omq/readable.rb +44 -0
  16. data/lib/omq/req_rep.rb +8 -8
  17. data/lib/omq/router_dealer.rb +8 -8
  18. data/lib/omq/routing/channel.rb +83 -0
  19. data/lib/omq/routing/client.rb +56 -0
  20. data/lib/omq/routing/dealer.rb +57 -0
  21. data/lib/omq/routing/dish.rb +78 -0
  22. data/lib/omq/routing/fan_out.rb +140 -0
  23. data/lib/omq/routing/gather.rb +46 -0
  24. data/lib/omq/routing/pair.rb +86 -0
  25. data/lib/omq/routing/peer.rb +101 -0
  26. data/lib/omq/routing/pub.rb +60 -0
  27. data/lib/omq/routing/pull.rb +46 -0
  28. data/lib/omq/routing/push.rb +81 -0
  29. data/lib/omq/routing/radio.rb +150 -0
  30. data/lib/omq/routing/rep.rb +101 -0
  31. data/lib/omq/routing/req.rb +65 -0
  32. data/lib/omq/routing/round_robin.rb +168 -0
  33. data/lib/omq/routing/router.rb +110 -0
  34. data/lib/omq/routing/scatter.rb +82 -0
  35. data/lib/omq/routing/server.rb +101 -0
  36. data/lib/omq/routing/sub.rb +78 -0
  37. data/lib/omq/routing/xpub.rb +72 -0
  38. data/lib/omq/routing/xsub.rb +83 -0
  39. data/lib/omq/routing.rb +66 -0
  40. data/lib/omq/scatter_gather.rb +8 -8
  41. data/lib/omq/single_frame.rb +18 -0
  42. data/lib/omq/socket.rb +32 -11
  43. data/lib/omq/transport/inproc.rb +355 -0
  44. data/lib/omq/transport/ipc.rb +117 -0
  45. data/lib/omq/transport/tcp.rb +111 -0
  46. data/lib/omq/transport/tls.rb +146 -0
  47. data/lib/omq/version.rb +1 -1
  48. data/lib/omq/writable.rb +66 -0
  49. data/lib/omq.rb +64 -4
  50. metadata +34 -33
  51. data/lib/omq/zmtp/engine.rb +0 -551
  52. data/lib/omq/zmtp/options.rb +0 -48
  53. data/lib/omq/zmtp/reactor.rb +0 -131
  54. data/lib/omq/zmtp/readable.rb +0 -29
  55. data/lib/omq/zmtp/routing/channel.rb +0 -81
  56. data/lib/omq/zmtp/routing/client.rb +0 -56
  57. data/lib/omq/zmtp/routing/dealer.rb +0 -57
  58. data/lib/omq/zmtp/routing/dish.rb +0 -80
  59. data/lib/omq/zmtp/routing/fan_out.rb +0 -131
  60. data/lib/omq/zmtp/routing/gather.rb +0 -48
  61. data/lib/omq/zmtp/routing/pair.rb +0 -84
  62. data/lib/omq/zmtp/routing/peer.rb +0 -100
  63. data/lib/omq/zmtp/routing/pub.rb +0 -62
  64. data/lib/omq/zmtp/routing/pull.rb +0 -48
  65. data/lib/omq/zmtp/routing/push.rb +0 -80
  66. data/lib/omq/zmtp/routing/radio.rb +0 -139
  67. data/lib/omq/zmtp/routing/rep.rb +0 -101
  68. data/lib/omq/zmtp/routing/req.rb +0 -65
  69. data/lib/omq/zmtp/routing/round_robin.rb +0 -143
  70. data/lib/omq/zmtp/routing/router.rb +0 -109
  71. data/lib/omq/zmtp/routing/scatter.rb +0 -81
  72. data/lib/omq/zmtp/routing/server.rb +0 -100
  73. data/lib/omq/zmtp/routing/sub.rb +0 -80
  74. data/lib/omq/zmtp/routing/xpub.rb +0 -74
  75. data/lib/omq/zmtp/routing/xsub.rb +0 -86
  76. data/lib/omq/zmtp/routing.rb +0 -65
  77. data/lib/omq/zmtp/single_frame.rb +0 -20
  78. data/lib/omq/zmtp/transport/inproc.rb +0 -359
  79. data/lib/omq/zmtp/transport/ipc.rb +0 -118
  80. data/lib/omq/zmtp/transport/tcp.rb +0 -117
  81. data/lib/omq/zmtp/writable.rb +0 -61
  82. data/lib/omq/zmtp.rb +0 -81
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module Routing
7
+ # SERVER socket routing: identity-based routing with auto-generated
8
+ # 4-byte routing IDs.
9
+ #
10
+ # Prepends routing ID on receive. Strips routing ID on send and
11
+ # routes to the identified connection.
12
+ #
13
+ class Server
14
+ # @param engine [Engine]
15
+ #
16
+ def initialize(engine)
17
+ @engine = engine
18
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
20
+ @connections_by_routing_id = {}
21
+ @routing_id_by_connection = {}
22
+ @tasks = []
23
+ @send_pump_started = false
24
+ @send_pump_idle = true
25
+ @written = Set.new
26
+ end
27
+
28
+ # @return [Async::LimitedQueue]
29
+ #
30
+ attr_reader :recv_queue, :send_queue
31
+
32
+ # @param connection [Connection]
33
+ #
34
+ def connection_added(connection)
35
+ routing_id = SecureRandom.bytes(4)
36
+ @connections_by_routing_id[routing_id] = connection
37
+ @routing_id_by_connection[connection] = routing_id
38
+
39
+ task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
40
+ [routing_id, *msg]
41
+ end
42
+ @tasks << task if task
43
+
44
+ start_send_pump unless @send_pump_started
45
+ end
46
+
47
+ # @param connection [Connection]
48
+ #
49
+ def connection_removed(connection)
50
+ routing_id = @routing_id_by_connection.delete(connection)
51
+ @connections_by_routing_id.delete(routing_id) if routing_id
52
+ end
53
+
54
+ # @param parts [Array<String>]
55
+ #
56
+ def enqueue(parts)
57
+ @send_queue.enqueue(parts)
58
+ end
59
+
60
+ def stop
61
+ @tasks.each(&:stop)
62
+ @tasks.clear
63
+ end
64
+
65
+ def send_pump_idle? = @send_pump_idle
66
+
67
+ private
68
+
69
+ def start_send_pump
70
+ @send_pump_started = true
71
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
72
+ loop do
73
+ @send_pump_idle = true
74
+ batch = [@send_queue.dequeue]
75
+ @send_pump_idle = false
76
+ Routing.drain_send_queue(@send_queue, batch)
77
+
78
+ @written.clear
79
+ batch.each do |parts|
80
+ routing_id = parts.first
81
+ conn = @connections_by_routing_id[routing_id]
82
+ next unless conn # silently drop if peer gone
83
+ begin
84
+ conn.write_message(parts[1..])
85
+ @written << conn
86
+ rescue *CONNECTION_LOST
87
+ # will be cleaned up
88
+ end
89
+ end
90
+
91
+ @written.each do |conn|
92
+ conn.flush
93
+ rescue *CONNECTION_LOST
94
+ # will be cleaned up
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # SUB socket routing: subscription-based receive from PUB peers.
6
+ #
7
+ # Sends SUBSCRIBE/CANCEL commands to connected PUB peers.
8
+ #
9
+ class Sub
10
+
11
+ # @param engine [Engine]
12
+ #
13
+ def initialize(engine)
14
+ @engine = engine
15
+ @connections = []
16
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
17
+ @subscriptions = Set.new
18
+ @tasks = []
19
+ end
20
+
21
+ # @return [Async::LimitedQueue]
22
+ #
23
+ attr_reader :recv_queue
24
+
25
+ # @param connection [Connection]
26
+ #
27
+ def connection_added(connection)
28
+ @connections << connection
29
+ # Send existing subscriptions to new peer
30
+ @subscriptions.each do |prefix|
31
+ connection.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
32
+ end
33
+ task = @engine.start_recv_pump(connection, @recv_queue)
34
+ @tasks << task if task
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_removed(connection)
40
+ @connections.delete(connection)
41
+ end
42
+
43
+ # SUB is read-only.
44
+ #
45
+ def enqueue(_parts)
46
+ raise "SUB sockets cannot send"
47
+ end
48
+
49
+ # Subscribes to a topic prefix.
50
+ #
51
+ # @param prefix [String]
52
+ #
53
+ def subscribe(prefix)
54
+ @subscriptions << prefix
55
+ @connections.each do |conn|
56
+ conn.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
57
+ end
58
+ end
59
+
60
+ # Unsubscribes from a topic prefix.
61
+ #
62
+ # @param prefix [String]
63
+ #
64
+ def unsubscribe(prefix)
65
+ @subscriptions.delete(prefix)
66
+ @connections.each do |conn|
67
+ conn.send_command(Protocol::ZMTP::Codec::Command.cancel(prefix))
68
+ end
69
+ end
70
+
71
+ def stop
72
+ @tasks.each(&:stop)
73
+ @tasks.clear
74
+ end
75
+
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # XPUB socket routing: like PUB but exposes subscription messages.
6
+ #
7
+ # Subscription/unsubscription messages from peers are delivered to
8
+ # the application as data frames: \x01 + prefix for subscribe,
9
+ # \x00 + prefix for unsubscribe.
10
+ #
11
+ class XPub
12
+ include FanOut
13
+
14
+ # @param engine [Engine]
15
+ #
16
+ def initialize(engine)
17
+ @engine = engine
18
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
+ @tasks = []
20
+ init_fan_out(engine)
21
+ end
22
+
23
+ # @return [Async::LimitedQueue]
24
+ #
25
+ attr_reader :recv_queue, :send_queue
26
+
27
+ # @param connection [Connection]
28
+ #
29
+ def connection_added(connection)
30
+ @connections << connection
31
+ @subscriptions[connection] = Set.new
32
+ start_subscription_listener(connection)
33
+ start_send_pump unless @send_pump_started
34
+ end
35
+
36
+ # @param connection [Connection]
37
+ #
38
+ def connection_removed(connection)
39
+ @connections.delete(connection)
40
+ @subscriptions.delete(connection)
41
+ end
42
+
43
+ # @param parts [Array<String>]
44
+ #
45
+ def enqueue(parts)
46
+ @send_queue.enqueue(parts)
47
+ end
48
+
49
+ #
50
+ def stop
51
+ @tasks.each(&:stop)
52
+ @tasks.clear
53
+ end
54
+
55
+ private
56
+
57
+ # Expose subscription to application as data message.
58
+ #
59
+ def on_subscribe(conn, prefix)
60
+ super
61
+ @recv_queue.enqueue(["\x01#{prefix}".b])
62
+ end
63
+
64
+ # Expose unsubscription to application as data message.
65
+ #
66
+ def on_cancel(conn, prefix)
67
+ super
68
+ @recv_queue.enqueue(["\x00#{prefix}".b])
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # XSUB socket routing: like SUB but subscriptions sent as data messages.
6
+ #
7
+ # Subscriptions are sent as data frames: \x01 + prefix for subscribe,
8
+ # \x00 + prefix for unsubscribe.
9
+ #
10
+ class XSub
11
+
12
+ # @param engine [Engine]
13
+ #
14
+ 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
22
+ end
23
+
24
+ # @return [Async::LimitedQueue]
25
+ #
26
+ attr_reader :recv_queue, :send_queue
27
+
28
+ # @param connection [Connection]
29
+ #
30
+ def connection_added(connection)
31
+ @connections << connection
32
+ task = @engine.start_recv_pump(connection, @recv_queue)
33
+ @tasks << task if task
34
+ start_send_pump unless @send_pump_started
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_removed(connection)
40
+ @connections.delete(connection)
41
+ end
42
+
43
+ # @param parts [Array<String>]
44
+ #
45
+ def enqueue(parts)
46
+ @send_queue.enqueue(parts)
47
+ end
48
+
49
+ #
50
+ def stop
51
+ @tasks.each(&:stop)
52
+ @tasks.clear
53
+ end
54
+
55
+ def send_pump_idle? = @send_pump_idle
56
+
57
+ private
58
+
59
+ def start_send_pump
60
+ @send_pump_started = true
61
+ @tasks << @engine.spawn_pump_task(annotation: "send pump") do
62
+ loop do
63
+ @send_pump_idle = true
64
+ parts = @send_queue.dequeue
65
+ @send_pump_idle = false
66
+ frame = parts.first&.b
67
+ next if frame.nil? || frame.empty?
68
+
69
+ flag = frame.getbyte(0)
70
+ 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)) }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "async/queue"
5
+ require "async/limited_queue"
6
+
7
+ module OMQ
8
+ # Routing strategies for each ZMQ socket type.
9
+ #
10
+ # Each strategy manages how messages flow between connections and
11
+ # the socket's send/recv queues.
12
+ #
13
+ module Routing
14
+ # Shared frozen empty binary string to avoid repeated allocations.
15
+ EMPTY_BINARY = "".b.freeze
16
+
17
+ # Drains all available messages from +queue+ into +batch+ without
18
+ # blocking. Call after the initial blocking dequeue.
19
+ #
20
+ # No cap is needed: IO::Stream auto-flushes at 64 KB, so the
21
+ # write buffer hits the wire naturally under sustained load.
22
+ # The explicit flush after the batch pushes out the remainder.
23
+ #
24
+ # @param queue [Async::LimitedQueue]
25
+ # @param batch [Array]
26
+ # @return [void]
27
+ #
28
+ def self.drain_send_queue(queue, batch)
29
+ loop do
30
+ msg = queue.dequeue(timeout: 0)
31
+ break unless msg
32
+ batch << msg
33
+ end
34
+ end
35
+
36
+ # Returns the routing strategy class for a socket type.
37
+ #
38
+ # @param socket_type [Symbol] e.g. :PAIR, :REQ
39
+ # @return [Class]
40
+ #
41
+ def self.for(socket_type)
42
+ case socket_type
43
+ when :PAIR then Pair
44
+ when :REQ then Req
45
+ when :REP then Rep
46
+ when :DEALER then Dealer
47
+ when :ROUTER then Router
48
+ when :PUB then Pub
49
+ when :SUB then Sub
50
+ when :XPUB then XPub
51
+ 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}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -2,21 +2,21 @@
2
2
 
3
3
  module OMQ
4
4
  class SCATTER < Socket
5
- include ZMTP::Writable
6
- include ZMTP::SingleFrame
5
+ include Writable
6
+ include SingleFrame
7
7
 
8
- def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil)
9
- _init_engine(:SCATTER, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout)
8
+ def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil, backend: nil)
9
+ _init_engine(:SCATTER, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout, backend: backend)
10
10
  _attach(endpoints, default: :connect)
11
11
  end
12
12
  end
13
13
 
14
14
  class GATHER < Socket
15
- include ZMTP::Readable
16
- include ZMTP::SingleFrame
15
+ include Readable
16
+ include SingleFrame
17
17
 
18
- def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil)
19
- _init_engine(:GATHER, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout)
18
+ def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil, backend: nil)
19
+ _init_engine(:GATHER, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout, backend: backend)
20
20
  _attach(endpoints, default: :bind)
21
21
  end
22
22
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ # Mixin that rejects multipart messages.
5
+ #
6
+ # All draft socket types (CLIENT, SERVER, RADIO, DISH, SCATTER,
7
+ # GATHER, PEER, CHANNEL) require single-frame messages for
8
+ # thread-safe atomic operations.
9
+ #
10
+ module SingleFrame
11
+ def send(message)
12
+ if message.is_a?(Array) && message.size > 1
13
+ raise ArgumentError, "#{self.class} does not support multipart messages"
14
+ end
15
+ super
16
+ end
17
+ end
18
+ end
data/lib/omq/socket.rb CHANGED
@@ -6,7 +6,7 @@ module OMQ
6
6
  # Socket base class.
7
7
  #
8
8
  class Socket
9
- # @return [ZMTP::Options]
9
+ # @return [Options]
10
10
  #
11
11
  attr_reader :options
12
12
 
@@ -36,7 +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=
39
+ :mechanism, :mechanism=,
40
+ :tls_context, :tls_context=
40
41
 
41
42
 
42
43
  # Creates a new socket and binds it to the given endpoint.
@@ -70,8 +71,11 @@ module OMQ
70
71
  # @return [void]
71
72
  #
72
73
  def bind(endpoint)
73
- @engine.bind(endpoint)
74
- @last_tcp_port = @engine.last_tcp_port
74
+ ensure_parent_task
75
+ Reactor.run do
76
+ @engine.bind(endpoint)
77
+ @last_tcp_port = @engine.last_tcp_port
78
+ end
75
79
  end
76
80
 
77
81
 
@@ -81,7 +85,8 @@ module OMQ
81
85
  # @return [void]
82
86
  #
83
87
  def connect(endpoint)
84
- @engine.connect(endpoint)
88
+ ensure_parent_task
89
+ Reactor.run { @engine.connect(endpoint) }
85
90
  end
86
91
 
87
92
 
@@ -91,7 +96,7 @@ module OMQ
91
96
  # @return [void]
92
97
  #
93
98
  def disconnect(endpoint)
94
- @engine.disconnect(endpoint)
99
+ Reactor.run { @engine.disconnect(endpoint) }
95
100
  end
96
101
 
97
102
 
@@ -101,7 +106,7 @@ module OMQ
101
106
  # @return [void]
102
107
  #
103
108
  def unbind(endpoint)
104
- @engine.unbind(endpoint)
109
+ Reactor.run { @engine.unbind(endpoint) }
105
110
  end
106
111
 
107
112
 
@@ -145,7 +150,7 @@ module OMQ
145
150
  # Closes the socket.
146
151
  #
147
152
  def close
148
- @engine.close
153
+ Reactor.run { @engine.close }
149
154
  nil
150
155
  end
151
156
 
@@ -187,6 +192,15 @@ module OMQ
187
192
  end
188
193
 
189
194
 
195
+ # Sets the engine's parent task before the first bind or connect.
196
+ # Must be called OUTSIDE Reactor.run so that non-Async callers
197
+ # get the IO thread's root task, not an ephemeral work task.
198
+ #
199
+ def ensure_parent_task
200
+ @engine.capture_parent_task
201
+ end
202
+
203
+
190
204
  # Connects or binds based on endpoint prefix convention.
191
205
  #
192
206
  # @param endpoints [String, nil]
@@ -211,14 +225,21 @@ module OMQ
211
225
  # @param linger [Integer]
212
226
  #
213
227
  def _init_engine(socket_type, linger:, send_hwm: nil, recv_hwm: nil,
214
- send_timeout: nil, recv_timeout: nil, conflate: false)
215
- @options = ZMTP::Options.new(linger: linger)
228
+ send_timeout: nil, recv_timeout: nil, conflate: false,
229
+ backend: nil)
230
+ @options = Options.new(linger: linger)
216
231
  @options.send_hwm = send_hwm if send_hwm
217
232
  @options.recv_hwm = recv_hwm if recv_hwm
218
233
  @options.send_timeout = send_timeout if send_timeout
219
234
  @options.recv_timeout = recv_timeout if recv_timeout
220
235
  @options.conflate = conflate
221
- @engine = ZMTP::Engine.new(socket_type, @options)
236
+ @recv_buffer = []
237
+ @recv_mutex = Mutex.new
238
+ @engine = case backend
239
+ when nil, :ruby then Engine.new(socket_type, @options)
240
+ when :ffi then FFI::Engine.new(socket_type, @options)
241
+ else raise ArgumentError, "unknown backend: #{backend}"
242
+ end
222
243
  end
223
244
  end
224
245
  end