omq 0.11.0 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b736b6b715f5c4f330d21be278450600b2642239b8bea9e85957f516e89211ed
4
- data.tar.gz: 34c057a32055e7333e4299e85c0fee9e798b43ea34d021f237ba6778338b65f1
3
+ metadata.gz: d5ea3ce51d24a9181cfe7a4c1f2680ed57c5dbba078010a54de5b1c13d27d484
4
+ data.tar.gz: 97f7e7fcb27a250e71af4732ff06298e7311af052a9d9a6b3868d73059373174
5
5
  SHA512:
6
- metadata.gz: e95a6df6d4eb56ac1fc0586f61cfdb77d0afe9de620d3e540ffbae1256c2fe6526556485215eda3de20c8f34a332f51a68e5bb075087665049c0219bb07f6960
7
- data.tar.gz: 8b5c8d38b7523e9c6ab30e17c21b0cf4adb2086e1bdde90298f62e42f4b8f2336cdd95c47293c3fb24fa41f376aed8a2ec6d1121fc56d41aa83d4afdc289d707
6
+ metadata.gz: 9f60b7d45d1b3783b2bde482a6f826ec3b89bfff6a9532836dbd2a8a22622709e7c298cc7c2abae6e596641973636b8972efa92de7258bd1e4164ff62d782237
7
+ data.tar.gz: 7024b97bb46c9005b73bc7959cc4138bd5498a514af26253441613aa05206d357d1fbd002afff5820c9a3baff0c4351d66bcf1aa738955783272a2c2b459a697
data/CHANGELOG.md CHANGED
@@ -1,5 +1,100 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - **QoS infrastructure** — `Options#qos` attribute (default 0) and inproc
8
+ command queue support for QoS-enabled connections. The
9
+ [omq-qos](https://github.com/paddor/omq-qos) gem activates delivery
10
+ guarantees via prepends.
11
+ - **REQ send/recv ordering** — REQ sockets now enforce strict
12
+ send/recv/send/recv alternation. Calling `#send` twice without a
13
+ `#receive` in between raises `SocketError`.
14
+ - **DirectPipe command frame support** — `DirectPipe#receive_message`
15
+ accepts a block for command frames, matching the `Protocol::ZMTP::Connection`
16
+ interface. Enables inproc transports to handle ACK/NACK and other
17
+ command-level protocols.
18
+
19
+ ### Fixed
20
+
21
+ - **`send_pump_idle?` visibility** — moved above `private` in `RoundRobin`
22
+ and `FanOut` so `Engine#drain_send_queues` can call it during socket close.
23
+
24
+ - **`Socket#monitor`** — observe connection lifecycle events via a
25
+ block-based API. Returns an `Async::Task` that yields `MonitorEvent`
26
+ (Data.define) instances for `:listening`, `:accepted`, `:connected`,
27
+ `:connect_delayed`, `:connect_retried`, `:handshake_succeeded`,
28
+ `:handshake_failed`, `:accept_failed`, `:bind_failed`, `:disconnected`,
29
+ `:closed`, and `:monitor_stopped`. Event types align with libzmq's
30
+ `zmq_socket_monitor` where applicable. Pattern-matchable, zero overhead
31
+ when no monitor is attached.
32
+ - **Pluggable transport registry** — `Engine.transports` is a scheme →
33
+ module hash. Built-in transports (`tcp`, `ipc`, `inproc`) are registered
34
+ at load time. External gems register via
35
+ `OMQ::Engine.transports["scheme"] = MyTransport`. Each transport
36
+ implements `.bind(endpoint, engine)` → Listener, `.connect(endpoint,
37
+ engine)`, and optionally `.validate_endpoint!(endpoint)`. Listeners
38
+ implement `#start_accept_loops(parent_task, &on_accepted)`, `#stop`,
39
+ `#endpoint`, and optionally `#port`.
40
+ - **Mutable error lists** — `CONNECTION_LOST` and `CONNECTION_FAILED` are
41
+ no longer frozen at load time. Transport plugins can append error classes
42
+ (e.g. `OpenSSL::SSL::SSLError`) before the first `#bind`/`#connect`,
43
+ which freezes both arrays.
44
+
45
+ - **`on_mute` option** — controls behavior when a socket enters the mute state
46
+ (HWM full). PUB, XPUB, and RADIO default to `on_mute: :drop_newest` — slow
47
+ subscribers are skipped in the fan-out rather than blocking the publisher.
48
+ SUB, XSUB, and DISH accept `on_mute: :drop_newest` or `:drop_oldest` to
49
+ drop messages on the receive side instead of applying backpressure. All other
50
+ socket types default to `:block` (existing behavior).
51
+ - **`DropQueue`** — bounded queue with `:drop_newest` (tail drop) and
52
+ `:drop_oldest` (head drop) strategies. Used by recv queues when `on_mute`
53
+ is a drop strategy.
54
+ - **`Routing.build_queue`** — factory method for building send/recv queues
55
+ based on HWM and mute strategy. Supports HWM of `0` or `nil` for unbounded
56
+ queues.
57
+
58
+ ### Changed
59
+
60
+ - **`max_message_size` defaults to 1 MiB** — frames exceeding this limit cause
61
+ the connection to be dropped before the body is read from the wire, preventing
62
+ a malicious peer from causing arbitrary memory allocation. Set `socket.max_message_size = nil`
63
+ to restore the previous unlimited behavior.
64
+ - **Accept loops moved into Listeners** — `TCP::Listener` and
65
+ `IPC::Listener` now own their accept loop logic via
66
+ `#start_accept_loops(parent_task, &on_accepted)`. Engine delegates
67
+ via duck-type check. This enables external transports to define
68
+ custom accept behavior without modifying Engine.
69
+ - `Engine#transport_for` uses registry lookup instead of `case/when`.
70
+ - `Engine#validate_endpoint!` delegates to transport module.
71
+ - `Engine#bind` reads `listener.port` instead of parsing the endpoint
72
+ string.
73
+
74
+ ### Removed
75
+
76
+ - **Draft socket types extracted** — `RADIO`, `DISH`, `CLIENT`, `SERVER`,
77
+ `SCATTER`, `GATHER`, `CHANNEL`, and `PEER` are no longer bundled with `omq`.
78
+ Use the [omq-draft](https://github.com/paddor/omq-draft) gem and require
79
+ the relevant entry point (`omq/draft/radiodish`, `omq/draft/clientserver`,
80
+ etc.).
81
+ - **UDP transport extracted** — `udp://` endpoints are provided by
82
+ `omq-draft` (via `require "omq/draft/radiodish"`). No longer registered by
83
+ default.
84
+ - **`Routing.for` plugin registry** — draft socket type removal added
85
+ `Routing.register(socket_type, strategy_class)` for external gems to
86
+ register routing strategies. Unknown types fall through the built-in
87
+ `case` to this registry before raising `ArgumentError`.
88
+
89
+ - **TLS transport** — extracted to the
90
+ [omq-transport-tls](https://github.com/paddor/omq-transport-tls) gem.
91
+ (Experimental) `require "omq/transport/tls"` to restore `tls+tcp://` support.
92
+ - `tls_context` / `tls_context=` removed from `Options` and `Socket`
93
+ (provided by omq-transport-tls).
94
+ - `OpenSSL::SSL::SSLError` removed from `CONNECTION_LOST` (added back
95
+ by omq-transport-tls).
96
+ - TLS benchmark transport removed from `bench_helper.rb` and `plot.rb`.
97
+
3
98
  ## 0.11.0
4
99
 
5
100
  ### Added
data/README.md CHANGED
@@ -147,7 +147,7 @@ end
147
147
 
148
148
  ## Socket Types
149
149
 
150
- All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes live under `OMQ::` (alias: `ØMQ`).
150
+ All sockets are thread-safe. Default HWM is 1000 messages per socket. Default `max_message_size` is **1 MiB** — frames larger than this cause the connection to be dropped before the body is read from the wire. Set `socket.max_message_size = nil` to disable the limit or raise it as needed. Classes live under `OMQ::` (alias: `ØMQ`).
151
151
 
152
152
  #### Standard (multipart messages)
153
153
 
@@ -162,6 +162,8 @@ All sockets are thread-safe. Default HWM is 1000 messages per socket. Classes li
162
162
 
163
163
  #### Draft (single-frame only)
164
164
 
165
+ These require the `omq-draft` gem.
166
+
165
167
  | Pattern | Send | Receive | When HWM full |
166
168
  |---------|------|---------|---------------|
167
169
  | **CLIENT** / **SERVER** | Round-robin / routing-ID | Fair-queue | Block |
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ # A bounded queue that drops messages when full instead of blocking.
5
+ #
6
+ # Two drop strategies:
7
+ # :drop_newest — discard the incoming message (tail drop)
8
+ # :drop_oldest — discard the head, then enqueue the new message
9
+ #
10
+ # Used by SUB/XSUB/DISH recv queues when on_mute is a drop strategy.
11
+ #
12
+ class DropQueue
13
+ # @param limit [Integer] maximum number of items
14
+ # @param strategy [Symbol] :drop_newest or :drop_oldest
15
+ #
16
+ def initialize(limit, strategy: :drop_newest)
17
+ @queue = Thread::SizedQueue.new(limit)
18
+ @strategy = strategy
19
+ end
20
+
21
+ # Enqueues an item. Drops according to the configured strategy if full.
22
+ #
23
+ # @param item [Object]
24
+ # @return [void]
25
+ #
26
+ def enqueue(item)
27
+ @queue.push(item, true)
28
+ rescue ThreadError
29
+ return if @strategy == :drop_newest
30
+
31
+ # :drop_oldest — discard head, enqueue new
32
+ @queue.pop(true) rescue nil
33
+ retry
34
+ end
35
+
36
+ # Removes and returns the next item, blocking if empty.
37
+ #
38
+ # @return [Object]
39
+ #
40
+ def dequeue(timeout: nil)
41
+ if timeout
42
+ @queue.pop(timeout: timeout)
43
+ else
44
+ @queue.pop
45
+ end
46
+ end
47
+
48
+ # @return [Boolean]
49
+ #
50
+ def empty?
51
+ @queue.empty?
52
+ end
53
+ end
54
+ end
data/lib/omq/engine.rb CHANGED
@@ -9,6 +9,17 @@ module OMQ
9
9
  # OMQ::Socket instance. Each socket type creates one Engine.
10
10
  #
11
11
  class Engine
12
+ # Scheme → transport module registry.
13
+ # Plugins add entries via +Engine.transports["scheme"] = MyTransport+.
14
+ #
15
+ @transports = {}
16
+
17
+ class << self
18
+ # @return [Hash{String => Module}] registered transports
19
+ attr_reader :transports
20
+ end
21
+
22
+
12
23
  # @return [Symbol] socket type (e.g. :REQ, :PAIR)
13
24
  #
14
25
  attr_reader :socket_type
@@ -57,12 +68,13 @@ module OMQ
57
68
  @on_io_thread = false
58
69
  @connection_promises = {} # connection => Async::Promise
59
70
  @fatal_error = nil
71
+ @monitor_queue = nil
60
72
  end
61
73
 
62
74
 
63
75
  attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
64
76
 
65
- attr_writer :reconnect_enabled
77
+ attr_writer :reconnect_enabled, :monitor_queue
66
78
 
67
79
  # Optional proc that wraps new connections (e.g. for serialization).
68
80
  # Called with the raw connection; must return the (possibly wrapped) connection.
@@ -92,12 +104,17 @@ module OMQ
92
104
  # @raise [ArgumentError] on unsupported transport
93
105
  #
94
106
  def bind(endpoint)
107
+ freeze_error_lists!
95
108
  transport = transport_for(endpoint)
96
109
  listener = transport.bind(endpoint, self)
97
110
  start_accept_loops(listener)
98
111
  @listeners << listener
99
112
  @last_endpoint = listener.endpoint
100
- @last_tcp_port = extract_tcp_port(listener.endpoint)
113
+ @last_tcp_port = listener.respond_to?(:port) ? listener.port : nil
114
+ emit_monitor_event(:listening, endpoint: listener.endpoint)
115
+ rescue => error
116
+ emit_monitor_event(:bind_failed, endpoint: endpoint, detail: { error: error })
117
+ raise
101
118
  end
102
119
 
103
120
 
@@ -107,6 +124,7 @@ module OMQ
107
124
  # @return [void]
108
125
  #
109
126
  def connect(endpoint)
127
+ freeze_error_lists!
110
128
  validate_endpoint!(endpoint)
111
129
  @connected_endpoints << endpoint
112
130
  if endpoint.start_with?("inproc://")
@@ -115,6 +133,7 @@ module OMQ
115
133
  transport.connect(endpoint, self)
116
134
  else
117
135
  # TCP/IPC connect in background — never blocks the caller
136
+ emit_monitor_event(:connect_delayed, endpoint: endpoint)
118
137
  schedule_reconnect(endpoint, delay: 0)
119
138
  end
120
139
  end
@@ -168,6 +187,7 @@ module OMQ
168
187
  # @return [void]
169
188
  #
170
189
  def handle_accepted(io, endpoint: nil)
190
+ emit_monitor_event(:accepted, endpoint: endpoint)
171
191
  spawn_connection(io, as_server: true, endpoint: endpoint)
172
192
  end
173
193
 
@@ -178,6 +198,7 @@ module OMQ
178
198
  # @return [void]
179
199
  #
180
200
  def handle_connected(io, endpoint: nil)
201
+ emit_monitor_event(:connected, endpoint: endpoint)
181
202
  spawn_connection(io, as_server: false, endpoint: endpoint)
182
203
  end
183
204
 
@@ -194,6 +215,7 @@ module OMQ
194
215
  @connection_endpoints[pipe] = endpoint if endpoint
195
216
  @routing.connection_added(pipe)
196
217
  @peer_connected.resolve(pipe)
218
+ emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
197
219
  end
198
220
 
199
221
 
@@ -335,6 +357,7 @@ module OMQ
335
357
  @connections.delete(connection)
336
358
  @routing.connection_removed(connection)
337
359
  connection.close
360
+ emit_monitor_event(:disconnected, endpoint: endpoint)
338
361
 
339
362
  # Signal the connection task to exit.
340
363
  done = @connection_promises.delete(connection)
@@ -393,6 +416,8 @@ module OMQ
393
416
  @routing.stop rescue nil
394
417
  @tasks.each { |t| t.stop rescue nil }
395
418
  @tasks.clear
419
+ emit_monitor_event(:closed)
420
+ close_monitor_queue
396
421
  end
397
422
 
398
423
 
@@ -519,8 +544,10 @@ module OMQ
519
544
  @connection_promises[conn] = done if done
520
545
  @routing.connection_added(conn)
521
546
  @peer_connected.resolve(conn)
547
+ emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
522
548
  conn
523
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
549
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST => error
550
+ emit_monitor_event(:handshake_failed, endpoint: endpoint, detail: { error: error })
524
551
  conn&.close
525
552
  raise
526
553
  end
@@ -585,6 +612,7 @@ module OMQ
585
612
  delay = [delay * 2, max_delay].min if max_delay
586
613
  # After first attempt with delay: 0, use the configured interval
587
614
  delay = ri.is_a?(Range) ? ri.begin : ri if delay == 0
615
+ emit_monitor_event(:connect_retried, endpoint: endpoint, detail: { interval: delay })
588
616
  end
589
617
  end
590
618
  rescue Async::Stop
@@ -601,102 +629,55 @@ module OMQ
601
629
  # resolution failures during reconnect are retried with backoff.
602
630
  #
603
631
  def validate_endpoint!(endpoint)
604
- case endpoint
605
- when /\Atcp:\/\//
606
- host = URI.parse(endpoint.sub("tcp://", "http://")).hostname
607
- when /\Atls\+tcp:\/\//
608
- host = URI.parse("http://#{endpoint.delete_prefix("tls+tcp://")}").hostname
609
- else
610
- return
611
- end
612
- Addrinfo.getaddrinfo(host, nil, nil, :STREAM) if host
632
+ transport = transport_for(endpoint)
633
+ transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
613
634
  end
614
635
 
615
636
 
637
+
616
638
  def transport_for(endpoint)
617
- case endpoint
618
- when /\Atls\+tcp:\/\// then Transport::TLS
619
- when /\Atcp:\/\// then Transport::TCP
620
- when /\Aipc:\/\// then Transport::IPC
621
- when /\Ainproc:\/\// then Transport::Inproc
622
- else raise ArgumentError, "unsupported transport: #{endpoint}"
639
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
640
+ self.class.transports[scheme] or
641
+ raise ArgumentError, "unsupported transport: #{endpoint}"
642
+ end
643
+
644
+
645
+ # Delegates accept loop startup to the listener.
646
+ #
647
+ # Stream-based listeners (TCP, IPC, TLS, …) implement
648
+ # +#start_accept_loops+. Inproc listeners do not — connections
649
+ # are established synchronously during +connect+.
650
+ #
651
+ def start_accept_loops(listener)
652
+ return unless listener.respond_to?(:start_accept_loops)
653
+ listener.start_accept_loops(@parent_task) do |io|
654
+ handle_accepted(io, endpoint: listener.endpoint)
623
655
  end
624
656
  end
625
657
 
626
658
 
627
- def extract_tcp_port(endpoint)
628
- return nil unless endpoint&.start_with?("tcp://") || endpoint&.start_with?("tls+tcp://")
629
- port = endpoint.split(":").last.to_i
630
- port.positive? ? port : nil
659
+ def freeze_error_lists!
660
+ return if OMQ::CONNECTION_LOST.frozen?
661
+ OMQ::CONNECTION_LOST.freeze
662
+ OMQ::CONNECTION_FAILED.freeze
631
663
  end
632
664
 
633
665
 
634
- # Spawns accept loops for a listener under @parent_task.
635
- #
636
- # TCP listeners have multiple server sockets (IPv4/IPv6);
637
- # IPC listeners have one. Inproc listeners have none.
638
- #
639
- def start_accept_loops(listener)
640
- case listener
641
- when Transport::TLS::Listener
642
- tasks = listener.servers.map do |server|
643
- @parent_task.async(transient: true, annotation: "tls accept #{listener.endpoint}") do
644
- loop do
645
- client = server.accept
646
- Async::Task.current.defer_stop do
647
- ssl = OpenSSL::SSL::SSLSocket.new(client, listener.ssl_context)
648
- ssl.sync_close = true
649
- ssl.accept
650
- handle_accepted(IO::Stream::Buffered.wrap(ssl), endpoint: listener.endpoint)
651
- rescue OpenSSL::SSL::SSLError
652
- # Bad certificate, protocol mismatch, etc. — drop this
653
- # connection but keep the accept loop running.
654
- ssl&.close rescue nil
655
- end
656
- end
657
- rescue Async::Stop
658
- rescue IOError
659
- # server closed
660
- ensure
661
- server.close rescue nil
662
- end
663
- end
664
- listener.accept_tasks = tasks
665
-
666
- when Transport::TCP::Listener
667
- tasks = listener.servers.map do |server|
668
- @parent_task.async(transient: true, annotation: "tcp accept #{listener.endpoint}") do
669
- loop do
670
- client = server.accept
671
- Async::Task.current.defer_stop do
672
- handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: listener.endpoint)
673
- end
674
- end
675
- rescue Async::Stop
676
- rescue IOError
677
- # server closed
678
- ensure
679
- server.close rescue nil
680
- end
681
- end
682
- listener.accept_tasks = tasks
666
+ def emit_monitor_event(type, endpoint: nil, detail: nil)
667
+ return unless @monitor_queue
668
+ @monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
669
+ rescue Async::Stop, ClosedQueueError
670
+ end
683
671
 
684
- when Transport::IPC::Listener
685
- task = @parent_task.async(transient: true, annotation: "ipc accept #{listener.endpoint}") do
686
- loop do
687
- client = listener.server.accept
688
- Async::Task.current.defer_stop do
689
- handle_accepted(IO::Stream::Buffered.wrap(client), endpoint: listener.endpoint)
690
- end
691
- end
692
- rescue Async::Stop
693
- rescue IOError
694
- # server closed
695
- ensure
696
- listener.server.close rescue nil
697
- end
698
- listener.accept_task = task
699
- end
672
+
673
+ def close_monitor_queue
674
+ return unless @monitor_queue
675
+ @monitor_queue.push(nil)
700
676
  end
701
677
  end
678
+
679
+ # Register built-in transports.
680
+ Engine.transports["tcp"] = Transport::TCP
681
+ Engine.transports["ipc"] = Transport::IPC
682
+ Engine.transports["inproc"] = Transport::Inproc
702
683
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ # Lifecycle event emitted by {Socket#monitor}.
5
+ #
6
+ # @!attribute [r] type
7
+ # @return [Symbol] event type (:listening, :connected, :disconnected, etc.)
8
+ # @!attribute [r] endpoint
9
+ # @return [String, nil] the endpoint involved
10
+ # @!attribute [r] detail
11
+ # @return [Hash, nil] extra context (e.g. { error: }, { interval: }, etc.)
12
+ #
13
+ MonitorEvent = Data.define(:type, :endpoint, :detail) do
14
+ def initialize(type:, endpoint: nil, detail: nil) = super
15
+ end
16
+ end
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
@@ -13,7 +13,7 @@ module OMQ
13
13
  #
14
14
  def initialize(engine)
15
15
  @engine = engine
16
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
16
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
17
17
  @tasks = []
18
18
  init_round_robin(engine)
19
19
  end
@@ -13,12 +13,16 @@ module OMQ
13
13
  module FanOut
14
14
  attr_reader :subscriber_joined
15
15
 
16
+ # @return [Boolean] true when the send pump is idle (not sending a batch)
17
+ def send_pump_idle? = @send_pump_idle
18
+
16
19
  private
17
20
 
18
21
  def init_fan_out(engine)
19
22
  @connections = []
20
23
  @subscriptions = {} # connection => Set of prefixes
21
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
24
+ @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
25
+ @on_mute = engine.options.on_mute
22
26
  @send_pump_started = false
23
27
  @send_pump_idle = true
24
28
  @conflate = engine.options.conflate
@@ -57,8 +61,14 @@ module OMQ
57
61
  @subscriptions[conn]&.delete(prefix)
58
62
  end
59
63
 
60
- # @return [Boolean] true when the send pump is idle (not sending a batch)
61
- def send_pump_idle? = @send_pump_idle
64
+ # Returns true if the connection is muted (recv queue full) and
65
+ # the on_mute strategy says to drop rather than block.
66
+ #
67
+ def muted?(conn)
68
+ return false if @on_mute == :block
69
+ q = conn.direct_recv_queue if conn.respond_to?(:direct_recv_queue)
70
+ q&.respond_to?(:limited?) && q.limited?
71
+ end
62
72
 
63
73
 
64
74
  def start_send_pump
@@ -83,6 +93,7 @@ module OMQ
83
93
  end
84
94
  end
85
95
  @latest.each do |conn, parts|
96
+ next if muted?(conn)
86
97
  begin
87
98
  conn.write_message(parts)
88
99
  @written << conn
@@ -96,6 +107,7 @@ module OMQ
96
107
 
97
108
  @connections.each do |conn|
98
109
  next unless subscribed?(conn, topic)
110
+ next if muted?(conn)
99
111
  begin
100
112
  if conn.respond_to?(:curve?) && conn.curve?
101
113
  conn.write_message(parts)
@@ -14,8 +14,8 @@ module OMQ
14
14
  def initialize(engine)
15
15
  @engine = engine
16
16
  @connection = nil
17
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
17
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
18
+ @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
19
19
  @tasks = []
20
20
  @send_pump_idle = true
21
21
  end
@@ -9,7 +9,7 @@ module OMQ
9
9
  #
10
10
  def initialize(engine)
11
11
  @engine = engine
12
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
12
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
13
13
  @tasks = []
14
14
  end
15
15
 
@@ -76,6 +76,8 @@ module OMQ
76
76
  @engine.connection_lost(conn)
77
77
  end
78
78
  end
79
+
80
+
79
81
  end
80
82
  end
81
83
  end
@@ -15,8 +15,8 @@ module OMQ
15
15
 
16
16
  def initialize(engine)
17
17
  @engine = engine
18
- @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
19
- @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
18
+ @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
19
+ @send_queue = Routing.build_queue(engine.options.send_hwm, :block)
20
20
  @pending_replies = []
21
21
  @tasks = []
22
22
  @send_pump_started = false