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/engine.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "async"
4
+ require_relative "engine/recv_pump"
5
+ require_relative "engine/heartbeat"
6
+ require_relative "engine/reconnect"
7
+ require_relative "engine/connection_setup"
4
8
 
5
9
  module OMQ
6
10
  # Per-socket orchestrator.
@@ -9,6 +13,23 @@ module OMQ
9
13
  # OMQ::Socket instance. Each socket type creates one Engine.
10
14
  #
11
15
  class Engine
16
+ # Scheme → transport module registry.
17
+ # Plugins add entries via +Engine.transports["scheme"] = MyTransport+.
18
+ #
19
+ @transports = {}
20
+
21
+ class << self
22
+ # @return [Hash{String => Module}] registered transports
23
+ attr_reader :transports
24
+ end
25
+
26
+ # Per-connection metadata: the endpoint it was established on and an
27
+ # optional Promise resolved when the connection is lost (used by
28
+ # {#spawn_connection} to await connection teardown).
29
+ #
30
+ ConnectionRecord = Data.define(:endpoint, :done)
31
+
32
+
12
33
  # @return [Symbol] socket type (e.g. :REQ, :PAIR)
13
34
  #
14
35
  attr_reader :socket_type
@@ -38,31 +59,31 @@ module OMQ
38
59
  # @param options [Options]
39
60
  #
40
61
  def initialize(socket_type, options)
41
- @socket_type = socket_type
42
- @options = options
43
- @routing = Routing.for(socket_type).new(self)
44
- @connections = []
45
- @connection_endpoints = {} # connection => endpoint (for reconnection)
46
- @connected_endpoints = [] # endpoints we connected to (not bound)
47
- @listeners = []
48
- @tasks = []
49
- @closed = false
50
- @closing = false
51
- @last_endpoint = nil
52
- @last_tcp_port = nil
53
- @peer_connected = Async::Promise.new
54
- @all_peers_gone = Async::Promise.new
55
- @reconnect_enabled = true
56
- @parent_task = nil
57
- @on_io_thread = false
58
- @connection_promises = {} # connection => Async::Promise
59
- @fatal_error = nil
60
- end
61
-
62
-
63
- attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task
64
-
65
- attr_writer :reconnect_enabled
62
+ @socket_type = socket_type
63
+ @options = options
64
+ @routing = Routing.for(socket_type).new(self)
65
+ @connections = {} # connection => ConnectionRecord
66
+ @dialed = Set.new # endpoints we called connect() on (reconnect intent)
67
+ @listeners = []
68
+ @tasks = []
69
+ @state = :open
70
+ @last_endpoint = nil
71
+ @last_tcp_port = nil
72
+ @peer_connected = Async::Promise.new
73
+ @all_peers_gone = Async::Promise.new
74
+ @reconnect_enabled = true
75
+ @parent_task = nil
76
+ @on_io_thread = false
77
+ @fatal_error = nil
78
+ @monitor_queue = nil
79
+ end
80
+
81
+
82
+ attr_reader :peer_connected, :all_peers_gone, :connections, :parent_task, :tasks
83
+
84
+ attr_writer :reconnect_enabled, :monitor_queue
85
+
86
+ def closed? = @state == :closed
66
87
 
67
88
  # Optional proc that wraps new connections (e.g. for serialization).
68
89
  # Called with the raw connection; must return the (possibly wrapped) connection.
@@ -92,12 +113,17 @@ module OMQ
92
113
  # @raise [ArgumentError] on unsupported transport
93
114
  #
94
115
  def bind(endpoint)
116
+ freeze_error_lists!
95
117
  transport = transport_for(endpoint)
96
118
  listener = transport.bind(endpoint, self)
97
119
  start_accept_loops(listener)
98
120
  @listeners << listener
99
121
  @last_endpoint = listener.endpoint
100
- @last_tcp_port = extract_tcp_port(listener.endpoint)
122
+ @last_tcp_port = listener.respond_to?(:port) ? listener.port : nil
123
+ emit_monitor_event(:listening, endpoint: listener.endpoint)
124
+ rescue => error
125
+ emit_monitor_event(:bind_failed, endpoint: endpoint, detail: { error: error })
126
+ raise
101
127
  end
102
128
 
103
129
 
@@ -107,14 +133,16 @@ module OMQ
107
133
  # @return [void]
108
134
  #
109
135
  def connect(endpoint)
136
+ freeze_error_lists!
110
137
  validate_endpoint!(endpoint)
111
- @connected_endpoints << endpoint
138
+ @dialed.add(endpoint)
112
139
  if endpoint.start_with?("inproc://")
113
140
  # Inproc connect is synchronous and instant
114
141
  transport = transport_for(endpoint)
115
142
  transport.connect(endpoint, self)
116
143
  else
117
144
  # TCP/IPC connect in background — never blocks the caller
145
+ emit_monitor_event(:connect_delayed, endpoint: endpoint)
118
146
  schedule_reconnect(endpoint, delay: 0)
119
147
  end
120
148
  end
@@ -127,14 +155,8 @@ module OMQ
127
155
  # @return [void]
128
156
  #
129
157
  def disconnect(endpoint)
130
- @connected_endpoints.delete(endpoint)
131
- conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
132
- conns.each do |conn|
133
- @connection_endpoints.delete(conn)
134
- @connections.delete(conn)
135
- @routing.connection_removed(conn)
136
- conn.close
137
- end
158
+ @dialed.delete(endpoint)
159
+ close_connections_at(endpoint)
138
160
  end
139
161
 
140
162
 
@@ -149,15 +171,7 @@ module OMQ
149
171
  return unless listener
150
172
  listener.stop
151
173
  @listeners.delete(listener)
152
-
153
- # Close connections accepted on this endpoint
154
- conns = @connection_endpoints.select { |_, ep| ep == endpoint }.keys
155
- conns.each do |conn|
156
- @connection_endpoints.delete(conn)
157
- @connections.delete(conn)
158
- @routing.connection_removed(conn)
159
- conn.close
160
- end
174
+ close_connections_at(endpoint)
161
175
  end
162
176
 
163
177
 
@@ -168,6 +182,7 @@ module OMQ
168
182
  # @return [void]
169
183
  #
170
184
  def handle_accepted(io, endpoint: nil)
185
+ emit_monitor_event(:accepted, endpoint: endpoint)
171
186
  spawn_connection(io, as_server: true, endpoint: endpoint)
172
187
  end
173
188
 
@@ -178,6 +193,7 @@ module OMQ
178
193
  # @return [void]
179
194
  #
180
195
  def handle_connected(io, endpoint: nil)
196
+ emit_monitor_event(:connected, endpoint: endpoint)
181
197
  spawn_connection(io, as_server: false, endpoint: endpoint)
182
198
  end
183
199
 
@@ -190,10 +206,10 @@ module OMQ
190
206
  #
191
207
  def connection_ready(pipe, endpoint: nil)
192
208
  pipe = @connection_wrapper.call(pipe) if @connection_wrapper
193
- @connections << pipe
194
- @connection_endpoints[pipe] = endpoint if endpoint
209
+ @connections[pipe] = ConnectionRecord.new(endpoint: endpoint, done: nil)
195
210
  @routing.connection_added(pipe)
196
211
  @peer_connected.resolve(pipe)
212
+ emit_monitor_event(:handshake_succeeded, endpoint: endpoint)
197
213
  end
198
214
 
199
215
 
@@ -251,77 +267,17 @@ module OMQ
251
267
  end
252
268
 
253
269
 
254
- # Starts a recv pump for a connection, or wires the inproc
255
- # fast path when the connection is a DirectPipe.
256
- #
257
- # @param conn [Connection, Transport::Inproc::DirectPipe]
258
- # Starts a recv pump that dequeues messages from a connection
259
- # and enqueues them into the routing strategy's recv queue.
260
- #
261
- # When a block is given, each message is yielded for transformation
262
- # before enqueueing. The block is compiled at the call site, giving
263
- # YJIT a monomorphic call per routing strategy instead of a shared
264
- # megamorphic `transform.call` dispatch.
270
+ # Starts a recv pump for a connection, or wires the inproc fast path.
265
271
  #
266
272
  # @param conn [Connection, Transport::Inproc::DirectPipe]
267
- # @param recv_queue [Async::LimitedQueue] routing strategy's recv queue
273
+ # @param recv_queue [SignalingQueue]
268
274
  # @yield [msg] optional per-message transform
269
- # @return [#stop, nil] pump task handle, or nil for DirectPipe bypass
275
+ # @return [Async::Task, nil]
270
276
  #
271
- # Fairness limits for the recv pump. Yield to the scheduler
272
- # after reading this many messages or bytes from one connection,
273
- # whichever comes first. Prevents a fast or large-message
274
- # connection from starving slower peers.
275
- RECV_FAIRNESS_MESSAGES = 64
276
- RECV_FAIRNESS_BYTES = 1 << 20 # 1 MB
277
-
278
277
  def start_recv_pump(conn, recv_queue, &transform)
279
- if conn.is_a?(Transport::Inproc::DirectPipe) && conn.peer
280
- conn.peer.direct_recv_queue = recv_queue
281
- conn.peer.direct_recv_transform = transform
282
- return nil
283
- end
284
-
285
- if transform
286
- @parent_task.async(transient: true, annotation: "recv pump") do |task|
287
- loop do
288
- count = 0
289
- bytes = 0
290
- while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
291
- msg = conn.receive_message
292
- msg = transform.call(msg).freeze
293
- recv_queue.enqueue(msg)
294
- count += 1
295
- bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
296
- end
297
- task.yield
298
- end
299
- rescue Async::Stop
300
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
301
- connection_lost(conn)
302
- rescue => error
303
- signal_fatal_error(error)
304
- end
305
- else
306
- @parent_task.async(transient: true, annotation: "recv pump") do |task|
307
- loop do
308
- count = 0
309
- bytes = 0
310
- while count < RECV_FAIRNESS_MESSAGES && bytes < RECV_FAIRNESS_BYTES
311
- msg = conn.receive_message
312
- recv_queue.enqueue(msg)
313
- count += 1
314
- bytes += msg.is_a?(Array) && msg.first.is_a?(String) ? msg.sum(&:bytesize) : 0
315
- end
316
- task.yield
317
- end
318
- rescue Async::Stop
319
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
320
- connection_lost(conn)
321
- rescue => error
322
- signal_fatal_error(error)
323
- end
324
- end
278
+ task = RecvPump.start(@parent_task, conn, recv_queue, self, transform)
279
+ @tasks << task if task
280
+ task
325
281
  end
326
282
 
327
283
 
@@ -331,24 +287,13 @@ module OMQ
331
287
  # @return [void]
332
288
  #
333
289
  def connection_lost(connection)
334
- endpoint = @connection_endpoints.delete(connection)
335
- @connections.delete(connection)
290
+ entry = @connections.delete(connection)
336
291
  @routing.connection_removed(connection)
337
292
  connection.close
338
-
339
- # Signal the connection task to exit.
340
- done = @connection_promises.delete(connection)
341
- done&.resolve(true)
342
-
343
- # Resolve all_peers_gone once: had peers, now have none.
344
- if @peer_connected.resolved? && @connections.empty?
345
- @all_peers_gone.resolve(true)
346
- end
347
-
348
- # Auto-reconnect if this was a connected (not bound) endpoint
349
- if endpoint && @connected_endpoints.include?(endpoint) && !@closed && !@closing && @reconnect_enabled
350
- schedule_reconnect(endpoint)
351
- end
293
+ emit_monitor_event(:disconnected, endpoint: entry&.endpoint)
294
+ entry&.done&.resolve(true)
295
+ @all_peers_gone.resolve(true) if @peer_connected.resolved? && @connections.empty?
296
+ maybe_reconnect(entry&.endpoint)
352
297
  end
353
298
 
354
299
 
@@ -357,42 +302,17 @@ module OMQ
357
302
  # @return [void]
358
303
  #
359
304
  def close
360
- return if @closed || @closing
361
- @closing = true
362
-
363
- # Stop accepting new connections but only if we already have
364
- # peers to drain to. With zero connections the listeners must
365
- # stay open so late-arriving peers can still receive queued
366
- # messages during the linger period.
367
- unless @connections.empty?
368
- @listeners.each(&:stop)
369
- @listeners.clear
370
- end
371
-
372
- # Linger: wait for send queues to drain before closing.
373
- # linger=0 → close immediately, linger=nil → wait forever.
374
- # @closed is set AFTER draining so reconnect tasks keep
375
- # running during the linger period.
376
- linger = @options.linger
377
- if linger.nil? || linger > 0
378
- drain_timeout = linger # nil = wait forever, >0 = seconds
379
- drain_send_queues(drain_timeout)
380
- end
381
-
382
- @closed = true
305
+ return unless @state == :open
306
+ @state = :closing
307
+ stop_listeners unless @connections.empty?
308
+ drain_send_queues(@options.linger) if @options.linger.nil? || @options.linger > 0
309
+ @state = :closed
383
310
  Reactor.untrack_linger(@options.linger) if @on_io_thread
384
-
385
- # Stop any remaining listeners.
386
- @listeners.each(&:stop)
387
- @listeners.clear
388
-
389
- # Close connections — causes pump tasks to get EOFError/IOError
390
- @connections.each(&:close)
391
- @connections.clear
392
- # Stop any remaining pump tasks
393
- @routing.stop rescue nil
394
- @tasks.each { |t| t.stop rescue nil }
395
- @tasks.clear
311
+ stop_listeners
312
+ close_connections
313
+ stop_tasks
314
+ emit_monitor_event(:closed)
315
+ close_monitor_queue
396
316
  end
397
317
 
398
318
 
@@ -426,13 +346,13 @@ module OMQ
426
346
  # @param error [Exception]
427
347
  #
428
348
  def signal_fatal_error(error)
429
- return if @closing || @closed
349
+ return unless @state == :open
430
350
  @fatal_error = begin
431
351
  raise OMQ::SocketDeadError, "internal error killed #{@socket_type} socket"
432
352
  rescue => wrapped
433
353
  wrapped
434
354
  end
435
- @routing.recv_queue.enqueue(nil) rescue nil
355
+ @routing.recv_queue.push(nil) rescue nil
436
356
  @peer_connected.resolve(nil) rescue nil
437
357
  end
438
358
 
@@ -454,18 +374,24 @@ module OMQ
454
374
  end
455
375
 
456
376
 
457
- private
377
+ def emit_monitor_event(type, endpoint: nil, detail: nil)
378
+ return unless @monitor_queue
379
+ @monitor_queue.push(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
380
+ rescue Async::Stop, ClosedQueueError
381
+ end
458
382
 
383
+ def transport_for(endpoint)
384
+ scheme = endpoint[/\A([^:]+):\/\//, 1]
385
+ self.class.transports[scheme] or
386
+ raise ArgumentError, "unsupported transport: #{endpoint}"
387
+ end
388
+
389
+ private
459
390
 
460
- # Spawns an isolated connection task as a sibling of accept/reconnect
461
- # tasks. All per-connection children (heartbeat, recv pump, reaper)
462
- # live inside this task. When the connection dies, the entire subtree
463
- # is cleaned up by Async.
464
- #
465
391
  def spawn_connection(io, as_server:, endpoint: nil)
466
392
  task = @parent_task&.async(transient: true, annotation: "conn #{endpoint}") do
467
393
  done = Async::Promise.new
468
- conn = setup_connection(io, as_server: as_server, endpoint: endpoint, done: done)
394
+ conn = ConnectionSetup.run(io, self, as_server: as_server, endpoint: endpoint, done: done)
469
395
  done.wait
470
396
  rescue Protocol::ZMTP::Error, *CONNECTION_LOST
471
397
  # handshake failed or connection lost — subtree cleaned up
@@ -475,228 +401,76 @@ module OMQ
475
401
  @tasks << task if task
476
402
  end
477
403
 
478
-
479
- # Waits for the send queue to drain.
480
- #
481
- # @param timeout [Numeric, nil] max seconds to wait (nil = forever)
482
- #
483
404
  def drain_send_queues(timeout)
484
- return unless @routing.respond_to?(:send_queue)
405
+ return unless @routing.respond_to?(:send_queues_drained?)
485
406
  deadline = timeout ? Async::Clock.now + timeout : nil
486
-
487
- until @routing.send_queue.empty? && @routing.send_pump_idle?
488
- if deadline
489
- remaining = deadline - Async::Clock.now
490
- break if remaining <= 0
491
- end
407
+ until @routing.send_queues_drained?
408
+ break if deadline && (deadline - Async::Clock.now) <= 0
492
409
  sleep 0.001
493
410
  end
494
411
  end
495
412
 
496
-
497
- # Performs the ZMTP handshake, starts heartbeating, and registers
498
- # the new connection with the routing strategy.
499
- #
500
- # @param io [#read, #write, #close] underlying transport stream
501
- # @param as_server [Boolean] whether we are the ZMTP server side
502
- # @param endpoint [String, nil] endpoint for reconnection tracking
503
- # @param done [Async::Promise, nil] resolved when the connection is lost
504
- #
505
- def setup_connection(io, as_server:, endpoint: nil, done: nil)
506
- conn = Protocol::ZMTP::Connection.new(
507
- io,
508
- socket_type: @socket_type.to_s,
509
- identity: @options.identity,
510
- as_server: as_server,
511
- mechanism: @options.mechanism&.dup,
512
- max_message_size: @options.max_message_size,
513
- )
514
- conn.handshake!
515
- start_heartbeat(conn)
516
- conn = @connection_wrapper.call(conn) if @connection_wrapper
517
- @connections << conn
518
- @connection_endpoints[conn] = endpoint if endpoint
519
- @connection_promises[conn] = done if done
520
- @routing.connection_added(conn)
521
- @peer_connected.resolve(conn)
522
- conn
523
- rescue Protocol::ZMTP::Error, *CONNECTION_LOST
524
- conn&.close
525
- raise
413
+ def maybe_reconnect(endpoint)
414
+ return unless endpoint && @dialed.include?(endpoint)
415
+ return unless @state == :open && @reconnect_enabled
416
+ Reconnect.schedule(endpoint, @options, @parent_task, self)
526
417
  end
527
418
 
528
-
529
- # Spawns a heartbeat task for the connection.
530
- # The connection only tracks timestamps — the engine drives the loop.
531
- #
532
- # @param conn [Connection]
533
- # @return [void]
534
- #
535
- def start_heartbeat(conn)
536
- interval = @options.heartbeat_interval
537
- return unless interval
538
-
539
- ttl = @options.heartbeat_ttl || interval
540
- timeout = @options.heartbeat_timeout || interval
541
- conn.touch_heartbeat
542
-
543
- @tasks << @parent_task.async(transient: true, annotation: "heartbeat") do
544
- loop do
545
- sleep interval
546
- conn.send_command(Protocol::ZMTP::Codec::Command.ping(ttl: ttl, context: "".b))
547
- if conn.heartbeat_expired?(timeout)
548
- conn.close
549
- break
550
- end
551
- end
552
- rescue Async::Stop
553
- rescue *CONNECTION_LOST
554
- # connection closed
555
- end
419
+ def schedule_reconnect(endpoint, delay: nil)
420
+ Reconnect.schedule(endpoint, @options, @parent_task, self, delay: delay)
556
421
  end
557
422
 
423
+ def validate_endpoint!(endpoint)
424
+ transport = transport_for(endpoint)
425
+ transport.validate_endpoint!(endpoint) if transport.respond_to?(:validate_endpoint!)
426
+ end
558
427
 
559
- # Spawns a background task that reconnects to the given endpoint
560
- # with exponential back-off based on the reconnect_interval option.
561
- #
562
- # @param endpoint [String] endpoint to reconnect to
563
- # @param delay [Numeric, nil] initial delay in seconds (defaults to reconnect_interval)
564
- #
565
- def schedule_reconnect(endpoint, delay: nil)
566
- ri = @options.reconnect_interval
567
- if ri.is_a?(Range)
568
- delay ||= ri.begin
569
- max_delay = ri.end
570
- else
571
- delay ||= ri
572
- max_delay = nil
573
- end
574
-
575
- @tasks << @parent_task.async(transient: true, annotation: "reconnect #{endpoint}") do
576
- loop do
577
- break if @closed
578
- sleep delay if delay > 0
579
- break if @closed
580
- begin
581
- transport = transport_for(endpoint)
582
- transport.connect(endpoint, self)
583
- break # connected successfully
584
- rescue *CONNECTION_LOST, *CONNECTION_FAILED, Protocol::ZMTP::Error
585
- delay = [delay * 2, max_delay].min if max_delay
586
- # After first attempt with delay: 0, use the configured interval
587
- delay = ri.is_a?(Range) ? ri.begin : ri if delay == 0
588
- end
589
- end
590
- rescue Async::Stop
591
- # normal shutdown
592
- rescue => error
593
- signal_fatal_error(error)
428
+ def start_accept_loops(listener)
429
+ return unless listener.respond_to?(:start_accept_loops)
430
+ listener.start_accept_loops(@parent_task) do |io|
431
+ handle_accepted(io, endpoint: listener.endpoint)
594
432
  end
595
433
  end
596
434
 
597
-
598
- # Eagerly validates TCP hostnames so resolution errors fail
599
- # on connect, not silently in the background reconnect loop.
600
- # Reconnects still re-resolve (DNS may change), and transient
601
- # resolution failures during reconnect are retried with backoff.
602
- #
603
- 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
435
+ def stop_listeners
436
+ @listeners.each(&:stop)
437
+ @listeners.clear
613
438
  end
614
439
 
440
+ def close_connections
441
+ @connections.each_key(&:close)
442
+ @connections.clear
443
+ end
615
444
 
616
- 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}"
445
+ def close_connections_at(endpoint)
446
+ conns = @connections.filter_map { |conn, e| conn if e.endpoint == endpoint }
447
+ conns.each do |conn|
448
+ @connections.delete(conn)
449
+ @routing.connection_removed(conn)
450
+ conn.close
623
451
  end
624
452
  end
625
453
 
626
-
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
454
+ def stop_tasks
455
+ @routing.stop rescue nil
456
+ @tasks.each { |t| t.stop rescue nil }
457
+ @tasks.clear
631
458
  end
632
459
 
460
+ def freeze_error_lists!
461
+ return if OMQ::CONNECTION_LOST.frozen?
462
+ OMQ::CONNECTION_LOST.freeze
463
+ OMQ::CONNECTION_FAILED.freeze
464
+ end
633
465
 
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
683
-
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
466
+ def close_monitor_queue
467
+ return unless @monitor_queue
468
+ @monitor_queue.push(nil)
700
469
  end
701
470
  end
471
+
472
+ # Register built-in transports.
473
+ Engine.transports["tcp"] = Transport::TCP
474
+ Engine.transports["ipc"] = Transport::IPC
475
+ Engine.transports["inproc"] = Transport::Inproc
702
476
  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