nnq 0.2.0 → 0.4.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 +41 -0
- data/lib/nnq/engine/connection_lifecycle.rb +15 -6
- data/lib/nnq/engine/reconnect.rb +103 -0
- data/lib/nnq/engine/socket_lifecycle.rb +32 -3
- data/lib/nnq/engine.rb +127 -23
- data/lib/nnq/monitor_event.rb +16 -0
- data/lib/nnq/push_pull.rb +7 -1
- data/lib/nnq/routing/pair.rb +6 -0
- data/lib/nnq/routing/pub.rb +1 -0
- data/lib/nnq/routing/pull.rb +8 -0
- data/lib/nnq/routing/rep.rb +5 -0
- data/lib/nnq/routing/send_pump.rb +1 -0
- data/lib/nnq/routing/sub.rb +5 -0
- data/lib/nnq/socket.rb +56 -2
- data/lib/nnq/version.rb +1 -1
- data/lib/nnq.rb +8 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed71a88238bae0611223d36bb3ec0a39795388d7b90227fb690cfc924cf85198
|
|
4
|
+
data.tar.gz: cda3bfff65005960b91672cd1c6e2a61e8fce104e202ed928932e7b0404d7eda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 87ddb00a39836f3699dbd5f17a18b34dbd1c0c18d284ad1cf0b14d6271e565444ffce926ce2514c912423f943ef9a994ebb79eb3620b238f872f840124e3ad38
|
|
7
|
+
data.tar.gz: 193a8a3a1830f18aba6399990a7f1ea81639165c6ffc072691874ca972e21732f4edaf6fff3a82c3bf40b1d3a117ff04adc381e1a3f65d2f2ca677b091ae9d9d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-04-09
|
|
4
|
+
|
|
5
|
+
- `Socket#all_peers_gone` — `Async::Promise` resolving the first time
|
|
6
|
+
the connection set becomes empty after at least one peer connected.
|
|
7
|
+
Edge-triggered, ported from OMQ.
|
|
8
|
+
- `Socket#close_read` — closes the recv side only. Buffered messages
|
|
9
|
+
drain, then `#receive` returns `nil`. Send side stays operational.
|
|
10
|
+
- `Socket#reconnect_enabled` / `#reconnect_enabled=` — flipped by
|
|
11
|
+
transient-mode consumers before draining to prevent the background
|
|
12
|
+
reconnect loop from revivifying a dying socket.
|
|
13
|
+
- `Socket#monitor` / `NNQ::MonitorEvent` — lifecycle event stream
|
|
14
|
+
emitting `:listening`, `:connect_delayed`, `:connect_retried`,
|
|
15
|
+
`:connected`, `:handshake_succeeded`/`_failed`, `:disconnected`,
|
|
16
|
+
`:closed`, and (when `verbose: true`) `:message_sent` /
|
|
17
|
+
`:message_received`. Ported from OMQ, minus the heartbeat/mechanism
|
|
18
|
+
events nnq doesn't have.
|
|
19
|
+
- Background reconnect — `NNQ::Engine::Reconnect` runs a `transient: true`
|
|
20
|
+
task per dialed endpoint, retrying with exponential back-off bounded
|
|
21
|
+
by `options.reconnect_interval` (Numeric or Range). `connect` becomes
|
|
22
|
+
non-blocking for `tcp://` and `ipc://`; `inproc://` stays synchronous.
|
|
23
|
+
`CONNECTION_FAILED` / `CONNECTION_LOST` mutable-at-load-time registries
|
|
24
|
+
let plugins append transport-specific error classes.
|
|
25
|
+
- `NNQ::PULL#receive` honors `options.read_timeout` via
|
|
26
|
+
`Fiber.scheduler.with_timeout`. Previously the option was declared
|
|
27
|
+
but inert.
|
|
28
|
+
- `NNQ.freeze_for_ractors!` — freezes `Engine::CONNECTION_FAILED`,
|
|
29
|
+
`Engine::CONNECTION_LOST`, and `Engine::TRANSPORTS` so NNQ sockets
|
|
30
|
+
can be used from non-main Ractors. Required for nnq-cli's `pipe -P N`
|
|
31
|
+
parallel worker mode.
|
|
32
|
+
|
|
33
|
+
## 0.3.0 — 2026-04-09
|
|
34
|
+
|
|
35
|
+
- `Socket#peer_connected` — `Async::Promise` that resolves with the
|
|
36
|
+
first connected peer (or `nil` on close without any peers). Ported
|
|
37
|
+
from OMQ. Held on `SocketLifecycle`, resolved by `ConnectionLifecycle`
|
|
38
|
+
on first `ready!`, and edge-triggered so callers don't need to poll.
|
|
39
|
+
- `bench/` — main throughput suite ported from OMQ. Four patterns
|
|
40
|
+
(push/pull, req/rep, pair, pub/sub) across inproc, ipc, and tcp.
|
|
41
|
+
Calibration-driven burst sizing, fastest-of-3 reporting, regression
|
|
42
|
+
report with `--update-readme` to regenerate README tables.
|
|
43
|
+
|
|
3
44
|
## 0.2.0 — 2026-04-09
|
|
4
45
|
|
|
5
46
|
- `NNQ::PUB` / `NNQ::SUB` with local prefix filtering (pub0/sub0).
|
|
@@ -71,23 +71,26 @@ module NNQ
|
|
|
71
71
|
sp.handshake!
|
|
72
72
|
ready!(NNQ::Connection.new(sp, endpoint: @endpoint))
|
|
73
73
|
@conn
|
|
74
|
-
rescue
|
|
74
|
+
rescue => e
|
|
75
|
+
@engine.emit_monitor_event(:handshake_failed, endpoint: @endpoint, detail: { error: e })
|
|
75
76
|
io.close rescue nil
|
|
76
77
|
transition!(:closed) unless @state == :closed
|
|
77
78
|
raise
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
82
|
+
# Unexpected loss of an established connection. Tears down and
|
|
83
|
+
# asks the engine to schedule a reconnect (if the endpoint is in
|
|
84
|
+
# the dialed set and reconnect is still enabled).
|
|
83
85
|
def lost!
|
|
86
|
+
ep = @endpoint
|
|
84
87
|
tear_down!
|
|
88
|
+
@engine.maybe_reconnect(ep)
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
# reconnect yet, so the two behave identically.
|
|
92
|
+
# Deliberate close (engine shutdown or routing eviction). Does
|
|
93
|
+
# not trigger reconnect.
|
|
91
94
|
def close!
|
|
92
95
|
tear_down!
|
|
93
96
|
end
|
|
@@ -102,9 +105,13 @@ module NNQ
|
|
|
102
105
|
begin
|
|
103
106
|
@engine.routing.connection_added(conn) if @engine.routing.respond_to?(:connection_added)
|
|
104
107
|
rescue ConnectionRejected
|
|
108
|
+
@engine.emit_monitor_event(:connection_rejected, endpoint: @endpoint)
|
|
105
109
|
tear_down!
|
|
106
110
|
raise
|
|
107
111
|
end
|
|
112
|
+
@engine.lifecycle.peer_connected.resolve(conn) unless @engine.lifecycle.peer_connected.resolved?
|
|
113
|
+
@engine.emit_monitor_event(:handshake_succeeded, endpoint: @endpoint)
|
|
114
|
+
@engine.emit_monitor_event(:connected, endpoint: @endpoint)
|
|
108
115
|
@engine.new_pipe.signal
|
|
109
116
|
end
|
|
110
117
|
|
|
@@ -116,6 +123,8 @@ module NNQ
|
|
|
116
123
|
@engine.connections.delete(@conn)
|
|
117
124
|
@engine.routing.connection_removed(@conn) if @engine.routing.respond_to?(:connection_removed)
|
|
118
125
|
@conn.close rescue nil
|
|
126
|
+
@engine.emit_monitor_event(:disconnected, endpoint: @endpoint)
|
|
127
|
+
@engine.resolve_all_peers_gone_if_empty
|
|
119
128
|
end
|
|
120
129
|
end
|
|
121
130
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
class Engine
|
|
5
|
+
# Connection errors that should trigger a reconnect retry rather
|
|
6
|
+
# than propagate. Mutable at load time so plugins (e.g. a future
|
|
7
|
+
# TLS transport) can append their own error classes; frozen on
|
|
8
|
+
# first {Engine#connect}.
|
|
9
|
+
CONNECTION_FAILED = [
|
|
10
|
+
Errno::ECONNREFUSED,
|
|
11
|
+
Errno::EHOSTUNREACH,
|
|
12
|
+
Errno::ENETUNREACH,
|
|
13
|
+
Errno::ENOENT,
|
|
14
|
+
Errno::EPIPE,
|
|
15
|
+
Errno::ETIMEDOUT,
|
|
16
|
+
Socket::ResolutionError,
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Errors that indicate an established connection went away. Used
|
|
20
|
+
# by the recv loop and pumps to silently terminate (the connection
|
|
21
|
+
# lifecycle's #lost! handler decides whether to reconnect).
|
|
22
|
+
CONNECTION_LOST = [
|
|
23
|
+
EOFError,
|
|
24
|
+
IOError,
|
|
25
|
+
Errno::ECONNRESET,
|
|
26
|
+
Errno::EPIPE,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Schedules reconnect attempts with exponential back-off.
|
|
31
|
+
#
|
|
32
|
+
# Runs a background task that loops until a connection is
|
|
33
|
+
# established or the engine is closed. Caller is non-blocking:
|
|
34
|
+
# {Engine#connect} returns immediately and the actual dial happens
|
|
35
|
+
# inside the task.
|
|
36
|
+
#
|
|
37
|
+
class Reconnect
|
|
38
|
+
# @param endpoint [String]
|
|
39
|
+
# @param options [Options]
|
|
40
|
+
# @param parent_task [Async::Task]
|
|
41
|
+
# @param engine [Engine]
|
|
42
|
+
# @param delay [Numeric, nil] initial delay (defaults to reconnect_interval)
|
|
43
|
+
def self.schedule(endpoint, options, parent_task, engine, delay: nil)
|
|
44
|
+
new(engine, endpoint, options).run(parent_task, delay: delay)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def initialize(engine, endpoint, options)
|
|
49
|
+
@engine = engine
|
|
50
|
+
@endpoint = endpoint
|
|
51
|
+
@options = options
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run(parent_task, delay: nil)
|
|
56
|
+
delay, max_delay = init_delay(delay)
|
|
57
|
+
|
|
58
|
+
task = parent_task.async(transient: true, annotation: "nnq reconnect #{@endpoint}") do
|
|
59
|
+
loop do
|
|
60
|
+
break if @engine.closed?
|
|
61
|
+
sleep delay if delay > 0
|
|
62
|
+
break if @engine.closed?
|
|
63
|
+
begin
|
|
64
|
+
@engine.transport_for(@endpoint).connect(@endpoint, @engine)
|
|
65
|
+
break
|
|
66
|
+
rescue *CONNECTION_FAILED, *CONNECTION_LOST => e
|
|
67
|
+
delay = next_delay(delay, max_delay)
|
|
68
|
+
@engine.emit_monitor_event(:connect_retried, endpoint: @endpoint, detail: { interval: delay, error: e })
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
rescue Async::Stop
|
|
72
|
+
end
|
|
73
|
+
@engine.tasks << task
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def init_delay(delay)
|
|
81
|
+
ri = @options.reconnect_interval
|
|
82
|
+
if ri.is_a?(Range)
|
|
83
|
+
[delay || ri.begin, ri.end]
|
|
84
|
+
else
|
|
85
|
+
[delay || ri, nil]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def next_delay(delay, max_delay)
|
|
91
|
+
ri = @options.reconnect_interval
|
|
92
|
+
if ri.is_a?(Range)
|
|
93
|
+
delay = delay * 2
|
|
94
|
+
delay = [delay, max_delay].min if max_delay
|
|
95
|
+
delay = ri.begin if delay == 0
|
|
96
|
+
delay
|
|
97
|
+
else
|
|
98
|
+
ri
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "async/promise"
|
|
4
|
+
|
|
3
5
|
module NNQ
|
|
4
6
|
class Engine
|
|
5
7
|
# Owns the socket-level state: `:new → :open → :closing → :closed`
|
|
@@ -31,11 +33,28 @@ module NNQ
|
|
|
31
33
|
# @return [Boolean] true if parent_task is the shared Reactor thread
|
|
32
34
|
attr_reader :on_io_thread
|
|
33
35
|
|
|
36
|
+
# @return [Async::Promise] resolves with the first connected peer
|
|
37
|
+
# (or nil if the socket closes before anyone connects)
|
|
38
|
+
attr_reader :peer_connected
|
|
39
|
+
|
|
40
|
+
# @return [Async::Promise] resolves with true the first time the
|
|
41
|
+
# connection set becomes empty after at least one peer connected.
|
|
42
|
+
# Edge-triggered: does not re-arm on reconnect.
|
|
43
|
+
attr_reader :all_peers_gone
|
|
44
|
+
|
|
45
|
+
# @return [Boolean] when false, the engine must not schedule new
|
|
46
|
+
# reconnect attempts. Default true. nnq has no automatic
|
|
47
|
+
# reconnect loop yet, so this currently just records intent.
|
|
48
|
+
attr_accessor :reconnect_enabled
|
|
49
|
+
|
|
34
50
|
|
|
35
51
|
def initialize
|
|
36
|
-
@state
|
|
37
|
-
@parent_task
|
|
38
|
-
@on_io_thread
|
|
52
|
+
@state = :new
|
|
53
|
+
@parent_task = nil
|
|
54
|
+
@on_io_thread = false
|
|
55
|
+
@peer_connected = Async::Promise.new
|
|
56
|
+
@all_peers_gone = Async::Promise.new
|
|
57
|
+
@reconnect_enabled = true
|
|
39
58
|
end
|
|
40
59
|
|
|
41
60
|
|
|
@@ -74,6 +93,16 @@ module NNQ
|
|
|
74
93
|
end
|
|
75
94
|
|
|
76
95
|
|
|
96
|
+
# Resolves `all_peers_gone` if we had peers and now have none.
|
|
97
|
+
# Idempotent.
|
|
98
|
+
# @param connections [Hash] current connection map
|
|
99
|
+
def resolve_all_peers_gone_if_empty(connections)
|
|
100
|
+
return unless @peer_connected.resolved? && connections.empty?
|
|
101
|
+
return if @all_peers_gone.resolved?
|
|
102
|
+
@all_peers_gone.resolve(true)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
77
106
|
private
|
|
78
107
|
|
|
79
108
|
def transition!(new_state)
|
data/lib/nnq/engine.rb
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require "async"
|
|
4
4
|
require "async/clock"
|
|
5
|
+
require "set"
|
|
5
6
|
require "protocol/sp"
|
|
6
7
|
require_relative "error"
|
|
7
8
|
require_relative "connection"
|
|
9
|
+
require_relative "monitor_event"
|
|
8
10
|
require_relative "reactor"
|
|
9
11
|
require_relative "engine/socket_lifecycle"
|
|
10
12
|
require_relative "engine/connection_lifecycle"
|
|
13
|
+
require_relative "engine/reconnect"
|
|
11
14
|
require_relative "transport/tcp"
|
|
12
15
|
require_relative "transport/ipc"
|
|
13
16
|
require_relative "transport/inproc"
|
|
@@ -47,20 +50,54 @@ module NNQ
|
|
|
47
50
|
# @return [Async::Condition] signaled when a new pipe is registered
|
|
48
51
|
attr_reader :new_pipe
|
|
49
52
|
|
|
53
|
+
# @return [Set<String>] endpoints we have called #connect on; used
|
|
54
|
+
# to decide whether to schedule a reconnect after a connection
|
|
55
|
+
# is lost.
|
|
56
|
+
attr_reader :dialed
|
|
57
|
+
|
|
58
|
+
# @return [Array<Async::Task>] transient tasks owned by the engine
|
|
59
|
+
# (currently just background reconnect loops). Stopped at #close.
|
|
60
|
+
attr_reader :tasks
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# @return [Async::Queue, nil] monitor event queue (set by Socket#monitor)
|
|
64
|
+
attr_accessor :monitor_queue
|
|
65
|
+
attr_accessor :verbose_monitor
|
|
66
|
+
|
|
50
67
|
|
|
51
68
|
# @param protocol [Integer] our SP protocol id (e.g. Protocols::PUSH_V0)
|
|
52
69
|
# @param options [Options]
|
|
53
70
|
# @yieldparam engine [Engine] used by the caller to build a routing
|
|
54
71
|
# strategy with access to the engine's connection map
|
|
55
72
|
def initialize(protocol:, options:)
|
|
56
|
-
@protocol
|
|
57
|
-
@options
|
|
58
|
-
@connections
|
|
59
|
-
@listeners
|
|
60
|
-
@lifecycle
|
|
61
|
-
@last_endpoint
|
|
62
|
-
@new_pipe
|
|
63
|
-
@
|
|
73
|
+
@protocol = protocol
|
|
74
|
+
@options = options
|
|
75
|
+
@connections = {}
|
|
76
|
+
@listeners = []
|
|
77
|
+
@lifecycle = SocketLifecycle.new
|
|
78
|
+
@last_endpoint = nil
|
|
79
|
+
@new_pipe = Async::Condition.new
|
|
80
|
+
@monitor_queue = nil
|
|
81
|
+
@verbose_monitor = false
|
|
82
|
+
@dialed = Set.new
|
|
83
|
+
@tasks = []
|
|
84
|
+
@routing = yield(self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Emits a monitor event to the attached queue (if any).
|
|
89
|
+
def emit_monitor_event(type, endpoint: nil, detail: nil)
|
|
90
|
+
return unless @monitor_queue
|
|
91
|
+
@monitor_queue.enqueue(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
|
|
92
|
+
rescue Async::Stop
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Emits a verbose-only monitor event. Only forwarded when
|
|
97
|
+
# {Socket#monitor} was called with +verbose: true+.
|
|
98
|
+
def emit_verbose_monitor_event(type, **detail)
|
|
99
|
+
return unless @verbose_monitor
|
|
100
|
+
emit_monitor_event(type, detail: detail)
|
|
64
101
|
end
|
|
65
102
|
|
|
66
103
|
|
|
@@ -75,11 +112,45 @@ module NNQ
|
|
|
75
112
|
def closed? = @lifecycle.closed?
|
|
76
113
|
|
|
77
114
|
|
|
115
|
+
# @return [Async::Promise] resolves with the first connected peer
|
|
116
|
+
def peer_connected = @lifecycle.peer_connected
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# @return [Async::Promise] resolves when all peers have disconnected
|
|
120
|
+
# (edge-triggered, after at least one peer connected)
|
|
121
|
+
def all_peers_gone = @lifecycle.all_peers_gone
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Called by ConnectionLifecycle teardown. Resolves `all_peers_gone`
|
|
125
|
+
# if the connection set is now empty and we had peers.
|
|
126
|
+
def resolve_all_peers_gone_if_empty
|
|
127
|
+
@lifecycle.resolve_all_peers_gone_if_empty(@connections)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# @return [Boolean]
|
|
132
|
+
def reconnect_enabled = @lifecycle.reconnect_enabled
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Disables or re-enables automatic reconnect. nnq has no reconnect
|
|
136
|
+
# loop yet, so this is forward-looking — {TransientMonitor} flips
|
|
137
|
+
# it before draining.
|
|
138
|
+
def reconnect_enabled=(value)
|
|
139
|
+
@lifecycle.reconnect_enabled = value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Closes only the recv side. Buffered messages drain, then
|
|
144
|
+
# {Socket#receive} returns nil. Send side remains operational.
|
|
145
|
+
def close_read
|
|
146
|
+
@routing.close_read if @routing.respond_to?(:close_read)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
|
|
78
150
|
# Stores the parent Async task that long-lived NNQ fibers will
|
|
79
151
|
# attach to. The caller (Socket) is responsible for picking the
|
|
80
152
|
# right one (the user's current task, or Reactor.root_task).
|
|
81
|
-
def capture_parent_task(task)
|
|
82
|
-
on_io_thread = task.equal?(Reactor.root_task)
|
|
153
|
+
def capture_parent_task(task, on_io_thread:)
|
|
83
154
|
@lifecycle.capture_parent_task(task, on_io_thread: on_io_thread)
|
|
84
155
|
end
|
|
85
156
|
|
|
@@ -93,17 +164,42 @@ module NNQ
|
|
|
93
164
|
end
|
|
94
165
|
@listeners << listener
|
|
95
166
|
@last_endpoint = listener.endpoint
|
|
167
|
+
emit_monitor_event(:listening, endpoint: @last_endpoint)
|
|
96
168
|
end
|
|
97
169
|
|
|
98
170
|
|
|
99
|
-
# Connects to +endpoint+.
|
|
100
|
-
#
|
|
171
|
+
# Connects to +endpoint+. Non-blocking for tcp:// and ipc:// — the
|
|
172
|
+
# actual dial happens inside a background reconnect task that
|
|
173
|
+
# retries with exponential back-off until the peer becomes
|
|
174
|
+
# reachable. Inproc connect is synchronous and instant.
|
|
101
175
|
def connect(endpoint)
|
|
102
|
-
|
|
103
|
-
transport.connect(endpoint, self)
|
|
176
|
+
@dialed << endpoint
|
|
104
177
|
@last_endpoint = endpoint
|
|
105
|
-
|
|
106
|
-
|
|
178
|
+
if endpoint.start_with?("inproc://")
|
|
179
|
+
transport_for(endpoint).connect(endpoint, self)
|
|
180
|
+
else
|
|
181
|
+
emit_monitor_event(:connect_delayed, endpoint: endpoint)
|
|
182
|
+
Reconnect.schedule(endpoint, @options, @lifecycle.parent_task, self, delay: 0)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Schedules a reconnect for +endpoint+ if auto-reconnect is enabled
|
|
188
|
+
# and the endpoint is still in the dialed set. Called from the
|
|
189
|
+
# connection lifecycle's `lost!` path.
|
|
190
|
+
def maybe_reconnect(endpoint)
|
|
191
|
+
return unless endpoint && @dialed.include?(endpoint)
|
|
192
|
+
return unless @lifecycle.alive? && @lifecycle.reconnect_enabled
|
|
193
|
+
return if endpoint.start_with?("inproc://")
|
|
194
|
+
Reconnect.schedule(endpoint, @options, @lifecycle.parent_task, self)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Public so {Reconnect} can dial directly without re-deriving the
|
|
199
|
+
# transport from the URL each iteration.
|
|
200
|
+
def transport_for(endpoint)
|
|
201
|
+
scheme = endpoint[/\A([a-z+]+):\/\//i, 1] or raise Error, "no scheme: #{endpoint}"
|
|
202
|
+
TRANSPORTS[scheme] or raise Error, "unsupported transport: #{scheme}"
|
|
107
203
|
end
|
|
108
204
|
|
|
109
205
|
|
|
@@ -146,6 +242,8 @@ module NNQ
|
|
|
146
242
|
return unless @lifecycle.alive?
|
|
147
243
|
@lifecycle.start_closing!
|
|
148
244
|
@listeners.each(&:stop)
|
|
245
|
+
@tasks.each { |t| t.stop rescue nil }
|
|
246
|
+
@tasks.clear
|
|
149
247
|
drain_send_queue(@options.linger)
|
|
150
248
|
@routing.close if @routing.respond_to?(:close)
|
|
151
249
|
# Tear down each remaining connection via its lifecycle. The
|
|
@@ -153,6 +251,11 @@ module NNQ
|
|
|
153
251
|
@connections.values.each(&:close!)
|
|
154
252
|
@lifecycle.finish_closing!
|
|
155
253
|
@new_pipe.signal
|
|
254
|
+
# Unblock anyone waiting on peer_connected when the socket is
|
|
255
|
+
# closed before a peer ever arrived.
|
|
256
|
+
@lifecycle.peer_connected.resolve(nil) unless @lifecycle.peer_connected.resolved?
|
|
257
|
+
emit_monitor_event(:closed)
|
|
258
|
+
close_monitor_queue
|
|
156
259
|
end
|
|
157
260
|
|
|
158
261
|
|
|
@@ -165,6 +268,12 @@ module NNQ
|
|
|
165
268
|
|
|
166
269
|
private
|
|
167
270
|
|
|
271
|
+
def close_monitor_queue
|
|
272
|
+
return unless @monitor_queue
|
|
273
|
+
@monitor_queue.enqueue(nil)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
|
|
168
277
|
def drain_send_queue(timeout)
|
|
169
278
|
return unless @routing.respond_to?(:send_queue_drained?)
|
|
170
279
|
return if @connections.empty?
|
|
@@ -180,19 +289,14 @@ module NNQ
|
|
|
180
289
|
@lifecycle.parent_task.async(annotation: "nnq recv #{conn.endpoint}") do
|
|
181
290
|
loop do
|
|
182
291
|
body = conn.receive_message
|
|
292
|
+
emit_verbose_monitor_event(:message_received, body: body)
|
|
183
293
|
@routing.enqueue(body, conn)
|
|
184
|
-
rescue
|
|
294
|
+
rescue *CONNECTION_LOST, Async::Stop
|
|
185
295
|
break
|
|
186
296
|
end
|
|
187
297
|
ensure
|
|
188
298
|
handle_connection_lost(conn)
|
|
189
299
|
end
|
|
190
300
|
end
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def transport_for(endpoint)
|
|
194
|
-
scheme = endpoint[/\A([a-z+]+):\/\//i, 1] or raise Error, "no scheme: #{endpoint}"
|
|
195
|
-
TRANSPORTS[scheme] or raise Error, "unsupported transport: #{scheme}"
|
|
196
|
-
end
|
|
197
301
|
end
|
|
198
302
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NNQ
|
|
4
|
+
# Lifecycle event emitted by {Socket#monitor}.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute [r] type
|
|
7
|
+
# @return [Symbol] event type (:listening, :connected, :disconnected, ...)
|
|
8
|
+
# @!attribute [r] endpoint
|
|
9
|
+
# @return [String, nil] the endpoint involved
|
|
10
|
+
# @!attribute [r] detail
|
|
11
|
+
# @return [Hash, nil] extra context
|
|
12
|
+
#
|
|
13
|
+
MonitorEvent = Data.define(:type, :endpoint, :detail) do
|
|
14
|
+
def initialize(type:, endpoint: nil, detail: nil) = super
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/nnq/push_pull.rb
CHANGED
|
@@ -34,7 +34,13 @@ module NNQ
|
|
|
34
34
|
#
|
|
35
35
|
class PULL < Socket
|
|
36
36
|
def receive
|
|
37
|
-
Reactor.run
|
|
37
|
+
Reactor.run do
|
|
38
|
+
if (timeout = @engine.options.read_timeout)
|
|
39
|
+
Fiber.scheduler.with_timeout(timeout) { @engine.routing.receive }
|
|
40
|
+
else
|
|
41
|
+
@engine.routing.receive
|
|
42
|
+
end
|
|
43
|
+
end
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
|
data/lib/nnq/routing/pair.rb
CHANGED
data/lib/nnq/routing/pub.rb
CHANGED
data/lib/nnq/routing/pull.rb
CHANGED
|
@@ -33,6 +33,14 @@ module NNQ
|
|
|
33
33
|
def close
|
|
34
34
|
@queue.enqueue(nil)
|
|
35
35
|
end
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Wakes any waiters with nil, leaving the send side untouched
|
|
39
|
+
# (PULL has no send side — close_read is identical to close here,
|
|
40
|
+
# but kept separate for the `Socket#close_read` contract).
|
|
41
|
+
def close_read
|
|
42
|
+
@queue.enqueue(nil)
|
|
43
|
+
end
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
end
|
data/lib/nnq/routing/rep.rb
CHANGED
data/lib/nnq/routing/sub.rb
CHANGED
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
|
|
@@ -57,6 +60,57 @@ module NNQ
|
|
|
57
60
|
def connection_count = @engine.connections.size
|
|
58
61
|
|
|
59
62
|
|
|
63
|
+
# Resolves with the first connected peer (or nil on close without
|
|
64
|
+
# any peers). Block on `.wait` to wait until a connection is ready.
|
|
65
|
+
def peer_connected = @engine.peer_connected
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Resolves with `true` the first time all peers have disconnected
|
|
69
|
+
# (after at least one peer was connected). Edge-triggered.
|
|
70
|
+
def all_peers_gone = @engine.all_peers_gone
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def reconnect_enabled = @engine.reconnect_enabled
|
|
74
|
+
def reconnect_enabled=(value)
|
|
75
|
+
@engine.reconnect_enabled = value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Closes the recv side only. Buffered messages drain, then
|
|
80
|
+
# {#receive} returns nil. Send side stays open.
|
|
81
|
+
def close_read
|
|
82
|
+
Reactor.run { @engine.close_read }
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Yields lifecycle events for this socket until it's closed or
|
|
88
|
+
# the returned task is stopped.
|
|
89
|
+
#
|
|
90
|
+
# @param verbose [Boolean] when true, also emits :message_sent /
|
|
91
|
+
# :message_received events
|
|
92
|
+
# @yield [event]
|
|
93
|
+
# @yieldparam event [MonitorEvent]
|
|
94
|
+
# @return [Async::Task]
|
|
95
|
+
def monitor(verbose: false, &block)
|
|
96
|
+
ensure_parent_task
|
|
97
|
+
queue = Async::Queue.new
|
|
98
|
+
@engine.monitor_queue = queue
|
|
99
|
+
@engine.verbose_monitor = verbose
|
|
100
|
+
Reactor.run do
|
|
101
|
+
@engine.spawn_task(annotation: "nnq monitor") do
|
|
102
|
+
while (event = queue.dequeue)
|
|
103
|
+
block.call(event)
|
|
104
|
+
end
|
|
105
|
+
rescue Async::Stop
|
|
106
|
+
ensure
|
|
107
|
+
@engine.monitor_queue = nil
|
|
108
|
+
block.call(MonitorEvent.new(type: :monitor_stopped))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
|
|
60
114
|
private
|
|
61
115
|
|
|
62
116
|
def ensure_parent_task
|
|
@@ -65,9 +119,9 @@ module NNQ
|
|
|
65
119
|
# that Reactor wraps each dispatched block in. Inside an Async
|
|
66
120
|
# reactor, the current task is the right parent.
|
|
67
121
|
if Async::Task.current?
|
|
68
|
-
@engine.capture_parent_task(Async::Task.current)
|
|
122
|
+
@engine.capture_parent_task(Async::Task.current, on_io_thread: false)
|
|
69
123
|
else
|
|
70
|
-
@engine.capture_parent_task(Reactor.root_task)
|
|
124
|
+
@engine.capture_parent_task(Reactor.root_task, on_io_thread: true)
|
|
71
125
|
end
|
|
72
126
|
end
|
|
73
127
|
|
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"
|
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.4.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: []
|
|
@@ -67,8 +67,10 @@ files:
|
|
|
67
67
|
- lib/nnq/connection.rb
|
|
68
68
|
- lib/nnq/engine.rb
|
|
69
69
|
- lib/nnq/engine/connection_lifecycle.rb
|
|
70
|
+
- lib/nnq/engine/reconnect.rb
|
|
70
71
|
- lib/nnq/engine/socket_lifecycle.rb
|
|
71
72
|
- lib/nnq/error.rb
|
|
73
|
+
- lib/nnq/monitor_event.rb
|
|
72
74
|
- lib/nnq/options.rb
|
|
73
75
|
- lib/nnq/pair.rb
|
|
74
76
|
- lib/nnq/pub_sub.rb
|