omq 0.4.2 → 0.5.1

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.
@@ -54,16 +54,28 @@ module OMQ
54
54
  @send_pump_started = true
55
55
  @tasks << Reactor.spawn_pump do
56
56
  loop do
57
- parts = @send_queue.dequeue
58
- topic = parts.first || "".b
59
- @connections.each do |conn|
60
- next unless subscribed?(conn, topic)
61
- begin
62
- conn.send_message(parts)
63
- rescue *ZMTP::CONNECTION_LOST
64
- # connection dead — will be cleaned up
57
+ batch = [@send_queue.dequeue]
58
+ Routing.drain_send_queue(@send_queue, batch)
59
+
60
+ written = Set.new
61
+ batch.each do |parts|
62
+ topic = parts.first || "".b
63
+ @connections.each do |conn|
64
+ next unless subscribed?(conn, topic)
65
+ begin
66
+ conn.write_message(parts)
67
+ written << conn
68
+ rescue *ZMTP::CONNECTION_LOST
69
+ # connection dead — will be cleaned up
70
+ end
65
71
  end
66
72
  end
73
+
74
+ written.each do |conn|
75
+ conn.flush
76
+ rescue *ZMTP::CONNECTION_LOST
77
+ # connection dead — will be cleaned up
78
+ end
67
79
  end
68
80
  end
69
81
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # GATHER socket routing: fair-queue receive from SCATTER peers.
7
+ #
8
+ class Gather
9
+ # @param engine [Engine]
10
+ #
11
+ def initialize(engine)
12
+ @engine = engine
13
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
14
+ @tasks = []
15
+ end
16
+
17
+ # @return [Async::LimitedQueue]
18
+ #
19
+ attr_reader :recv_queue
20
+
21
+ # @param connection [Connection]
22
+ #
23
+ def connection_added(connection)
24
+ task = @engine.start_recv_pump(connection, @recv_queue)
25
+ @tasks << task if task
26
+ end
27
+
28
+ # @param connection [Connection]
29
+ #
30
+ def connection_removed(connection)
31
+ # recv pump stops on CONNECTION_LOST
32
+ end
33
+
34
+ # GATHER is read-only.
35
+ #
36
+ def enqueue(_parts)
37
+ raise "GATHER sockets cannot send"
38
+ end
39
+
40
+ #
41
+ def stop
42
+ @tasks.each(&:stop)
43
+ @tasks.clear
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -38,7 +38,11 @@ module OMQ
38
38
  # @param connection [Connection]
39
39
  #
40
40
  def connection_removed(connection)
41
- @connection = nil if @connection == connection
41
+ if @connection == connection
42
+ @connection = nil
43
+ @send_pump&.stop
44
+ @send_pump = nil
45
+ end
42
46
  end
43
47
 
44
48
  # @param parts [Array<String>]
@@ -56,11 +60,17 @@ module OMQ
56
60
  private
57
61
 
58
62
  def start_send_pump(conn)
59
- @tasks << Reactor.spawn_pump do
60
- loop { conn.send_message(@send_queue.dequeue) }
63
+ @send_pump = Reactor.spawn_pump do
64
+ loop do
65
+ batch = [@send_queue.dequeue]
66
+ Routing.drain_send_queue(@send_queue, batch)
67
+ batch.each { |parts| conn.write_message(parts) }
68
+ conn.flush
69
+ end
61
70
  rescue *ZMTP::CONNECTION_LOST
62
71
  @engine.connection_lost(conn)
63
72
  end
73
+ @tasks << @send_pump
64
74
  end
65
75
  end
66
76
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module ZMTP
7
+ module Routing
8
+ # PEER socket routing: bidirectional multi-peer with auto-generated
9
+ # 4-byte routing IDs.
10
+ #
11
+ # Prepends routing ID on receive. Strips routing ID on send and
12
+ # routes to the identified connection.
13
+ #
14
+ class Peer
15
+ # @param engine [Engine]
16
+ #
17
+ def initialize(engine)
18
+ @engine = engine
19
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
20
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
21
+ @connections_by_routing_id = {}
22
+ @tasks = []
23
+ @send_pump_started = false
24
+ end
25
+
26
+ # @return [Async::LimitedQueue]
27
+ #
28
+ attr_reader :recv_queue, :send_queue
29
+
30
+ # @param connection [Connection]
31
+ #
32
+ def connection_added(connection)
33
+ routing_id = SecureRandom.bytes(4)
34
+ @connections_by_routing_id[routing_id] = connection
35
+
36
+ task = @engine.start_recv_pump(connection, @recv_queue,
37
+ transform: ->(msg) { [routing_id, *msg] })
38
+ @tasks << task if task
39
+
40
+ start_send_pump unless @send_pump_started
41
+ end
42
+
43
+ # @param connection [Connection]
44
+ #
45
+ def connection_removed(connection)
46
+ @connections_by_routing_id.reject! { |_, c| c == connection }
47
+ end
48
+
49
+ # @param parts [Array<String>]
50
+ #
51
+ def enqueue(parts)
52
+ @send_queue.enqueue(parts)
53
+ end
54
+
55
+ def stop
56
+ @tasks.each(&:stop)
57
+ @tasks.clear
58
+ end
59
+
60
+ private
61
+
62
+ def start_send_pump
63
+ @send_pump_started = true
64
+ @tasks << Reactor.spawn_pump do
65
+ loop do
66
+ batch = [@send_queue.dequeue]
67
+ Routing.drain_send_queue(@send_queue, batch)
68
+
69
+ written = Set.new
70
+ batch.each do |parts|
71
+ routing_id = parts.first
72
+ conn = @connections_by_routing_id[routing_id]
73
+ next unless conn # silently drop if peer gone
74
+ begin
75
+ conn.write_message(parts[1..])
76
+ written << conn
77
+ rescue *ZMTP::CONNECTION_LOST
78
+ # will be cleaned up
79
+ end
80
+ end
81
+
82
+ written.each do |conn|
83
+ conn.flush
84
+ rescue *ZMTP::CONNECTION_LOST
85
+ # will be cleaned up
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -32,6 +32,7 @@ module OMQ
32
32
  @connections << connection
33
33
  signal_connection_available
34
34
  start_send_pump unless @send_pump_started
35
+ start_monitor(connection)
35
36
  end
36
37
 
37
38
  # @param connection [Connection]
@@ -51,6 +52,21 @@ module OMQ
51
52
  @tasks.each(&:stop)
52
53
  @tasks.clear
53
54
  end
55
+
56
+ private
57
+
58
+ # Monitors a connection for disconnection.
59
+ # Write-only sockets have no recv pump, so without this monitor
60
+ # a dead peer is only detected on the next send — which may
61
+ # succeed if the kernel send buffer absorbs the data.
62
+ #
63
+ def start_monitor(conn)
64
+ @tasks << Reactor.spawn_pump do
65
+ conn.receive_message # blocks until peer disconnects
66
+ rescue *ZMTP::CONNECTION_LOST
67
+ @engine.connection_lost(conn)
68
+ end
69
+ end
54
70
  end
55
71
  end
56
72
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # RADIO socket routing: group-based fan-out to DISH peers.
7
+ #
8
+ # Like PUB/FanOut but with exact group matching and JOIN/LEAVE
9
+ # commands instead of SUBSCRIBE/CANCEL.
10
+ #
11
+ # Messages are sent as two frames on the wire:
12
+ # group (MORE=1) + body (MORE=0)
13
+ #
14
+ class Radio
15
+
16
+ # @param engine [Engine]
17
+ #
18
+ def initialize(engine)
19
+ @engine = engine
20
+ @connections = []
21
+ @groups = {} # connection => Set of joined groups
22
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
23
+ @send_pump_started = false
24
+ @tasks = []
25
+ end
26
+
27
+ # @return [Async::LimitedQueue]
28
+ #
29
+ attr_reader :send_queue
30
+
31
+ # RADIO is write-only.
32
+ #
33
+ def recv_queue
34
+ raise "RADIO sockets cannot receive"
35
+ end
36
+
37
+ # @param connection [Connection]
38
+ #
39
+ def connection_added(connection)
40
+ @connections << connection
41
+ @groups[connection] = Set.new
42
+ start_group_listener(connection)
43
+ start_send_pump unless @send_pump_started
44
+ end
45
+
46
+ # @param connection [Connection]
47
+ #
48
+ def connection_removed(connection)
49
+ @connections.delete(connection)
50
+ @groups.delete(connection)
51
+ end
52
+
53
+ # Enqueues a message for sending.
54
+ #
55
+ # @param parts [Array<String>] [group, body]
56
+ #
57
+ def enqueue(parts)
58
+ @send_queue.enqueue(parts)
59
+ end
60
+
61
+ def stop
62
+ @tasks.each(&:stop)
63
+ @tasks.clear
64
+ end
65
+
66
+ private
67
+
68
+ def start_send_pump
69
+ @send_pump_started = true
70
+ @tasks << Reactor.spawn_pump do
71
+ loop do
72
+ batch = [@send_queue.dequeue]
73
+ Routing.drain_send_queue(@send_queue, batch)
74
+
75
+ written = Set.new
76
+ batch.each do |parts|
77
+ group = parts[0]
78
+ body = parts[1] || "".b
79
+ @connections.each do |conn|
80
+ next unless @groups[conn]&.include?(group)
81
+ begin
82
+ # Wire format: group frame (MORE) + body frame
83
+ conn.write_message([group, body])
84
+ written << conn
85
+ rescue *ZMTP::CONNECTION_LOST
86
+ # connection dead — will be cleaned up
87
+ end
88
+ end
89
+ end
90
+
91
+ written.each do |conn|
92
+ conn.flush
93
+ rescue *ZMTP::CONNECTION_LOST
94
+ # connection dead — will be cleaned up
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def start_group_listener(conn)
101
+ @tasks << Reactor.spawn_pump do
102
+ loop do
103
+ frame = conn.read_frame
104
+ next unless frame.command?
105
+ cmd = Codec::Command.from_body(frame.body)
106
+ case cmd.name
107
+ when "JOIN" then @groups[conn]&.add(cmd.data)
108
+ when "LEAVE" then @groups[conn]&.delete(cmd.data)
109
+ end
110
+ end
111
+ rescue *ZMTP::CONNECTION_LOST
112
+ @engine.connection_lost(conn)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -68,13 +68,28 @@ module OMQ
68
68
  @send_pump_started = true
69
69
  @tasks << Reactor.spawn_pump do
70
70
  loop do
71
- parts = @send_queue.dequeue
72
- reply_info = @pending_replies.shift
73
- next unless reply_info
74
- reply_info[:conn].send_message([*reply_info[:envelope], "".b, *parts])
71
+ batch = [@send_queue.dequeue]
72
+ Routing.drain_send_queue(@send_queue, batch)
73
+
74
+ written = Set.new
75
+ batch.each do |parts|
76
+ reply_info = @pending_replies.shift
77
+ next unless reply_info
78
+ conn = reply_info[:conn]
79
+ begin
80
+ conn.write_message([*reply_info[:envelope], "".b, *parts])
81
+ written << conn
82
+ rescue *ZMTP::CONNECTION_LOST
83
+ # connection lost mid-write
84
+ end
85
+ end
86
+
87
+ written.each do |conn|
88
+ conn.flush
89
+ rescue *ZMTP::CONNECTION_LOST
90
+ # connection lost mid-flush
91
+ end
75
92
  end
76
- rescue *ZMTP::CONNECTION_LOST
77
- # connection lost mid-write
78
93
  end
79
94
  end
80
95
  end
@@ -55,8 +55,14 @@ module OMQ
55
55
  @send_pump_started = true
56
56
  @tasks << Reactor.spawn_pump do
57
57
  loop do
58
- parts = @send_queue.dequeue
59
- send_with_retry(parts)
58
+ batch = [@send_queue.dequeue]
59
+ Routing.drain_send_queue(@send_queue, batch)
60
+
61
+ if batch.size == 1
62
+ send_with_retry(batch[0])
63
+ else
64
+ send_batch(batch)
65
+ end
60
66
  end
61
67
  end
62
68
  end
@@ -68,6 +74,35 @@ module OMQ
68
74
  @engine.connection_lost(conn)
69
75
  retry
70
76
  end
77
+
78
+ def send_batch(batch)
79
+ written = Set.new
80
+ batch.each_with_index do |parts, i|
81
+ conn = next_connection
82
+ begin
83
+ conn.write_message(transform_send(parts))
84
+ written << conn
85
+ rescue *ZMTP::CONNECTION_LOST
86
+ @engine.connection_lost(conn)
87
+ # Flush what we've written so far
88
+ written.each do |c|
89
+ c.flush
90
+ rescue *ZMTP::CONNECTION_LOST
91
+ # will be cleaned up
92
+ end
93
+ written.clear
94
+ # Fall back to send_with_retry for this and remaining
95
+ send_with_retry(parts)
96
+ batch[(i + 1)..].each { |p| send_with_retry(p) }
97
+ return
98
+ end
99
+ end
100
+ written.each do |conn|
101
+ conn.flush
102
+ rescue *ZMTP::CONNECTION_LOST
103
+ # will be cleaned up
104
+ end
105
+ end
71
106
  end
72
107
  end
73
108
  end
@@ -52,6 +52,12 @@ module OMQ
52
52
  # @param parts [Array<String>]
53
53
  #
54
54
  def enqueue(parts)
55
+ if @engine.options.router_mandatory?
56
+ identity = parts.first
57
+ unless @connections_by_identity[identity]
58
+ raise SocketError, "no route to identity #{identity.inspect}"
59
+ end
60
+ end
55
61
  @send_queue.enqueue(parts)
56
62
  end
57
63
 
@@ -66,19 +72,27 @@ module OMQ
66
72
  @send_pump_started = true
67
73
  @tasks << Reactor.spawn_pump do
68
74
  loop do
69
- parts = @send_queue.dequeue
70
- identity = parts.first
71
- conn = @connections_by_identity[identity]
75
+ batch = [@send_queue.dequeue]
76
+ Routing.drain_send_queue(@send_queue, batch)
72
77
 
73
- unless conn
74
- if @engine.options.router_mandatory?
75
- raise SocketError, "no route to identity #{identity.inspect}"
78
+ written = Set.new
79
+ batch.each do |parts|
80
+ identity = parts.first
81
+ conn = @connections_by_identity[identity]
82
+ next unless conn # silently drop (peer may have disconnected)
83
+ begin
84
+ conn.write_message(parts[1..])
85
+ written << conn
86
+ rescue *ZMTP::CONNECTION_LOST
87
+ # will be cleaned up
76
88
  end
77
- next # silently drop
78
89
  end
79
90
 
80
- # Send everything after the identity frame
81
- conn.send_message(parts[1..])
91
+ written.each do |conn|
92
+ conn.flush
93
+ rescue *ZMTP::CONNECTION_LOST
94
+ # will be cleaned up
95
+ end
82
96
  end
83
97
  end
84
98
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module ZMTP
5
+ module Routing
6
+ # SCATTER socket routing: round-robin send to GATHER peers.
7
+ #
8
+ class Scatter
9
+ include RoundRobin
10
+
11
+ # @param engine [Engine]
12
+ #
13
+ def initialize(engine)
14
+ @engine = engine
15
+ @tasks = []
16
+ init_round_robin(engine)
17
+ end
18
+
19
+ # @return [Async::LimitedQueue]
20
+ #
21
+ attr_reader :send_queue
22
+
23
+ # SCATTER is write-only.
24
+ #
25
+ def recv_queue
26
+ raise "SCATTER sockets cannot receive"
27
+ end
28
+
29
+ # @param connection [Connection]
30
+ #
31
+ def connection_added(connection)
32
+ @connections << connection
33
+ signal_connection_available
34
+ start_send_pump unless @send_pump_started
35
+ start_monitor(connection)
36
+ end
37
+
38
+ # @param connection [Connection]
39
+ #
40
+ def connection_removed(connection)
41
+ @connections.delete(connection)
42
+ end
43
+
44
+ # @param parts [Array<String>]
45
+ #
46
+ def enqueue(parts)
47
+ @send_queue.enqueue(parts)
48
+ end
49
+
50
+ #
51
+ def stop
52
+ @tasks.each(&:stop)
53
+ @tasks.clear
54
+ end
55
+
56
+ private
57
+
58
+ def start_monitor(conn)
59
+ @tasks << Reactor.spawn_pump do
60
+ conn.receive_message
61
+ rescue *ZMTP::CONNECTION_LOST
62
+ @engine.connection_lost(conn)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module OMQ
6
+ module ZMTP
7
+ module Routing
8
+ # SERVER socket routing: identity-based routing with auto-generated
9
+ # 4-byte routing IDs.
10
+ #
11
+ # Prepends routing ID on receive. Strips routing ID on send and
12
+ # routes to the identified connection.
13
+ #
14
+ class Server
15
+ # @param engine [Engine]
16
+ #
17
+ def initialize(engine)
18
+ @engine = engine
19
+ @recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
20
+ @send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
21
+ @connections_by_routing_id = {}
22
+ @tasks = []
23
+ @send_pump_started = false
24
+ end
25
+
26
+ # @return [Async::LimitedQueue]
27
+ #
28
+ attr_reader :recv_queue, :send_queue
29
+
30
+ # @param connection [Connection]
31
+ #
32
+ def connection_added(connection)
33
+ routing_id = SecureRandom.bytes(4)
34
+ @connections_by_routing_id[routing_id] = connection
35
+
36
+ task = @engine.start_recv_pump(connection, @recv_queue,
37
+ transform: ->(msg) { [routing_id, *msg] })
38
+ @tasks << task if task
39
+
40
+ start_send_pump unless @send_pump_started
41
+ end
42
+
43
+ # @param connection [Connection]
44
+ #
45
+ def connection_removed(connection)
46
+ @connections_by_routing_id.reject! { |_, c| c == connection }
47
+ end
48
+
49
+ # @param parts [Array<String>]
50
+ #
51
+ def enqueue(parts)
52
+ @send_queue.enqueue(parts)
53
+ end
54
+
55
+ def stop
56
+ @tasks.each(&:stop)
57
+ @tasks.clear
58
+ end
59
+
60
+ private
61
+
62
+ def start_send_pump
63
+ @send_pump_started = true
64
+ @tasks << Reactor.spawn_pump do
65
+ loop do
66
+ batch = [@send_queue.dequeue]
67
+ Routing.drain_send_queue(@send_queue, batch)
68
+
69
+ written = Set.new
70
+ batch.each do |parts|
71
+ routing_id = parts.first
72
+ conn = @connections_by_routing_id[routing_id]
73
+ next unless conn # silently drop if peer gone
74
+ begin
75
+ conn.write_message(parts[1..])
76
+ written << conn
77
+ rescue *ZMTP::CONNECTION_LOST
78
+ # will be cleaned up
79
+ end
80
+ end
81
+
82
+ written.each do |conn|
83
+ conn.flush
84
+ rescue *ZMTP::CONNECTION_LOST
85
+ # will be cleaned up
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end