omq 0.12.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.
@@ -9,52 +9,60 @@ module OMQ
9
9
  # on send.
10
10
  #
11
11
  class Rep
12
- # @param engine [Engine]
13
- #
12
+ include FairRecv
13
+
14
14
  EMPTY_FRAME = "".b.freeze
15
15
 
16
+ # @param engine [Engine]
17
+ #
16
18
  def initialize(engine)
17
- @engine = engine
18
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
20
- @pending_replies = []
21
- @tasks = []
22
- @send_pump_started = false
23
- @send_pump_idle = true
24
- @written = Set.new
19
+ @engine = engine
20
+ @recv_queue = FairQueue.new
21
+ @pending_replies = []
22
+ @conn_queues = {} # connection => per-connection send queue
23
+ @conn_send_tasks = {} # connection => send pump task
24
+ @tasks = []
25
25
  end
26
26
 
27
- # @return [Async::LimitedQueue]
27
+ # @return [FairQueue]
28
28
  #
29
- attr_reader :recv_queue, :send_queue
29
+ attr_reader :recv_queue
30
30
 
31
31
  # @param connection [Connection]
32
32
  #
33
33
  def connection_added(connection)
34
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
34
+ add_fair_recv_connection(connection) do |msg|
35
35
  delimiter = msg.index(&:empty?) || msg.size
36
36
  envelope = msg[0, delimiter]
37
37
  body = msg[(delimiter + 1)..] || []
38
38
  @pending_replies << { conn: connection, envelope: envelope }
39
39
  body
40
40
  end
41
- @tasks << task if task
42
- start_send_pump unless @send_pump_started
41
+
42
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
43
+ @conn_queues[connection] = q
44
+ @conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
43
45
  end
44
46
 
45
47
  # @param connection [Connection]
46
48
  #
47
49
  def connection_removed(connection)
48
- # Remove any pending replies for this connection
49
50
  @pending_replies.reject! { |r| r[:conn] == connection }
51
+ @recv_queue.remove_queue(connection)
52
+ @conn_queues.delete(connection)
53
+ @conn_send_tasks.delete(connection)&.stop
50
54
  end
51
55
 
52
- # Enqueues a reply for sending.
56
+ # Enqueues a reply. Routes to the connection that sent the matching
57
+ # request by consuming the next pending_reply entry.
53
58
  #
54
59
  # @param parts [Array<String>]
55
60
  #
56
61
  def enqueue(parts)
57
- @send_queue.enqueue(parts)
62
+ reply_info = @pending_replies.shift
63
+ return unless reply_info
64
+ conn = reply_info[:conn]
65
+ @conn_queues[conn]&.enqueue([*reply_info[:envelope], EMPTY_FRAME, *parts])
58
66
  end
59
67
 
60
68
  def stop
@@ -62,40 +70,12 @@ module OMQ
62
70
  @tasks.clear
63
71
  end
64
72
 
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
- reply_info = @pending_replies.shift
81
- next unless reply_info
82
- conn = reply_info[:conn]
83
- begin
84
- conn.write_message([*reply_info[:envelope], EMPTY_FRAME, *parts])
85
- @written << conn
86
- rescue *CONNECTION_LOST
87
- # connection lost mid-write
88
- end
89
- end
90
-
91
- @written.each do |conn|
92
- conn.flush
93
- rescue *CONNECTION_LOST
94
- # connection lost mid-flush
95
- end
96
- end
97
- end
73
+ # True when all per-connection send queues are empty.
74
+ #
75
+ def send_queues_drained?
76
+ @conn_queues.values.all?(&:empty?)
98
77
  end
78
+
99
79
  end
100
80
  end
101
81
  end
@@ -8,41 +8,40 @@ module OMQ
8
8
  #
9
9
  class Req
10
10
  include RoundRobin
11
+ include FairRecv
11
12
 
12
13
  # @param engine [Engine]
13
14
  #
14
15
  def initialize(engine)
15
16
  @engine = engine
16
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
17
+ @recv_queue = FairQueue.new
17
18
  @tasks = []
18
19
  @state = :ready # :ready or :waiting_reply
19
20
  init_round_robin(engine)
20
21
  end
21
22
 
22
- # @return [Async::LimitedQueue]
23
+ # @return [FairQueue]
23
24
  #
24
- attr_reader :recv_queue, :send_queue
25
+ attr_reader :recv_queue
25
26
 
26
27
 
27
28
  # @param connection [Connection]
28
29
  #
29
30
  def connection_added(connection)
30
31
  @connections << connection
31
- signal_connection_available
32
- update_direct_pipe
33
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
32
+ add_fair_recv_connection(connection) do |msg|
34
33
  @state = :ready
35
34
  msg.first&.empty? ? msg[1..] : msg
36
35
  end
37
- @tasks << task if task
38
- start_send_pump unless @send_pump_started
36
+ add_round_robin_send_connection(connection)
39
37
  end
40
38
 
41
39
  # @param connection [Connection]
42
40
  #
43
41
  def connection_removed(connection)
44
42
  @connections.delete(connection)
45
- update_direct_pipe
43
+ @recv_queue.remove_queue(connection)
44
+ remove_round_robin_send_connection(connection)
46
45
  end
47
46
 
48
47
  # @param parts [Array<String>]
@@ -64,7 +63,6 @@ module OMQ
64
63
  # REQ prepends empty delimiter frame on the wire.
65
64
  #
66
65
  def transform_send(parts) = [EMPTY_BINARY, *parts]
67
-
68
66
  end
69
67
  end
70
68
  end
@@ -8,16 +8,22 @@ module OMQ
8
8
  # for the first connection, Array#cycle handles round-robin,
9
9
  # and a new Promise is created when all connections drop.
10
10
  #
11
+ # Each connected peer gets its own bounded send queue and a
12
+ # dedicated send pump fiber, ensuring HWM is enforced per peer.
13
+ #
11
14
  # Including classes must call `init_round_robin(engine)` from
12
15
  # their #initialize.
13
16
  #
14
17
  module RoundRobin
15
- # @return [Boolean] true when the send pump is idle (not sending a batch)
16
- def send_pump_idle? = @send_pump_idle
18
+ # True when the staging queue and all per-connection send queues
19
+ # are empty. Used by Engine#drain_send_queues during linger.
20
+ #
21
+ def send_queues_drained?
22
+ @staging_queue.empty? && @conn_queues.values.all?(&:empty?)
23
+ end
17
24
 
18
25
  private
19
26
 
20
-
21
27
  # Initializes round-robin state for the including class.
22
28
  #
23
29
  # @param engine [Engine]
@@ -26,11 +32,37 @@ module OMQ
26
32
  @connections = []
27
33
  @cycle = @connections.cycle
28
34
  @connection_available = Async::Promise.new
29
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
30
- @send_pump_started = false
31
- @send_pump_idle = true
35
+ @conn_queues = {} # connection => send queue
36
+ @conn_send_tasks = {} # connection => send pump task
32
37
  @direct_pipe = nil
33
- @written = Set.new
38
+ @staging_queue = Routing.build_queue(@engine.options.send_hwm, :block)
39
+ end
40
+
41
+
42
+ # Creates a per-connection send queue and starts its send pump.
43
+ # Call from #connection_added after appending to @connections.
44
+ #
45
+ # @param conn [Connection]
46
+ #
47
+ def add_round_robin_send_connection(conn)
48
+ update_direct_pipe
49
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
50
+ @conn_queues[conn] = q
51
+ drain_staging_to(q)
52
+ start_conn_send_pump(conn, q)
53
+ signal_connection_available
54
+ end
55
+
56
+
57
+ # Stops the per-connection send pump and removes the queue.
58
+ # Call from #connection_removed.
59
+ #
60
+ # @param conn [Connection]
61
+ #
62
+ def remove_round_robin_send_connection(conn)
63
+ update_direct_pipe
64
+ @conn_queues.delete(conn)
65
+ @conn_send_tasks.delete(conn)&.stop
34
66
  end
35
67
 
36
68
 
@@ -56,15 +88,33 @@ module OMQ
56
88
  end
57
89
 
58
90
 
59
- # Enqueues directly to the inproc peer's recv queue if possible,
60
- # otherwise falls back to the send queue for the send pump.
91
+ # Enqueues directly to the inproc peer's recv queue if possible.
92
+ # When peers are connected, picks the next one round-robin and
93
+ # enqueues into its per-connection send queue (blocking if full).
94
+ # When no peers are connected yet, buffers in a staging queue
95
+ # (bounded by send_hwm) — drained into the first peer's queue
96
+ # when it connects.
61
97
  #
62
98
  def enqueue_round_robin(parts)
63
99
  pipe = @direct_pipe
64
100
  if pipe&.direct_recv_queue
65
101
  pipe.send_message(transform_send(parts))
102
+ elsif @connections.empty?
103
+ @staging_queue.enqueue(parts)
66
104
  else
67
- @send_queue.enqueue(parts)
105
+ conn = next_connection
106
+ @conn_queues[conn]&.enqueue(parts)
107
+ end
108
+ end
109
+
110
+
111
+ # Drains the staging queue into the given per-connection queue.
112
+ # Called when the first peer connects, to deliver messages that
113
+ # were enqueued before any connection existed.
114
+ #
115
+ def drain_staging_to(q)
116
+ while (msg = @staging_queue.dequeue(timeout: 0))
117
+ q.enqueue(msg)
68
118
  end
69
119
  end
70
120
 
@@ -93,66 +143,34 @@ module OMQ
93
143
  def transform_send(parts) = parts
94
144
 
95
145
 
96
- def start_send_pump
97
- @send_pump_started = true
98
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
99
- loop do
100
- @send_pump_idle = true
101
- batch = [@send_queue.dequeue]
102
- @send_pump_idle = false
103
- Routing.drain_send_queue(@send_queue, batch)
104
-
105
- if batch.size == 1
106
- send_with_retry(batch[0])
107
- else
108
- send_batch(batch)
109
- end
110
- end
111
- end
112
- end
113
-
114
-
115
- # Sends a single message, retrying on a new connection if
116
- # the current one is lost.
146
+ # Starts a dedicated send pump for one connection.
147
+ # Batches messages for throughput; flushes after each batch.
148
+ # Calls Engine#connection_lost on disconnect so reconnect fires.
117
149
  #
118
- # @param parts [Array<String>]
150
+ # @param conn [Connection]
151
+ # @param q [Async::LimitedQueue] the connection's send queue
119
152
  #
120
- def send_with_retry(parts)
121
- conn = next_connection
122
- conn.send_message(transform_send(parts))
123
- rescue *CONNECTION_LOST
124
- @engine.connection_lost(conn)
125
- retry
126
- end
127
-
128
-
129
- # Sends a batch of messages, writing without flushing for
130
- # throughput. Falls back to #send_with_retry on failure.
131
- #
132
- # @param batch [Array<Array<String>>]
133
- #
134
- def send_batch(batch)
135
- @written.clear
136
- batch.each_with_index do |parts, i|
137
- conn = next_connection
138
- begin
139
- conn.write_message(transform_send(parts))
140
- @written << conn
141
- rescue *CONNECTION_LOST
153
+ def start_conn_send_pump(conn, q)
154
+ task = @engine.spawn_pump_task(annotation: "send pump") do
155
+ loop do
156
+ batch = [q.dequeue]
157
+ Routing.drain_send_queue(q, batch)
158
+ write_batch(conn, batch)
159
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
142
160
  @engine.connection_lost(conn)
143
- @written.each do |c|
144
- c.flush
145
- rescue *CONNECTION_LOST
146
- end
147
- @written.clear
148
- send_with_retry(parts)
149
- batch[(i + 1)..].each { |p| send_with_retry(p) }
150
- return
161
+ break
151
162
  end
152
163
  end
153
- @written.each do |conn|
164
+ @conn_send_tasks[conn] = task
165
+ @tasks << task
166
+ end
167
+
168
+ def write_batch(conn, batch)
169
+ if batch.size == 1
170
+ conn.send_message(transform_send(batch[0]))
171
+ else
172
+ batch.each { |parts| conn.write_message(transform_send(parts)) }
154
173
  conn.flush
155
- rescue *CONNECTION_LOST
156
174
  end
157
175
  end
158
176
  end
@@ -11,23 +11,22 @@ module OMQ
11
11
  # routing identity on send.
12
12
  #
13
13
  class Router
14
+ include FairRecv
14
15
  # @param engine [Engine]
15
16
  #
16
17
  def initialize(engine)
17
18
  @engine = engine
18
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
- @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
19
+ @recv_queue = FairQueue.new
20
20
  @connections_by_identity = {}
21
21
  @identity_by_connection = {}
22
+ @conn_queues = {} # connection => per-connection send queue
23
+ @conn_send_tasks = {} # connection => send pump task
22
24
  @tasks = []
23
- @send_pump_started = false
24
- @send_pump_idle = true
25
- @written = Set.new
26
25
  end
27
26
 
28
- # @return [Async::LimitedQueue]
27
+ # @return [FairQueue]
29
28
  #
30
- attr_reader :recv_queue, :send_queue
29
+ attr_reader :recv_queue
31
30
 
32
31
  # @param connection [Connection]
33
32
  #
@@ -37,12 +36,11 @@ module OMQ
37
36
  @connections_by_identity[identity] = connection
38
37
  @identity_by_connection[connection] = identity
39
38
 
40
- task = @engine.start_recv_pump(connection, @recv_queue) do |msg|
41
- [identity, *msg]
42
- end
43
- @tasks << task if task
39
+ add_fair_recv_connection(connection) { |msg| [identity, *msg] }
44
40
 
45
- start_send_pump unless @send_pump_started
41
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
42
+ @conn_queues[connection] = q
43
+ @conn_send_tasks[connection] = ConnSendPump.start(@engine, connection, q, @tasks)
46
44
  end
47
45
 
48
46
  # @param connection [Connection]
@@ -50,20 +48,25 @@ module OMQ
50
48
  def connection_removed(connection)
51
49
  identity = @identity_by_connection.delete(connection)
52
50
  @connections_by_identity.delete(identity) if identity
51
+ @recv_queue.remove_queue(connection)
52
+ @conn_queues.delete(connection)
53
+ @conn_send_tasks.delete(connection)&.stop
53
54
  end
54
55
 
55
- # Enqueues a message for sending.
56
+ # Enqueues a message for sending. The first frame is the routing identity.
56
57
  #
57
58
  # @param parts [Array<String>]
58
59
  #
59
60
  def enqueue(parts)
61
+ identity = parts.first
60
62
  if @engine.options.router_mandatory?
61
- identity = parts.first
62
63
  unless @connections_by_identity[identity]
63
64
  raise SocketError, "no route to identity #{identity.inspect}"
64
65
  end
65
66
  end
66
- @send_queue.enqueue(parts)
67
+ conn = @connections_by_identity[identity]
68
+ return unless conn # silently drop if peer disconnected
69
+ @conn_queues[conn]&.enqueue(parts[1..])
67
70
  end
68
71
 
69
72
  def stop
@@ -71,40 +74,12 @@ module OMQ
71
74
  @tasks.clear
72
75
  end
73
76
 
74
- def send_pump_idle? = @send_pump_idle
75
-
76
- private
77
-
78
- def start_send_pump
79
- @send_pump_started = true
80
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
81
- loop do
82
- @send_pump_idle = true
83
- batch = [@send_queue.dequeue]
84
- @send_pump_idle = false
85
- Routing.drain_send_queue(@send_queue, batch)
86
-
87
- @written.clear
88
- batch.each do |parts|
89
- identity = parts.first
90
- conn = @connections_by_identity[identity]
91
- next unless conn # silently drop (peer may have disconnected)
92
- begin
93
- conn.write_message(parts[1..])
94
- @written << conn
95
- rescue *CONNECTION_LOST
96
- # will be cleaned up
97
- end
98
- end
99
-
100
- @written.each do |conn|
101
- conn.flush
102
- rescue *CONNECTION_LOST
103
- # will be cleaned up
104
- end
105
- end
106
- end
77
+ # True when all per-connection send queues are empty.
78
+ #
79
+ def send_queues_drained?
80
+ @conn_queues.values.all?(&:empty?)
107
81
  end
82
+
108
83
  end
109
84
  end
110
85
  end
@@ -12,13 +12,13 @@ module OMQ
12
12
  #
13
13
  def initialize(engine)
14
14
  @engine = engine
15
- @connections = []
16
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, engine.options.on_mute)
15
+ @connections = Set.new
16
+ @recv_queue = FairQueue.new
17
17
  @subscriptions = Set.new
18
18
  @tasks = []
19
19
  end
20
20
 
21
- # @return [Async::LimitedQueue]
21
+ # @return [FairQueue]
22
22
  #
23
23
  attr_reader :recv_queue
24
24
 
@@ -29,7 +29,10 @@ module OMQ
29
29
  @subscriptions.each do |prefix|
30
30
  connection.send_command(Protocol::ZMTP::Codec::Command.subscribe(prefix))
31
31
  end
32
- task = @engine.start_recv_pump(connection, @recv_queue)
32
+ conn_q = Routing.build_queue(@engine.options.recv_hwm, @engine.options.on_mute)
33
+ signaling = SignalingQueue.new(conn_q, @recv_queue)
34
+ @recv_queue.add_queue(connection, conn_q)
35
+ task = @engine.start_recv_pump(connection, signaling)
33
36
  @tasks << task if task
34
37
  end
35
38
 
@@ -37,6 +40,7 @@ module OMQ
37
40
  #
38
41
  def connection_removed(connection)
39
42
  @connections.delete(connection)
43
+ @recv_queue.remove_queue(connection)
40
44
  end
41
45
 
42
46
  # SUB is read-only.
@@ -71,7 +75,6 @@ module OMQ
71
75
  @tasks.each(&:stop)
72
76
  @tasks.clear
73
77
  end
74
-
75
78
  end
76
79
  end
77
80
  end
@@ -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
 
@@ -22,7 +25,7 @@ module OMQ
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
  #