nnq 0.4.0 → 0.6.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 +114 -0
- data/lib/nnq/bus.rb +37 -0
- data/lib/nnq/connection.rb +16 -6
- data/lib/nnq/engine/connection_lifecycle.rb +61 -10
- data/lib/nnq/engine/reconnect.rb +12 -3
- data/lib/nnq/engine/socket_lifecycle.rb +10 -2
- data/lib/nnq/engine.rb +77 -30
- data/lib/nnq/error.rb +26 -6
- data/lib/nnq/monitor_event.rb +3 -1
- data/lib/nnq/options.rb +10 -1
- data/lib/nnq/pair.rb +6 -1
- data/lib/nnq/pub_sub.rb +9 -2
- data/lib/nnq/push_pull.rb +9 -2
- data/lib/nnq/reactor.rb +12 -11
- data/lib/nnq/req_rep.rb +61 -13
- data/lib/nnq/routing/backtrace.rb +47 -0
- data/lib/nnq/routing/bus.rb +108 -0
- data/lib/nnq/routing/pair.rb +4 -1
- data/lib/nnq/routing/pub.rb +9 -5
- data/lib/nnq/routing/pull.rb +2 -1
- data/lib/nnq/routing/push.rb +2 -0
- data/lib/nnq/routing/rep.rb +7 -22
- data/lib/nnq/routing/rep_raw.rb +63 -0
- data/lib/nnq/routing/req.rb +7 -3
- data/lib/nnq/routing/req_raw.rb +73 -0
- data/lib/nnq/routing/respondent.rb +84 -0
- data/lib/nnq/routing/respondent_raw.rb +54 -0
- data/lib/nnq/routing/send_pump.rb +27 -6
- data/lib/nnq/routing/sub.rb +4 -0
- data/lib/nnq/routing/surveyor.rb +138 -0
- data/lib/nnq/routing/surveyor_raw.rb +107 -0
- data/lib/nnq/socket.rb +51 -8
- data/lib/nnq/surveyor_respondent.rb +98 -0
- data/lib/nnq/transport/inproc.rb +5 -0
- data/lib/nnq/transport/ipc.rb +3 -0
- data/lib/nnq/transport/tcp.rb +27 -5
- data/lib/nnq/version.rb +1 -1
- data/lib/nnq.rb +2 -0
- metadata +13 -3
data/lib/nnq/routing/sub.rb
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/queue"
|
|
5
|
+
require "async/limited_queue"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module NNQ
|
|
9
|
+
module Routing
|
|
10
|
+
# SURVEYOR: broadcast side of the survey0 pattern.
|
|
11
|
+
#
|
|
12
|
+
# Wire format: each survey is prepended with a 4-byte BE survey ID
|
|
13
|
+
# (high bit set — same terminal-marker convention as REQ). Replies
|
|
14
|
+
# carry the same ID back. Stale replies (wrong ID) are dropped.
|
|
15
|
+
#
|
|
16
|
+
# Send side: fan-out to all connected respondents (like PUB). Each
|
|
17
|
+
# peer gets its own bounded queue and pump.
|
|
18
|
+
#
|
|
19
|
+
# Recv side: replies are matched by survey ID. Only replies
|
|
20
|
+
# matching the current survey are delivered. After `survey_time`
|
|
21
|
+
# elapses, {#receive} raises {NNQ::TimedOut}.
|
|
22
|
+
#
|
|
23
|
+
class Surveyor
|
|
24
|
+
def initialize(engine)
|
|
25
|
+
@engine = engine
|
|
26
|
+
@queues = {} # conn => Async::LimitedQueue
|
|
27
|
+
@pump_tasks = {} # conn => Async::Task
|
|
28
|
+
@recv_queue = Async::Queue.new
|
|
29
|
+
@current_id = nil
|
|
30
|
+
@mutex = Mutex.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Broadcasts +body+ as a survey to all connected respondents.
|
|
35
|
+
# Starts a new survey window; any previous survey is abandoned.
|
|
36
|
+
#
|
|
37
|
+
# @param body [String]
|
|
38
|
+
def send_survey(body)
|
|
39
|
+
id = SecureRandom.random_number(0x80000000) | 0x80000000
|
|
40
|
+
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@current_id = id
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
header = [id].pack("N")
|
|
46
|
+
wire = header + body
|
|
47
|
+
|
|
48
|
+
@queues.each_value do |q|
|
|
49
|
+
q.enqueue(wire) unless q.limited?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Receives the next reply within the survey window. Raises
|
|
55
|
+
# {NNQ::TimedOut} when the window expires.
|
|
56
|
+
#
|
|
57
|
+
# @return [String] reply body
|
|
58
|
+
def receive
|
|
59
|
+
survey_time = @engine.options.survey_time
|
|
60
|
+
Fiber.scheduler.with_timeout(survey_time) { @recv_queue.dequeue }
|
|
61
|
+
rescue Async::TimeoutError
|
|
62
|
+
raise NNQ::TimedOut, "survey timed out"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Called by the engine recv loop with each received message.
|
|
67
|
+
def enqueue(body, _conn)
|
|
68
|
+
return if body.bytesize < 4
|
|
69
|
+
|
|
70
|
+
id = body.unpack1("N")
|
|
71
|
+
payload = body.byteslice(4..)
|
|
72
|
+
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
return unless @current_id == id
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@recv_queue.enqueue(payload)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def connection_added(conn)
|
|
82
|
+
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
83
|
+
@queues[conn] = queue
|
|
84
|
+
@pump_tasks[conn] = spawn_pump(conn, queue)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def connection_removed(conn)
|
|
89
|
+
@queues.delete(conn)
|
|
90
|
+
task = @pump_tasks.delete(conn)
|
|
91
|
+
|
|
92
|
+
return unless task
|
|
93
|
+
return if task == Async::Task.current
|
|
94
|
+
|
|
95
|
+
task.stop
|
|
96
|
+
rescue IOError, Errno::EPIPE
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def send_queue_drained?
|
|
101
|
+
@queues.each_value.all? { |q| q.empty? }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def close
|
|
106
|
+
@pump_tasks.each_value(&:stop)
|
|
107
|
+
@pump_tasks.clear
|
|
108
|
+
@queues.clear
|
|
109
|
+
@recv_queue.enqueue(nil)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def close_read
|
|
114
|
+
@recv_queue.enqueue(nil)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def spawn_pump(conn, queue)
|
|
122
|
+
annotation = "nnq surveyor pump #{conn.endpoint}"
|
|
123
|
+
parent = @engine.connections[conn]&.barrier || @engine.barrier
|
|
124
|
+
|
|
125
|
+
@engine.spawn_task(annotation:, parent:) do
|
|
126
|
+
loop do
|
|
127
|
+
body = queue.dequeue
|
|
128
|
+
conn.send_message(body)
|
|
129
|
+
@engine.emit_verbose_msg_sent(body)
|
|
130
|
+
rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
131
|
+
break
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/limited_queue"
|
|
5
|
+
require_relative "backtrace"
|
|
6
|
+
|
|
7
|
+
module NNQ
|
|
8
|
+
module Routing
|
|
9
|
+
# Raw SURVEYOR: fans out surveys to all peers like cooked
|
|
10
|
+
# {Surveyor}, but without a survey window, survey-id matching,
|
|
11
|
+
# or timeout. Replies are delivered as `[pipe, header, body]`
|
|
12
|
+
# tuples so the app can correlate by header verbatim.
|
|
13
|
+
#
|
|
14
|
+
# Each per-conn send queue holds `[header, body]` pairs and the
|
|
15
|
+
# pump calls `conn.write_message(body, header: header)` so the
|
|
16
|
+
# protocol-sp header kwarg is threaded through the fan-out —
|
|
17
|
+
# zero concat even on the broadcast path.
|
|
18
|
+
#
|
|
19
|
+
class SurveyorRaw
|
|
20
|
+
include Backtrace
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def initialize(engine)
|
|
24
|
+
@engine = engine
|
|
25
|
+
@queues = {} # conn => Async::LimitedQueue
|
|
26
|
+
@pump_tasks = {} # conn => Async::Task
|
|
27
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def send(body, header:)
|
|
32
|
+
@queues.each_value do |q|
|
|
33
|
+
q.enqueue([header, body]) unless q.limited?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def receive
|
|
39
|
+
@recv_queue.dequeue
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def enqueue(wire_bytes, conn)
|
|
44
|
+
header, payload = parse_backtrace(wire_bytes)
|
|
45
|
+
return unless header
|
|
46
|
+
@recv_queue.enqueue([conn, header, payload])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def connection_added(conn)
|
|
51
|
+
queue = Async::LimitedQueue.new(@engine.options.send_hwm)
|
|
52
|
+
@queues[conn] = queue
|
|
53
|
+
@pump_tasks[conn] = spawn_pump(conn, queue)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def connection_removed(conn)
|
|
58
|
+
@queues.delete(conn)
|
|
59
|
+
task = @pump_tasks.delete(conn)
|
|
60
|
+
|
|
61
|
+
return unless task
|
|
62
|
+
return if task == Async::Task.current
|
|
63
|
+
|
|
64
|
+
task.stop
|
|
65
|
+
rescue IOError, Errno::EPIPE
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def send_queue_drained?
|
|
70
|
+
@queues.each_value.all? { |q| q.empty? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def close
|
|
75
|
+
@pump_tasks.each_value(&:stop)
|
|
76
|
+
@pump_tasks.clear
|
|
77
|
+
@queues.clear
|
|
78
|
+
@recv_queue.enqueue(nil)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def close_read
|
|
83
|
+
@recv_queue.enqueue(nil)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def spawn_pump(conn, queue)
|
|
91
|
+
annotation = "nnq surveyor_raw pump #{conn.endpoint}"
|
|
92
|
+
parent = @engine.connections[conn]&.barrier || @engine.barrier
|
|
93
|
+
|
|
94
|
+
@engine.spawn_task(annotation:, parent:) do
|
|
95
|
+
loop do
|
|
96
|
+
header, body = queue.dequeue
|
|
97
|
+
conn.send_message(body, header: header)
|
|
98
|
+
@engine.emit_verbose_msg_sent(body)
|
|
99
|
+
rescue EOFError, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
100
|
+
break
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/nnq/socket.rb
CHANGED
|
@@ -30,9 +30,23 @@ module NNQ
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
# @yieldparam [self] the socket; when a block is passed the socket
|
|
34
|
+
# is {#close}d when the block returns (or raises), File.open-style.
|
|
35
|
+
def initialize(raw: false, linger: Float::INFINITY, send_hwm: Options::DEFAULT_HWM, recv_hwm: Options::DEFAULT_HWM)
|
|
36
|
+
@raw = raw
|
|
37
|
+
@options = Options.new(linger: linger, send_hwm: send_hwm, recv_hwm: recv_hwm)
|
|
35
38
|
@engine = Engine.new(protocol: protocol, options: @options) { |engine| build_routing(engine) }
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
yield self
|
|
42
|
+
ensure
|
|
43
|
+
close
|
|
44
|
+
end if block_given?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def raw?
|
|
49
|
+
@raw
|
|
36
50
|
end
|
|
37
51
|
|
|
38
52
|
|
|
@@ -54,23 +68,35 @@ module NNQ
|
|
|
54
68
|
end
|
|
55
69
|
|
|
56
70
|
|
|
57
|
-
def last_endpoint
|
|
71
|
+
def last_endpoint
|
|
72
|
+
@engine.last_endpoint
|
|
73
|
+
end
|
|
58
74
|
|
|
59
75
|
|
|
60
|
-
def connection_count
|
|
76
|
+
def connection_count
|
|
77
|
+
@engine.connections.size
|
|
78
|
+
end
|
|
61
79
|
|
|
62
80
|
|
|
63
81
|
# Resolves with the first connected peer (or nil on close without
|
|
64
82
|
# any peers). Block on `.wait` to wait until a connection is ready.
|
|
65
|
-
def peer_connected
|
|
83
|
+
def peer_connected
|
|
84
|
+
@engine.peer_connected
|
|
85
|
+
end
|
|
66
86
|
|
|
67
87
|
|
|
68
88
|
# Resolves with `true` the first time all peers have disconnected
|
|
69
89
|
# (after at least one peer was connected). Edge-triggered.
|
|
70
|
-
def all_peers_gone
|
|
90
|
+
def all_peers_gone
|
|
91
|
+
@engine.all_peers_gone
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def reconnect_enabled
|
|
96
|
+
@engine.reconnect_enabled
|
|
97
|
+
end
|
|
71
98
|
|
|
72
99
|
|
|
73
|
-
def reconnect_enabled = @engine.reconnect_enabled
|
|
74
100
|
def reconnect_enabled=(value)
|
|
75
101
|
@engine.reconnect_enabled = value
|
|
76
102
|
end
|
|
@@ -94,9 +120,11 @@ module NNQ
|
|
|
94
120
|
# @return [Async::Task]
|
|
95
121
|
def monitor(verbose: false, &block)
|
|
96
122
|
ensure_parent_task
|
|
97
|
-
|
|
123
|
+
|
|
124
|
+
queue = Async::Queue.new
|
|
98
125
|
@engine.monitor_queue = queue
|
|
99
126
|
@engine.verbose_monitor = verbose
|
|
127
|
+
|
|
100
128
|
Reactor.run do
|
|
101
129
|
@engine.spawn_task(annotation: "nnq monitor") do
|
|
102
130
|
while (event = queue.dequeue)
|
|
@@ -111,8 +139,22 @@ module NNQ
|
|
|
111
139
|
end
|
|
112
140
|
|
|
113
141
|
|
|
142
|
+
# Coerces +body+ to a frozen binary string. Called by every send
|
|
143
|
+
# method so a caller can't mutate the string after it's been
|
|
144
|
+
# enqueued (the body sits in a send queue or per-peer queue until
|
|
145
|
+
# the pump writes it, and an unfrozen caller-owned buffer could be
|
|
146
|
+
# appended to mid-flight).
|
|
147
|
+
#
|
|
148
|
+
# Fast-path: already frozen + binary → returned as-is.
|
|
149
|
+
def frozen_binary(body)
|
|
150
|
+
return body if body.frozen? && body.encoding == Encoding::BINARY
|
|
151
|
+
body.b.freeze
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
|
|
114
155
|
private
|
|
115
156
|
|
|
157
|
+
|
|
116
158
|
def ensure_parent_task
|
|
117
159
|
# Must run OUTSIDE Reactor.run so that non-Async callers capture
|
|
118
160
|
# the IO thread's root task, not the ephemeral work-item task
|
|
@@ -136,5 +178,6 @@ module NNQ
|
|
|
136
178
|
def build_routing(_engine)
|
|
137
179
|
raise NotImplementedError
|
|
138
180
|
end
|
|
181
|
+
|
|
139
182
|
end
|
|
140
183
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "socket"
|
|
4
|
+
require_relative "routing/surveyor"
|
|
5
|
+
require_relative "routing/respondent"
|
|
6
|
+
require_relative "routing/surveyor_raw"
|
|
7
|
+
require_relative "routing/respondent_raw"
|
|
8
|
+
|
|
9
|
+
module NNQ
|
|
10
|
+
# SURVEYOR (nng surveyor0): broadcast side of the survey pattern.
|
|
11
|
+
# Cooked mode enforces a timed survey window and matches replies by
|
|
12
|
+
# survey id; raw mode fans out with a caller-supplied +header+ and
|
|
13
|
+
# delivers replies as `[pipe, header, body]` with no timer.
|
|
14
|
+
#
|
|
15
|
+
class SURVEYOR0 < Socket
|
|
16
|
+
# Cooked: broadcasts +body+ as a survey to all connected respondents.
|
|
17
|
+
def send_survey(body)
|
|
18
|
+
raise Error, "SURVEYOR#send_survey not available in raw mode" if raw?
|
|
19
|
+
body = frozen_binary(body)
|
|
20
|
+
Reactor.run { @engine.routing.send_survey(body) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Raw: broadcasts +body+ with +header+ to all connected respondents
|
|
25
|
+
# via per-conn send pumps (header is threaded through the
|
|
26
|
+
# protocol-sp header kwarg — no concat). Raises in cooked mode.
|
|
27
|
+
def send(body, header:)
|
|
28
|
+
raise Error, "SURVEYOR#send not available in cooked mode" unless raw?
|
|
29
|
+
body = frozen_binary(body)
|
|
30
|
+
Reactor.run { @engine.routing.send(body, header: header) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Cooked: receives the next reply within the survey window, raises
|
|
35
|
+
# {NNQ::TimedOut} on window expiry. Raw: returns `[pipe, header, body]`
|
|
36
|
+
# and blocks indefinitely (no survey window).
|
|
37
|
+
def receive
|
|
38
|
+
Reactor.run { @engine.routing.receive }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def protocol
|
|
46
|
+
Protocol::SP::Protocols::SURVEYOR_V0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_routing(engine)
|
|
51
|
+
raw? ? Routing::SurveyorRaw.new(engine) : Routing::Surveyor.new(engine)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# RESPONDENT (nng respondent0): reply side of the survey pattern.
|
|
57
|
+
# Cooked mode strictly alternates #receive / #send_reply; raw mode
|
|
58
|
+
# exposes the backtrace as an opaque +header+ and the originating
|
|
59
|
+
# surveyor pipe as a live Connection.
|
|
60
|
+
#
|
|
61
|
+
class RESPONDENT0 < Socket
|
|
62
|
+
# Cooked: blocks until the next survey arrives. Raw: returns
|
|
63
|
+
# `[pipe, header, body]`.
|
|
64
|
+
def receive
|
|
65
|
+
Reactor.run { @engine.routing.receive }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Cooked: routes +body+ back to the surveyor that sent the most
|
|
70
|
+
# recent survey. Raises in raw mode.
|
|
71
|
+
def send_reply(body)
|
|
72
|
+
raise Error, "RESPONDENT#send_reply not available in raw mode" if raw?
|
|
73
|
+
body = frozen_binary(body)
|
|
74
|
+
Reactor.run { @engine.routing.send_reply(body) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Raw: writes +body+ with +header+ back to +to+. Raises in cooked mode.
|
|
79
|
+
def send(body, to:, header:)
|
|
80
|
+
raise Error, "RESPONDENT#send not available in cooked mode" unless raw?
|
|
81
|
+
body = frozen_binary(body)
|
|
82
|
+
Reactor.run { @engine.routing.send(body, to: to, header: header) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def protocol
|
|
90
|
+
Protocol::SP::Protocols::RESPONDENT_V0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_routing(engine)
|
|
95
|
+
raw? ? Routing::RespondentRaw.new(engine) : Routing::Respondent.new(engine)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/nnq/transport/inproc.rb
CHANGED
|
@@ -31,6 +31,7 @@ module NNQ
|
|
|
31
31
|
raise Error, "inproc endpoint already bound: #{endpoint}" if @registry.key?(endpoint)
|
|
32
32
|
@registry[endpoint] = engine
|
|
33
33
|
end
|
|
34
|
+
|
|
34
35
|
Listener.new(endpoint)
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -46,7 +47,9 @@ module NNQ
|
|
|
46
47
|
def connect(endpoint, engine)
|
|
47
48
|
bound = @mutex.synchronize { @registry[endpoint] }
|
|
48
49
|
raise Error, "inproc endpoint not bound: #{endpoint}" unless bound
|
|
50
|
+
|
|
49
51
|
a, b = UNIXSocket.pair
|
|
52
|
+
|
|
50
53
|
# Handshake on the bound side must run concurrently with
|
|
51
54
|
# ours — if we called bound.handle_accepted synchronously
|
|
52
55
|
# it would block on reading our greeting before we've had
|
|
@@ -75,6 +78,7 @@ module NNQ
|
|
|
75
78
|
class Listener
|
|
76
79
|
attr_reader :endpoint
|
|
77
80
|
|
|
81
|
+
|
|
78
82
|
def initialize(endpoint)
|
|
79
83
|
@endpoint = endpoint
|
|
80
84
|
end
|
|
@@ -88,6 +92,7 @@ module NNQ
|
|
|
88
92
|
def stop
|
|
89
93
|
Inproc.unbind(@endpoint)
|
|
90
94
|
end
|
|
95
|
+
|
|
91
96
|
end
|
|
92
97
|
end
|
|
93
98
|
end
|
data/lib/nnq/transport/ipc.rb
CHANGED
|
@@ -22,7 +22,9 @@ module NNQ
|
|
|
22
22
|
def bind(endpoint, engine)
|
|
23
23
|
path = parse_path(endpoint)
|
|
24
24
|
sock_path = to_socket_path(path)
|
|
25
|
+
|
|
25
26
|
File.delete(sock_path) if !abstract?(path) && File.exist?(sock_path)
|
|
27
|
+
|
|
26
28
|
server = UNIXServer.new(sock_path)
|
|
27
29
|
Listener.new(endpoint, server, path, engine)
|
|
28
30
|
end
|
|
@@ -91,6 +93,7 @@ module NNQ
|
|
|
91
93
|
@server.close rescue nil
|
|
92
94
|
File.delete(@path) rescue nil unless IPC.abstract?(@path)
|
|
93
95
|
end
|
|
96
|
+
|
|
94
97
|
end
|
|
95
98
|
end
|
|
96
99
|
end
|
data/lib/nnq/transport/tcp.rb
CHANGED
|
@@ -19,10 +19,11 @@ module NNQ
|
|
|
19
19
|
# @return [Listener]
|
|
20
20
|
def bind(endpoint, engine)
|
|
21
21
|
host, port = parse_endpoint(endpoint)
|
|
22
|
-
host
|
|
23
|
-
server
|
|
24
|
-
actual
|
|
25
|
-
host_part
|
|
22
|
+
host = "0.0.0.0" if host == "*"
|
|
23
|
+
server = TCPServer.new(host, port)
|
|
24
|
+
actual = server.local_address.ip_port
|
|
25
|
+
host_part = host.include?(":") ? "[#{host}]" : host
|
|
26
|
+
|
|
26
27
|
Listener.new("tcp://#{host_part}:#{actual}", server, actual, engine)
|
|
27
28
|
end
|
|
28
29
|
|
|
@@ -35,11 +36,23 @@ module NNQ
|
|
|
35
36
|
# @return [void]
|
|
36
37
|
def connect(endpoint, engine)
|
|
37
38
|
host, port = parse_endpoint(endpoint)
|
|
38
|
-
sock
|
|
39
|
+
sock = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
|
|
40
|
+
|
|
39
41
|
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
|
|
45
|
+
# Connect timeout: cap each attempt at the reconnect interval so
|
|
46
|
+
# a hung connect(2) (e.g. macOS kqueue + IPv6 ECONNREFUSED not
|
|
47
|
+
# delivered) doesn't block the retry loop. Floor at 0.5s for
|
|
48
|
+
# real-network latency.
|
|
49
|
+
def connect_timeout(options)
|
|
50
|
+
ri = options.reconnect_interval
|
|
51
|
+
ri = ri.end if ri.is_a?(Range)
|
|
52
|
+
[ri, 0.5].max
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
43
56
|
def parse_endpoint(endpoint)
|
|
44
57
|
uri = URI.parse(endpoint)
|
|
45
58
|
[uri.hostname, uri.port]
|
|
@@ -50,9 +63,17 @@ module NNQ
|
|
|
50
63
|
# A bound TCP listener.
|
|
51
64
|
#
|
|
52
65
|
class Listener
|
|
66
|
+
# @return [String] the normalised endpoint URL this listener is
|
|
67
|
+
# bound to (with the resolved port substituted when the user
|
|
68
|
+
# bound to port 0).
|
|
53
69
|
attr_reader :endpoint
|
|
70
|
+
|
|
71
|
+
# @return [Integer] the actual TCP port the listener is bound
|
|
72
|
+
# to. Equals the requested port unless 0 was requested, in
|
|
73
|
+
# which case it reflects the kernel-assigned ephemeral port.
|
|
54
74
|
attr_reader :port
|
|
55
75
|
|
|
76
|
+
|
|
56
77
|
def initialize(endpoint, server, port, engine)
|
|
57
78
|
@endpoint = endpoint
|
|
58
79
|
@server = server
|
|
@@ -84,6 +105,7 @@ module NNQ
|
|
|
84
105
|
@task&.stop
|
|
85
106
|
@server.close rescue nil
|
|
86
107
|
end
|
|
108
|
+
|
|
87
109
|
end
|
|
88
110
|
end
|
|
89
111
|
end
|
data/lib/nnq/version.rb
CHANGED
data/lib/nnq.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nnq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -43,14 +43,14 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '0.
|
|
46
|
+
version: '0.3'
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '0.
|
|
53
|
+
version: '0.3'
|
|
54
54
|
description: Pure Ruby implementation of nanomsg's Scalability Protocols (SP) on top
|
|
55
55
|
of async + io-stream. Per-socket HWM, opportunistic batching, wire-compatible with
|
|
56
56
|
libnng over inproc/ipc/tcp.
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- LICENSE
|
|
65
65
|
- README.md
|
|
66
66
|
- lib/nnq.rb
|
|
67
|
+
- lib/nnq/bus.rb
|
|
67
68
|
- lib/nnq/connection.rb
|
|
68
69
|
- lib/nnq/engine.rb
|
|
69
70
|
- lib/nnq/engine/connection_lifecycle.rb
|
|
@@ -77,15 +78,24 @@ files:
|
|
|
77
78
|
- lib/nnq/push_pull.rb
|
|
78
79
|
- lib/nnq/reactor.rb
|
|
79
80
|
- lib/nnq/req_rep.rb
|
|
81
|
+
- lib/nnq/routing/backtrace.rb
|
|
82
|
+
- lib/nnq/routing/bus.rb
|
|
80
83
|
- lib/nnq/routing/pair.rb
|
|
81
84
|
- lib/nnq/routing/pub.rb
|
|
82
85
|
- lib/nnq/routing/pull.rb
|
|
83
86
|
- lib/nnq/routing/push.rb
|
|
84
87
|
- lib/nnq/routing/rep.rb
|
|
88
|
+
- lib/nnq/routing/rep_raw.rb
|
|
85
89
|
- lib/nnq/routing/req.rb
|
|
90
|
+
- lib/nnq/routing/req_raw.rb
|
|
91
|
+
- lib/nnq/routing/respondent.rb
|
|
92
|
+
- lib/nnq/routing/respondent_raw.rb
|
|
86
93
|
- lib/nnq/routing/send_pump.rb
|
|
87
94
|
- lib/nnq/routing/sub.rb
|
|
95
|
+
- lib/nnq/routing/surveyor.rb
|
|
96
|
+
- lib/nnq/routing/surveyor_raw.rb
|
|
88
97
|
- lib/nnq/socket.rb
|
|
98
|
+
- lib/nnq/surveyor_respondent.rb
|
|
89
99
|
- lib/nnq/transport/inproc.rb
|
|
90
100
|
- lib/nnq/transport/ipc.rb
|
|
91
101
|
- lib/nnq/transport/tcp.rb
|