omq 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +30 -0
  3. data/LICENSE +15 -0
  4. data/README.md +145 -0
  5. data/lib/omq/pair.rb +13 -0
  6. data/lib/omq/pub_sub.rb +77 -0
  7. data/lib/omq/push_pull.rb +21 -0
  8. data/lib/omq/req_rep.rb +23 -0
  9. data/lib/omq/router_dealer.rb +36 -0
  10. data/lib/omq/socket.rb +178 -0
  11. data/lib/omq/version.rb +5 -0
  12. data/lib/omq/zmtp/codec/command.rb +207 -0
  13. data/lib/omq/zmtp/codec/frame.rb +104 -0
  14. data/lib/omq/zmtp/codec/greeting.rb +96 -0
  15. data/lib/omq/zmtp/codec.rb +18 -0
  16. data/lib/omq/zmtp/connection.rb +233 -0
  17. data/lib/omq/zmtp/engine.rb +339 -0
  18. data/lib/omq/zmtp/mechanism/null.rb +70 -0
  19. data/lib/omq/zmtp/options.rb +57 -0
  20. data/lib/omq/zmtp/reactor.rb +142 -0
  21. data/lib/omq/zmtp/readable.rb +29 -0
  22. data/lib/omq/zmtp/routing/dealer.rb +57 -0
  23. data/lib/omq/zmtp/routing/fan_out.rb +89 -0
  24. data/lib/omq/zmtp/routing/pair.rb +68 -0
  25. data/lib/omq/zmtp/routing/pub.rb +62 -0
  26. data/lib/omq/zmtp/routing/pull.rb +48 -0
  27. data/lib/omq/zmtp/routing/push.rb +57 -0
  28. data/lib/omq/zmtp/routing/rep.rb +83 -0
  29. data/lib/omq/zmtp/routing/req.rb +70 -0
  30. data/lib/omq/zmtp/routing/round_robin.rb +69 -0
  31. data/lib/omq/zmtp/routing/router.rb +88 -0
  32. data/lib/omq/zmtp/routing/sub.rb +80 -0
  33. data/lib/omq/zmtp/routing/xpub.rb +74 -0
  34. data/lib/omq/zmtp/routing/xsub.rb +80 -0
  35. data/lib/omq/zmtp/routing.rb +38 -0
  36. data/lib/omq/zmtp/transport/inproc.rb +299 -0
  37. data/lib/omq/zmtp/transport/ipc.rb +114 -0
  38. data/lib/omq/zmtp/transport/tcp.rb +98 -0
  39. data/lib/omq/zmtp/valid_peers.rb +21 -0
  40. data/lib/omq/zmtp/writable.rb +44 -0
  41. data/lib/omq/zmtp.rb +47 -0
  42. data/lib/omq.rb +19 -0
  43. metadata +110 -0
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+
5
+ module OMQ
6
+ module ZMTP
7
+ # Per-socket orchestrator.
8
+ #
9
+ # Manages connections, transports, and the routing strategy for one
10
+ # OMQ::Socket instance. Each socket type creates one Engine.
11
+ #
12
+ class Engine
13
+ # @return [Symbol] socket type (e.g. :REQ, :PAIR)
14
+ #
15
+ attr_reader :socket_type
16
+
17
+ # @return [Options] socket options
18
+ #
19
+ attr_reader :options
20
+
21
+ # @return [Routing] routing strategy
22
+ #
23
+ attr_reader :routing
24
+
25
+ # @return [String, nil] last bound endpoint
26
+ #
27
+ attr_reader :last_endpoint
28
+
29
+ # @return [Integer, nil] last auto-selected TCP port
30
+ #
31
+ attr_reader :last_tcp_port
32
+
33
+ # @param socket_type [Symbol] e.g. :REQ, :REP, :PAIR
34
+ # @param options [Options]
35
+ #
36
+ def initialize(socket_type, options)
37
+ @socket_type = socket_type
38
+ @options = options
39
+ @routing = Routing.for(socket_type).new(self)
40
+ @connections = []
41
+ @connection_endpoints = {} # connection => endpoint (for reconnection)
42
+ @connected_endpoints = [] # endpoints we connected to (not bound)
43
+ @listeners = []
44
+ @tasks = []
45
+ @closed = false
46
+ @last_endpoint = nil
47
+ @last_tcp_port = nil
48
+ end
49
+
50
+ # Binds to an endpoint.
51
+ #
52
+ # @param endpoint [String] e.g. "tcp://127.0.0.1:5555", "inproc://foo"
53
+ # @return [void]
54
+ # @raise [ArgumentError] on unsupported transport
55
+ #
56
+ def bind(endpoint)
57
+ transport = transport_for(endpoint)
58
+ listener = transport.bind(endpoint, self)
59
+ @listeners << listener
60
+ @last_endpoint = listener.endpoint
61
+ @last_tcp_port = extract_tcp_port(listener.endpoint)
62
+ end
63
+
64
+ # Connects to an endpoint.
65
+ #
66
+ # @param endpoint [String]
67
+ # @return [void]
68
+ #
69
+ def connect(endpoint)
70
+ @connected_endpoints << endpoint
71
+ transport = transport_for(endpoint)
72
+ transport.connect(endpoint, self)
73
+ rescue Errno::ECONNREFUSED, Errno::ENOENT, Errno::ETIMEDOUT, IOError, ProtocolError
74
+ # Server not up yet — schedule background reconnect
75
+ schedule_reconnect(endpoint)
76
+ end
77
+
78
+ # Disconnects from an endpoint. Closes connections to that endpoint
79
+ # and stops auto-reconnection for it.
80
+ #
81
+ # @param endpoint [String]
82
+ # @return [void]
83
+ #
84
+ def disconnect(endpoint)
85
+ @connected_endpoints.delete(endpoint)
86
+ conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
87
+ conns.each do |conn|
88
+ @connection_endpoints.delete(conn)
89
+ @connections.delete(conn)
90
+ @routing.connection_removed(conn)
91
+ conn.close
92
+ end
93
+ end
94
+
95
+ # Unbinds from an endpoint. Stops the listener and closes all
96
+ # connections that were accepted on it.
97
+ #
98
+ # @param endpoint [String]
99
+ # @return [void]
100
+ #
101
+ def unbind(endpoint)
102
+ listener = @listeners.find { |l| l.endpoint == endpoint }
103
+ return unless listener
104
+ listener.stop
105
+ @listeners.delete(listener)
106
+
107
+ # Close connections accepted on this endpoint
108
+ conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
109
+ conns.each do |conn|
110
+ @connection_endpoints.delete(conn)
111
+ @connections.delete(conn)
112
+ @routing.connection_removed(conn)
113
+ conn.close
114
+ end
115
+ end
116
+
117
+ # Called by a transport when an incoming connection is accepted.
118
+ #
119
+ # @param io [#read, #write, #close]
120
+ # @param endpoint [String, nil] the endpoint this was accepted on
121
+ # @return [void]
122
+ #
123
+ def handle_accepted(io, endpoint: nil)
124
+ setup_connection(io, as_server: true, endpoint: endpoint)
125
+ end
126
+
127
+ # Called by a transport when an outgoing connection is established.
128
+ #
129
+ # @param io [#read, #write, #close]
130
+ # @return [void]
131
+ #
132
+ def handle_connected(io, endpoint: nil)
133
+ setup_connection(io, as_server: false, endpoint: endpoint)
134
+ end
135
+
136
+ # Called by inproc transport with a pre-validated DirectPipe.
137
+ # Skips ZMTP handshake — just registers with routing strategy.
138
+ #
139
+ # @param pipe [Transport::Inproc::DirectPipe]
140
+ # @return [void]
141
+ #
142
+ def connection_ready(pipe, endpoint: nil)
143
+ @connections << pipe
144
+ @connection_endpoints[pipe] = endpoint if endpoint
145
+ @routing.connection_added(pipe)
146
+ end
147
+
148
+ # Dequeues the next received message. Blocks until available.
149
+ #
150
+ # @return [Array<String>] message parts
151
+ #
152
+ def dequeue_recv
153
+ @routing.recv_queue.dequeue
154
+ end
155
+
156
+ # Enqueues a message for sending. Blocks at HWM.
157
+ #
158
+ # @param parts [Array<String>]
159
+ # @return [void]
160
+ #
161
+ def enqueue_send(parts)
162
+ @routing.enqueue(parts)
163
+ end
164
+
165
+ # Starts a recv pump for a connection, or wires the inproc
166
+ # fast path when the connection is a DirectPipe.
167
+ #
168
+ # @param conn [Connection, Transport::Inproc::DirectPipe]
169
+ # @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
170
+ # @param transform [#call, nil] optional message transform
171
+ # @return [#stop, nil] pump task handle, or nil for DirectPipe bypass
172
+ #
173
+ def start_recv_pump(conn, recv_queue, transform: nil)
174
+ if conn.is_a?(Transport::Inproc::DirectPipe) && conn.peer
175
+ conn.peer.direct_recv_queue = recv_queue
176
+ conn.peer.direct_recv_transform = transform
177
+ return nil
178
+ end
179
+
180
+ Reactor.spawn_pump do
181
+ loop do
182
+ msg = conn.receive_message
183
+ msg = transform ? transform.call(msg) : msg
184
+ recv_queue.enqueue(msg)
185
+ end
186
+ rescue EOFError, IOError
187
+ connection_lost(conn)
188
+ end
189
+ end
190
+
191
+ # Called when a connection is lost.
192
+ #
193
+ # @param connection [Connection]
194
+ # @return [void]
195
+ #
196
+ def connection_lost(connection)
197
+ endpoint = @connection_endpoints.delete(connection)
198
+ @connections.delete(connection)
199
+ @routing.connection_removed(connection)
200
+ connection.close
201
+
202
+ # Auto-reconnect if this was a connected (not bound) endpoint
203
+ if endpoint && @connected_endpoints.include?(endpoint) && !@closed
204
+ schedule_reconnect(endpoint)
205
+ end
206
+ end
207
+
208
+ # Closes all connections and listeners.
209
+ #
210
+ # @return [void]
211
+ #
212
+ def close
213
+ return if @closed
214
+ @closed = true
215
+
216
+ # Linger: wait for send queues to drain before closing.
217
+ # linger=0 → close immediately, linger=nil → wait forever.
218
+ linger = @options.linger
219
+ if linger.nil? || linger > 0
220
+ drain_timeout = linger # nil = wait forever, >0 = seconds
221
+ drain_send_queues(drain_timeout)
222
+ end
223
+
224
+ # Close connections — causes pump tasks to get EOFError/IOError
225
+ @connections.each(&:close)
226
+ @connections.clear
227
+ @listeners.each(&:stop)
228
+ @listeners.clear
229
+ # Stop any remaining pump tasks
230
+ @routing.stop rescue nil
231
+ @tasks.each { |t| t.stop rescue nil }
232
+ @tasks.clear
233
+ end
234
+
235
+ private
236
+
237
+ # Waits for the send queue to drain.
238
+ #
239
+ # @param timeout [Numeric, nil] max seconds to wait (nil = forever)
240
+ #
241
+ def drain_send_queues(timeout)
242
+ return unless @routing.respond_to?(:send_queue)
243
+ deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
244
+
245
+ until @routing.send_queue.empty?
246
+ if deadline
247
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
248
+ break if remaining <= 0
249
+ end
250
+ sleep 0.001
251
+ end
252
+ end
253
+
254
+ def setup_connection(io, as_server:, endpoint: nil)
255
+ conn = Connection.new(
256
+ io,
257
+ socket_type: @socket_type.to_s,
258
+ identity: @options.identity,
259
+ as_server: as_server,
260
+ mechanism: build_mechanism,
261
+ heartbeat_interval: @options.heartbeat_interval,
262
+ heartbeat_ttl: @options.heartbeat_ttl,
263
+ heartbeat_timeout: @options.heartbeat_timeout,
264
+ max_message_size: @options.max_message_size,
265
+ )
266
+ conn.handshake!
267
+ conn.start_heartbeat
268
+ @connections << conn
269
+ @connection_endpoints[conn] = endpoint if endpoint
270
+ @routing.connection_added(conn)
271
+ rescue ProtocolError, EOFError
272
+ conn&.close
273
+ raise
274
+ end
275
+
276
+ def schedule_reconnect(endpoint)
277
+ ri = @options.reconnect_interval
278
+ if ri.is_a?(Range)
279
+ delay = ri.begin
280
+ max_delay = ri.end
281
+ else
282
+ delay = ri
283
+ max_delay = nil
284
+ end
285
+
286
+ @tasks << Reactor.spawn_pump do
287
+ loop do
288
+ break if @closed
289
+ sleep delay
290
+ break if @closed
291
+ begin
292
+ transport = transport_for(endpoint)
293
+ transport.connect(endpoint, self)
294
+ break # reconnected successfully
295
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, IOError, ProtocolError
296
+ delay = [delay * 2, max_delay].min if max_delay
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+
303
+ def build_mechanism
304
+ case @options.mechanism
305
+ when :null
306
+ Mechanism::Null.new
307
+ when :curve
308
+ unless defined?(Mechanism::Curve)
309
+ raise LoadError, "require 'omq-curve' to use CURVE security"
310
+ end
311
+ Mechanism::Curve.new(
312
+ server_key: @options.curve_server_key,
313
+ public_key: @options.curve_public_key,
314
+ secret_key: @options.curve_secret_key,
315
+ as_server: @options.curve_server,
316
+ authenticator: @options.curve_authenticator,
317
+ )
318
+ else
319
+ raise ArgumentError, "unknown mechanism: #{@options.mechanism}"
320
+ end
321
+ end
322
+
323
+ def transport_for(endpoint)
324
+ case endpoint
325
+ when /\Atcp:\/\// then Transport::TCP
326
+ when /\Aipc:\/\// then Transport::IPC
327
+ when /\Ainproc:\/\// then Transport::Inproc
328
+ else raise ArgumentError, "unsupported transport: #{endpoint}"
329
+ end
330
+ end
331
+
332
+ def extract_tcp_port(endpoint)
333
+ return nil unless endpoint&.start_with?("tcp://")
334
+ port = endpoint.split(":").last.to_i
335
+ port.positive? ? port : nil
336
+ end
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Mechanism
6
+ # NULL security mechanism — no encryption, no authentication.
7
+ #
8
+ # Performs the ZMTP 3.1 greeting exchange and READY command handshake.
9
+ #
10
+ class Null
11
+ MECHANISM_NAME = "NULL"
12
+
13
+ # Performs the full NULL handshake over +io+.
14
+ #
15
+ # 1. Exchange 64-byte greetings
16
+ # 2. Validate peer greeting (version, mechanism)
17
+ # 3. Exchange READY commands (socket type + identity)
18
+ #
19
+ # @param io [#read, #write] transport IO
20
+ # @param as_server [Boolean]
21
+ # @param socket_type [String]
22
+ # @param identity [String]
23
+ # @return [Hash] { peer_socket_type:, peer_identity: }
24
+ # @raise [ProtocolError]
25
+ #
26
+ def handshake!(io, as_server:, socket_type:, identity:)
27
+ # Send our greeting
28
+ io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
29
+
30
+ # Read peer greeting
31
+ greeting_data = io.read_exactly(Codec::Greeting::SIZE)
32
+ peer_greeting = Codec::Greeting.decode(greeting_data)
33
+
34
+ unless peer_greeting[:mechanism] == MECHANISM_NAME
35
+ raise ProtocolError, "unsupported mechanism: #{peer_greeting[:mechanism]}"
36
+ end
37
+
38
+ # Send our READY command
39
+ ready_cmd = Codec::Command.ready(socket_type: socket_type, identity: identity)
40
+ io.write(ready_cmd.to_frame.to_wire)
41
+
42
+ # Read peer READY command
43
+ frame = Codec::Frame.read_from(io)
44
+ unless frame.command?
45
+ raise ProtocolError, "expected command frame, got data frame"
46
+ end
47
+
48
+ peer_cmd = Codec::Command.from_body(frame.body)
49
+ unless peer_cmd.name == "READY"
50
+ raise ProtocolError, "expected READY command, got #{peer_cmd.name}"
51
+ end
52
+
53
+ props = peer_cmd.properties
54
+ peer_socket_type = props["Socket-Type"]
55
+ peer_identity = props["Identity"] || ""
56
+
57
+ unless peer_socket_type
58
+ raise ProtocolError, "peer READY missing Socket-Type"
59
+ end
60
+
61
+ { peer_socket_type: peer_socket_type, peer_identity: peer_identity }
62
+ end
63
+
64
+ # @return [Boolean] false — NULL does not encrypt frames
65
+ #
66
+ def encrypted? = false
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ # Pure Ruby socket options.
6
+ #
7
+ # All timeouts are in seconds (Numeric) or nil (no timeout).
8
+ # HWM values are integers.
9
+ #
10
+ class Options
11
+ DEFAULT_HWM = 1000
12
+
13
+ # @param linger [Integer] linger period in seconds (default 0)
14
+ #
15
+ def initialize(linger: 0)
16
+ @send_hwm = DEFAULT_HWM
17
+ @recv_hwm = DEFAULT_HWM
18
+ @linger = linger
19
+ @identity = "".b
20
+ @router_mandatory = false
21
+ @read_timeout = nil # seconds, nil = no timeout
22
+ @write_timeout = nil
23
+ @reconnect_interval = 0.1 # seconds, or Range for backoff (e.g. 0.1..5.0)
24
+ @heartbeat_interval = nil # seconds, nil = disabled
25
+ @heartbeat_ttl = nil # seconds, nil = use heartbeat_interval
26
+ @heartbeat_timeout = nil # seconds, nil = use heartbeat_interval
27
+ @max_message_size = nil # bytes, nil = unlimited
28
+ @connect_timeout = 60 # seconds, nil = OS default
29
+ @mechanism = :null # :null or :curve
30
+ @curve_server = false
31
+ @curve_server_key = nil # 32-byte binary (server's permanent public key)
32
+ @curve_public_key = nil # 32-byte binary (our permanent public key)
33
+ @curve_secret_key = nil # 32-byte binary (our permanent secret key)
34
+ @curve_authenticator = nil # nil = allow all, Set = allowlist, #call = custom
35
+ end
36
+
37
+ attr_accessor :send_hwm, :recv_hwm,
38
+ :linger, :identity,
39
+ :router_mandatory,
40
+ :read_timeout, :write_timeout,
41
+ :reconnect_interval,
42
+ :heartbeat_interval, :heartbeat_ttl, :heartbeat_timeout,
43
+ :max_message_size,
44
+ :connect_timeout,
45
+ :mechanism,
46
+ :curve_server, :curve_server_key,
47
+ :curve_public_key, :curve_secret_key,
48
+ :curve_authenticator
49
+
50
+ alias_method :router_mandatory?, :router_mandatory
51
+ alias_method :recv_timeout, :read_timeout
52
+ alias_method :recv_timeout=, :read_timeout=
53
+ alias_method :send_timeout, :write_timeout
54
+ alias_method :send_timeout=, :write_timeout=
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+
5
+ module OMQ
6
+ module ZMTP
7
+ # Shared IO reactor for the Ruby backend.
8
+ #
9
+ # When user code runs inside an Async reactor, pump tasks are spawned
10
+ # as transient Async tasks directly. When no reactor is available
11
+ # (e.g. bare Thread.new), a single shared IO thread hosts all pump
12
+ # tasks — mirroring libzmq's IO thread architecture.
13
+ #
14
+ module Reactor
15
+ @work_queue = Thread::Queue.new
16
+ @thread = nil
17
+ @mutex = Mutex.new
18
+ @wake_r = nil
19
+ @wake_w = nil
20
+
21
+ class << self
22
+ # Spawns a pump task (recv loop, send loop, accept loop).
23
+ #
24
+ # Inside an Async reactor: spawns as transient Async task.
25
+ # Outside: dispatches to the shared IO thread.
26
+ #
27
+ # @return [#stop] a stoppable handle
28
+ #
29
+ def spawn_pump(&block)
30
+ if Async::Task.current?
31
+ Async(transient: true, &block)
32
+ else
33
+ handle = PumpHandle.new
34
+ ensure_started
35
+ @work_queue.push([:spawn, block, handle])
36
+ @wake_w.write_nonblock(".") rescue nil
37
+ handle
38
+ end
39
+ end
40
+
41
+ # Runs a block synchronously within an Async context.
42
+ #
43
+ # Inside an Async reactor: runs directly.
44
+ # Outside: dispatches to the shared IO thread and waits.
45
+ #
46
+ # @return [Object] the block's return value
47
+ #
48
+ def run(&block)
49
+ if Async::Task.current?
50
+ yield
51
+ else
52
+ result_queue = Thread::Queue.new
53
+ ensure_started
54
+ @work_queue.push([:run, block, result_queue])
55
+ @wake_w.write_nonblock(".") rescue nil
56
+ status, value = result_queue.pop
57
+ raise value if status == :error
58
+ value
59
+ end
60
+ end
61
+
62
+ # Ensures the shared IO thread is running.
63
+ #
64
+ # @return [void]
65
+ #
66
+ def ensure_started
67
+ @mutex.synchronize do
68
+ return if @thread&.alive?
69
+ @wake_r, @wake_w = IO.pipe
70
+ ready = Thread::Queue.new
71
+ @thread = Thread.new { run_reactor(ready, @wake_r) }
72
+ @thread.name = "omq-io"
73
+ ready.pop
74
+ end
75
+ end
76
+
77
+ # Stops the shared IO thread. Used in tests.
78
+ #
79
+ # @return [void]
80
+ #
81
+ def stop!
82
+ @work_queue.push([:stop])
83
+ @wake_w&.write_nonblock(".") rescue nil
84
+ @thread&.join(2)
85
+ @thread = nil
86
+ @wake_r&.close rescue nil
87
+ @wake_w&.close rescue nil
88
+ @wake_r = nil
89
+ @wake_w = nil
90
+ end
91
+
92
+ private
93
+
94
+ def run_reactor(ready, wake_r)
95
+ Async do |task|
96
+ ready.push(true)
97
+ loop do
98
+ # Wait for wakeup signal (non-blocking for Async scheduler)
99
+ wake_r.wait_readable
100
+ wake_r.read_nonblock(256) rescue nil
101
+
102
+ # Drain all pending work items
103
+ while (item = @work_queue.pop(true) rescue nil)
104
+ case item[0]
105
+ when :spawn
106
+ _, block, handle = item
107
+ async_task = task.async(transient: true, &block)
108
+ handle.task = async_task
109
+ when :run
110
+ _, block, result_queue = item
111
+ task.async do
112
+ result_queue.push([:ok, block.call])
113
+ rescue => e
114
+ result_queue.push([:error, e])
115
+ end
116
+ when :stop
117
+ return
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ # A stoppable handle for a pump task running in the shared reactor.
126
+ #
127
+ class PumpHandle
128
+ # @return [Async::Task, nil]
129
+ #
130
+ attr_accessor :task
131
+
132
+ # Stops the pump task.
133
+ #
134
+ # @return [void]
135
+ #
136
+ def stop
137
+ @task&.stop
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module OMQ
6
+ module ZMTP
7
+ # Pure Ruby Readable mixin. Dequeues messages from the engine's recv queue.
8
+ #
9
+ module Readable
10
+ # Receives the next message.
11
+ #
12
+ # @return [Array<String>] message parts
13
+ # @raise [IO::TimeoutError] if read_timeout exceeded
14
+ #
15
+ def receive
16
+ with_timeout(@options.read_timeout) { @engine.dequeue_recv }
17
+ end
18
+
19
+ # Waits until the socket is readable.
20
+ #
21
+ # @param timeout [Numeric, nil] timeout in seconds
22
+ # @return [true]
23
+ #
24
+ def wait_readable(timeout = @options.read_timeout)
25
+ true
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # DEALER socket routing: round-robin send, fair-queue receive.
7
+ #
8
+ # No envelope manipulation — messages pass through unchanged.
9
+ #
10
+ class Dealer
11
+ include RoundRobin
12
+
13
+ # @param engine [Engine]
14
+ #
15
+ def initialize(engine)
16
+ @engine = engine
17
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
18
+ @tasks = []
19
+ init_round_robin(engine)
20
+ end
21
+
22
+ # @return [Async::LimitedQueue]
23
+ #
24
+ attr_reader :recv_queue, :send_queue
25
+
26
+ # @param connection [Connection]
27
+ #
28
+ def connection_added(connection)
29
+ @connections << connection
30
+ signal_connection_available
31
+ task = @engine.start_recv_pump(connection, @recv_queue)
32
+ @tasks << task if task
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
+ end
41
+
42
+ # @param parts [Array<String>]
43
+ #
44
+ def enqueue(parts)
45
+ @send_queue.enqueue(parts)
46
+ end
47
+
48
+ #
49
+ def stop
50
+ @tasks.each(&:stop)
51
+ @tasks.clear
52
+ end
53
+
54
+ end
55
+ end
56
+ end
57
+ end