nnq 0.2.0 → 0.5.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 +118 -0
- data/lib/nnq/bus.rb +37 -0
- data/lib/nnq/connection.rb +9 -2
- data/lib/nnq/engine/connection_lifecycle.rb +72 -12
- data/lib/nnq/engine/reconnect.rb +112 -0
- data/lib/nnq/engine/socket_lifecycle.rb +40 -3
- data/lib/nnq/engine.rb +186 -35
- data/lib/nnq/error.rb +26 -6
- data/lib/nnq/monitor_event.rb +18 -0
- data/lib/nnq/options.rb +8 -1
- data/lib/nnq/pair.rb +6 -1
- data/lib/nnq/pub_sub.rb +9 -2
- data/lib/nnq/push_pull.rb +16 -3
- data/lib/nnq/reactor.rb +12 -11
- data/lib/nnq/req_rep.rb +10 -2
- data/lib/nnq/routing/backtrace.rb +39 -0
- data/lib/nnq/routing/bus.rb +108 -0
- data/lib/nnq/routing/pair.rb +10 -1
- data/lib/nnq/routing/pub.rb +9 -4
- data/lib/nnq/routing/pull.rb +10 -1
- data/lib/nnq/routing/push.rb +2 -0
- data/lib/nnq/routing/rep.rb +10 -20
- data/lib/nnq/routing/req.rb +6 -2
- data/lib/nnq/routing/respondent.rb +84 -0
- data/lib/nnq/routing/send_pump.rb +27 -5
- data/lib/nnq/routing/sub.rb +9 -0
- data/lib/nnq/routing/surveyor.rb +138 -0
- data/lib/nnq/socket.rb +102 -5
- data/lib/nnq/surveyor_respondent.rb +78 -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 +10 -0
- metadata +11 -3
data/lib/nnq/socket.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "async/queue"
|
|
4
|
+
|
|
3
5
|
require_relative "options"
|
|
4
6
|
require_relative "engine"
|
|
7
|
+
require_relative "monitor_event"
|
|
5
8
|
require_relative "reactor"
|
|
6
9
|
|
|
7
10
|
module NNQ
|
|
@@ -27,9 +30,23 @@ module NNQ
|
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
|
|
30
|
-
|
|
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)
|
|
36
|
+
@raw = raw
|
|
31
37
|
@options = Options.new(linger: linger, send_hwm: send_hwm)
|
|
32
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
|
|
33
50
|
end
|
|
34
51
|
|
|
35
52
|
|
|
@@ -51,23 +68,102 @@ module NNQ
|
|
|
51
68
|
end
|
|
52
69
|
|
|
53
70
|
|
|
54
|
-
def last_endpoint
|
|
71
|
+
def last_endpoint
|
|
72
|
+
@engine.last_endpoint
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def connection_count
|
|
77
|
+
@engine.connections.size
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Resolves with the first connected peer (or nil on close without
|
|
82
|
+
# any peers). Block on `.wait` to wait until a connection is ready.
|
|
83
|
+
def peer_connected
|
|
84
|
+
@engine.peer_connected
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Resolves with `true` the first time all peers have disconnected
|
|
89
|
+
# (after at least one peer was connected). Edge-triggered.
|
|
90
|
+
def all_peers_gone
|
|
91
|
+
@engine.all_peers_gone
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def reconnect_enabled
|
|
96
|
+
@engine.reconnect_enabled
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def reconnect_enabled=(value)
|
|
101
|
+
@engine.reconnect_enabled = value
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Closes the recv side only. Buffered messages drain, then
|
|
106
|
+
# {#receive} returns nil. Send side stays open.
|
|
107
|
+
def close_read
|
|
108
|
+
Reactor.run { @engine.close_read }
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Yields lifecycle events for this socket until it's closed or
|
|
114
|
+
# the returned task is stopped.
|
|
115
|
+
#
|
|
116
|
+
# @param verbose [Boolean] when true, also emits :message_sent /
|
|
117
|
+
# :message_received events
|
|
118
|
+
# @yield [event]
|
|
119
|
+
# @yieldparam event [MonitorEvent]
|
|
120
|
+
# @return [Async::Task]
|
|
121
|
+
def monitor(verbose: false, &block)
|
|
122
|
+
ensure_parent_task
|
|
123
|
+
|
|
124
|
+
queue = Async::Queue.new
|
|
125
|
+
@engine.monitor_queue = queue
|
|
126
|
+
@engine.verbose_monitor = verbose
|
|
127
|
+
|
|
128
|
+
Reactor.run do
|
|
129
|
+
@engine.spawn_task(annotation: "nnq monitor") do
|
|
130
|
+
while (event = queue.dequeue)
|
|
131
|
+
block.call(event)
|
|
132
|
+
end
|
|
133
|
+
rescue Async::Stop
|
|
134
|
+
ensure
|
|
135
|
+
@engine.monitor_queue = nil
|
|
136
|
+
block.call(MonitorEvent.new(type: :monitor_stopped))
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
55
140
|
|
|
56
141
|
|
|
57
|
-
|
|
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
|
|
58
153
|
|
|
59
154
|
|
|
60
155
|
private
|
|
61
156
|
|
|
157
|
+
|
|
62
158
|
def ensure_parent_task
|
|
63
159
|
# Must run OUTSIDE Reactor.run so that non-Async callers capture
|
|
64
160
|
# the IO thread's root task, not the ephemeral work-item task
|
|
65
161
|
# that Reactor wraps each dispatched block in. Inside an Async
|
|
66
162
|
# reactor, the current task is the right parent.
|
|
67
163
|
if Async::Task.current?
|
|
68
|
-
@engine.capture_parent_task(Async::Task.current)
|
|
164
|
+
@engine.capture_parent_task(Async::Task.current, on_io_thread: false)
|
|
69
165
|
else
|
|
70
|
-
@engine.capture_parent_task(Reactor.root_task)
|
|
166
|
+
@engine.capture_parent_task(Reactor.root_task, on_io_thread: true)
|
|
71
167
|
end
|
|
72
168
|
end
|
|
73
169
|
|
|
@@ -82,5 +178,6 @@ module NNQ
|
|
|
82
178
|
def build_routing(_engine)
|
|
83
179
|
raise NotImplementedError
|
|
84
180
|
end
|
|
181
|
+
|
|
85
182
|
end
|
|
86
183
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "socket"
|
|
4
|
+
require_relative "routing/surveyor"
|
|
5
|
+
require_relative "routing/respondent"
|
|
6
|
+
|
|
7
|
+
module NNQ
|
|
8
|
+
# SURVEYOR (nng surveyor0): broadcast side of the survey pattern.
|
|
9
|
+
# Sends a survey to all connected respondents, then collects replies
|
|
10
|
+
# within a timed window (`options.survey_time`, default 1s).
|
|
11
|
+
#
|
|
12
|
+
# Only one outstanding survey at a time — sending a new survey
|
|
13
|
+
# abandons the previous one. Respondents are not obliged to reply.
|
|
14
|
+
#
|
|
15
|
+
class SURVEYOR0 < Socket
|
|
16
|
+
# Broadcasts +body+ as a survey to all connected respondents.
|
|
17
|
+
def send_survey(body)
|
|
18
|
+
body = frozen_binary(body)
|
|
19
|
+
Reactor.run { @engine.routing.send_survey(body) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Receives the next reply. Raises {NNQ::TimedOut} when the survey
|
|
24
|
+
# window expires.
|
|
25
|
+
#
|
|
26
|
+
# @return [String] reply body
|
|
27
|
+
def receive
|
|
28
|
+
Reactor.run { @engine.routing.receive }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def protocol
|
|
36
|
+
Protocol::SP::Protocols::SURVEYOR_V0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_routing(engine)
|
|
41
|
+
Routing::Surveyor.new(engine)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# RESPONDENT (nng respondent0): reply side of the survey pattern.
|
|
47
|
+
# Receives surveys, processes them, and optionally sends replies.
|
|
48
|
+
# Strict alternation: #receive then #send_reply.
|
|
49
|
+
#
|
|
50
|
+
class RESPONDENT0 < Socket
|
|
51
|
+
# Blocks until the next survey arrives.
|
|
52
|
+
#
|
|
53
|
+
# @return [String, nil] survey body, or nil if the socket was closed
|
|
54
|
+
def receive
|
|
55
|
+
Reactor.run { @engine.routing.receive }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Routes +body+ back to the surveyor that sent the most recent survey.
|
|
60
|
+
def send_reply(body)
|
|
61
|
+
body = frozen_binary(body)
|
|
62
|
+
Reactor.run { @engine.routing.send_reply(body) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def protocol
|
|
70
|
+
Protocol::SP::Protocols::RESPONDENT_V0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def build_routing(engine)
|
|
75
|
+
Routing::Respondent.new(engine)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
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
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
require "protocol/sp"
|
|
4
4
|
|
|
5
5
|
module NNQ
|
|
6
|
+
# Freezes module-level state so NNQ sockets can be used inside Ractors.
|
|
7
|
+
# Call this once before spawning any Ractors that create NNQ sockets.
|
|
8
|
+
#
|
|
9
|
+
def self.freeze_for_ractors!
|
|
10
|
+
Engine::CONNECTION_FAILED.freeze
|
|
11
|
+
Engine::CONNECTION_LOST.freeze
|
|
12
|
+
Engine::TRANSPORTS.freeze
|
|
13
|
+
end
|
|
6
14
|
end
|
|
7
15
|
|
|
8
16
|
require_relative "nnq/version"
|
|
@@ -15,3 +23,5 @@ require_relative "nnq/push_pull"
|
|
|
15
23
|
require_relative "nnq/pair"
|
|
16
24
|
require_relative "nnq/req_rep"
|
|
17
25
|
require_relative "nnq/pub_sub"
|
|
26
|
+
require_relative "nnq/bus"
|
|
27
|
+
require_relative "nnq/surveyor_respondent"
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -52,8 +52,8 @@ dependencies:
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.1'
|
|
54
54
|
description: Pure Ruby implementation of nanomsg's Scalability Protocols (SP) on top
|
|
55
|
-
of async + io-stream.
|
|
56
|
-
over inproc/ipc/tcp.
|
|
55
|
+
of async + io-stream. Per-socket HWM, opportunistic batching, wire-compatible with
|
|
56
|
+
libnng over inproc/ipc/tcp.
|
|
57
57
|
email:
|
|
58
58
|
- paddor@gmail.com
|
|
59
59
|
executables: []
|
|
@@ -64,26 +64,34 @@ 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
|
|
71
|
+
- lib/nnq/engine/reconnect.rb
|
|
70
72
|
- lib/nnq/engine/socket_lifecycle.rb
|
|
71
73
|
- lib/nnq/error.rb
|
|
74
|
+
- lib/nnq/monitor_event.rb
|
|
72
75
|
- lib/nnq/options.rb
|
|
73
76
|
- lib/nnq/pair.rb
|
|
74
77
|
- lib/nnq/pub_sub.rb
|
|
75
78
|
- lib/nnq/push_pull.rb
|
|
76
79
|
- lib/nnq/reactor.rb
|
|
77
80
|
- lib/nnq/req_rep.rb
|
|
81
|
+
- lib/nnq/routing/backtrace.rb
|
|
82
|
+
- lib/nnq/routing/bus.rb
|
|
78
83
|
- lib/nnq/routing/pair.rb
|
|
79
84
|
- lib/nnq/routing/pub.rb
|
|
80
85
|
- lib/nnq/routing/pull.rb
|
|
81
86
|
- lib/nnq/routing/push.rb
|
|
82
87
|
- lib/nnq/routing/rep.rb
|
|
83
88
|
- lib/nnq/routing/req.rb
|
|
89
|
+
- lib/nnq/routing/respondent.rb
|
|
84
90
|
- lib/nnq/routing/send_pump.rb
|
|
85
91
|
- lib/nnq/routing/sub.rb
|
|
92
|
+
- lib/nnq/routing/surveyor.rb
|
|
86
93
|
- lib/nnq/socket.rb
|
|
94
|
+
- lib/nnq/surveyor_respondent.rb
|
|
87
95
|
- lib/nnq/transport/inproc.rb
|
|
88
96
|
- lib/nnq/transport/ipc.rb
|
|
89
97
|
- lib/nnq/transport/tcp.rb
|