omq 0.22.1 → 0.24.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 +162 -0
  3. data/README.md +17 -21
  4. data/lib/omq/channel.rb +35 -0
  5. data/lib/omq/client_server.rb +72 -0
  6. data/lib/omq/constants.rb +68 -0
  7. data/lib/omq/engine/connection_lifecycle.rb +22 -8
  8. data/lib/omq/engine/heartbeat.rb +3 -4
  9. data/lib/omq/engine/maintenance.rb +4 -5
  10. data/lib/omq/engine/reconnect.rb +12 -11
  11. data/lib/omq/engine/recv_pump.rb +10 -10
  12. data/lib/omq/engine/socket_lifecycle.rb +26 -9
  13. data/lib/omq/engine.rb +202 -90
  14. data/lib/omq/peer.rb +49 -0
  15. data/lib/omq/pub_sub.rb +2 -2
  16. data/lib/omq/radio_dish.rb +122 -0
  17. data/lib/omq/reactor.rb +14 -5
  18. data/lib/omq/readable.rb +5 -1
  19. data/lib/omq/routing/channel.rb +110 -0
  20. data/lib/omq/routing/client.rb +70 -0
  21. data/lib/omq/routing/conn_send_pump.rb +5 -8
  22. data/lib/omq/routing/dealer.rb +3 -15
  23. data/lib/omq/routing/dish.rb +94 -0
  24. data/lib/omq/routing/fan_out.rb +12 -16
  25. data/lib/omq/routing/gather.rb +60 -0
  26. data/lib/omq/routing/pair.rb +7 -26
  27. data/lib/omq/routing/peer.rb +95 -0
  28. data/lib/omq/routing/pub.rb +2 -13
  29. data/lib/omq/routing/pull.rb +3 -15
  30. data/lib/omq/routing/push.rb +4 -13
  31. data/lib/omq/routing/radio.rb +187 -0
  32. data/lib/omq/routing/rep.rb +5 -19
  33. data/lib/omq/routing/req.rb +6 -18
  34. data/lib/omq/routing/round_robin.rb +15 -19
  35. data/lib/omq/routing/router.rb +5 -19
  36. data/lib/omq/routing/scatter.rb +76 -0
  37. data/lib/omq/routing/server.rb +90 -0
  38. data/lib/omq/routing/sub.rb +3 -15
  39. data/lib/omq/routing/xpub.rb +2 -13
  40. data/lib/omq/routing/xsub.rb +8 -25
  41. data/lib/omq/scatter_gather.rb +56 -0
  42. data/lib/omq/socket.rb +8 -23
  43. data/lib/omq/transport/inproc/{direct_pipe.rb → pipe.rb} +26 -24
  44. data/lib/omq/transport/inproc.rb +22 -14
  45. data/lib/omq/transport/ipc.rb +41 -13
  46. data/lib/omq/transport/tcp.rb +59 -23
  47. data/lib/omq/transport/udp.rb +281 -0
  48. data/lib/omq/version.rb +1 -1
  49. data/lib/omq/writable.rb +11 -42
  50. data/lib/omq.rb +9 -64
  51. metadata +17 -3
  52. data/lib/omq/monitor_event.rb +0 -16
@@ -22,8 +22,6 @@ module OMQ
22
22
  @connection = nil
23
23
  @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
24
24
  @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
25
- @send_pump = nil
26
- @tasks = []
27
25
  end
28
26
 
29
27
 
@@ -45,30 +43,25 @@ module OMQ
45
43
  end
46
44
 
47
45
 
48
- # @param connection [Connection]
46
+ # @param connection [Protocol::ZMTP::Connection]
49
47
  # @raise [RuntimeError] if a connection already exists
50
48
  #
51
49
  def connection_added(connection)
52
50
  raise "PAIR allows only one peer" if @connection
53
51
  @connection = connection
54
52
 
55
- task = @engine.start_recv_pump(connection, @recv_queue)
56
- @tasks << task if task
53
+ @engine.start_recv_pump(connection, @recv_queue)
57
54
 
58
- unless connection.is_a?(Transport::Inproc::DirectPipe)
55
+ unless connection.is_a?(Transport::Inproc::Pipe)
59
56
  start_send_pump(connection)
60
57
  end
61
58
  end
62
59
 
63
60
 
64
- # @param connection [Connection]
61
+ # @param connection [Protocol::ZMTP::Connection]
65
62
  #
66
63
  def connection_removed(connection)
67
- if @connection == connection
68
- @connection = nil
69
- @send_pump&.stop
70
- @send_pump = nil
71
- end
64
+ @connection = nil if @connection == connection
72
65
  end
73
66
 
74
67
 
@@ -76,7 +69,7 @@ module OMQ
76
69
  #
77
70
  def enqueue(parts)
78
71
  conn = @connection
79
- if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
72
+ if conn.is_a?(Transport::Inproc::Pipe) && conn.direct_recv_queue
80
73
  conn.send_message(parts)
81
74
  else
82
75
  @send_queue.enqueue(parts)
@@ -84,16 +77,6 @@ module OMQ
84
77
  end
85
78
 
86
79
 
87
- # Stops all background tasks.
88
- #
89
- # @return [void]
90
- #
91
- def stop
92
- @tasks.each(&:stop)
93
- @tasks.clear
94
- end
95
-
96
-
97
80
  # @return [Boolean] true when the shared send queue is empty
98
81
  #
99
82
  def send_queues_drained?
@@ -105,7 +88,7 @@ module OMQ
105
88
 
106
89
 
107
90
  def start_send_pump(conn)
108
- @send_pump = @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
91
+ @engine.spawn_conn_pump_task(conn, annotation: "send pump") do
109
92
  batch = []
110
93
 
111
94
  loop do
@@ -124,8 +107,6 @@ module OMQ
124
107
  batch.clear
125
108
  end
126
109
  end
127
-
128
- @tasks << @send_pump
129
110
  end
130
111
 
131
112
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module Routing
7
+ # PEER socket routing: bidirectional multi-peer 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 Peer
14
+ # @return [Async::LimitedQueue]
15
+ #
16
+ attr_reader :recv_queue
17
+
18
+
19
+ # @return [Hash{String => Connection}] routing_id → connection
20
+ #
21
+ attr_reader :connections_by_routing_id
22
+
23
+
24
+ # @param engine [Engine]
25
+ #
26
+ def initialize(engine)
27
+ @engine = engine
28
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
29
+ @connections_by_routing_id = {}
30
+ @routing_id_by_connection = {}
31
+ @conn_queues = {}
32
+ end
33
+
34
+
35
+ # Dequeues the next received message. Blocks until one is available.
36
+ #
37
+ # @return [Array<String>, nil]
38
+ #
39
+ def dequeue_recv
40
+ @recv_queue.dequeue
41
+ end
42
+
43
+
44
+ # Wakes a blocked {#dequeue_recv} with a nil sentinel.
45
+ #
46
+ # @return [void]
47
+ #
48
+ def unblock_recv
49
+ @recv_queue.enqueue(nil)
50
+ end
51
+
52
+
53
+ # @param connection [Protocol::ZMTP::Connection]
54
+ #
55
+ def connection_added(connection)
56
+ routing_id = SecureRandom.bytes(4)
57
+ @connections_by_routing_id[routing_id] = connection
58
+ @routing_id_by_connection[connection] = routing_id
59
+
60
+ @engine.start_recv_pump(connection, @recv_queue) { |msg| [routing_id, *msg] }
61
+
62
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
63
+ @conn_queues[connection] = q
64
+ ConnSendPump.start(@engine, connection, q)
65
+ end
66
+
67
+
68
+ # @param connection [Protocol::ZMTP::Connection]
69
+ #
70
+ def connection_removed(connection)
71
+ routing_id = @routing_id_by_connection.delete(connection)
72
+ @connections_by_routing_id.delete(routing_id) if routing_id
73
+ @conn_queues.delete(connection)
74
+ end
75
+
76
+
77
+ # @param parts [Array<String>]
78
+ #
79
+ def enqueue(parts)
80
+ routing_id = parts.first
81
+ conn = @connections_by_routing_id[routing_id]
82
+ return unless conn
83
+ @conn_queues[conn]&.enqueue(parts[1..])
84
+ end
85
+
86
+
87
+ # True when all per-connection send queues are empty.
88
+ #
89
+ def send_queues_drained?
90
+ @conn_queues.values.all?(&:empty?)
91
+ end
92
+
93
+ end
94
+ end
95
+ end
@@ -15,7 +15,6 @@ module OMQ
15
15
  #
16
16
  def initialize(engine)
17
17
  @engine = engine
18
- @tasks = []
19
18
  init_fan_out(engine)
20
19
  end
21
20
 
@@ -37,7 +36,7 @@ module OMQ
37
36
  end
38
37
 
39
38
 
40
- # @param connection [Connection]
39
+ # @param connection [Protocol::ZMTP::Connection]
41
40
  #
42
41
  def connection_added(connection)
43
42
  @connections << connection
@@ -47,7 +46,7 @@ module OMQ
47
46
  end
48
47
 
49
48
 
50
- # @param connection [Connection]
49
+ # @param connection [Protocol::ZMTP::Connection]
51
50
  #
52
51
  def connection_removed(connection)
53
52
  @connections.delete(connection)
@@ -62,16 +61,6 @@ module OMQ
62
61
  fan_out_enqueue(parts)
63
62
  end
64
63
 
65
-
66
- # Stops all background tasks.
67
- #
68
- # @return [void]
69
- #
70
- def stop
71
- @tasks.each(&:stop)
72
- @tasks.clear
73
- end
74
-
75
64
  end
76
65
  end
77
66
  end
@@ -10,7 +10,6 @@ module OMQ
10
10
  def initialize(engine)
11
11
  @engine = engine
12
12
  @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
13
- @tasks = []
14
13
  end
15
14
 
16
15
 
@@ -39,15 +38,14 @@ module OMQ
39
38
  end
40
39
 
41
40
 
42
- # @param connection [Connection]
41
+ # @param connection [Protocol::ZMTP::Connection]
43
42
  #
44
43
  def connection_added(connection)
45
- task = @engine.start_recv_pump(connection, @recv_queue)
46
- @tasks << task if task
44
+ @engine.start_recv_pump(connection, @recv_queue)
47
45
  end
48
46
 
49
47
 
50
- # @param connection [Connection]
48
+ # @param connection [Protocol::ZMTP::Connection]
51
49
  #
52
50
  def connection_removed(connection)
53
51
  # recv pump stops on EOFError via its connection barrier
@@ -60,16 +58,6 @@ module OMQ
60
58
  raise "PULL sockets cannot send"
61
59
  end
62
60
 
63
-
64
- # Stops all background tasks.
65
- #
66
- # @return [void]
67
- #
68
- def stop
69
- @tasks.each(&:stop)
70
- @tasks.clear
71
- end
72
-
73
61
  end
74
62
  end
75
63
  end
@@ -12,7 +12,6 @@ module OMQ
12
12
  #
13
13
  def initialize(engine)
14
14
  @engine = engine
15
- @tasks = []
16
15
  init_round_robin(engine)
17
16
  end
18
17
 
@@ -34,7 +33,7 @@ module OMQ
34
33
  end
35
34
 
36
35
 
37
- # @param connection [Connection]
36
+ # @param connection [Protocol::ZMTP::Connection]
38
37
  #
39
38
  def connection_added(connection)
40
39
  add_round_robin_send_connection(connection)
@@ -42,7 +41,7 @@ module OMQ
42
41
  end
43
42
 
44
43
 
45
- # @param connection [Connection]
44
+ # @param connection [Protocol::ZMTP::Connection]
46
45
  #
47
46
  def connection_removed(connection)
48
47
  @connections.delete(connection)
@@ -57,14 +56,6 @@ module OMQ
57
56
  end
58
57
 
59
58
 
60
- # Stops all background tasks (send pumps, reapers).
61
- #
62
- def stop
63
- @tasks.each(&:stop)
64
- @tasks.clear
65
- end
66
-
67
-
68
59
  private
69
60
 
70
61
 
@@ -73,8 +64,8 @@ module OMQ
73
64
  # may succeed if the kernel send buffer absorbs the data.
74
65
  #
75
66
  def start_reaper(conn)
76
- return if conn.is_a?(Transport::Inproc::DirectPipe)
77
- @tasks << @engine.spawn_conn_pump_task(conn, annotation: "reaper") do
67
+ return if conn.is_a?(Transport::Inproc::Pipe)
68
+ @engine.spawn_conn_pump_task(conn, annotation: "reaper") do
78
69
  conn.receive_message # blocks until peer disconnects; then exits
79
70
  end
80
71
  end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # RADIO socket routing: group-based fan-out to DISH peers.
6
+ #
7
+ # Like PUB/FanOut but with exact group matching and JOIN/LEAVE
8
+ # commands instead of SUBSCRIBE/CANCEL.
9
+ #
10
+ # Messages are sent as two frames on the wire:
11
+ # group (MORE=1) + body (MORE=0)
12
+ #
13
+ class Radio
14
+ # Sentinel used for UDP connections that have no group filter:
15
+ # any group is considered a match.
16
+ #
17
+ ANY_GROUPS = Object.new.tap { |o| o.define_singleton_method(:include?) { |_| true } }.freeze
18
+
19
+
20
+ # @return [Async::LimitedQueue]
21
+ #
22
+ attr_reader :send_queue
23
+
24
+
25
+ # @param engine [Engine]
26
+ #
27
+ def initialize(engine)
28
+ @engine = engine
29
+ @connections = []
30
+ @groups = {} # connection => Set of joined groups (or ANY_GROUPS for UDP)
31
+ @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
32
+ @on_mute = engine.options.on_mute
33
+ @send_pump_started = false
34
+ @conflate = engine.options.conflate
35
+ @written = Set.new
36
+ @latest = {} if @conflate
37
+ end
38
+
39
+
40
+ # RADIO is write-only.
41
+ #
42
+ def recv_queue
43
+ raise "RADIO sockets cannot receive"
44
+ end
45
+
46
+
47
+ # No-op; RADIO has no recv queue to unblock.
48
+ #
49
+ def unblock_recv
50
+ end
51
+
52
+
53
+ # @param connection [Protocol::ZMTP::Connection]
54
+ #
55
+ def connection_added(connection)
56
+ @connections << connection
57
+ if connection.respond_to?(:read_frame)
58
+ @groups[connection] = Set.new
59
+ start_group_listener(connection)
60
+ else
61
+ @groups[connection] = ANY_GROUPS # UDP: fan-out to all groups
62
+ end
63
+ start_send_pump unless @send_pump_started
64
+ end
65
+
66
+
67
+ # @param connection [Protocol::ZMTP::Connection]
68
+ #
69
+ def connection_removed(connection)
70
+ @connections.delete(connection)
71
+ @groups.delete(connection)
72
+ end
73
+
74
+
75
+ # Enqueues a message for sending.
76
+ #
77
+ # @param parts [Array<String>] [group, body]
78
+ #
79
+ def enqueue(parts)
80
+ @send_queue.enqueue(parts)
81
+ end
82
+
83
+
84
+ # True when the send queue is empty.
85
+ #
86
+ def send_queues_drained?
87
+ @send_queue.empty?
88
+ end
89
+
90
+
91
+ private
92
+
93
+
94
+ def muted?(conn)
95
+ return false if @on_mute == :block
96
+ q = conn.direct_recv_queue if conn.respond_to?(:direct_recv_queue)
97
+ q&.respond_to?(:limited?) && q.limited?
98
+ end
99
+
100
+
101
+ def start_send_pump
102
+ @send_pump_started = true
103
+ @engine.spawn_pump_task(annotation: "send pump", parent: @engine.barrier) do
104
+ batch = []
105
+
106
+ loop do
107
+ @send_pump_idle = true
108
+ Routing.dequeue_batch(@send_queue, batch)
109
+ @send_pump_idle = false
110
+
111
+ @written.clear
112
+
113
+ if @conflate
114
+ # Keep only the last matching message per connection.
115
+ @latest.clear
116
+ batch.each do |parts|
117
+ group = parts[0]
118
+ body = parts[1] || EMPTY_BINARY
119
+ @connections.each do |conn|
120
+ next unless @groups[conn]&.include?(group)
121
+ @latest[conn] = [group, body]
122
+ end
123
+ end
124
+ @latest.each do |conn, msg|
125
+ next if muted?(conn)
126
+ begin
127
+ conn.write_message(msg)
128
+ @written << conn
129
+ rescue *CONNECTION_LOST
130
+ end
131
+ end
132
+ else
133
+ batch.each do |parts|
134
+ group = parts[0]
135
+ body = parts[1] || EMPTY_BINARY
136
+ msg = [group, body]
137
+ wire_bytes = nil
138
+
139
+ @connections.each do |conn|
140
+ next unless @groups[conn]&.include?(group)
141
+ next if muted?(conn)
142
+ begin
143
+ if conn.respond_to?(:curve?) && conn.curve?
144
+ conn.write_message(msg)
145
+ elsif conn.respond_to?(:write_wire)
146
+ wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(msg)
147
+ conn.write_wire(wire_bytes)
148
+ else
149
+ conn.write_message(msg)
150
+ end
151
+ @written << conn
152
+ rescue *CONNECTION_LOST
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ @written.each do |conn|
159
+ conn.flush
160
+ rescue *CONNECTION_LOST
161
+ end
162
+
163
+ batch.clear
164
+ end
165
+ end
166
+ end
167
+
168
+
169
+ def start_group_listener(conn)
170
+ @engine.spawn_conn_pump_task(conn, annotation: "group listener") do
171
+ loop do
172
+ frame = conn.read_frame
173
+ next unless frame.command?
174
+ cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
175
+ case cmd.name
176
+ when "JOIN"
177
+ @groups[conn]&.add(cmd.data)
178
+ when "LEAVE"
179
+ @groups[conn]&.delete(cmd.data)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ end
186
+ end
187
+ end
@@ -23,9 +23,7 @@ module OMQ
23
23
  @engine = engine
24
24
  @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
25
25
  @pending_replies = []
26
- @conn_queues = {} # connection => per-connection send queue
27
- @conn_send_tasks = {} # connection => send pump task
28
- @tasks = []
26
+ @conn_queues = {}
29
27
  end
30
28
 
31
29
 
@@ -47,10 +45,10 @@ module OMQ
47
45
  end
48
46
 
49
47
 
50
- # @param connection [Connection]
48
+ # @param connection [Protocol::ZMTP::Connection]
51
49
  #
52
50
  def connection_added(connection)
53
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
51
+ @engine.start_recv_pump(connection, @recv_queue) do |msg|
54
52
  delimiter = msg.index { |p| p.empty? } || msg.size
55
53
  envelope = msg[0, delimiter]
56
54
  body = msg[(delimiter + 1)..] || []
@@ -58,20 +56,18 @@ module OMQ
58
56
  @pending_replies << [connection, envelope]
59
57
  body
60
58
  end
61
- @tasks << task if task
62
59
 
63
60
  q = Routing.build_queue(@engine.options.send_hwm, :block)
64
61
  @conn_queues[connection] = q
65
- @conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
62
+ ConnSendPump.start(@engine, connection, q)
66
63
  end
67
64
 
68
65
 
69
- # @param connection [Connection]
66
+ # @param connection [Protocol::ZMTP::Connection]
70
67
  #
71
68
  def connection_removed(connection)
72
69
  @pending_replies.reject! { |r| r[0] == connection }
73
70
  @conn_queues.delete(connection)
74
- @conn_send_tasks.delete(connection)&.stop
75
71
  end
76
72
 
77
73
 
@@ -92,16 +88,6 @@ module OMQ
92
88
  end
93
89
 
94
90
 
95
- # Stops all background tasks.
96
- #
97
- # @return [void]
98
- #
99
- def stop
100
- @tasks.each(&:stop)
101
- @tasks.clear
102
- end
103
-
104
-
105
91
  # @return [Boolean] true when all per-connection send queues are empty
106
92
  #
107
93
  def send_queues_drained?
@@ -21,10 +21,9 @@ module OMQ
21
21
  # @param engine [Engine]
22
22
  #
23
23
  def initialize(engine)
24
- @engine = engine
25
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
26
- @tasks = []
27
- @state = :ready # :ready or :waiting_reply
24
+ @engine = engine
25
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
26
+ @state = :ready # :ready or :waiting_reply
28
27
  init_round_robin(engine)
29
28
  end
30
29
 
@@ -47,20 +46,19 @@ module OMQ
47
46
  end
48
47
 
49
48
 
50
- # @param connection [Connection]
49
+ # @param connection [Protocol::ZMTP::Connection]
51
50
  #
52
51
  def connection_added(connection)
53
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
52
+ @engine.start_recv_pump(connection, @recv_queue) do |msg|
54
53
  @state = :ready
55
54
  msg.first&.empty? ? msg[1..] : msg
56
55
  end
57
56
 
58
- @tasks << task if task
59
57
  add_round_robin_send_connection(connection)
60
58
  end
61
59
 
62
60
 
63
- # @param connection [Connection]
61
+ # @param connection [Protocol::ZMTP::Connection]
64
62
  #
65
63
  def connection_removed(connection)
66
64
  @connections.delete(connection)
@@ -77,16 +75,6 @@ module OMQ
77
75
  end
78
76
 
79
77
 
80
- # Stops all background tasks.
81
- #
82
- # @return [void]
83
- #
84
- def stop
85
- @tasks.each(&:stop)
86
- @tasks.clear
87
- end
88
-
89
-
90
78
  private
91
79
 
92
80