omq 0.11.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.md +3 -1
  4. data/lib/omq/drop_queue.rb +54 -0
  5. data/lib/omq/engine/connection_setup.rb +47 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/reconnect.rb +56 -0
  8. data/lib/omq/engine/recv_pump.rb +76 -0
  9. data/lib/omq/engine.rb +145 -371
  10. data/lib/omq/monitor_event.rb +16 -0
  11. data/lib/omq/options.rb +5 -3
  12. data/lib/omq/pub_sub.rb +9 -8
  13. data/lib/omq/routing/conn_send_pump.rb +36 -0
  14. data/lib/omq/routing/dealer.rb +8 -10
  15. data/lib/omq/routing/fair_queue.rb +144 -0
  16. data/lib/omq/routing/fair_recv.rb +27 -0
  17. data/lib/omq/routing/fan_out.rb +116 -63
  18. data/lib/omq/routing/pair.rb +39 -20
  19. data/lib/omq/routing/pub.rb +5 -7
  20. data/lib/omq/routing/pull.rb +5 -4
  21. data/lib/omq/routing/push.rb +3 -10
  22. data/lib/omq/routing/rep.rb +31 -51
  23. data/lib/omq/routing/req.rb +15 -12
  24. data/lib/omq/routing/round_robin.rb +82 -72
  25. data/lib/omq/routing/router.rb +23 -48
  26. data/lib/omq/routing/sub.rb +8 -6
  27. data/lib/omq/routing/xpub.rb +8 -4
  28. data/lib/omq/routing/xsub.rb +43 -27
  29. data/lib/omq/routing.rb +44 -11
  30. data/lib/omq/socket.rb +46 -5
  31. data/lib/omq/transport/inproc/direct_pipe.rb +162 -0
  32. data/lib/omq/transport/inproc.rb +37 -200
  33. data/lib/omq/transport/ipc.rb +16 -4
  34. data/lib/omq/transport/tcp.rb +31 -8
  35. data/lib/omq/version.rb +1 -1
  36. data/lib/omq.rb +5 -19
  37. metadata +11 -16
  38. data/lib/omq/channel.rb +0 -14
  39. data/lib/omq/client_server.rb +0 -37
  40. data/lib/omq/peer.rb +0 -26
  41. data/lib/omq/radio_dish.rb +0 -74
  42. data/lib/omq/routing/channel.rb +0 -83
  43. data/lib/omq/routing/client.rb +0 -56
  44. data/lib/omq/routing/dish.rb +0 -78
  45. data/lib/omq/routing/gather.rb +0 -46
  46. data/lib/omq/routing/peer.rb +0 -101
  47. data/lib/omq/routing/radio.rb +0 -150
  48. data/lib/omq/routing/scatter.rb +0 -82
  49. data/lib/omq/routing/server.rb +0 -101
  50. data/lib/omq/scatter_gather.rb +0 -23
  51. data/lib/omq/single_frame.rb +0 -18
  52. data/lib/omq/transport/tls.rb +0 -146
data/lib/omq/options.rb CHANGED
@@ -23,10 +23,11 @@ module OMQ
23
23
  @heartbeat_interval = nil # seconds, nil = disabled
24
24
  @heartbeat_ttl = nil # seconds, nil = use heartbeat_interval
25
25
  @heartbeat_timeout = nil # seconds, nil = use heartbeat_interval
26
- @max_message_size = nil # bytes, nil = unlimited
26
+ @max_message_size = 1 << 20 # bytes (1 MiB default)
27
27
  @conflate = false
28
+ @on_mute = :block # :block, :drop_newest, :drop_oldest
28
29
  @mechanism = Protocol::ZMTP::Mechanism::Null.new
29
- @tls_context = nil # OpenSSL::SSL::SSLContext for tls+tcp://
30
+ @qos = 0 # 0 = fire-and-forget, 1 = at-least-once (see omq-qos gem)
30
31
  end
31
32
 
32
33
  attr_accessor :send_hwm, :recv_hwm,
@@ -36,8 +37,9 @@ module OMQ
36
37
  :reconnect_interval,
37
38
  :heartbeat_interval, :heartbeat_ttl, :heartbeat_timeout,
38
39
  :max_message_size,
40
+ :on_mute,
39
41
  :mechanism,
40
- :tls_context
42
+ :qos
41
43
 
42
44
  alias_method :router_mandatory?, :router_mandatory
43
45
  alias_method :recv_timeout, :read_timeout
data/lib/omq/pub_sub.rb CHANGED
@@ -4,8 +4,8 @@ module OMQ
4
4
  class PUB < Socket
5
5
  include Writable
6
6
 
7
- def initialize(endpoints = nil, linger: 0, conflate: false, backend: nil)
8
- _init_engine(:PUB, linger: linger, conflate: conflate, backend: backend)
7
+ def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, conflate: false, backend: nil)
8
+ _init_engine(:PUB, linger: linger, on_mute: on_mute, conflate: conflate, backend: backend)
9
9
  _attach(endpoints, default: :bind)
10
10
  end
11
11
  end
@@ -23,9 +23,10 @@ module OMQ
23
23
  # @param linger [Integer]
24
24
  # @param subscribe [String, nil] subscription prefix; +nil+ (default)
25
25
  # means no subscription — call {#subscribe} explicitly.
26
+ # @param on_mute [Symbol] :block (default), :drop_newest, or :drop_oldest
26
27
  #
27
- def initialize(endpoints = nil, linger: 0, subscribe: nil, backend: nil)
28
- _init_engine(:SUB, linger: linger, backend: backend)
28
+ def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
29
+ _init_engine(:SUB, linger: linger, on_mute: on_mute, backend: backend)
29
30
  _attach(endpoints, default: :connect)
30
31
  self.subscribe(subscribe) unless subscribe.nil?
31
32
  end
@@ -53,8 +54,8 @@ module OMQ
53
54
  include Readable
54
55
  include Writable
55
56
 
56
- def initialize(endpoints = nil, linger: 0, backend: nil)
57
- _init_engine(:XPUB, linger: linger, backend: backend)
57
+ def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, backend: nil)
58
+ _init_engine(:XPUB, linger: linger, on_mute: on_mute, backend: backend)
58
59
  _attach(endpoints, default: :bind)
59
60
  end
60
61
  end
@@ -68,8 +69,8 @@ module OMQ
68
69
  # @param subscribe [String, nil] subscription prefix; +nil+ (default)
69
70
  # means no subscription — send a subscribe frame explicitly.
70
71
  #
71
- def initialize(endpoints = nil, linger: 0, subscribe: nil, backend: nil)
72
- _init_engine(:XSUB, linger: linger, backend: backend)
72
+ def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
73
+ _init_engine(:XSUB, linger: linger, on_mute: on_mute, backend: backend)
73
74
  _attach(endpoints, default: :connect)
74
75
  send("\x01#{subscribe}".b) unless subscribe.nil?
75
76
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Starts a dedicated send pump for one per-connection send queue.
6
+ #
7
+ # Used by Router and Rep, which have per-connection queues but do not
8
+ # include the RoundRobin mixin.
9
+ #
10
+ class ConnSendPump
11
+ # Spawns the pump task and registers it in +tasks+.
12
+ #
13
+ # @param engine [Engine]
14
+ # @param conn [Connection]
15
+ # @param q [Async::LimitedQueue]
16
+ # @param tasks [Array]
17
+ # @return [Async::Task]
18
+ #
19
+ def self.start(engine, conn, q, tasks)
20
+ task = engine.spawn_pump_task(annotation: "send pump") do
21
+ loop do
22
+ batch = [q.dequeue]
23
+ Routing.drain_send_queue(q, batch)
24
+ batch.each { |parts| conn.write_message(parts) }
25
+ conn.flush
26
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
27
+ engine.connection_lost(conn)
28
+ break
29
+ end
30
+ end
31
+ tasks << task
32
+ task
33
+ end
34
+ end
35
+ end
36
+ end
@@ -8,36 +8,35 @@ module OMQ
8
8
  #
9
9
  class Dealer
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 = Async::LimitedQueue.new(engine.options.recv_hwm)
17
+ @recv_queue = FairQueue.new
17
18
  @tasks = []
18
19
  init_round_robin(engine)
19
20
  end
20
21
 
21
- # @return [Async::LimitedQueue]
22
+ # @return [FairQueue]
22
23
  #
23
- attr_reader :recv_queue, :send_queue
24
+ attr_reader :recv_queue
24
25
 
25
26
  # @param connection [Connection]
26
27
  #
27
28
  def connection_added(connection)
28
29
  @connections << connection
29
- signal_connection_available
30
- update_direct_pipe
31
- task = @engine.start_recv_pump(connection, @recv_queue)
32
- @tasks << task if task
33
- start_send_pump unless @send_pump_started
30
+ add_fair_recv_connection(connection)
31
+ add_round_robin_send_connection(connection)
34
32
  end
35
33
 
36
34
  # @param connection [Connection]
37
35
  #
38
36
  def connection_removed(connection)
39
37
  @connections.delete(connection)
40
- update_direct_pipe
38
+ @recv_queue.remove_queue(connection)
39
+ remove_round_robin_send_connection(connection)
41
40
  end
42
41
 
43
42
  # @param parts [Array<String>]
@@ -51,7 +50,6 @@ module OMQ
51
50
  @tasks.each(&:stop)
52
51
  @tasks.clear
53
52
  end
54
-
55
53
  end
56
54
  end
57
55
  end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Per-connection recv queue aggregator.
6
+ #
7
+ # Maintains one bounded queue per connected peer. #dequeue
8
+ # returns the next available message from any peer in fair
9
+ # round-robin order, blocking until one arrives.
10
+ #
11
+ # Recv pumps do not enqueue directly — they write through a
12
+ # SignalingQueue wrapper, which also wakes a blocked #dequeue.
13
+ #
14
+ class FairQueue
15
+ def initialize
16
+ @queues = [] # ordered list of per-connection inner queues
17
+ @mapping = {} # connection => inner queue
18
+ @cycle = @queues.cycle # live reference — sees adds/removes
19
+ @condition = Async::Condition.new
20
+ @pending = 0 # signals received before #dequeue waits
21
+ @closed = false
22
+ end
23
+
24
+ # Registers a per-connection queue. Called when a connection is added.
25
+ #
26
+ # @param conn [Connection]
27
+ # @param q [Async::LimitedQueue]
28
+ #
29
+ def add_queue(conn, q)
30
+ @mapping[conn] = q
31
+ @queues << q
32
+ end
33
+
34
+ # Removes the per-connection queue for a disconnected peer.
35
+ #
36
+ # If the queue is empty it is removed immediately. If it still has
37
+ # pending messages it is kept in @queues so the application can drain
38
+ # them via #dequeue; it will be cleaned up lazily by try_dequeue once
39
+ # it is empty.
40
+ #
41
+ # @param conn [Connection]
42
+ #
43
+ def remove_queue(conn)
44
+ q = @mapping.delete(conn)
45
+ return unless q
46
+ @queues.delete(q) if q.empty?
47
+ # Non-empty orphaned queues stay in @queues until drained
48
+ end
49
+
50
+ # Wakes a blocked #dequeue. Called by SignalingQueue after each enqueue.
51
+ #
52
+ def signal
53
+ @pending += 1
54
+ @condition.signal
55
+ end
56
+
57
+ # Returns the next message from any per-connection queue, in fair
58
+ # round-robin order. Blocks until a message is available.
59
+ #
60
+ # Pass +timeout: 0+ for a non-blocking poll (returns nil immediately
61
+ # if no messages are available).
62
+ #
63
+ # @param timeout [Numeric, nil] 0 = non-blocking, nil = block forever
64
+ # @return [Array<String>, nil]
65
+ #
66
+ def dequeue(timeout: nil)
67
+ return try_dequeue if timeout == 0
68
+
69
+ loop do
70
+ return nil if @closed && @queues.all?(&:empty?)
71
+
72
+ msg = try_dequeue
73
+ return msg if msg
74
+
75
+ if @pending > 0
76
+ @pending -= 1
77
+ next
78
+ end
79
+
80
+ @condition.wait
81
+ end
82
+ end
83
+
84
+ # Injects a nil sentinel to unblock a waiting #dequeue.
85
+ # Called by Engine on close or fatal error.
86
+ #
87
+ def push(nil_sentinel)
88
+ @closed = true
89
+ @condition.signal
90
+ end
91
+
92
+ # @return [Boolean]
93
+ #
94
+ def empty?
95
+ @queues.all?(&:empty?)
96
+ end
97
+
98
+ private
99
+
100
+ # Tries each per-connection queue once in round-robin order.
101
+ # Returns the first message found, or nil if all are empty.
102
+ # Lazily removes empty orphaned queues (disconnected peers that have
103
+ # been fully drained).
104
+ #
105
+ def try_dequeue
106
+ @queues.size.times do
107
+ q = begin
108
+ @cycle.next
109
+ rescue StopIteration
110
+ @cycle = @queues.cycle
111
+ break
112
+ end
113
+ msg = q.dequeue(timeout: 0)
114
+ return msg if msg
115
+ if q.empty? && !@mapping.value?(q)
116
+ @queues.delete(q)
117
+ break
118
+ end
119
+ end
120
+ nil
121
+ end
122
+ end
123
+
124
+
125
+ # Wraps a per-connection bounded queue so that each #enqueue also
126
+ # signals the FairQueue to wake a blocked #dequeue.
127
+ #
128
+ class SignalingQueue
129
+ def initialize(inner, fair_queue)
130
+ @inner = inner
131
+ @fair = fair_queue
132
+ end
133
+
134
+ def enqueue(msg)
135
+ @inner.enqueue(msg)
136
+ @fair.signal
137
+ end
138
+
139
+ def dequeue(timeout: nil) = @inner.dequeue(timeout: timeout)
140
+ def empty? = @inner.empty?
141
+ def push(item) = @inner.push(item)
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Mixin that adds per-connection recv queue setup for fair-queued sockets.
6
+ #
7
+ # Including classes must have @engine, @recv_queue (FairQueue), and @tasks.
8
+ #
9
+ module FairRecv
10
+ private
11
+
12
+ # Creates a per-connection recv queue, registers it with @recv_queue,
13
+ # and starts a recv pump for the connection. Called from #connection_added.
14
+ #
15
+ # @param conn [Connection]
16
+ # @yield [msg] optional per-message transform
17
+ #
18
+ def add_fair_recv_connection(conn, &transform)
19
+ conn_q = Routing.build_queue(@engine.options.recv_hwm, :block)
20
+ signaling = SignalingQueue.new(conn_q, @recv_queue)
21
+ @recv_queue.add_queue(conn, conn_q)
22
+ task = @engine.start_recv_pump(conn, signaling, &transform)
23
+ @tasks << task if task
24
+ end
25
+ end
26
+ end
27
+ end
@@ -5,7 +5,13 @@ module OMQ
5
5
  # Mixin for routing strategies that fan-out to subscribers.
6
6
  #
7
7
  # Manages per-connection subscription sets, subscription command
8
- # listeners, and a send pump that delivers to all matching peers.
8
+ # listeners, and per-connection send queues/pumps that deliver
9
+ # to each matching peer independently.
10
+ #
11
+ # HWM is enforced per subscriber: each connection gets its own
12
+ # bounded send queue. DropQueues (for :drop_newest/:drop_oldest)
13
+ # silently drop messages for a slow subscriber without affecting
14
+ # others. LimitedQueues (for :block) block the publisher.
9
15
  #
10
16
  # Including classes must call `init_fan_out(engine)` from
11
17
  # their #initialize.
@@ -13,20 +19,26 @@ module OMQ
13
19
  module FanOut
14
20
  attr_reader :subscriber_joined
15
21
 
22
+ # True when all per-connection send queues are empty.
23
+ # Used by Engine#drain_send_queues during linger.
24
+ #
25
+ def send_queues_drained?
26
+ @conn_queues.values.all?(&:empty?)
27
+ end
28
+
16
29
  private
17
30
 
18
31
  def init_fan_out(engine)
19
- @connections = []
32
+ @connections = Set.new
20
33
  @subscriptions = {} # connection => Set of prefixes
21
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
22
- @send_pump_started = false
23
- @send_pump_idle = true
34
+ @conn_queues = {} # connection => per-connection send queue
35
+ @conn_send_tasks = {} # connection => send pump task
24
36
  @conflate = engine.options.conflate
25
37
  @subscriber_joined = Async::Promise.new
26
- @written = Set.new
27
38
  @latest = {} if @conflate
28
39
  end
29
40
 
41
+
30
42
  # @return [Boolean] whether the connection is subscribed to the topic
31
43
  #
32
44
  def subscribed?(conn, topic)
@@ -57,69 +69,50 @@ module OMQ
57
69
  @subscriptions[conn]&.delete(prefix)
58
70
  end
59
71
 
60
- # @return [Boolean] true when the send pump is idle (not sending a batch)
61
- def send_pump_idle? = @send_pump_idle
62
72
 
73
+ # Creates a per-connection send queue and starts its send pump.
74
+ # Call from #connection_added.
75
+ #
76
+ # @param conn [Connection]
77
+ #
78
+ def add_fan_out_send_connection(conn)
79
+ q = Routing.build_queue(@engine.options.send_hwm, :block)
80
+ @conn_queues[conn] = q
81
+ start_conn_send_pump(conn, q)
82
+ end
63
83
 
64
- def start_send_pump
65
- @send_pump_started = true
66
- @tasks << @engine.spawn_pump_task(annotation: "send pump") do
67
- loop do
68
- @send_pump_idle = true
69
- batch = [@send_queue.dequeue]
70
- @send_pump_idle = false
71
- Routing.drain_send_queue(@send_queue, batch)
72
-
73
- @written.clear
74
-
75
- if @conflate
76
- # Keep only the last matching message per connection.
77
- @latest.clear
78
- batch.each do |parts|
79
- topic = parts.first || EMPTY_BINARY
80
- @connections.each do |conn|
81
- next unless subscribed?(conn, topic)
82
- @latest[conn] = parts
83
- end
84
- end
85
- @latest.each do |conn, parts|
86
- begin
87
- conn.write_message(parts)
88
- @written << conn
89
- rescue *CONNECTION_LOST
90
- end
91
- end
92
- else
93
- batch.each do |parts|
94
- topic = parts.first || EMPTY_BINARY
95
- wire_bytes = nil
96
-
97
- @connections.each do |conn|
98
- next unless subscribed?(conn, topic)
99
- begin
100
- if conn.respond_to?(:curve?) && conn.curve?
101
- conn.write_message(parts)
102
- elsif conn.respond_to?(:write_wire)
103
- wire_bytes ||= Protocol::ZMTP::Codec::Frame.encode_message(parts)
104
- conn.write_wire(wire_bytes)
105
- else
106
- conn.write_message(parts)
107
- end
108
- @written << conn
109
- rescue *CONNECTION_LOST
110
- end
111
- end
112
- end
113
- end
114
84
 
115
- @written.each do |conn|
116
- conn.flush
117
- rescue *CONNECTION_LOST
118
- end
119
- end
85
+ # Stops the per-connection send pump and removes the queue.
86
+ # Call from #connection_removed.
87
+ #
88
+ # @param conn [Connection]
89
+ #
90
+ def remove_fan_out_send_connection(conn)
91
+ @conn_queues.delete(conn)
92
+ @conn_send_tasks.delete(conn)&.stop
93
+ end
94
+
95
+
96
+ # Fans a message out to every connected peer's send queue.
97
+ # Subscription filtering happens in the per-connection send pump so
98
+ # that late-arriving subscriptions (e.g. inproc connect-before-subscribe)
99
+ # are respected: a message enqueued before the async subscription listener
100
+ # has processed SUBSCRIBE commands will still be delivered correctly.
101
+ #
102
+ # Per-connection queues use :block (Async::LimitedQueue) for
103
+ # backpressure: when a subscriber's queue is full, the publisher
104
+ # yields until the send pump drains it. This matches the old
105
+ # shared-queue behavior and keeps the publisher fiber-friendly.
106
+ #
107
+ # @param parts [Array<String>]
108
+ #
109
+ def fan_out_enqueue(parts)
110
+ @connections.each do |conn|
111
+ @conn_queues[conn]&.enqueue(parts)
120
112
  end
121
113
  end
122
114
 
115
+
123
116
  def start_subscription_listener(conn)
124
117
  @tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
125
118
  loop do
@@ -135,6 +128,66 @@ module OMQ
135
128
  @engine.connection_lost(conn)
136
129
  end
137
130
  end
131
+
132
+
133
+ # Starts a dedicated send pump for one subscriber connection.
134
+ # Uses write_wire (pre-encoded bytes) for non-CURVE TCP connections
135
+ # to avoid re-encoding the same message N times during fan-out.
136
+ # In conflate mode, drains the batch and keeps only the latest
137
+ # message per topic before writing.
138
+ #
139
+ # @param conn [Connection]
140
+ # @param q [Async::LimitedQueue, DropQueue]
141
+ #
142
+ def start_conn_send_pump(conn, q)
143
+ use_wire = conn.respond_to?(:write_wire) && !(conn.respond_to?(:curve?) && conn.curve?)
144
+ task = @conflate ? start_conn_send_pump_conflate(conn, q) : start_conn_send_pump_normal(conn, q, use_wire)
145
+ @conn_send_tasks[conn] = task
146
+ @tasks << task
147
+ end
148
+
149
+ def start_conn_send_pump_normal(conn, q, use_wire)
150
+ @engine.spawn_pump_task(annotation: "send pump") do
151
+ loop do
152
+ batch = [q.dequeue]
153
+ Routing.drain_send_queue(q, batch)
154
+ conn.flush if write_matching_batch(conn, batch, use_wire)
155
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
156
+ @engine.connection_lost(conn)
157
+ break
158
+ end
159
+ end
160
+ end
161
+
162
+ def write_matching_batch(conn, batch, use_wire)
163
+ sent = false
164
+ batch.each do |parts|
165
+ next unless subscribed?(conn, parts.first || EMPTY_BINARY)
166
+ use_wire ? conn.write_wire(Protocol::ZMTP::Codec::Frame.encode_message(parts)) : conn.write_message(parts)
167
+ sent = true
168
+ end
169
+ sent
170
+ end
171
+
172
+
173
+ def start_conn_send_pump_conflate(conn, q)
174
+ @engine.spawn_pump_task(annotation: "send pump") do
175
+ loop do
176
+ batch = [q.dequeue]
177
+ Routing.drain_send_queue(q, batch)
178
+ # Keep only the latest message that matches the subscription.
179
+ latest = batch.reverse.find { |parts| subscribed?(conn, parts.first || EMPTY_BINARY) }
180
+ next unless latest
181
+ begin
182
+ conn.write_message(latest)
183
+ conn.flush
184
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
185
+ @engine.connection_lost(conn)
186
+ break
187
+ end
188
+ end
189
+ end
190
+ end
138
191
  end
139
192
  end
140
193
  end
@@ -4,25 +4,28 @@ module OMQ
4
4
  module Routing
5
5
  # PAIR socket routing: exclusive 1-to-1 bidirectional.
6
6
  #
7
- # Only one peer connection is allowed. Messages flow through
8
- # internal send/recv queues backed by Async::LimitedQueue.
7
+ # Only one peer connection is allowed. Send and recv queues are
8
+ # created per-connection (and destroyed on disconnection) so
9
+ # HWM is consistent with multi-peer socket types.
9
10
  #
10
11
  class Pair
12
+ include FairRecv
11
13
 
12
14
  # @param engine [Engine]
13
15
  #
14
16
  def initialize(engine)
15
- @engine = engine
16
- @connection = nil
17
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
17
+ @engine = engine
18
+ @connection = nil
19
+ @recv_queue = FairQueue.new
20
+ @send_queue = nil # created per-connection
21
+ @staging_queue = Routing.build_queue(@engine.options.send_hwm, :block)
22
+ @send_pump = nil
19
23
  @tasks = []
20
- @send_pump_idle = true
21
24
  end
22
25
 
23
- # @return [Async::LimitedQueue]
26
+ # @return [FairQueue]
24
27
  #
25
- attr_reader :recv_queue, :send_queue
28
+ attr_reader :recv_queue
26
29
 
27
30
  # @param connection [Connection]
28
31
  # @raise [RuntimeError] if a connection already exists
@@ -30,9 +33,16 @@ module OMQ
30
33
  def connection_added(connection)
31
34
  raise "PAIR allows only one peer" if @connection
32
35
  @connection = connection
33
- task = @engine.start_recv_pump(connection, @recv_queue)
34
- @tasks << task if task
35
- start_send_pump(connection) unless connection.is_a?(Transport::Inproc::DirectPipe)
36
+
37
+ add_fair_recv_connection(connection)
38
+
39
+ unless connection.is_a?(Transport::Inproc::DirectPipe)
40
+ @send_queue = Routing.build_queue(@engine.options.send_hwm, :block)
41
+ while (msg = @staging_queue.dequeue(timeout: 0))
42
+ @send_queue.enqueue(msg)
43
+ end
44
+ start_send_pump(connection)
45
+ end
36
46
  end
37
47
 
38
48
  # @param connection [Connection]
@@ -40,6 +50,8 @@ module OMQ
40
50
  def connection_removed(connection)
41
51
  if @connection == connection
42
52
  @connection = nil
53
+ @recv_queue.remove_queue(connection)
54
+ @send_queue = nil
43
55
  @send_pump&.stop
44
56
  @send_pump = nil
45
57
  end
@@ -51,8 +63,10 @@ module OMQ
51
63
  conn = @connection
52
64
  if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
53
65
  conn.send_message(parts)
54
- else
66
+ elsif @send_queue
55
67
  @send_queue.enqueue(parts)
68
+ else
69
+ @staging_queue.enqueue(parts)
56
70
  end
57
71
  end
58
72
 
@@ -62,22 +76,27 @@ module OMQ
62
76
  @tasks.clear
63
77
  end
64
78
 
65
- def send_pump_idle? = @send_pump_idle
79
+ # True when the staging and send queues are empty.
80
+ #
81
+ def send_queues_drained?
82
+ @staging_queue.empty? && (@send_queue.nil? || @send_queue.empty?)
83
+ end
66
84
 
67
85
  private
68
86
 
69
87
  def start_send_pump(conn)
70
88
  @send_pump = @engine.spawn_pump_task(annotation: "send pump") do
71
89
  loop do
72
- @send_pump_idle = true
73
90
  batch = [@send_queue.dequeue]
74
- @send_pump_idle = false
75
91
  Routing.drain_send_queue(@send_queue, batch)
76
- batch.each { |parts| conn.write_message(parts) }
77
- conn.flush
92
+ begin
93
+ batch.each { |parts| conn.write_message(parts) }
94
+ conn.flush
95
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
96
+ @engine.connection_lost(conn)
97
+ break
98
+ end
78
99
  end
79
- rescue *CONNECTION_LOST
80
- @engine.connection_lost(conn)
81
100
  end
82
101
  @tasks << @send_pump
83
102
  end