omq 0.8.0 → 0.10.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 +4 -4
- data/CHANGELOG.md +87 -0
- data/README.md +9 -49
- data/lib/omq/channel.rb +3 -3
- data/lib/omq/client_server.rb +6 -6
- data/lib/omq/engine.rb +641 -0
- data/lib/omq/options.rb +46 -0
- data/lib/omq/pair.rb +2 -2
- data/lib/omq/peer.rb +3 -3
- data/lib/omq/pub_sub.rb +6 -6
- data/lib/omq/push_pull.rb +2 -2
- data/lib/omq/radio_dish.rb +2 -2
- data/lib/omq/reactor.rb +128 -0
- data/lib/omq/readable.rb +42 -0
- data/lib/omq/req_rep.rb +4 -4
- data/lib/omq/router_dealer.rb +4 -4
- data/lib/omq/routing/channel.rb +83 -0
- data/lib/omq/routing/client.rb +56 -0
- data/lib/omq/routing/dealer.rb +57 -0
- data/lib/omq/routing/dish.rb +78 -0
- data/lib/omq/routing/fan_out.rb +131 -0
- data/lib/omq/routing/gather.rb +46 -0
- data/lib/omq/routing/pair.rb +86 -0
- data/lib/omq/routing/peer.rb +101 -0
- data/lib/omq/routing/pub.rb +60 -0
- data/lib/omq/routing/pull.rb +46 -0
- data/lib/omq/routing/push.rb +81 -0
- data/lib/omq/routing/radio.rb +140 -0
- data/lib/omq/routing/rep.rb +101 -0
- data/lib/omq/routing/req.rb +65 -0
- data/lib/omq/routing/round_robin.rb +168 -0
- data/lib/omq/routing/router.rb +110 -0
- data/lib/omq/routing/scatter.rb +82 -0
- data/lib/omq/routing/server.rb +101 -0
- data/lib/omq/routing/sub.rb +78 -0
- data/lib/omq/routing/xpub.rb +72 -0
- data/lib/omq/routing/xsub.rb +83 -0
- data/lib/omq/routing.rb +66 -0
- data/lib/omq/scatter_gather.rb +4 -4
- data/lib/omq/single_frame.rb +18 -0
- data/lib/omq/socket.rb +24 -9
- data/lib/omq/transport/inproc.rb +355 -0
- data/lib/omq/transport/ipc.rb +117 -0
- data/lib/omq/transport/tcp.rb +111 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq/writable.rb +65 -0
- data/lib/omq.rb +60 -4
- metadata +38 -58
- data/exe/omq +0 -6
- data/lib/omq/cli/base_runner.rb +0 -459
- data/lib/omq/cli/channel.rb +0 -8
- data/lib/omq/cli/client_server.rb +0 -111
- data/lib/omq/cli/config.rb +0 -54
- data/lib/omq/cli/formatter.rb +0 -75
- data/lib/omq/cli/pair.rb +0 -31
- data/lib/omq/cli/peer.rb +0 -8
- data/lib/omq/cli/pipe.rb +0 -265
- data/lib/omq/cli/pub_sub.rb +0 -14
- data/lib/omq/cli/push_pull.rb +0 -14
- data/lib/omq/cli/radio_dish.rb +0 -27
- data/lib/omq/cli/req_rep.rb +0 -83
- data/lib/omq/cli/router_dealer.rb +0 -76
- data/lib/omq/cli/scatter_gather.rb +0 -14
- data/lib/omq/cli.rb +0 -540
- data/lib/omq/zmtp/engine.rb +0 -551
- data/lib/omq/zmtp/options.rb +0 -48
- data/lib/omq/zmtp/reactor.rb +0 -131
- data/lib/omq/zmtp/readable.rb +0 -29
- data/lib/omq/zmtp/routing/channel.rb +0 -81
- data/lib/omq/zmtp/routing/client.rb +0 -56
- data/lib/omq/zmtp/routing/dealer.rb +0 -57
- data/lib/omq/zmtp/routing/dish.rb +0 -80
- data/lib/omq/zmtp/routing/fan_out.rb +0 -131
- data/lib/omq/zmtp/routing/gather.rb +0 -48
- data/lib/omq/zmtp/routing/pair.rb +0 -84
- data/lib/omq/zmtp/routing/peer.rb +0 -100
- data/lib/omq/zmtp/routing/pub.rb +0 -62
- data/lib/omq/zmtp/routing/pull.rb +0 -48
- data/lib/omq/zmtp/routing/push.rb +0 -80
- data/lib/omq/zmtp/routing/radio.rb +0 -139
- data/lib/omq/zmtp/routing/rep.rb +0 -101
- data/lib/omq/zmtp/routing/req.rb +0 -65
- data/lib/omq/zmtp/routing/round_robin.rb +0 -143
- data/lib/omq/zmtp/routing/router.rb +0 -109
- data/lib/omq/zmtp/routing/scatter.rb +0 -81
- data/lib/omq/zmtp/routing/server.rb +0 -100
- data/lib/omq/zmtp/routing/sub.rb +0 -80
- data/lib/omq/zmtp/routing/xpub.rb +0 -74
- data/lib/omq/zmtp/routing/xsub.rb +0 -86
- data/lib/omq/zmtp/routing.rb +0 -65
- data/lib/omq/zmtp/single_frame.rb +0 -20
- data/lib/omq/zmtp/transport/inproc.rb +0 -359
- data/lib/omq/zmtp/transport/ipc.rb +0 -118
- data/lib/omq/zmtp/transport/tcp.rb +0 -117
- data/lib/omq/zmtp/writable.rb +0 -61
- data/lib/omq/zmtp.rb +0 -81
data/lib/omq/pub_sub.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class PUB < Socket
|
|
5
|
-
include
|
|
5
|
+
include Writable
|
|
6
6
|
|
|
7
7
|
def initialize(endpoints = nil, linger: 0, conflate: false)
|
|
8
8
|
_init_engine(:PUB, linger: linger, conflate: conflate)
|
|
@@ -13,7 +13,7 @@ module OMQ
|
|
|
13
13
|
# SUB socket.
|
|
14
14
|
#
|
|
15
15
|
class SUB < Socket
|
|
16
|
-
include
|
|
16
|
+
include Readable
|
|
17
17
|
|
|
18
18
|
# @return [String] subscription prefix to subscribe to everything
|
|
19
19
|
#
|
|
@@ -50,8 +50,8 @@ module OMQ
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
class XPUB < Socket
|
|
53
|
-
include
|
|
54
|
-
include
|
|
53
|
+
include Readable
|
|
54
|
+
include Writable
|
|
55
55
|
|
|
56
56
|
def initialize(endpoints = nil, linger: 0)
|
|
57
57
|
_init_engine(:XPUB, linger: linger)
|
|
@@ -60,8 +60,8 @@ module OMQ
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
class XSUB < Socket
|
|
63
|
-
include
|
|
64
|
-
include
|
|
63
|
+
include Readable
|
|
64
|
+
include Writable
|
|
65
65
|
|
|
66
66
|
# @param endpoints [String, nil]
|
|
67
67
|
# @param linger [Integer]
|
data/lib/omq/push_pull.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class PUSH < Socket
|
|
5
|
-
include
|
|
5
|
+
include Writable
|
|
6
6
|
|
|
7
7
|
def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil)
|
|
8
8
|
_init_engine(:PUSH, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout)
|
|
@@ -11,7 +11,7 @@ module OMQ
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
class PULL < Socket
|
|
14
|
-
include
|
|
14
|
+
include Readable
|
|
15
15
|
|
|
16
16
|
def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil)
|
|
17
17
|
_init_engine(:PULL, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout)
|
data/lib/omq/radio_dish.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class RADIO < Socket
|
|
5
|
-
include
|
|
5
|
+
include Writable
|
|
6
6
|
|
|
7
7
|
def initialize(endpoints = nil, linger: 0, conflate: false)
|
|
8
8
|
_init_engine(:RADIO, linger: linger, conflate: conflate)
|
|
@@ -45,7 +45,7 @@ module OMQ
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
class DISH < Socket
|
|
48
|
-
include
|
|
48
|
+
include Readable
|
|
49
49
|
|
|
50
50
|
def initialize(endpoints = nil, linger: 0, group: nil)
|
|
51
51
|
_init_engine(:DISH, linger: linger)
|
data/lib/omq/reactor.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
# Shared IO reactor for the Ruby backend.
|
|
7
|
+
#
|
|
8
|
+
# When user code runs inside an Async reactor, engine tasks are
|
|
9
|
+
# spawned directly under the caller's Async task. When no reactor
|
|
10
|
+
# is available (e.g. bare Thread.new), a single shared IO thread
|
|
11
|
+
# hosts all engine tasks — mirroring libzmq's IO thread.
|
|
12
|
+
#
|
|
13
|
+
# Engines obtain the IO thread's root task via {.root_task} and
|
|
14
|
+
# use it as their @parent_task. Blocking operations from the main
|
|
15
|
+
# thread are dispatched to the IO thread via {.run}.
|
|
16
|
+
#
|
|
17
|
+
module Reactor
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
@thread = nil
|
|
20
|
+
@root_task = nil
|
|
21
|
+
@work_queue = nil
|
|
22
|
+
@lingers = Hash.new(0) # linger value → count of active sockets
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
# Returns the root Async task inside the shared IO thread.
|
|
26
|
+
# Starts the thread exactly once (double-checked lock).
|
|
27
|
+
#
|
|
28
|
+
# @return [Async::Task]
|
|
29
|
+
#
|
|
30
|
+
def root_task
|
|
31
|
+
return @root_task if @root_task
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
return @root_task if @root_task
|
|
34
|
+
ready = Thread::Queue.new
|
|
35
|
+
@work_queue = Async::Queue.new
|
|
36
|
+
@thread = Thread.new { run_reactor(ready) }
|
|
37
|
+
@thread.name = "omq-io"
|
|
38
|
+
@root_task = ready.pop
|
|
39
|
+
at_exit { stop! }
|
|
40
|
+
end
|
|
41
|
+
@root_task
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Runs a block inside the Async reactor.
|
|
46
|
+
#
|
|
47
|
+
# Inside an Async reactor: runs directly.
|
|
48
|
+
# Outside: dispatches to the shared IO thread and blocks
|
|
49
|
+
# the calling thread until the result is available.
|
|
50
|
+
#
|
|
51
|
+
# @return [Object] the block's return value
|
|
52
|
+
#
|
|
53
|
+
def run(&block)
|
|
54
|
+
if Async::Task.current?
|
|
55
|
+
yield
|
|
56
|
+
else
|
|
57
|
+
result = Thread::Queue.new
|
|
58
|
+
root_task # ensure started
|
|
59
|
+
@work_queue.push([block, result])
|
|
60
|
+
status, value = result.pop
|
|
61
|
+
raise value if status == :error
|
|
62
|
+
value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Registers a socket's linger value.
|
|
68
|
+
#
|
|
69
|
+
# @param seconds [Numeric, nil] linger value
|
|
70
|
+
#
|
|
71
|
+
def track_linger(seconds)
|
|
72
|
+
@lingers[seconds || 0] += 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Unregisters a socket's linger value.
|
|
77
|
+
#
|
|
78
|
+
# @param seconds [Numeric, nil] linger value
|
|
79
|
+
#
|
|
80
|
+
def untrack_linger(seconds)
|
|
81
|
+
key = seconds || 0
|
|
82
|
+
@lingers[key] -= 1
|
|
83
|
+
@lingers.delete(key) if @lingers[key] <= 0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Stops the shared IO thread.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
#
|
|
91
|
+
def stop!
|
|
92
|
+
return unless @thread&.alive?
|
|
93
|
+
max_linger = @lingers.empty? ? 0 : @lingers.keys.max
|
|
94
|
+
@work_queue&.push(nil)
|
|
95
|
+
@thread&.join(max_linger + 1)
|
|
96
|
+
@thread = nil
|
|
97
|
+
@root_task = nil
|
|
98
|
+
@work_queue = nil
|
|
99
|
+
@lingers = Hash.new(0)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Runs the shared Async reactor.
|
|
105
|
+
#
|
|
106
|
+
# Processes work items dispatched via {.run} while engine
|
|
107
|
+
# tasks (accept loops, pumps, etc.) run as transient children.
|
|
108
|
+
#
|
|
109
|
+
# @param ready [Thread::Queue] receives the root task once started
|
|
110
|
+
#
|
|
111
|
+
def run_reactor(ready)
|
|
112
|
+
Async do |task|
|
|
113
|
+
ready.push(task)
|
|
114
|
+
loop do
|
|
115
|
+
item = @work_queue.dequeue
|
|
116
|
+
break if item.nil?
|
|
117
|
+
block, result = item
|
|
118
|
+
task.async do
|
|
119
|
+
result.push([:ok, block.call])
|
|
120
|
+
rescue => e
|
|
121
|
+
result.push([:error, e])
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/omq/readable.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
# Pure Ruby Readable mixin. Dequeues messages from the engine's recv queue.
|
|
7
|
+
#
|
|
8
|
+
module Readable
|
|
9
|
+
# Maximum messages to prefetch from the recv queue per drain.
|
|
10
|
+
RECV_BATCH_SIZE = 64
|
|
11
|
+
|
|
12
|
+
# Receives the next message. Returns from a local prefetch
|
|
13
|
+
# buffer when available, otherwise drains up to
|
|
14
|
+
# {RECV_BATCH_SIZE} messages from the recv queue in one
|
|
15
|
+
# synchronized dequeue.
|
|
16
|
+
#
|
|
17
|
+
# @return [Array<String>] message parts
|
|
18
|
+
# @raise [IO::TimeoutError] if read_timeout exceeded
|
|
19
|
+
#
|
|
20
|
+
def receive
|
|
21
|
+
@recv_mutex.synchronize { @recv_buffer.shift } || fill_recv_buffer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Waits until the socket is readable.
|
|
25
|
+
#
|
|
26
|
+
# @param timeout [Numeric, nil] timeout in seconds
|
|
27
|
+
# @return [true]
|
|
28
|
+
#
|
|
29
|
+
def wait_readable(timeout = @options.read_timeout)
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def fill_recv_buffer
|
|
36
|
+
batch = Reactor.run { with_timeout(@options.read_timeout) { @engine.dequeue_recv_batch(RECV_BATCH_SIZE) } }
|
|
37
|
+
msg = batch.shift
|
|
38
|
+
@recv_mutex.synchronize { @recv_buffer.concat(batch) } unless batch.empty?
|
|
39
|
+
msg
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/omq/req_rep.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class REQ < Socket
|
|
5
|
-
include
|
|
6
|
-
include
|
|
5
|
+
include Readable
|
|
6
|
+
include Writable
|
|
7
7
|
|
|
8
8
|
def initialize(endpoints = nil, linger: 0)
|
|
9
9
|
_init_engine(:REQ, linger: linger)
|
|
@@ -12,8 +12,8 @@ module OMQ
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
class REP < Socket
|
|
15
|
-
include
|
|
16
|
-
include
|
|
15
|
+
include Readable
|
|
16
|
+
include Writable
|
|
17
17
|
|
|
18
18
|
def initialize(endpoints = nil, linger: 0)
|
|
19
19
|
_init_engine(:REP, linger: linger)
|
data/lib/omq/router_dealer.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module OMQ
|
|
4
4
|
class DEALER < Socket
|
|
5
|
-
include
|
|
6
|
-
include
|
|
5
|
+
include Readable
|
|
6
|
+
include Writable
|
|
7
7
|
|
|
8
8
|
def initialize(endpoints = nil, linger: 0)
|
|
9
9
|
_init_engine(:DEALER, linger: linger)
|
|
@@ -14,8 +14,8 @@ module OMQ
|
|
|
14
14
|
# ROUTER socket.
|
|
15
15
|
#
|
|
16
16
|
class ROUTER < Socket
|
|
17
|
-
include
|
|
18
|
-
include
|
|
17
|
+
include Readable
|
|
18
|
+
include Writable
|
|
19
19
|
|
|
20
20
|
def initialize(endpoints = nil, linger: 0)
|
|
21
21
|
_init_engine(:ROUTER, linger: linger)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# CHANNEL socket routing: exclusive 1-to-1 bidirectional.
|
|
6
|
+
#
|
|
7
|
+
class Channel
|
|
8
|
+
|
|
9
|
+
# @param engine [Engine]
|
|
10
|
+
#
|
|
11
|
+
def initialize(engine)
|
|
12
|
+
@engine = engine
|
|
13
|
+
@connection = nil
|
|
14
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
15
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
16
|
+
@tasks = []
|
|
17
|
+
@send_pump_idle = true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Async::LimitedQueue]
|
|
21
|
+
#
|
|
22
|
+
attr_reader :recv_queue, :send_queue
|
|
23
|
+
|
|
24
|
+
# @param connection [Connection]
|
|
25
|
+
# @raise [RuntimeError] if a connection already exists
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
raise "CHANNEL allows only one peer" if @connection
|
|
29
|
+
@connection = connection
|
|
30
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
31
|
+
@tasks << task if task
|
|
32
|
+
start_send_pump(connection) unless connection.is_a?(Transport::Inproc::DirectPipe)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param connection [Connection]
|
|
36
|
+
#
|
|
37
|
+
def connection_removed(connection)
|
|
38
|
+
if @connection == connection
|
|
39
|
+
@connection = nil
|
|
40
|
+
@send_pump&.stop
|
|
41
|
+
@send_pump = nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param parts [Array<String>]
|
|
46
|
+
#
|
|
47
|
+
def enqueue(parts)
|
|
48
|
+
conn = @connection
|
|
49
|
+
if conn.is_a?(Transport::Inproc::DirectPipe) && conn.direct_recv_queue
|
|
50
|
+
conn.send_message(parts)
|
|
51
|
+
else
|
|
52
|
+
@send_queue.enqueue(parts)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#
|
|
57
|
+
def stop
|
|
58
|
+
@tasks.each(&:stop)
|
|
59
|
+
@tasks.clear
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def send_pump_idle? = @send_pump_idle
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def start_send_pump(conn)
|
|
67
|
+
@send_pump = @engine.spawn_pump_task(annotation: "send pump") do
|
|
68
|
+
loop do
|
|
69
|
+
@send_pump_idle = true
|
|
70
|
+
batch = [@send_queue.dequeue]
|
|
71
|
+
@send_pump_idle = false
|
|
72
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
73
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
74
|
+
conn.flush
|
|
75
|
+
end
|
|
76
|
+
rescue *CONNECTION_LOST
|
|
77
|
+
@engine.connection_lost(conn)
|
|
78
|
+
end
|
|
79
|
+
@tasks << @send_pump
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# CLIENT socket routing: round-robin send, fair-queue receive.
|
|
6
|
+
#
|
|
7
|
+
# Same as DEALER — no envelope manipulation.
|
|
8
|
+
#
|
|
9
|
+
class Client
|
|
10
|
+
include RoundRobin
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
17
|
+
@tasks = []
|
|
18
|
+
init_round_robin(engine)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Async::LimitedQueue]
|
|
22
|
+
#
|
|
23
|
+
attr_reader :recv_queue, :send_queue
|
|
24
|
+
|
|
25
|
+
# @param connection [Connection]
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
@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
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param connection [Connection]
|
|
37
|
+
#
|
|
38
|
+
def connection_removed(connection)
|
|
39
|
+
@connections.delete(connection)
|
|
40
|
+
update_direct_pipe
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param parts [Array<String>]
|
|
44
|
+
#
|
|
45
|
+
def enqueue(parts)
|
|
46
|
+
enqueue_round_robin(parts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
def stop
|
|
51
|
+
@tasks.each(&:stop)
|
|
52
|
+
@tasks.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# DEALER socket routing: round-robin send, fair-queue receive.
|
|
6
|
+
#
|
|
7
|
+
# No envelope manipulation — messages pass through unchanged.
|
|
8
|
+
#
|
|
9
|
+
class Dealer
|
|
10
|
+
include RoundRobin
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
17
|
+
@tasks = []
|
|
18
|
+
init_round_robin(engine)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Async::LimitedQueue]
|
|
22
|
+
#
|
|
23
|
+
attr_reader :recv_queue, :send_queue
|
|
24
|
+
|
|
25
|
+
# @param connection [Connection]
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
@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
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param connection [Connection]
|
|
37
|
+
#
|
|
38
|
+
def connection_removed(connection)
|
|
39
|
+
@connections.delete(connection)
|
|
40
|
+
update_direct_pipe
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param parts [Array<String>]
|
|
44
|
+
#
|
|
45
|
+
def enqueue(parts)
|
|
46
|
+
enqueue_round_robin(parts)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
def stop
|
|
51
|
+
@tasks.each(&:stop)
|
|
52
|
+
@tasks.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# DISH socket routing: group-based receive from RADIO peers.
|
|
6
|
+
#
|
|
7
|
+
# Sends JOIN/LEAVE commands to connected RADIO peers.
|
|
8
|
+
# Receives two-frame messages (group + body) from RADIO.
|
|
9
|
+
#
|
|
10
|
+
class Dish
|
|
11
|
+
|
|
12
|
+
# @param engine [Engine]
|
|
13
|
+
#
|
|
14
|
+
def initialize(engine)
|
|
15
|
+
@engine = engine
|
|
16
|
+
@connections = []
|
|
17
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
18
|
+
@groups = Set.new
|
|
19
|
+
@tasks = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Async::LimitedQueue]
|
|
23
|
+
#
|
|
24
|
+
attr_reader :recv_queue
|
|
25
|
+
|
|
26
|
+
# @param connection [Connection]
|
|
27
|
+
#
|
|
28
|
+
def connection_added(connection)
|
|
29
|
+
@connections << connection
|
|
30
|
+
# Send existing group memberships to new peer
|
|
31
|
+
@groups.each do |group|
|
|
32
|
+
connection.send_command(Protocol::ZMTP::Codec::Command.join(group))
|
|
33
|
+
end
|
|
34
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
35
|
+
@tasks << task if task
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param connection [Connection]
|
|
39
|
+
#
|
|
40
|
+
def connection_removed(connection)
|
|
41
|
+
@connections.delete(connection)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# DISH is read-only.
|
|
45
|
+
#
|
|
46
|
+
def enqueue(_parts)
|
|
47
|
+
raise "DISH sockets cannot send"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Joins a group.
|
|
51
|
+
#
|
|
52
|
+
# @param group [String]
|
|
53
|
+
#
|
|
54
|
+
def join(group)
|
|
55
|
+
@groups << group
|
|
56
|
+
@connections.each do |conn|
|
|
57
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.join(group))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Leaves a group.
|
|
62
|
+
#
|
|
63
|
+
# @param group [String]
|
|
64
|
+
#
|
|
65
|
+
def leave(group)
|
|
66
|
+
@groups.delete(group)
|
|
67
|
+
@connections.each do |conn|
|
|
68
|
+
conn.send_command(Protocol::ZMTP::Codec::Command.leave(group))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stop
|
|
73
|
+
@tasks.each(&:stop)
|
|
74
|
+
@tasks.clear
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module Routing
|
|
5
|
+
# Mixin for routing strategies that fan-out to subscribers.
|
|
6
|
+
#
|
|
7
|
+
# Manages per-connection subscription sets, subscription command
|
|
8
|
+
# listeners, and a send pump that delivers to all matching peers.
|
|
9
|
+
#
|
|
10
|
+
# Including classes must call `init_fan_out(engine)` from
|
|
11
|
+
# their #initialize.
|
|
12
|
+
#
|
|
13
|
+
module FanOut
|
|
14
|
+
attr_reader :subscriber_joined
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def init_fan_out(engine)
|
|
19
|
+
@connections = []
|
|
20
|
+
@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
|
|
24
|
+
@conflate = engine.options.conflate
|
|
25
|
+
@subscriber_joined = Async::Promise.new
|
|
26
|
+
@written = Set.new
|
|
27
|
+
@latest = {} if @conflate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Boolean] whether the connection is subscribed to the topic
|
|
31
|
+
#
|
|
32
|
+
def subscribed?(conn, topic)
|
|
33
|
+
subs = @subscriptions[conn]
|
|
34
|
+
return false unless subs
|
|
35
|
+
subs.any? { |prefix| topic.start_with?(prefix) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Called when a subscription command is received from a peer.
|
|
39
|
+
# Override in subclasses to expose subscriptions to the
|
|
40
|
+
# application (e.g. XPUB enqueues to recv_queue).
|
|
41
|
+
#
|
|
42
|
+
# @param conn [Connection]
|
|
43
|
+
# @param prefix [String]
|
|
44
|
+
#
|
|
45
|
+
def on_subscribe(conn, prefix)
|
|
46
|
+
@subscriptions[conn] << prefix.b.freeze
|
|
47
|
+
@subscriber_joined.resolve(conn) unless @subscriber_joined.resolved?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Called when a cancel command is received from a peer.
|
|
51
|
+
# Override in subclasses (e.g. XPUB enqueues to recv_queue).
|
|
52
|
+
#
|
|
53
|
+
# @param conn [Connection]
|
|
54
|
+
# @param prefix [String]
|
|
55
|
+
#
|
|
56
|
+
def on_cancel(conn, prefix)
|
|
57
|
+
@subscriptions[conn]&.delete(prefix)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] true when the send pump is idle (not sending a batch)
|
|
61
|
+
def send_pump_idle? = @send_pump_idle
|
|
62
|
+
|
|
63
|
+
|
|
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
|
+
@connections.each do |conn|
|
|
96
|
+
next unless subscribed?(conn, topic)
|
|
97
|
+
begin
|
|
98
|
+
conn.write_message(parts)
|
|
99
|
+
@written << conn
|
|
100
|
+
rescue *CONNECTION_LOST
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
@written.each do |conn|
|
|
107
|
+
conn.flush
|
|
108
|
+
rescue *CONNECTION_LOST
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def start_subscription_listener(conn)
|
|
115
|
+
@tasks << @engine.spawn_pump_task(annotation: "subscription listener") do
|
|
116
|
+
loop do
|
|
117
|
+
frame = conn.read_frame
|
|
118
|
+
next unless frame.command?
|
|
119
|
+
cmd = Protocol::ZMTP::Codec::Command.from_body(frame.body)
|
|
120
|
+
case cmd.name
|
|
121
|
+
when "SUBSCRIBE" then on_subscribe(conn, cmd.data)
|
|
122
|
+
when "CANCEL" then on_cancel(conn, cmd.data)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
rescue *CONNECTION_LOST
|
|
126
|
+
@engine.connection_lost(conn)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|