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.
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
- def initialize(linger: nil, send_hwm: Options::DEFAULT_HWM)
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 = @engine.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
- def connection_count = @engine.connections.size
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
@@ -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
@@ -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
@@ -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 = "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
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 = TCPSocket.new(host, port)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NNQ
4
- VERSION = "0.2.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.2.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. No HWM, opportunistic batching, wire-compatible with libnng
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