nats-pure 0.7.2 → 2.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/LICENSE +201 -0
- data/README.md +251 -0
- data/lib/nats/client.rb +16 -0
- data/lib/nats/io/client.rb +1559 -1277
- data/lib/nats/io/errors.rb +74 -0
- data/lib/nats/io/jetstream/api.rb +309 -0
- data/lib/nats/io/jetstream/errors.rb +104 -0
- data/lib/nats/io/jetstream/js/config.rb +26 -0
- data/lib/nats/io/jetstream/js/header.rb +31 -0
- data/lib/nats/io/jetstream/js/status.rb +27 -0
- data/lib/nats/io/jetstream/js/sub.rb +30 -0
- data/lib/nats/io/jetstream/js.rb +93 -0
- data/lib/nats/io/jetstream/manager.rb +303 -0
- data/lib/nats/io/jetstream/msg/ack.rb +57 -0
- data/lib/nats/io/jetstream/msg/ack_methods.rb +111 -0
- data/lib/nats/io/jetstream/msg/metadata.rb +37 -0
- data/lib/nats/io/jetstream/msg.rb +26 -0
- data/lib/nats/io/jetstream/pull_subscription.rb +260 -0
- data/lib/nats/io/jetstream/push_subscription.rb +42 -0
- data/lib/nats/io/jetstream.rb +344 -0
- data/lib/nats/io/kv/api.rb +39 -0
- data/lib/nats/io/kv/bucket_status.rb +38 -0
- data/lib/nats/io/kv/errors.rb +60 -0
- data/lib/nats/io/kv/manager.rb +89 -0
- data/lib/nats/io/kv.rb +178 -0
- data/lib/nats/io/msg.rb +58 -0
- data/lib/nats/io/parser.rb +7 -7
- data/lib/nats/io/rails.rb +29 -0
- data/lib/nats/io/subscription.rb +157 -0
- data/lib/nats/io/version.rb +8 -4
- data/lib/nats/io/websocket.rb +75 -0
- data/lib/nats/nuid.rb +3 -1
- data/lib/nats.rb +39 -0
- data/sig/nats/io/client.rbs +304 -0
- data/sig/nats/io/errors.rbs +54 -0
- data/sig/nats/io/jetstream/api.rbs +35 -0
- data/sig/nats/io/jetstream/errors.rbs +54 -0
- data/sig/nats/io/jetstream/js/config.rbs +11 -0
- data/sig/nats/io/jetstream/js/header.rbs +17 -0
- data/sig/nats/io/jetstream/js/status.rbs +13 -0
- data/sig/nats/io/jetstream/js/sub.rbs +14 -0
- data/sig/nats/io/jetstream/js.rbs +27 -0
- data/sig/nats/io/jetstream/manager.rbs +33 -0
- data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
- data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
- data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
- data/sig/nats/io/jetstream/msg.rbs +6 -0
- data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
- data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
- data/sig/nats/io/jetstream.rbs +15 -0
- data/sig/nats/io/kv/api.rbs +8 -0
- data/sig/nats/io/kv/bucket_status.rbs +17 -0
- data/sig/nats/io/kv/errors.rbs +30 -0
- data/sig/nats/io/kv/manager.rbs +11 -0
- data/sig/nats/io/kv.rbs +39 -0
- data/sig/nats/io/msg.rbs +14 -0
- data/sig/nats/io/parser.rbs +32 -0
- data/sig/nats/io/subscription.rbs +33 -0
- data/sig/nats/io/version.rbs +9 -0
- data/sig/nats/nuid.rbs +32 -0
- metadata +74 -4
data/lib/nats/io/client.rb
CHANGED
@@ -12,8 +12,13 @@
|
|
12
12
|
# limitations under the License.
|
13
13
|
#
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
require_relative 'parser'
|
16
|
+
require_relative 'version'
|
17
|
+
require_relative 'errors'
|
18
|
+
require_relative 'msg'
|
19
|
+
require_relative 'subscription'
|
20
|
+
require_relative 'jetstream'
|
21
|
+
|
17
22
|
require 'nats/nuid'
|
18
23
|
require 'thread'
|
19
24
|
require 'socket'
|
@@ -21,6 +26,7 @@ require 'json'
|
|
21
26
|
require 'monitor'
|
22
27
|
require 'uri'
|
23
28
|
require 'securerandom'
|
29
|
+
require 'concurrent'
|
24
30
|
|
25
31
|
begin
|
26
32
|
require "openssl"
|
@@ -29,43 +35,75 @@ end
|
|
29
35
|
|
30
36
|
module NATS
|
31
37
|
class << self
|
38
|
+
# NATS.connect creates a connection to the NATS Server.
|
39
|
+
# @param uri [String] URL endpoint of the NATS Server or cluster.
|
40
|
+
# @param opts [Hash] Options to customize the NATS connection.
|
41
|
+
# @return [NATS::Client]
|
42
|
+
#
|
43
|
+
# @example
|
44
|
+
# require 'nats'
|
45
|
+
# nc = NATS.connect("demo.nats.io")
|
46
|
+
# nc.publish("hello", "world")
|
47
|
+
# nc.close
|
48
|
+
#
|
32
49
|
def connect(uri=nil, opts={})
|
33
|
-
nc = NATS::
|
50
|
+
nc = NATS::Client.new
|
34
51
|
nc.connect(uri, opts)
|
35
52
|
|
36
53
|
nc
|
37
54
|
end
|
38
55
|
end
|
39
56
|
|
40
|
-
|
57
|
+
# Status represents the different states from a NATS connection.
|
58
|
+
# A client starts from the DISCONNECTED state to CONNECTING during
|
59
|
+
# the initial connect, then CONNECTED. If the connection is reset
|
60
|
+
# then it goes from DISCONNECTED to RECONNECTING until it is back to
|
61
|
+
# the CONNECTED state. In case the client gives up reconnecting or
|
62
|
+
# the connection is manually closed then it will reach the CLOSED
|
63
|
+
# connection state after which it will not reconnect again.
|
64
|
+
module Status
|
65
|
+
# When the client is not actively connected.
|
66
|
+
DISCONNECTED = 0
|
41
67
|
|
42
|
-
|
43
|
-
|
68
|
+
# When the client is connected.
|
69
|
+
CONNECTED = 1
|
44
70
|
|
45
|
-
|
46
|
-
|
71
|
+
# When the client will no longer attempt to connect to a NATS Server.
|
72
|
+
CLOSED = 2
|
47
73
|
|
48
|
-
#
|
49
|
-
|
74
|
+
# When the client has disconnected and is attempting to reconnect.
|
75
|
+
RECONNECTING = 3
|
50
76
|
|
51
|
-
#
|
52
|
-
|
77
|
+
# When the client is attempting to connect to a NATS Server for the first time.
|
78
|
+
CONNECTING = 4
|
53
79
|
|
54
|
-
#
|
55
|
-
|
56
|
-
|
80
|
+
# When the client is draining a connection before closing.
|
81
|
+
DRAINING_SUBS = 5
|
82
|
+
DRAINING_PUBS = 6
|
83
|
+
end
|
57
84
|
|
58
|
-
|
59
|
-
|
60
|
-
|
85
|
+
# Fork Detection handling
|
86
|
+
# Based from similar approach as mperham/connection_pool: https://github.com/mperham/connection_pool/pull/166
|
87
|
+
if Process.respond_to?(:fork) && Process.respond_to?(:_fork) # MRI 3.1+
|
88
|
+
module ForkTracker
|
89
|
+
def _fork
|
90
|
+
super.tap do |pid|
|
91
|
+
Client.after_fork if pid.zero? # in the child process
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
Process.singleton_class.prepend(ForkTracker)
|
96
|
+
end
|
61
97
|
|
62
|
-
|
63
|
-
|
64
|
-
|
98
|
+
# Client creates a connection to the NATS Server.
|
99
|
+
class Client
|
100
|
+
include MonitorMixin
|
101
|
+
include Status
|
65
102
|
|
66
|
-
|
67
|
-
|
68
|
-
|
103
|
+
attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri, :subscription_executor, :reloader
|
104
|
+
|
105
|
+
DEFAULT_PORT = { nats: 4222, ws: 80, wss: 443 }.freeze
|
106
|
+
DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT[:nats]}".freeze)
|
69
107
|
|
70
108
|
CR_LF = ("\r\n".freeze)
|
71
109
|
CR_LF_SIZE = (CR_LF.bytesize)
|
@@ -82,1510 +120,1751 @@ module NATS
|
|
82
120
|
SUB_OP = ('SUB'.freeze)
|
83
121
|
EMPTY_MSG = (''.freeze)
|
84
122
|
|
85
|
-
#
|
86
|
-
|
87
|
-
CONNECTED = 1
|
88
|
-
CLOSED = 2
|
89
|
-
RECONNECTING = 3
|
90
|
-
CONNECTING = 4
|
91
|
-
|
92
|
-
class Error < StandardError; end
|
123
|
+
INSTANCES = ObjectSpace::WeakMap.new # tracks all alive client instances
|
124
|
+
private_constant :INSTANCES
|
93
125
|
|
94
|
-
|
95
|
-
|
126
|
+
class << self
|
127
|
+
# Reloader should free resources managed by external framework
|
128
|
+
# that were implicitly acquired in subscription callbacks.
|
129
|
+
attr_writer :default_reloader
|
96
130
|
|
97
|
-
|
98
|
-
|
131
|
+
def default_reloader
|
132
|
+
@default_reloader ||= proc { |&block| block.call }.tap { |r| Ractor.make_shareable(r) if defined? Ractor }
|
133
|
+
end
|
99
134
|
|
100
|
-
|
101
|
-
|
135
|
+
# Re-establish connection in a new process after forking to start new threads.
|
136
|
+
def after_fork
|
137
|
+
INSTANCES.each do |client|
|
138
|
+
if client.options[:reconnect]
|
139
|
+
was_connected = !client.disconnected?
|
140
|
+
client.send(:close_connection, Status::DISCONNECTED, true)
|
141
|
+
client.connect if was_connected
|
142
|
+
else
|
143
|
+
client.send(:err_cb_call, self, NATS::IO::ForkDetectedError, nil)
|
144
|
+
client.close
|
145
|
+
end
|
146
|
+
rescue => e
|
147
|
+
warn "nats: Error during handling after_fork callback: #{e}" # TODO: Report as async error via error callback?
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
102
151
|
|
103
|
-
|
104
|
-
|
152
|
+
def initialize(uri = nil, opts = {})
|
153
|
+
super() # required to initialize monitor
|
154
|
+
@initial_uri = uri
|
155
|
+
@initial_options = opts
|
105
156
|
|
106
|
-
|
107
|
-
|
157
|
+
# Read/Write IO
|
158
|
+
@io = nil
|
108
159
|
|
109
|
-
|
110
|
-
|
160
|
+
# Queues for coalescing writes of commands we need to send to server.
|
161
|
+
@flush_queue = nil
|
162
|
+
@pending_queue = nil
|
111
163
|
|
112
|
-
|
113
|
-
|
164
|
+
# Parser with state
|
165
|
+
@parser = NATS::Protocol::Parser.new(self)
|
114
166
|
|
115
|
-
|
116
|
-
|
167
|
+
# Threads for both reading and flushing command
|
168
|
+
@flusher_thread = nil
|
169
|
+
@read_loop_thread = nil
|
170
|
+
@ping_interval_thread = nil
|
117
171
|
|
118
|
-
|
119
|
-
|
172
|
+
# Info that we get from the server
|
173
|
+
@server_info = { }
|
120
174
|
|
121
|
-
|
122
|
-
|
175
|
+
# URI from server to which we are currently connected
|
176
|
+
@uri = nil
|
177
|
+
@server_pool = []
|
123
178
|
|
124
|
-
|
125
|
-
class SlowConsumer < Error; end
|
179
|
+
@status = nil
|
126
180
|
|
127
|
-
|
128
|
-
|
181
|
+
# Subscriptions
|
182
|
+
@subs = { }
|
183
|
+
@ssid = 0
|
129
184
|
|
130
|
-
|
185
|
+
# Ping interval
|
186
|
+
@pings_outstanding = 0
|
187
|
+
@pongs_received = 0
|
188
|
+
@pongs = []
|
189
|
+
@pongs.extend(MonitorMixin)
|
131
190
|
|
132
|
-
|
133
|
-
|
134
|
-
|
191
|
+
# Accounting
|
192
|
+
@pending_size = 0
|
193
|
+
@stats = {
|
194
|
+
in_msgs: 0,
|
195
|
+
out_msgs: 0,
|
196
|
+
in_bytes: 0,
|
197
|
+
out_bytes: 0,
|
198
|
+
reconnects: 0
|
199
|
+
}
|
135
200
|
|
136
|
-
|
137
|
-
|
201
|
+
# Sticky error
|
202
|
+
@last_err = nil
|
138
203
|
|
139
|
-
|
140
|
-
|
141
|
-
|
204
|
+
# Async callbacks, no ops by default.
|
205
|
+
@err_cb = proc { }
|
206
|
+
@close_cb = proc { }
|
207
|
+
@disconnect_cb = proc { }
|
208
|
+
@reconnect_cb = proc { }
|
142
209
|
|
143
|
-
|
144
|
-
|
210
|
+
# Secure TLS options
|
211
|
+
@tls = nil
|
145
212
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
213
|
+
# Hostname of current server; used for when TLS host
|
214
|
+
# verification is enabled.
|
215
|
+
@hostname = nil
|
216
|
+
@single_url_connect_used = false
|
150
217
|
|
151
|
-
|
152
|
-
|
218
|
+
# Track whether connect has been already been called.
|
219
|
+
@connect_called = false
|
153
220
|
|
154
|
-
|
155
|
-
|
156
|
-
|
221
|
+
# New style request/response implementation.
|
222
|
+
@resp_sub = nil
|
223
|
+
@resp_map = nil
|
224
|
+
@resp_sub_prefix = nil
|
225
|
+
@nuid = NATS::NUID.new
|
157
226
|
|
158
|
-
|
227
|
+
# NKEYS
|
228
|
+
@user_credentials = nil
|
229
|
+
@nkeys_seed = nil
|
230
|
+
@user_nkey_cb = nil
|
231
|
+
@user_jwt_cb = nil
|
232
|
+
@signature_cb = nil
|
159
233
|
|
160
|
-
|
161
|
-
|
162
|
-
@ssid = 0
|
163
|
-
|
164
|
-
# Ping interval
|
165
|
-
@pings_outstanding = 0
|
166
|
-
@pongs_received = 0
|
167
|
-
@pongs = []
|
168
|
-
@pongs.extend(MonitorMixin)
|
169
|
-
|
170
|
-
# Accounting
|
171
|
-
@pending_size = 0
|
172
|
-
@stats = {
|
173
|
-
in_msgs: 0,
|
174
|
-
out_msgs: 0,
|
175
|
-
in_bytes: 0,
|
176
|
-
out_bytes: 0,
|
177
|
-
reconnects: 0
|
178
|
-
}
|
234
|
+
# Tokens
|
235
|
+
@auth_token = nil
|
179
236
|
|
180
|
-
|
181
|
-
@last_err = nil
|
237
|
+
@inbox_prefix = "_INBOX"
|
182
238
|
|
183
|
-
|
184
|
-
|
185
|
-
@close_cb = proc { }
|
186
|
-
@disconnect_cb = proc { }
|
187
|
-
@reconnect_cb = proc { }
|
239
|
+
# Draining
|
240
|
+
@drain_t = nil
|
188
241
|
|
189
|
-
|
190
|
-
|
242
|
+
# Prepare for calling connect or automatic delayed connection
|
243
|
+
parse_and_validate_options if uri || opts.any?
|
191
244
|
|
192
|
-
|
193
|
-
|
194
|
-
@hostname = nil
|
195
|
-
@single_url_connect_used = false
|
245
|
+
# Keep track of all client instances to handle them after process forking in Ruby 3.1+
|
246
|
+
INSTANCES[self] = self if !defined?(Ractor) || Ractor.current == Ractor.main # Ractors doesn't work in forked processes
|
196
247
|
|
197
|
-
|
198
|
-
|
248
|
+
@reloader = opts.fetch(:reloader, self.class.default_reloader)
|
249
|
+
end
|
199
250
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
@
|
204
|
-
@
|
251
|
+
# Prepare connecting to NATS, but postpone real connection until first usage.
|
252
|
+
def connect(uri=nil, opts={})
|
253
|
+
if uri || opts.any?
|
254
|
+
@initial_uri = uri
|
255
|
+
@initial_options = opts
|
256
|
+
end
|
205
257
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
@
|
210
|
-
@user_jwt_cb = nil
|
211
|
-
@signature_cb = nil
|
258
|
+
synchronize do
|
259
|
+
# In case it has been connected already, then do not need to call this again.
|
260
|
+
return if @connect_called
|
261
|
+
@connect_called = true
|
212
262
|
end
|
213
263
|
|
214
|
-
|
215
|
-
|
216
|
-
synchronize do
|
217
|
-
# In case it has been connected already, then do not need to call this again.
|
218
|
-
return if @connect_called
|
219
|
-
@connect_called = true
|
220
|
-
end
|
264
|
+
parse_and_validate_options
|
265
|
+
establish_connection!
|
221
266
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
case uri
|
226
|
-
when String
|
227
|
-
# Initialize TLS defaults in case any url is using it.
|
228
|
-
srvs = opts[:servers] = process_uri(uri)
|
229
|
-
if srvs.any? {|u| u.scheme == 'tls'} and !opts[:tls]
|
230
|
-
tls_context = OpenSSL::SSL::SSLContext.new
|
231
|
-
tls_context.set_params
|
232
|
-
opts[:tls] = {
|
233
|
-
context: tls_context
|
234
|
-
}
|
235
|
-
end
|
236
|
-
@single_url_connect_used = true if srvs.size == 1
|
237
|
-
when Hash
|
238
|
-
opts = uri
|
239
|
-
end
|
267
|
+
self
|
268
|
+
end
|
240
269
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
URI.parse(u)
|
270
|
-
end
|
271
|
-
@server_pool << {
|
272
|
-
:uri => nats_uri,
|
273
|
-
:hostname => nats_uri.host
|
274
|
-
}
|
270
|
+
private def parse_and_validate_options
|
271
|
+
# Reset these in case we have reconnected via fork.
|
272
|
+
@server_pool = []
|
273
|
+
@resp_sub = nil
|
274
|
+
@resp_map = nil
|
275
|
+
@resp_sub_prefix = nil
|
276
|
+
@nuid = NATS::NUID.new
|
277
|
+
@stats = {
|
278
|
+
in_msgs: 0,
|
279
|
+
out_msgs: 0,
|
280
|
+
in_bytes: 0,
|
281
|
+
out_bytes: 0,
|
282
|
+
reconnects: 0
|
283
|
+
}
|
284
|
+
@status = DISCONNECTED
|
285
|
+
|
286
|
+
# Convert URI to string if needed.
|
287
|
+
uri = @initial_uri.dup
|
288
|
+
uri = uri.to_s if uri.is_a?(URI)
|
289
|
+
|
290
|
+
opts = @initial_options.dup
|
291
|
+
|
292
|
+
case uri
|
293
|
+
when String
|
294
|
+
# Initialize TLS defaults in case any url is using it.
|
295
|
+
srvs = opts[:servers] = process_uri(uri)
|
296
|
+
if srvs.any? {|u| %w[tls wss].include? u.scheme } and !opts[:tls]
|
297
|
+
opts[:tls] = { context: tls_context }
|
275
298
|
end
|
299
|
+
@single_url_connect_used = true if srvs.size == 1
|
300
|
+
when Hash
|
301
|
+
opts = uri
|
302
|
+
end
|
276
303
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
304
|
+
opts[:verbose] = false if opts[:verbose].nil?
|
305
|
+
opts[:pedantic] = false if opts[:pedantic].nil?
|
306
|
+
opts[:reconnect] = true if opts[:reconnect].nil?
|
307
|
+
opts[:old_style_request] = false if opts[:old_style_request].nil?
|
308
|
+
opts[:ignore_discovered_urls] = false if opts[:ignore_discovered_urls].nil?
|
309
|
+
opts[:reconnect_time_wait] = NATS::IO::RECONNECT_TIME_WAIT if opts[:reconnect_time_wait].nil?
|
310
|
+
opts[:max_reconnect_attempts] = NATS::IO::MAX_RECONNECT_ATTEMPTS if opts[:max_reconnect_attempts].nil?
|
311
|
+
opts[:ping_interval] = NATS::IO::DEFAULT_PING_INTERVAL if opts[:ping_interval].nil?
|
312
|
+
opts[:max_outstanding_pings] = NATS::IO::DEFAULT_PING_MAX if opts[:max_outstanding_pings].nil?
|
313
|
+
|
314
|
+
# Override with ENV
|
315
|
+
opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil?
|
316
|
+
opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil?
|
317
|
+
opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil?
|
318
|
+
opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil?
|
319
|
+
opts[:ignore_discovered_urls] = ENV['NATS_IGNORE_DISCOVERED_URLS'].downcase == 'true' unless ENV['NATS_IGNORE_DISCOVERED_URLS'].nil?
|
320
|
+
opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil?
|
321
|
+
opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil?
|
322
|
+
opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil?
|
323
|
+
opts[:connect_timeout] ||= NATS::IO::DEFAULT_CONNECT_TIMEOUT
|
324
|
+
opts[:drain_timeout] ||= NATS::IO::DEFAULT_DRAIN_TIMEOUT
|
325
|
+
@options = opts
|
326
|
+
|
327
|
+
# Process servers in the NATS cluster and pick one to connect
|
328
|
+
uris = opts[:servers] || [DEFAULT_URI]
|
329
|
+
uris.shuffle! unless @options[:dont_randomize_servers]
|
330
|
+
uris.each do |u|
|
331
|
+
nats_uri = case u
|
332
|
+
when URI
|
333
|
+
u.dup
|
334
|
+
else
|
335
|
+
URI.parse(u)
|
336
|
+
end
|
337
|
+
@server_pool << {
|
338
|
+
:uri => nats_uri,
|
339
|
+
:hostname => nats_uri.hostname
|
340
|
+
}
|
341
|
+
end
|
282
342
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
343
|
+
if @options[:old_style_request]
|
344
|
+
# Replace for this instance the implementation
|
345
|
+
# of request to use the old_request style.
|
346
|
+
class << self; alias_method :request, :old_request; end
|
347
|
+
end
|
287
348
|
|
288
|
-
|
289
|
-
|
349
|
+
# NKEYS
|
350
|
+
@signature_cb ||= opts[:user_signature_cb]
|
351
|
+
@user_jwt_cb ||= opts[:user_jwt_cb]
|
352
|
+
@user_nkey_cb ||= opts[:user_nkey_cb]
|
353
|
+
@user_credentials ||= opts[:user_credentials]
|
354
|
+
@nkeys_seed ||= opts[:nkeys_seed]
|
290
355
|
|
291
|
-
|
292
|
-
begin
|
293
|
-
srv = select_next_server
|
356
|
+
setup_nkeys_connect if @user_credentials or @nkeys_seed
|
294
357
|
|
295
|
-
|
296
|
-
|
297
|
-
@io.connect
|
358
|
+
# Tokens, if set will take preference over the user@server uri token
|
359
|
+
@auth_token ||= opts[:auth_token]
|
298
360
|
|
299
|
-
|
300
|
-
|
301
|
-
srv[:was_connected] = true
|
361
|
+
# Check for TLS usage
|
362
|
+
@tls = @options[:tls]
|
302
363
|
|
303
|
-
|
304
|
-
@status = CONNECTING
|
364
|
+
@inbox_prefix = opts.fetch(:custom_inbox_prefix, @inbox_prefix)
|
305
365
|
|
306
|
-
|
307
|
-
if client_using_secure_connection? and single_url_connect_used?
|
308
|
-
# Always reuse the original hostname used to connect.
|
309
|
-
@hostname ||= srv[:hostname]
|
310
|
-
else
|
311
|
-
@hostname = srv[:hostname]
|
312
|
-
end
|
366
|
+
validate_settings!
|
313
367
|
|
314
|
-
|
315
|
-
|
368
|
+
self
|
369
|
+
end
|
316
370
|
|
317
|
-
|
318
|
-
|
319
|
-
srv[:auth_required] ||= true if @server_info[:auth_required]
|
371
|
+
private def establish_connection!
|
372
|
+
@ruby_pid = Process.pid # For fork detection
|
320
373
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
@disconnect_cb.call(e) if @disconnect_cb
|
325
|
-
raise @last_err || e
|
326
|
-
rescue => e
|
327
|
-
# Capture sticky error
|
328
|
-
synchronize do
|
329
|
-
@last_err = e
|
330
|
-
srv[:auth_required] ||= true if @server_info[:auth_required]
|
331
|
-
server_pool << srv if can_reuse_server?(srv)
|
332
|
-
end
|
374
|
+
srv = nil
|
375
|
+
begin
|
376
|
+
srv = select_next_server
|
333
377
|
|
334
|
-
|
378
|
+
# Use the hostname from the server for TLS hostname verification.
|
379
|
+
if client_using_secure_connection? and single_url_connect_used?
|
380
|
+
# Always reuse the original hostname used to connect.
|
381
|
+
@hostname ||= srv[:hostname]
|
382
|
+
else
|
383
|
+
@hostname = srv[:hostname]
|
384
|
+
end
|
335
385
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
end
|
386
|
+
# Create TCP socket connection to NATS.
|
387
|
+
@io = create_socket
|
388
|
+
@io.connect
|
340
389
|
|
341
|
-
|
342
|
-
|
343
|
-
|
390
|
+
# Capture state that we have had a TCP connection established against
|
391
|
+
# this server and could potentially be used for reconnecting.
|
392
|
+
srv[:was_connected] = true
|
344
393
|
|
345
|
-
|
346
|
-
|
347
|
-
sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
|
394
|
+
# Connection established and now in process of sending CONNECT to NATS
|
395
|
+
@status = CONNECTING
|
348
396
|
|
349
|
-
|
350
|
-
|
397
|
+
# Established TCP connection successfully so can start connect
|
398
|
+
process_connect_init
|
399
|
+
|
400
|
+
# Reset reconnection attempts if connection is valid
|
401
|
+
srv[:reconnect_attempts] = 0
|
402
|
+
srv[:auth_required] ||= true if @server_info[:auth_required]
|
403
|
+
|
404
|
+
# Add back to rotation since successfully connected
|
405
|
+
server_pool << srv
|
406
|
+
rescue NATS::IO::NoServersError => e
|
407
|
+
@disconnect_cb.call(e) if @disconnect_cb
|
408
|
+
raise @last_err || e
|
409
|
+
rescue => e
|
410
|
+
# Capture sticky error
|
411
|
+
synchronize do
|
412
|
+
@last_err = e
|
413
|
+
srv[:auth_required] ||= true if @server_info[:auth_required]
|
414
|
+
server_pool << srv if can_reuse_server?(srv)
|
351
415
|
end
|
352
416
|
|
353
|
-
|
354
|
-
@flush_queue = SizedQueue.new(MAX_FLUSH_KICK_SIZE)
|
355
|
-
@pending_queue = SizedQueue.new(MAX_PENDING_SIZE)
|
356
|
-
@pings_outstanding = 0
|
357
|
-
@pongs_received = 0
|
358
|
-
@pending_size = 0
|
417
|
+
err_cb_call(self, e, nil) if @err_cb
|
359
418
|
|
360
|
-
|
361
|
-
|
419
|
+
if should_not_reconnect?
|
420
|
+
@disconnect_cb.call(e) if @disconnect_cb
|
421
|
+
raise e
|
422
|
+
end
|
423
|
+
|
424
|
+
# Clean up any connecting state and close connection without
|
425
|
+
# triggering the disconnection/closed callbacks.
|
426
|
+
close_connection(DISCONNECTED, false)
|
362
427
|
|
363
|
-
#
|
364
|
-
|
428
|
+
# Always sleep here to safe guard against errors before current[:was_connected]
|
429
|
+
# is set for the first time.
|
430
|
+
sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
|
431
|
+
|
432
|
+
# Continue retrying until there are no options left in the server pool
|
433
|
+
retry
|
365
434
|
end
|
366
435
|
|
367
|
-
|
368
|
-
|
369
|
-
|
436
|
+
# Initialize queues and loops for message dispatching and processing engine
|
437
|
+
@flush_queue = SizedQueue.new(NATS::IO::MAX_FLUSH_KICK_SIZE)
|
438
|
+
@pending_queue = SizedQueue.new(NATS::IO::MAX_PENDING_SIZE)
|
439
|
+
@pings_outstanding = 0
|
440
|
+
@pongs_received = 0
|
441
|
+
@pending_size = 0
|
442
|
+
|
443
|
+
# Server roundtrip went ok so consider to be connected at this point
|
444
|
+
@status = CONNECTED
|
370
445
|
|
371
|
-
|
372
|
-
|
373
|
-
@stats[:out_bytes] += msg_size
|
446
|
+
# Connected to NATS so Ready to start parser loop, flusher and ping interval
|
447
|
+
start_threads!
|
374
448
|
|
375
|
-
|
376
|
-
|
449
|
+
self
|
450
|
+
end
|
451
|
+
|
452
|
+
def publish(subject, msg=EMPTY_MSG, opt_reply=nil, **options, &blk)
|
453
|
+
raise NATS::IO::BadSubject if !subject or subject.empty?
|
454
|
+
if options[:header]
|
455
|
+
return publish_msg(NATS::Msg.new(subject: subject, data: msg, reply: opt_reply, header: options[:header]))
|
377
456
|
end
|
378
457
|
|
379
|
-
#
|
380
|
-
|
381
|
-
|
382
|
-
|
458
|
+
# Accounting
|
459
|
+
msg_size = msg.bytesize
|
460
|
+
@stats[:out_msgs] += 1
|
461
|
+
@stats[:out_bytes] += msg_size
|
383
462
|
|
384
|
-
|
385
|
-
|
386
|
-
|
463
|
+
send_command("PUB #{subject} #{opt_reply} #{msg_size}\r\n#{msg}\r\n")
|
464
|
+
@flush_queue << :pub if @flush_queue.empty?
|
465
|
+
end
|
387
466
|
|
388
|
-
|
389
|
-
|
390
|
-
|
467
|
+
# Publishes a NATS::Msg that may include headers.
|
468
|
+
def publish_msg(msg)
|
469
|
+
raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
|
470
|
+
raise NATS::IO::BadSubject if !msg.subject or msg.subject.empty?
|
391
471
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
msg.header.each do |k, v|
|
396
|
-
hdr << "#{k}: #{v}#{CR_LF}"
|
397
|
-
end
|
398
|
-
hdr << CR_LF
|
399
|
-
hdr_len = hdr.bytesize
|
400
|
-
total_size = msg_size + hdr_len
|
401
|
-
send_command("HPUB #{msg.subject} #{msg.reply} #{hdr_len} #{total_size}\r\n#{hdr}#{msg.data}\r\n")
|
402
|
-
else
|
403
|
-
send_command("PUB #{msg.subject} #{msg.reply} #{msg_size}\r\n#{msg.data}\r\n")
|
404
|
-
end
|
472
|
+
msg.reply ||= ''
|
473
|
+
msg.data ||= ''
|
474
|
+
msg_size = msg.data.bytesize
|
405
475
|
|
406
|
-
|
407
|
-
|
476
|
+
# Accounting
|
477
|
+
@stats[:out_msgs] += 1
|
478
|
+
@stats[:out_bytes] += msg_size
|
408
479
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
synchronize do
|
415
|
-
sid = (@ssid += 1)
|
416
|
-
sub = @subs[sid] = Subscription.new
|
480
|
+
if msg.header
|
481
|
+
hdr = ''
|
482
|
+
hdr << NATS_HDR_LINE
|
483
|
+
msg.header.each do |k, v|
|
484
|
+
hdr << "#{k}: #{v}#{CR_LF}"
|
417
485
|
end
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
sub.max = opts[:max] if opts[:max]
|
426
|
-
sub.pending_msgs_limit = opts[:pending_msgs_limit]
|
427
|
-
sub.pending_bytes_limit = opts[:pending_bytes_limit]
|
428
|
-
sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
|
429
|
-
|
430
|
-
send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
|
431
|
-
@flush_queue << :sub
|
432
|
-
|
433
|
-
# Setup server support for auto-unsubscribe when receiving enough messages
|
434
|
-
unsubscribe(sid, opts[:max]) if opts[:max]
|
435
|
-
|
436
|
-
# Async subscriptions each own a single thread for the
|
437
|
-
# delivery of messages.
|
438
|
-
# FIXME: Support shared thread pool with configurable limits
|
439
|
-
# to better support case of having a lot of subscriptions.
|
440
|
-
sub.wait_for_msgs_t = Thread.new do
|
441
|
-
loop do
|
442
|
-
msg = sub.pending_queue.pop
|
443
|
-
|
444
|
-
cb = nil
|
445
|
-
sub.synchronize do
|
446
|
-
# Decrease pending size since consumed already
|
447
|
-
sub.pending_size -= msg.data.size
|
448
|
-
cb = sub.callback
|
449
|
-
end
|
486
|
+
hdr << CR_LF
|
487
|
+
hdr_len = hdr.bytesize
|
488
|
+
total_size = msg_size + hdr_len
|
489
|
+
send_command("HPUB #{msg.subject} #{msg.reply} #{hdr_len} #{total_size}\r\n#{hdr}#{msg.data}\r\n")
|
490
|
+
else
|
491
|
+
send_command("PUB #{msg.subject} #{msg.reply} #{msg_size}\r\n#{msg.data}\r\n")
|
492
|
+
end
|
450
493
|
|
451
|
-
|
452
|
-
|
453
|
-
when 0 then cb.call
|
454
|
-
when 1 then cb.call(msg.data)
|
455
|
-
when 2 then cb.call(msg.data, msg.reply)
|
456
|
-
when 3 then cb.call(msg.data, msg.reply, msg.subject)
|
457
|
-
else cb.call(msg.data, msg.reply, msg.subject, msg.header)
|
458
|
-
end
|
459
|
-
rescue => e
|
460
|
-
synchronize do
|
461
|
-
@err_cb.call(e) if @err_cb
|
462
|
-
end
|
463
|
-
end
|
464
|
-
end
|
465
|
-
end
|
494
|
+
@flush_queue << :pub if @flush_queue.empty?
|
495
|
+
end
|
466
496
|
|
467
|
-
|
497
|
+
# Create subscription which is dispatched asynchronously
|
498
|
+
# messages to a callback.
|
499
|
+
def subscribe(subject, opts={}, &callback)
|
500
|
+
raise NATS::IO::ConnectionDrainingError.new("nats: connection draining") if draining?
|
501
|
+
|
502
|
+
sid = nil
|
503
|
+
sub = nil
|
504
|
+
synchronize do
|
505
|
+
sid = (@ssid += 1)
|
506
|
+
sub = @subs[sid] = Subscription.new
|
507
|
+
sub.nc = self
|
508
|
+
sub.sid = sid
|
509
|
+
end
|
510
|
+
opts[:pending_msgs_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
|
511
|
+
opts[:pending_bytes_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
|
512
|
+
|
513
|
+
sub.subject = subject
|
514
|
+
sub.callback = callback
|
515
|
+
sub.received = 0
|
516
|
+
sub.queue = opts[:queue] if opts[:queue]
|
517
|
+
sub.max = opts[:max] if opts[:max]
|
518
|
+
sub.pending_msgs_limit = opts[:pending_msgs_limit]
|
519
|
+
sub.pending_bytes_limit = opts[:pending_bytes_limit]
|
520
|
+
sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
|
521
|
+
sub.processing_concurrency = opts[:processing_concurrency] if opts.key?(:processing_concurrency)
|
522
|
+
|
523
|
+
send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
|
524
|
+
@flush_queue << :sub
|
525
|
+
|
526
|
+
# Setup server support for auto-unsubscribe when receiving enough messages
|
527
|
+
sub.unsubscribe(opts[:max]) if opts[:max]
|
528
|
+
|
529
|
+
unless callback
|
530
|
+
cond = sub.new_cond
|
531
|
+
sub.wait_for_msgs_cond = cond
|
468
532
|
end
|
469
533
|
|
470
|
-
|
471
|
-
|
472
|
-
# It times out in case the request is not retrieved within the
|
473
|
-
# specified deadline.
|
474
|
-
# If given a callback, then the request happens asynchronously.
|
475
|
-
def request(subject, payload="", opts={}, &blk)
|
476
|
-
raise BadSubject if !subject or subject.empty?
|
534
|
+
sub
|
535
|
+
end
|
477
536
|
|
478
|
-
|
479
|
-
|
480
|
-
|
537
|
+
# Sends a request using expecting a single response using a
|
538
|
+
# single subscription per connection for receiving the responses.
|
539
|
+
# It times out in case the request is not retrieved within the
|
540
|
+
# specified deadline.
|
541
|
+
# If given a callback, then the request happens asynchronously.
|
542
|
+
def request(subject, payload="", **opts, &blk)
|
543
|
+
raise NATS::IO::BadSubject if !subject or subject.empty?
|
481
544
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
response = nil
|
486
|
-
timeout = opts[:timeout] ||= 0.5
|
487
|
-
synchronize do
|
488
|
-
start_resp_mux_sub! unless @resp_sub_prefix
|
545
|
+
# If a block was given then fallback to method using auto unsubscribe.
|
546
|
+
return old_request(subject, payload, opts, &blk) if blk
|
547
|
+
return old_request(subject, payload, opts) if opts[:old_style]
|
489
548
|
|
490
|
-
|
491
|
-
|
492
|
-
|
549
|
+
if opts[:header]
|
550
|
+
return request_msg(NATS::Msg.new(subject: subject, data: payload, header: opts[:header]), **opts)
|
551
|
+
end
|
493
552
|
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
553
|
+
token = nil
|
554
|
+
inbox = nil
|
555
|
+
future = nil
|
556
|
+
response = nil
|
557
|
+
timeout = opts[:timeout] ||= 0.5
|
558
|
+
synchronize do
|
559
|
+
start_resp_mux_sub! unless @resp_sub_prefix
|
560
|
+
|
561
|
+
# Create token for this request.
|
562
|
+
token = @nuid.next
|
563
|
+
inbox = "#{@resp_sub_prefix}.#{token}"
|
564
|
+
|
565
|
+
# Create the a future for the request that will
|
566
|
+
# get signaled when it receives the request.
|
567
|
+
future = @resp_sub.new_cond
|
568
|
+
@resp_map[token][:future] = future
|
569
|
+
end
|
499
570
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
end
|
571
|
+
# Publish request and wait for reply.
|
572
|
+
publish(subject, payload, inbox)
|
573
|
+
begin
|
574
|
+
MonotonicTime::with_nats_timeout(timeout) do
|
575
|
+
@resp_sub.synchronize do
|
576
|
+
future.wait(timeout)
|
507
577
|
end
|
508
|
-
rescue NATS::IO::Timeout => e
|
509
|
-
synchronize { @resp_map.delete(token) }
|
510
|
-
raise e
|
511
578
|
end
|
579
|
+
rescue NATS::Timeout => e
|
580
|
+
synchronize { @resp_map.delete(token) }
|
581
|
+
raise e
|
582
|
+
end
|
512
583
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
584
|
+
# Check if there is a response already.
|
585
|
+
synchronize do
|
586
|
+
result = @resp_map[token]
|
587
|
+
response = result[:response]
|
588
|
+
@resp_map.delete(token)
|
589
|
+
end
|
590
|
+
|
591
|
+
if response and response.header
|
592
|
+
status = response.header[STATUS_HDR]
|
593
|
+
raise NATS::IO::NoRespondersError if status == "503"
|
594
|
+
end
|
595
|
+
|
596
|
+
response
|
597
|
+
end
|
598
|
+
|
599
|
+
# request_msg makes a NATS request using a NATS::Msg that may include headers.
|
600
|
+
def request_msg(msg, **opts)
|
601
|
+
raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
|
602
|
+
raise NATS::IO::BadSubject if !msg.subject or msg.subject.empty?
|
603
|
+
|
604
|
+
token = nil
|
605
|
+
inbox = nil
|
606
|
+
future = nil
|
607
|
+
response = nil
|
608
|
+
timeout = opts[:timeout] ||= 0.5
|
609
|
+
synchronize do
|
610
|
+
start_resp_mux_sub! unless @resp_sub_prefix
|
611
|
+
|
612
|
+
# Create token for this request.
|
613
|
+
token = @nuid.next
|
614
|
+
inbox = "#{@resp_sub_prefix}.#{token}"
|
615
|
+
|
616
|
+
# Create the a future for the request that will
|
617
|
+
# get signaled when it receives the request.
|
618
|
+
future = @resp_sub.new_cond
|
619
|
+
@resp_map[token][:future] = future
|
620
|
+
end
|
621
|
+
msg.reply = inbox
|
622
|
+
msg.data ||= ''
|
623
|
+
msg_size = msg.data.bytesize
|
624
|
+
|
625
|
+
# Publish request and wait for reply.
|
626
|
+
publish_msg(msg)
|
627
|
+
begin
|
628
|
+
MonotonicTime::with_nats_timeout(timeout) do
|
629
|
+
@resp_sub.synchronize do
|
630
|
+
future.wait(timeout)
|
631
|
+
end
|
518
632
|
end
|
633
|
+
rescue NATS::Timeout => e
|
634
|
+
synchronize { @resp_map.delete(token) }
|
635
|
+
raise e
|
636
|
+
end
|
637
|
+
|
638
|
+
# Check if there is a response already.
|
639
|
+
synchronize do
|
640
|
+
result = @resp_map[token]
|
641
|
+
response = result[:response]
|
642
|
+
@resp_map.delete(token)
|
643
|
+
end
|
644
|
+
|
645
|
+
if response and response.header
|
646
|
+
status = response.header[STATUS_HDR]
|
647
|
+
raise NATS::IO::NoRespondersError if status == "503"
|
648
|
+
end
|
649
|
+
|
650
|
+
response
|
651
|
+
end
|
519
652
|
|
520
|
-
|
521
|
-
|
522
|
-
|
653
|
+
# Sends a request creating an ephemeral subscription for the request,
|
654
|
+
# expecting a single response or raising a timeout in case the request
|
655
|
+
# is not retrieved within the specified deadline.
|
656
|
+
# If given a callback, then the request happens asynchronously.
|
657
|
+
def old_request(subject, payload, opts={}, &blk)
|
658
|
+
return unless subject
|
659
|
+
inbox = new_inbox
|
660
|
+
|
661
|
+
# If a callback was passed, then have it process
|
662
|
+
# the messages asynchronously and return the sid.
|
663
|
+
if blk
|
664
|
+
opts[:max] ||= 1
|
665
|
+
s = subscribe(inbox, opts) do |msg|
|
666
|
+
case blk.arity
|
667
|
+
when 0 then blk.call
|
668
|
+
when 1 then blk.call(msg)
|
669
|
+
when 2 then blk.call(msg.data, msg.reply)
|
670
|
+
when 3 then blk.call(msg.data, msg.reply, msg.subject)
|
671
|
+
else blk.call(msg.data, msg.reply, msg.subject, msg.header)
|
672
|
+
end
|
523
673
|
end
|
674
|
+
publish(subject, payload, inbox)
|
524
675
|
|
525
|
-
|
676
|
+
return s
|
526
677
|
end
|
527
678
|
|
528
|
-
#
|
529
|
-
|
530
|
-
|
531
|
-
|
679
|
+
# In case block was not given, handle synchronously
|
680
|
+
# with a timeout and only allow a single response.
|
681
|
+
timeout = opts[:timeout] ||= 0.5
|
682
|
+
opts[:max] = 1
|
532
683
|
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
start_resp_mux_sub! unless @resp_sub_prefix
|
684
|
+
sub = Subscription.new
|
685
|
+
sub.subject = inbox
|
686
|
+
sub.received = 0
|
687
|
+
future = sub.new_cond
|
688
|
+
sub.future = future
|
689
|
+
sub.nc = self
|
540
690
|
|
541
|
-
|
542
|
-
|
543
|
-
|
691
|
+
sid = nil
|
692
|
+
synchronize do
|
693
|
+
sid = (@ssid += 1)
|
694
|
+
sub.sid = sid
|
695
|
+
@subs[sid] = sub
|
696
|
+
end
|
544
697
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
@resp_map[token][:future] = future
|
549
|
-
end
|
550
|
-
msg.reply = inbox
|
551
|
-
msg.data ||= ''
|
552
|
-
msg_size = msg.data.bytesize
|
698
|
+
send_command("SUB #{inbox} #{sid}#{CR_LF}")
|
699
|
+
@flush_queue << :sub
|
700
|
+
unsubscribe(sub, 1)
|
553
701
|
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
with_nats_timeout(timeout) do
|
558
|
-
@resp_sub.synchronize do
|
559
|
-
future.wait(timeout)
|
560
|
-
end
|
561
|
-
end
|
562
|
-
rescue NATS::IO::Timeout => e
|
563
|
-
synchronize { @resp_map.delete(token) }
|
564
|
-
raise e
|
565
|
-
end
|
702
|
+
sub.synchronize do
|
703
|
+
# Publish the request and then wait for the response...
|
704
|
+
publish(subject, payload, inbox)
|
566
705
|
|
567
|
-
|
568
|
-
|
569
|
-
result = @resp_map[token]
|
570
|
-
response = result[:response]
|
571
|
-
@resp_map.delete(token)
|
706
|
+
MonotonicTime::with_nats_timeout(timeout) do
|
707
|
+
future.wait(timeout)
|
572
708
|
end
|
709
|
+
end
|
710
|
+
response = sub.response
|
573
711
|
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
712
|
+
if response and response.header
|
713
|
+
status = response.header[STATUS_HDR]
|
714
|
+
raise NATS::IO::NoRespondersError if status == "503"
|
715
|
+
end
|
578
716
|
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
# Sends a request creating an ephemeral subscription for the request,
|
583
|
-
# expecting a single response or raising a timeout in case the request
|
584
|
-
# is not retrieved within the specified deadline.
|
585
|
-
# If given a callback, then the request happens asynchronously.
|
586
|
-
def old_request(subject, payload, opts={}, &blk)
|
587
|
-
return unless subject
|
588
|
-
inbox = new_inbox
|
589
|
-
|
590
|
-
# If a callback was passed, then have it process
|
591
|
-
# the messages asynchronously and return the sid.
|
592
|
-
if blk
|
593
|
-
opts[:max] ||= 1
|
594
|
-
s = subscribe(inbox, opts) do |msg, reply, subject, header|
|
595
|
-
case blk.arity
|
596
|
-
when 0 then blk.call
|
597
|
-
when 1 then blk.call(msg)
|
598
|
-
when 2 then blk.call(msg, reply)
|
599
|
-
when 3 then blk.call(msg, reply, subject)
|
600
|
-
else blk.call(msg, reply, subject, header)
|
601
|
-
end
|
602
|
-
end
|
603
|
-
publish(subject, payload, inbox)
|
717
|
+
response
|
718
|
+
end
|
604
719
|
|
605
|
-
|
720
|
+
# Send a ping and wait for a pong back within a timeout.
|
721
|
+
def flush(timeout=10)
|
722
|
+
# Schedule sending a PING, and block until we receive PONG back,
|
723
|
+
# or raise a timeout in case the response is past the deadline.
|
724
|
+
pong = @pongs.new_cond
|
725
|
+
@pongs.synchronize do
|
726
|
+
@pongs << pong
|
727
|
+
|
728
|
+
# Flush once pong future has been prepared
|
729
|
+
@pending_queue << PING_REQUEST
|
730
|
+
@flush_queue << :ping
|
731
|
+
MonotonicTime::with_nats_timeout(timeout) do
|
732
|
+
pong.wait(timeout)
|
606
733
|
end
|
734
|
+
end
|
735
|
+
end
|
607
736
|
|
608
|
-
|
609
|
-
# with a timeout and only allow a single response.
|
610
|
-
timeout = opts[:timeout] ||= 0.5
|
611
|
-
opts[:max] = 1
|
737
|
+
alias :servers :server_pool
|
612
738
|
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
739
|
+
# discovered_servers returns the NATS Servers that have been discovered
|
740
|
+
# via INFO protocol updates.
|
741
|
+
def discovered_servers
|
742
|
+
servers.select {|s| s[:discovered] }
|
743
|
+
end
|
618
744
|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
745
|
+
# Close connection to NATS, flushing in case connection is alive
|
746
|
+
# and there are any pending messages, should not be used while
|
747
|
+
# holding the lock.
|
748
|
+
def close
|
749
|
+
close_connection(CLOSED, true)
|
750
|
+
end
|
624
751
|
|
625
|
-
|
626
|
-
|
627
|
-
|
752
|
+
# new_inbox returns a unique inbox used for subscriptions.
|
753
|
+
# @return [String]
|
754
|
+
def new_inbox
|
755
|
+
"#{@inbox_prefix}.#{@nuid.next}"
|
756
|
+
end
|
628
757
|
|
629
|
-
|
630
|
-
|
631
|
-
|
758
|
+
def connected_server
|
759
|
+
connected? ? @uri : nil
|
760
|
+
end
|
632
761
|
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
end
|
637
|
-
response = sub.response
|
762
|
+
def disconnected?
|
763
|
+
!@status or @status == DISCONNECTED
|
764
|
+
end
|
638
765
|
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
end
|
766
|
+
def connected?
|
767
|
+
@status == CONNECTED
|
768
|
+
end
|
643
769
|
|
644
|
-
|
645
|
-
|
770
|
+
def connecting?
|
771
|
+
@status == CONNECTING
|
772
|
+
end
|
646
773
|
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
opt_max_str = " #{opt_max}" unless opt_max.nil?
|
651
|
-
send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}")
|
652
|
-
@flush_queue << :unsub
|
774
|
+
def reconnecting?
|
775
|
+
@status == RECONNECTING
|
776
|
+
end
|
653
777
|
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
@subs.delete(sid) unless (sub.max && (sub.received < sub.max))
|
778
|
+
def closed?
|
779
|
+
@status == CLOSED
|
780
|
+
end
|
658
781
|
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
sub.pending_queue.clear
|
663
|
-
end
|
664
|
-
end
|
782
|
+
def draining?
|
783
|
+
if @status == DRAINING_PUBS or @status == DRAINING_SUBS
|
784
|
+
return true
|
665
785
|
end
|
666
786
|
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
# or raise a timeout in case the response is past the deadline.
|
671
|
-
pong = @pongs.new_cond
|
672
|
-
@pongs.synchronize do
|
673
|
-
@pongs << pong
|
674
|
-
|
675
|
-
# Flush once pong future has been prepared
|
676
|
-
@pending_queue << PING_REQUEST
|
677
|
-
@flush_queue << :ping
|
678
|
-
with_nats_timeout(timeout) do
|
679
|
-
pong.wait(timeout)
|
680
|
-
end
|
681
|
-
end
|
787
|
+
is_draining = false
|
788
|
+
synchronize do
|
789
|
+
is_draining = true if @drain_t
|
682
790
|
end
|
683
791
|
|
684
|
-
|
792
|
+
is_draining
|
793
|
+
end
|
685
794
|
|
686
|
-
|
687
|
-
|
688
|
-
|
795
|
+
def on_error(&callback)
|
796
|
+
@err_cb = callback
|
797
|
+
end
|
689
798
|
|
690
|
-
|
799
|
+
def on_disconnect(&callback)
|
800
|
+
@disconnect_cb = callback
|
801
|
+
end
|
691
802
|
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
803
|
+
def on_reconnect(&callback)
|
804
|
+
@reconnect_cb = callback
|
805
|
+
end
|
806
|
+
|
807
|
+
def on_close(&callback)
|
808
|
+
@close_cb = callback
|
809
|
+
end
|
810
|
+
|
811
|
+
def last_error
|
812
|
+
synchronize do
|
813
|
+
@last_err
|
700
814
|
end
|
815
|
+
end
|
701
816
|
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
817
|
+
# drain will put a connection into a drain state. All subscriptions will
|
818
|
+
# immediately be put into a drain state. Upon completion, the publishers
|
819
|
+
# will be drained and can not publish any additional messages. Upon draining
|
820
|
+
# of the publishers, the connection will be closed. Use the `on_close`
|
821
|
+
# callback option to know when the connection has moved from draining to closed.
|
822
|
+
def drain
|
823
|
+
return if draining?
|
824
|
+
|
825
|
+
synchronize do
|
826
|
+
@drain_t ||= Thread.new { do_drain }
|
708
827
|
end
|
828
|
+
end
|
709
829
|
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
830
|
+
# Create a JetStream context.
|
831
|
+
# @param opts [Hash] Options to customize the JetStream context.
|
832
|
+
# @option params [String] :prefix JetStream API prefix to use for the requests.
|
833
|
+
# @option params [String] :domain JetStream Domain to use for the requests.
|
834
|
+
# @option params [Float] :timeout Default timeout to use for JS requests.
|
835
|
+
# @return [NATS::JetStream]
|
836
|
+
def jetstream(opts={})
|
837
|
+
::NATS::JetStream.new(self, opts)
|
838
|
+
end
|
839
|
+
alias_method :JetStream, :jetstream
|
840
|
+
alias_method :jsm, :jetstream
|
841
|
+
|
842
|
+
private
|
843
|
+
|
844
|
+
def validate_settings!
|
845
|
+
raise(NATS::IO::ClientError, "custom inbox may not include '>'") if @inbox_prefix.include?(">")
|
846
|
+
raise(NATS::IO::ClientError, "custom inbox may not include '*'") if @inbox_prefix.include?("*")
|
847
|
+
raise(NATS::IO::ClientError, "custom inbox may not end in '.'") if @inbox_prefix.end_with?(".")
|
848
|
+
raise(NATS::IO::ClientError, "custom inbox may not begin with '.'") if @inbox_prefix.start_with?(".")
|
849
|
+
end
|
850
|
+
|
851
|
+
def process_info(line)
|
852
|
+
parsed_info = JSON.parse(line)
|
853
|
+
|
854
|
+
# INFO can be received asynchronously too,
|
855
|
+
# so has to be done under the lock.
|
856
|
+
synchronize do
|
857
|
+
# Symbolize keys from parsed info line
|
858
|
+
@server_info = parsed_info.reduce({}) do |info, (k,v)|
|
859
|
+
info[k.to_sym] = v
|
860
|
+
|
861
|
+
info
|
727
862
|
end
|
728
|
-
process_op_error(e)
|
729
|
-
end
|
730
863
|
|
731
|
-
|
732
|
-
@
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
if sub.max
|
746
|
-
case
|
747
|
-
when sub.received > sub.max
|
748
|
-
# Client side support in case server did not receive unsubscribe
|
749
|
-
unsubscribe(sid)
|
750
|
-
return
|
751
|
-
when sub.received == sub.max
|
752
|
-
# Cleanup here if we have hit the max..
|
753
|
-
synchronize { @subs.delete(sid) }
|
864
|
+
# Detect any announced server that we might not be aware of...
|
865
|
+
connect_urls = @server_info[:connect_urls]
|
866
|
+
if !@options[:ignore_discovered_urls] && connect_urls
|
867
|
+
srvs = []
|
868
|
+
connect_urls.each do |url|
|
869
|
+
# Use the same scheme as the currently in use URI.
|
870
|
+
scheme = @uri.scheme
|
871
|
+
u = URI.parse("#{scheme}://#{url}")
|
872
|
+
|
873
|
+
# Skip in case it is the current server which we already know
|
874
|
+
next if @uri.hostname == u.hostname && @uri.port == u.port
|
875
|
+
|
876
|
+
present = server_pool.detect do |srv|
|
877
|
+
srv[:uri].hostname == u.hostname && srv[:uri].port == u.port
|
754
878
|
end
|
755
|
-
end
|
756
879
|
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
hdr = process_hdr(header)
|
762
|
-
sub.response = Msg.new(subject: subject, reply: reply, data: data, header: hdr)
|
763
|
-
future.signal
|
880
|
+
if not present
|
881
|
+
# Let explicit user and pass options set the credentials.
|
882
|
+
u.user = options[:user] if options[:user]
|
883
|
+
u.password = options[:pass] if options[:pass]
|
764
884
|
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
or sub.pending_size >= sub.pending_bytes_limit then
|
771
|
-
err = SlowConsumer.new("nats: slow consumer, messages dropped")
|
772
|
-
else
|
773
|
-
hdr = process_hdr(header)
|
885
|
+
# Use creds from the current server if not set explicitly.
|
886
|
+
if @uri
|
887
|
+
u.user ||= @uri.user if @uri.user
|
888
|
+
u.password ||= @uri.password if @uri.password
|
889
|
+
end
|
774
890
|
|
775
|
-
#
|
776
|
-
|
777
|
-
|
778
|
-
sub.pending_queue << msg
|
779
|
-
sub.pending_size += data.size
|
891
|
+
# NOTE: Auto discovery won't work here when TLS host verification is enabled.
|
892
|
+
srv = { :uri => u, :reconnect_attempts => 0, :discovered => true, :hostname => u.hostname }
|
893
|
+
srvs << srv
|
780
894
|
end
|
781
895
|
end
|
782
|
-
|
896
|
+
srvs.shuffle! unless @options[:dont_randomize_servers]
|
783
897
|
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
end if err
|
898
|
+
# Include in server pool but keep current one as the first one.
|
899
|
+
server_pool.push(*srvs)
|
900
|
+
end
|
788
901
|
end
|
789
902
|
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
903
|
+
@server_info
|
904
|
+
end
|
905
|
+
|
906
|
+
def process_hdr(header)
|
907
|
+
hdr = nil
|
908
|
+
if header
|
909
|
+
hdr = {}
|
910
|
+
lines = header.lines
|
795
911
|
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
912
|
+
# Check if the first line has an inline status and description.
|
913
|
+
if lines.count > 0
|
914
|
+
status_hdr = lines.first.rstrip
|
915
|
+
status = status_hdr.slice(NATS_HDR_LINE_SIZE-1, STATUS_MSG_LEN)
|
916
|
+
|
917
|
+
if status and !status.empty?
|
918
|
+
hdr[STATUS_HDR] = status
|
800
919
|
|
801
920
|
if NATS_HDR_LINE_SIZE+2 < status_hdr.bytesize
|
802
921
|
desc = status_hdr.slice(NATS_HDR_LINE_SIZE+STATUS_MSG_LEN, status_hdr.bytesize)
|
803
922
|
hdr[DESC_HDR] = desc unless desc.empty?
|
804
923
|
end
|
805
924
|
end
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
rescue => e
|
814
|
-
err = e
|
925
|
+
end
|
926
|
+
begin
|
927
|
+
lines.slice(1, header.size).each do |line|
|
928
|
+
line.rstrip!
|
929
|
+
next if line.empty?
|
930
|
+
key, value = line.strip.split(/\s*:\s*/, 2)
|
931
|
+
hdr[key] = value
|
815
932
|
end
|
933
|
+
rescue => e
|
934
|
+
err = e
|
816
935
|
end
|
817
|
-
|
818
|
-
hdr
|
819
936
|
end
|
820
937
|
|
821
|
-
|
822
|
-
|
938
|
+
hdr
|
939
|
+
end
|
823
940
|
|
824
|
-
|
825
|
-
# so has to be done under the lock.
|
826
|
-
synchronize do
|
827
|
-
# Symbolize keys from parsed info line
|
828
|
-
@server_info = parsed_info.reduce({}) do |info, (k,v)|
|
829
|
-
info[k.to_sym] = v
|
941
|
+
# Methods only used by the parser
|
830
942
|
|
831
|
-
|
832
|
-
|
943
|
+
def process_pong
|
944
|
+
# Take first pong wait and signal any flush in case there was one
|
945
|
+
@pongs.synchronize do
|
946
|
+
pong = @pongs.pop
|
947
|
+
pong.signal unless pong.nil?
|
948
|
+
end
|
949
|
+
@pings_outstanding -= 1
|
950
|
+
@pongs_received += 1
|
951
|
+
end
|
952
|
+
|
953
|
+
# Received a ping so respond back with a pong
|
954
|
+
def process_ping
|
955
|
+
@pending_queue << PONG_RESPONSE
|
956
|
+
@flush_queue << :ping
|
957
|
+
pong = @pongs.new_cond
|
958
|
+
@pongs.synchronize { @pongs << pong }
|
959
|
+
end
|
960
|
+
|
961
|
+
# Handles protocol errors being sent by the server.
|
962
|
+
def process_err(err)
|
963
|
+
# In case of permissions violation then dispatch the error callback
|
964
|
+
# while holding the lock.
|
965
|
+
e = synchronize do
|
966
|
+
current = server_pool.first
|
967
|
+
case
|
968
|
+
when err =~ /'Stale Connection'/
|
969
|
+
@last_err = NATS::IO::StaleConnectionError.new(err)
|
970
|
+
when current && current[:auth_required]
|
971
|
+
# We cannot recover from auth errors so mark it to avoid
|
972
|
+
# retrying to unecessarily next time.
|
973
|
+
current[:error_received] = true
|
974
|
+
@last_err = NATS::IO::AuthError.new(err)
|
975
|
+
else
|
976
|
+
@last_err = NATS::IO::ServerError.new(err)
|
977
|
+
end
|
978
|
+
end
|
979
|
+
process_op_error(e)
|
980
|
+
end
|
833
981
|
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
srvs = []
|
838
|
-
connect_urls.each do |url|
|
839
|
-
scheme = client_using_secure_connection? ? "tls" : "nats"
|
840
|
-
u = URI.parse("#{scheme}://#{url}")
|
982
|
+
def process_msg(subject, sid, reply, data, header)
|
983
|
+
@stats[:in_msgs] += 1
|
984
|
+
@stats[:in_bytes] += data.size
|
841
985
|
|
842
|
-
|
843
|
-
|
986
|
+
# Throw away in case we no longer manage the subscription
|
987
|
+
sub = nil
|
988
|
+
synchronize { sub = @subs[sid] }
|
989
|
+
return unless sub
|
844
990
|
|
845
|
-
|
846
|
-
|
847
|
-
|
991
|
+
err = nil
|
992
|
+
sub.synchronize do
|
993
|
+
sub.received += 1
|
848
994
|
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
995
|
+
# Check for auto_unsubscribe
|
996
|
+
if sub.max
|
997
|
+
case
|
998
|
+
when sub.received > sub.max
|
999
|
+
# Client side support in case server did not receive unsubscribe
|
1000
|
+
unsubscribe(sid)
|
1001
|
+
return
|
1002
|
+
when sub.received == sub.max
|
1003
|
+
# Cleanup here if we have hit the max..
|
1004
|
+
synchronize { @subs.delete(sid) }
|
1005
|
+
end
|
1006
|
+
end
|
853
1007
|
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
1008
|
+
# In case of a request which requires a future
|
1009
|
+
# do so here already while holding the lock and return
|
1010
|
+
if sub.future
|
1011
|
+
future = sub.future
|
1012
|
+
hdr = process_hdr(header)
|
1013
|
+
sub.response = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
|
1014
|
+
future.signal
|
1015
|
+
|
1016
|
+
return
|
1017
|
+
elsif sub.pending_queue
|
1018
|
+
# Async subscribers use a sized queue for processing
|
1019
|
+
# and should be able to consume messages in parallel.
|
1020
|
+
if sub.pending_queue.size >= sub.pending_msgs_limit \
|
1021
|
+
or sub.pending_size >= sub.pending_bytes_limit then
|
1022
|
+
err = NATS::IO::SlowConsumer.new("nats: slow consumer, messages dropped")
|
1023
|
+
else
|
1024
|
+
hdr = process_hdr(header)
|
859
1025
|
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
end
|
864
|
-
end
|
865
|
-
srvs.shuffle! unless @options[:dont_randomize_servers]
|
1026
|
+
# Only dispatch message when sure that it would not block
|
1027
|
+
# the main read loop from the parser.
|
1028
|
+
msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
|
866
1029
|
|
867
|
-
|
868
|
-
server_pool.push(*srvs)
|
1030
|
+
sub.dispatch(msg)
|
869
1031
|
end
|
870
1032
|
end
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
synchronize do
|
1036
|
+
@last_err = err
|
1037
|
+
err_cb_call(self, err, sub) if @err_cb
|
1038
|
+
end if err
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
def select_next_server
|
1042
|
+
raise NATS::IO::NoServersError.new("nats: No servers available") if server_pool.empty?
|
1043
|
+
|
1044
|
+
# Pick next from head of the list
|
1045
|
+
srv = server_pool.shift
|
1046
|
+
|
1047
|
+
# Track connection attempts to this server
|
1048
|
+
srv[:reconnect_attempts] ||= 0
|
1049
|
+
srv[:reconnect_attempts] += 1
|
1050
|
+
|
1051
|
+
# Back off in case we are reconnecting to it and have been connected
|
1052
|
+
sleep @options[:reconnect_time_wait] if should_delay_connect?(srv)
|
871
1053
|
|
872
|
-
|
1054
|
+
# Set url of the server to which we would be connected
|
1055
|
+
@uri = srv[:uri]
|
1056
|
+
@uri.user = @options[:user] if @options[:user]
|
1057
|
+
@uri.password = @options[:pass] if @options[:pass]
|
1058
|
+
|
1059
|
+
srv
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
def server_using_secure_connection?
|
1063
|
+
@server_info[:ssl_required] || @server_info[:tls_required]
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
def client_using_secure_connection?
|
1067
|
+
@uri.scheme == "tls" || @tls
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
def tls_context
|
1071
|
+
return nil unless @tls
|
1072
|
+
|
1073
|
+
# Allow prepared context and customizations via :tls opts
|
1074
|
+
return @tls[:context] if @tls[:context]
|
1075
|
+
|
1076
|
+
@tls_context ||= OpenSSL::SSL::SSLContext.new.tap do |tls_context|
|
1077
|
+
# Use the default verification options from Ruby:
|
1078
|
+
# https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
|
1079
|
+
#
|
1080
|
+
# Insecure TLS versions not supported already:
|
1081
|
+
# https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
|
1082
|
+
#
|
1083
|
+
tls_context.set_params
|
873
1084
|
end
|
1085
|
+
end
|
874
1086
|
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
1087
|
+
def single_url_connect_used?
|
1088
|
+
@single_url_connect_used
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
def send_command(command)
|
1092
|
+
raise NATS::IO::ConnectionClosedError if closed?
|
1093
|
+
|
1094
|
+
establish_connection! if !status || (disconnected? && should_reconnect?)
|
1095
|
+
|
1096
|
+
@pending_size += command.bytesize
|
1097
|
+
@pending_queue << command
|
1098
|
+
|
1099
|
+
# TODO: kick flusher here in case pending_size growing large
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
# Auto unsubscribes the server by sending UNSUB command and throws away
|
1103
|
+
# subscription in case already present and has received enough messages.
|
1104
|
+
def unsubscribe(sub, opt_max=nil)
|
1105
|
+
sid = nil
|
1106
|
+
closed = nil
|
1107
|
+
sub.synchronize do
|
1108
|
+
sid = sub.sid
|
1109
|
+
closed = sub.closed
|
880
1110
|
end
|
1111
|
+
raise NATS::IO::BadSubscription.new("nats: invalid subscription") if closed
|
881
1112
|
|
882
|
-
|
883
|
-
|
1113
|
+
opt_max_str = " #{opt_max}" unless opt_max.nil?
|
1114
|
+
send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}")
|
1115
|
+
@flush_queue << :unsub
|
1116
|
+
|
1117
|
+
synchronize { sub = @subs[sid] }
|
1118
|
+
return unless sub
|
1119
|
+
synchronize do
|
1120
|
+
sub.max = opt_max
|
1121
|
+
@subs.delete(sid) unless (sub.max && (sub.received < sub.max))
|
884
1122
|
end
|
885
1123
|
|
886
|
-
|
887
|
-
|
1124
|
+
sub.synchronize do
|
1125
|
+
sub.closed = true
|
888
1126
|
end
|
1127
|
+
end
|
889
1128
|
|
890
|
-
|
891
|
-
|
1129
|
+
def drain_sub(sub)
|
1130
|
+
sid = nil
|
1131
|
+
closed = nil
|
1132
|
+
sub.synchronize do
|
1133
|
+
sid = sub.sid
|
1134
|
+
closed = sub.closed
|
892
1135
|
end
|
1136
|
+
return if closed
|
1137
|
+
|
1138
|
+
send_command("UNSUB #{sid}#{CR_LF}")
|
1139
|
+
@flush_queue << :drain
|
1140
|
+
|
1141
|
+
synchronize { sub = @subs[sid] }
|
1142
|
+
return unless sub
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def do_drain
|
1146
|
+
synchronize { @status = DRAINING_SUBS }
|
1147
|
+
|
1148
|
+
# Do unsubscribe protocol for all the susbcriptions, then have a single thread
|
1149
|
+
# waiting until all subs are done or drain timeout error reported to async error cb.
|
1150
|
+
subs = []
|
1151
|
+
@subs.each do |_, sub|
|
1152
|
+
next if sub == @resp_sub
|
1153
|
+
drain_sub(sub)
|
1154
|
+
subs << sub
|
1155
|
+
end
|
1156
|
+
force_flush!
|
1157
|
+
|
1158
|
+
# Wait until all subs have no pending messages.
|
1159
|
+
drain_timeout = MonotonicTime::now + @options[:drain_timeout]
|
1160
|
+
to_delete = []
|
893
1161
|
|
894
|
-
|
895
|
-
|
1162
|
+
loop do
|
1163
|
+
break if MonotonicTime::now > drain_timeout
|
1164
|
+
sleep 0.1
|
1165
|
+
|
1166
|
+
# Wait until all subs are done.
|
1167
|
+
@subs.each do |_, sub|
|
1168
|
+
if sub != @resp_sub and sub.pending_queue.size == 0
|
1169
|
+
to_delete << sub
|
1170
|
+
end
|
1171
|
+
end
|
1172
|
+
next if to_delete.empty?
|
1173
|
+
|
1174
|
+
to_delete.each do |sub|
|
1175
|
+
@subs.delete(sub.sid)
|
1176
|
+
end
|
1177
|
+
to_delete.clear
|
1178
|
+
|
1179
|
+
# Wait until only the resp mux is remaining or there are no subscriptions.
|
1180
|
+
if @subs.count == 1
|
1181
|
+
sid, sub = @subs.first
|
1182
|
+
if sub == @resp_sub
|
1183
|
+
break
|
1184
|
+
end
|
1185
|
+
elsif @subs.count == 0
|
1186
|
+
break
|
1187
|
+
end
|
896
1188
|
end
|
897
1189
|
|
898
|
-
|
899
|
-
|
900
|
-
end
|
1190
|
+
subscription_executor.shutdown
|
1191
|
+
subscription_executor.wait_for_termination(@options[:drain_timeout])
|
901
1192
|
|
902
|
-
|
903
|
-
|
1193
|
+
if MonotonicTime::now > drain_timeout
|
1194
|
+
e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
|
1195
|
+
err_cb_call(self, e, nil) if @err_cb
|
904
1196
|
end
|
1197
|
+
synchronize { @status = DRAINING_PUBS }
|
905
1198
|
|
906
|
-
|
907
|
-
|
908
|
-
|
1199
|
+
# Remove resp mux handler in case there is one.
|
1200
|
+
unsubscribe(@resp_sub) if @resp_sub
|
1201
|
+
close
|
1202
|
+
end
|
909
1203
|
|
910
|
-
|
911
|
-
|
912
|
-
|
1204
|
+
def send_flush_queue(s)
|
1205
|
+
@flush_queue << s
|
1206
|
+
end
|
913
1207
|
|
914
|
-
|
915
|
-
|
916
|
-
|
1208
|
+
def delete_sid(sid)
|
1209
|
+
@subs.delete(sid)
|
1210
|
+
end
|
917
1211
|
|
918
|
-
|
919
|
-
|
920
|
-
end
|
1212
|
+
def err_cb_call(nc, e, sub)
|
1213
|
+
return unless @err_cb
|
921
1214
|
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
1215
|
+
cb = @err_cb
|
1216
|
+
case cb.arity
|
1217
|
+
when 0 then cb.call
|
1218
|
+
when 1 then cb.call(e)
|
1219
|
+
when 2 then cb.call(e, sub)
|
1220
|
+
else cb.call(nc, e, sub)
|
926
1221
|
end
|
1222
|
+
end
|
927
1223
|
|
928
|
-
|
1224
|
+
def auth_connection?
|
1225
|
+
!@uri.user.nil?
|
1226
|
+
end
|
929
1227
|
|
930
|
-
|
931
|
-
|
1228
|
+
def connect_command
|
1229
|
+
cs = {
|
1230
|
+
:verbose => @options[:verbose],
|
1231
|
+
:pedantic => @options[:pedantic],
|
1232
|
+
:lang => NATS::IO::LANG,
|
1233
|
+
:version => NATS::IO::VERSION,
|
1234
|
+
:protocol => NATS::IO::PROTOCOL
|
1235
|
+
}
|
1236
|
+
cs[:name] = @options[:name] if @options[:name]
|
932
1237
|
|
933
|
-
|
934
|
-
|
1238
|
+
case
|
1239
|
+
when auth_connection?
|
1240
|
+
if @uri.password
|
1241
|
+
cs[:user] = @uri.user
|
1242
|
+
cs[:pass] = @uri.password
|
1243
|
+
else
|
1244
|
+
cs[:auth_token] = @uri.user
|
1245
|
+
end
|
1246
|
+
when @user_jwt_cb && @signature_cb
|
1247
|
+
nonce = @server_info[:nonce]
|
1248
|
+
cs[:jwt] = @user_jwt_cb.call
|
1249
|
+
cs[:sig] = @signature_cb.call(nonce)
|
1250
|
+
when @user_nkey_cb && @signature_cb
|
1251
|
+
nonce = @server_info[:nonce]
|
1252
|
+
cs[:nkey] = @user_nkey_cb.call
|
1253
|
+
cs[:sig] = @signature_cb.call(nonce)
|
1254
|
+
end
|
935
1255
|
|
936
|
-
|
937
|
-
srv[:reconnect_attempts] ||= 0
|
938
|
-
srv[:reconnect_attempts] += 1
|
1256
|
+
cs[:auth_token] = @auth_token if @auth_token
|
939
1257
|
|
940
|
-
|
941
|
-
|
1258
|
+
if @server_info[:headers]
|
1259
|
+
cs[:headers] = @server_info[:headers]
|
1260
|
+
cs[:no_responders] = if @options[:no_responders] == false
|
1261
|
+
@options[:no_responders]
|
1262
|
+
else
|
1263
|
+
@server_info[:headers]
|
1264
|
+
end
|
1265
|
+
end
|
942
1266
|
|
943
|
-
|
944
|
-
|
945
|
-
@uri.user = @options[:user] if @options[:user]
|
946
|
-
@uri.password = @options[:pass] if @options[:pass]
|
1267
|
+
"CONNECT #{cs.to_json}#{CR_LF}"
|
1268
|
+
end
|
947
1269
|
|
948
|
-
|
1270
|
+
# Handles errors from reading, parsing the protocol or stale connection.
|
1271
|
+
# the lock should not be held entering this function.
|
1272
|
+
def process_op_error(e)
|
1273
|
+
should_bail = synchronize do
|
1274
|
+
connecting? || closed? || reconnecting?
|
949
1275
|
end
|
1276
|
+
return if should_bail
|
950
1277
|
|
951
|
-
|
952
|
-
@
|
953
|
-
|
1278
|
+
synchronize do
|
1279
|
+
@last_err = e
|
1280
|
+
err_cb_call(self, e, nil) if @err_cb
|
954
1281
|
|
955
|
-
|
956
|
-
|
957
|
-
|
1282
|
+
# If we were connected and configured to reconnect,
|
1283
|
+
# then trigger disconnect and start reconnection logic
|
1284
|
+
if connected? and should_reconnect?
|
1285
|
+
@status = RECONNECTING
|
1286
|
+
@io.close if @io
|
1287
|
+
@io = nil
|
958
1288
|
|
959
|
-
|
960
|
-
@single_url_connect_used
|
961
|
-
end
|
1289
|
+
# TODO: Reconnecting pending buffer?
|
962
1290
|
|
963
|
-
|
964
|
-
|
965
|
-
|
1291
|
+
# Do reconnect under a different thread than the one
|
1292
|
+
# in which we got the error.
|
1293
|
+
Thread.new do
|
1294
|
+
begin
|
1295
|
+
# Abort currently running reads in case they're around
|
1296
|
+
# FIXME: There might be more graceful way here...
|
1297
|
+
@read_loop_thread.exit if @read_loop_thread.alive?
|
1298
|
+
@flusher_thread.exit if @flusher_thread.alive?
|
1299
|
+
@ping_interval_thread.exit if @ping_interval_thread.alive?
|
1300
|
+
|
1301
|
+
attempt_reconnect
|
1302
|
+
rescue NATS::IO::NoServersError => e
|
1303
|
+
@last_err = e
|
1304
|
+
close
|
1305
|
+
end
|
1306
|
+
end
|
966
1307
|
|
967
|
-
|
968
|
-
|
1308
|
+
Thread.exit
|
1309
|
+
return
|
1310
|
+
end
|
969
1311
|
|
970
|
-
|
971
|
-
|
1312
|
+
# Otherwise, stop trying to reconnect and close the connection
|
1313
|
+
@status = DISCONNECTED
|
972
1314
|
end
|
973
1315
|
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
:pedantic => @options[:pedantic],
|
978
|
-
:lang => NATS::IO::LANG,
|
979
|
-
:version => NATS::IO::VERSION,
|
980
|
-
:protocol => NATS::IO::PROTOCOL
|
981
|
-
}
|
982
|
-
cs[:name] = @options[:name] if @options[:name]
|
1316
|
+
# Otherwise close the connection to NATS
|
1317
|
+
close
|
1318
|
+
end
|
983
1319
|
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
1320
|
+
# Gathers data from the socket and sends it to the parser.
|
1321
|
+
def read_loop
|
1322
|
+
loop do
|
1323
|
+
begin
|
1324
|
+
should_bail = synchronize do
|
1325
|
+
# FIXME: In case of reconnect as well?
|
1326
|
+
@status == CLOSED or @status == RECONNECTING
|
1327
|
+
end
|
1328
|
+
if !@io or @io.closed? or should_bail
|
1329
|
+
return
|
991
1330
|
end
|
992
|
-
when @user_credentials
|
993
|
-
nonce = @server_info[:nonce]
|
994
|
-
cs[:jwt] = @user_jwt_cb.call
|
995
|
-
cs[:sig] = @signature_cb.call(nonce)
|
996
|
-
when @nkeys_seed
|
997
|
-
nonce = @server_info[:nonce]
|
998
|
-
cs[:nkey] = @user_nkey_cb.call
|
999
|
-
cs[:sig] = @signature_cb.call(nonce)
|
1000
|
-
end
|
1001
1331
|
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1332
|
+
# TODO: Remove timeout and just wait to be ready
|
1333
|
+
data = @io.read(NATS::IO::MAX_SOCKET_READ_BYTES)
|
1334
|
+
@parser.parse(data) if data
|
1335
|
+
rescue Errno::ETIMEDOUT
|
1336
|
+
# FIXME: We do not really need a timeout here...
|
1337
|
+
retry
|
1338
|
+
rescue => e
|
1339
|
+
# In case of reading/parser errors, trigger
|
1340
|
+
# reconnection logic in case desired.
|
1341
|
+
process_op_error(e)
|
1009
1342
|
end
|
1010
|
-
|
1011
|
-
"CONNECT #{cs.to_json}#{CR_LF}"
|
1012
1343
|
end
|
1344
|
+
end
|
1013
1345
|
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
end
|
1346
|
+
# Waits for client to notify the flusher that it will be
|
1347
|
+
# it is sending a command.
|
1348
|
+
def flusher_loop
|
1349
|
+
loop do
|
1350
|
+
# Blocks waiting for the flusher to be kicked...
|
1351
|
+
@flush_queue.pop
|
1021
1352
|
|
1022
|
-
# Handles errors from reading, parsing the protocol or stale connection.
|
1023
|
-
# the lock should not be held entering this function.
|
1024
|
-
def process_op_error(e)
|
1025
1353
|
should_bail = synchronize do
|
1026
|
-
|
1354
|
+
(@status != CONNECTED && !draining? ) || @status == CONNECTING
|
1027
1355
|
end
|
1028
1356
|
return if should_bail
|
1029
1357
|
|
1030
|
-
|
1031
|
-
|
1032
|
-
@err_cb.call(e) if @err_cb
|
1033
|
-
|
1034
|
-
# If we were connected and configured to reconnect,
|
1035
|
-
# then trigger disconnect and start reconnection logic
|
1036
|
-
if connected? and should_reconnect?
|
1037
|
-
@status = RECONNECTING
|
1038
|
-
@io.close if @io
|
1039
|
-
@io = nil
|
1040
|
-
|
1041
|
-
# TODO: Reconnecting pending buffer?
|
1042
|
-
|
1043
|
-
# Do reconnect under a different thread than the one
|
1044
|
-
# in which we got the error.
|
1045
|
-
Thread.new do
|
1046
|
-
begin
|
1047
|
-
# Abort currently running reads in case they're around
|
1048
|
-
# FIXME: There might be more graceful way here...
|
1049
|
-
@read_loop_thread.exit if @read_loop_thread.alive?
|
1050
|
-
@flusher_thread.exit if @flusher_thread.alive?
|
1051
|
-
@ping_interval_thread.exit if @ping_interval_thread.alive?
|
1052
|
-
|
1053
|
-
attempt_reconnect
|
1054
|
-
rescue NoServersError => e
|
1055
|
-
@last_err = e
|
1056
|
-
close
|
1057
|
-
end
|
1058
|
-
end
|
1358
|
+
# Skip in case nothing remains pending already.
|
1359
|
+
next if @pending_queue.empty?
|
1059
1360
|
|
1060
|
-
|
1061
|
-
return
|
1062
|
-
end
|
1361
|
+
force_flush!
|
1063
1362
|
|
1064
|
-
|
1065
|
-
@
|
1363
|
+
synchronize do
|
1364
|
+
@pending_size = 0
|
1066
1365
|
end
|
1067
|
-
|
1068
|
-
# Otherwise close the connection to NATS
|
1069
|
-
close
|
1070
1366
|
end
|
1367
|
+
end
|
1071
1368
|
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
# TODO: Remove timeout and just wait to be ready
|
1085
|
-
data = @io.read(MAX_SOCKET_READ_BYTES)
|
1086
|
-
@parser.parse(data) if data
|
1087
|
-
rescue Errno::ETIMEDOUT
|
1088
|
-
# FIXME: We do not really need a timeout here...
|
1089
|
-
retry
|
1090
|
-
rescue => e
|
1091
|
-
# In case of reading/parser errors, trigger
|
1092
|
-
# reconnection logic in case desired.
|
1093
|
-
process_op_error(e)
|
1094
|
-
end
|
1369
|
+
def force_flush!
|
1370
|
+
# FIXME: should limit how many commands to take at once
|
1371
|
+
# since producers could be adding as many as possible
|
1372
|
+
# until reaching the max pending queue size.
|
1373
|
+
cmds = []
|
1374
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
1375
|
+
begin
|
1376
|
+
@io.write(cmds.join) unless cmds.empty?
|
1377
|
+
rescue => e
|
1378
|
+
synchronize do
|
1379
|
+
@last_err = e
|
1380
|
+
err_cb_call(self, e, nil) if @err_cb
|
1095
1381
|
end
|
1096
|
-
end
|
1097
1382
|
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
# Blocks waiting for the flusher to be kicked...
|
1103
|
-
@flush_queue.pop
|
1383
|
+
process_op_error(e)
|
1384
|
+
return
|
1385
|
+
end if @io
|
1386
|
+
end
|
1104
1387
|
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
return if should_bail
|
1388
|
+
def ping_interval_loop
|
1389
|
+
loop do
|
1390
|
+
sleep @options[:ping_interval]
|
1109
1391
|
|
1110
|
-
|
1111
|
-
|
1392
|
+
# Skip ping interval until connected
|
1393
|
+
next if !connected?
|
1112
1394
|
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
cmds << @pending_queue.pop until @pending_queue.empty?
|
1118
|
-
begin
|
1119
|
-
@io.write(cmds.join) unless cmds.empty?
|
1120
|
-
rescue => e
|
1121
|
-
synchronize do
|
1122
|
-
@last_err = e
|
1123
|
-
@err_cb.call(e) if @err_cb
|
1124
|
-
end
|
1395
|
+
if @pings_outstanding >= @options[:max_outstanding_pings]
|
1396
|
+
process_op_error(NATS::IO::StaleConnectionError.new("nats: stale connection"))
|
1397
|
+
return
|
1398
|
+
end
|
1125
1399
|
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1400
|
+
@pings_outstanding += 1
|
1401
|
+
send_command(PING_REQUEST)
|
1402
|
+
@flush_queue << :ping
|
1403
|
+
end
|
1404
|
+
rescue => e
|
1405
|
+
process_op_error(e)
|
1406
|
+
end
|
1129
1407
|
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1408
|
+
def process_connect_init
|
1409
|
+
# FIXME: Can receive PING as well here in recent versions.
|
1410
|
+
line = @io.read_line(options[:connect_timeout])
|
1411
|
+
if !line or line.empty?
|
1412
|
+
raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not received")
|
1134
1413
|
end
|
1135
1414
|
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1415
|
+
if match = line.match(NATS::Protocol::INFO)
|
1416
|
+
info_json = match.captures.first
|
1417
|
+
process_info(info_json)
|
1418
|
+
else
|
1419
|
+
raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not valid")
|
1420
|
+
end
|
1139
1421
|
|
1140
|
-
|
1141
|
-
|
1422
|
+
case
|
1423
|
+
when (server_using_secure_connection? and client_using_secure_connection?)
|
1424
|
+
@io.setup_tls!
|
1425
|
+
# Server > v2.9.19 returns tls_required regardless of no_tls for WebSocket config being used so need to check URI.
|
1426
|
+
when (server_using_secure_connection? and !client_using_secure_connection? and @uri.scheme != "ws")
|
1427
|
+
raise NATS::IO::ConnectError.new('TLS/SSL required by server')
|
1428
|
+
# Server < v2.9.19 requiring TLS/SSL over websocket but not requiring it over standard protocol
|
1429
|
+
# doesn't send `tls_required` in its INFO so we need to check the URI scheme for WebSocket.
|
1430
|
+
when (client_using_secure_connection? and !server_using_secure_connection? and @uri.scheme != "wss")
|
1431
|
+
raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
|
1432
|
+
else
|
1433
|
+
# Otherwise, use a regular connection.
|
1434
|
+
end
|
1142
1435
|
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
end
|
1436
|
+
# Send connect and process synchronously. If using TLS,
|
1437
|
+
# it should have handled upgrading at this point.
|
1438
|
+
@io.write(connect_command)
|
1147
1439
|
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1440
|
+
# Send ping/pong after connect
|
1441
|
+
@io.write(PING_REQUEST)
|
1442
|
+
|
1443
|
+
next_op = @io.read_line(options[:connect_timeout])
|
1444
|
+
if @options[:verbose]
|
1445
|
+
# Need to get another command here if verbose
|
1446
|
+
raise NATS::IO::ConnectError.new("expected to receive +OK") unless next_op =~ NATS::Protocol::OK
|
1447
|
+
next_op = @io.read_line(options[:connect_timeout])
|
1154
1448
|
end
|
1155
1449
|
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1450
|
+
case next_op
|
1451
|
+
when NATS::Protocol::PONG
|
1452
|
+
when NATS::Protocol::ERR
|
1453
|
+
if @server_info[:auth_required]
|
1454
|
+
raise NATS::IO::AuthError.new($1)
|
1455
|
+
else
|
1456
|
+
raise NATS::IO::ServerError.new($1)
|
1160
1457
|
end
|
1458
|
+
else
|
1459
|
+
raise NATS::IO::ConnectError.new("expected PONG, got #{next_op}")
|
1460
|
+
end
|
1461
|
+
end
|
1462
|
+
|
1463
|
+
# Reconnect logic, this is done while holding the lock.
|
1464
|
+
def attempt_reconnect
|
1465
|
+
@disconnect_cb.call(@last_err) if @disconnect_cb
|
1161
1466
|
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1467
|
+
# Clear sticky error
|
1468
|
+
@last_err = nil
|
1469
|
+
|
1470
|
+
# Do reconnect
|
1471
|
+
srv = nil
|
1472
|
+
begin
|
1473
|
+
srv = select_next_server
|
1474
|
+
|
1475
|
+
# Set hostname to use for TLS hostname verification
|
1476
|
+
if client_using_secure_connection? and single_url_connect_used?
|
1477
|
+
# Reuse original hostname name in case of using TLS.
|
1478
|
+
@hostname ||= srv[:hostname]
|
1165
1479
|
else
|
1166
|
-
|
1480
|
+
@hostname = srv[:hostname]
|
1167
1481
|
end
|
1168
1482
|
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1483
|
+
# Establish TCP connection with new server
|
1484
|
+
@io = create_socket
|
1485
|
+
@io.connect
|
1486
|
+
@stats[:reconnects] += 1
|
1172
1487
|
|
1173
|
-
|
1174
|
-
|
1175
|
-
tls_context = @tls[:context] if @tls[:context]
|
1176
|
-
else
|
1177
|
-
# Defaults
|
1178
|
-
tls_context = OpenSSL::SSL::SSLContext.new
|
1179
|
-
|
1180
|
-
# Use the default verification options from Ruby:
|
1181
|
-
# https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
|
1182
|
-
#
|
1183
|
-
# Insecure TLS versions not supported already:
|
1184
|
-
# https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
|
1185
|
-
#
|
1186
|
-
tls_context.set_params
|
1187
|
-
end
|
1488
|
+
# Established TCP connection successfully so can start connect
|
1489
|
+
process_connect_init
|
1188
1490
|
|
1189
|
-
|
1190
|
-
|
1491
|
+
# Reset reconnection attempts if connection is valid
|
1492
|
+
srv[:reconnect_attempts] = 0
|
1493
|
+
srv[:auth_required] ||= true if @server_info[:auth_required]
|
1191
1494
|
|
1192
|
-
|
1193
|
-
|
1495
|
+
# Add back to rotation since successfully connected
|
1496
|
+
server_pool << srv
|
1497
|
+
rescue NATS::IO::NoServersError => e
|
1498
|
+
raise e
|
1499
|
+
rescue => e
|
1500
|
+
# In case there was an error from the server check
|
1501
|
+
# to see whether need to take it out from rotation
|
1502
|
+
srv[:auth_required] ||= true if @server_info[:auth_required]
|
1503
|
+
server_pool << srv if can_reuse_server?(srv)
|
1194
1504
|
|
1195
|
-
|
1196
|
-
# https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
|
1197
|
-
tls_socket.hostname = @hostname
|
1505
|
+
@last_err = e
|
1198
1506
|
|
1199
|
-
|
1200
|
-
|
1201
|
-
when (server_using_secure_connection? and !client_using_secure_connection?)
|
1202
|
-
raise NATS::IO::ConnectError.new('TLS/SSL required by server')
|
1203
|
-
when (client_using_secure_connection? and !server_using_secure_connection?)
|
1204
|
-
raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
|
1205
|
-
else
|
1206
|
-
# Otherwise, use a regular connection.
|
1207
|
-
end
|
1507
|
+
# Trigger async error handler
|
1508
|
+
err_cb_call(self, e, nil) if @err_cb
|
1208
1509
|
|
1209
|
-
#
|
1210
|
-
|
1211
|
-
|
1510
|
+
# Continue retrying until there are no options left in the server pool
|
1511
|
+
retry
|
1512
|
+
end
|
1212
1513
|
|
1213
|
-
|
1214
|
-
|
1514
|
+
# Clear pending flush calls and reset state before restarting loops
|
1515
|
+
@flush_queue.clear
|
1516
|
+
@pings_outstanding = 0
|
1517
|
+
@pongs_received = 0
|
1215
1518
|
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
next_op = @io.read_line(options[:connect_timeout])
|
1221
|
-
end
|
1519
|
+
# Replay all subscriptions
|
1520
|
+
@subs.each_pair do |sid, sub|
|
1521
|
+
@io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
|
1522
|
+
end
|
1222
1523
|
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1524
|
+
# Flush anything which was left pending, in case of errors during flush
|
1525
|
+
# then we should raise error then retry the reconnect logic
|
1526
|
+
cmds = []
|
1527
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
1528
|
+
@io.write(cmds.join) unless cmds.empty?
|
1529
|
+
@status = CONNECTED
|
1530
|
+
@pending_size = 0
|
1531
|
+
|
1532
|
+
# Reset parser state here to avoid unknown protocol errors
|
1533
|
+
# on reconnect...
|
1534
|
+
@parser.reset!
|
1535
|
+
|
1536
|
+
# Now connected to NATS, and we can restart parser loop, flusher
|
1537
|
+
# and ping interval
|
1538
|
+
start_threads!
|
1539
|
+
|
1540
|
+
# Dispatch the reconnected callback while holding lock
|
1541
|
+
# which we should have already
|
1542
|
+
@reconnect_cb.call if @reconnect_cb
|
1543
|
+
end
|
1544
|
+
|
1545
|
+
def close_connection(conn_status, do_cbs=true)
|
1546
|
+
synchronize do
|
1547
|
+
@connect_called = false
|
1548
|
+
if @status == CLOSED
|
1549
|
+
@status = conn_status
|
1550
|
+
return
|
1233
1551
|
end
|
1234
1552
|
end
|
1235
1553
|
|
1236
|
-
#
|
1237
|
-
|
1238
|
-
|
1554
|
+
# Kick the flusher so it bails due to closed state
|
1555
|
+
@flush_queue << :fallout if @flush_queue
|
1556
|
+
Thread.pass
|
1239
1557
|
|
1240
|
-
|
1241
|
-
|
1558
|
+
# FIXME: More graceful way of handling the following?
|
1559
|
+
# Ensure ping interval and flusher are not running anymore
|
1560
|
+
if @ping_interval_thread and @ping_interval_thread.alive?
|
1561
|
+
@ping_interval_thread.exit
|
1562
|
+
end
|
1242
1563
|
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
srv = select_next_server
|
1564
|
+
if @flusher_thread and @flusher_thread.alive?
|
1565
|
+
@flusher_thread.exit
|
1566
|
+
end
|
1247
1567
|
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
@stats[:reconnects] += 1
|
1568
|
+
if @read_loop_thread and @read_loop_thread.alive?
|
1569
|
+
@read_loop_thread.exit
|
1570
|
+
end
|
1252
1571
|
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
@hostname = srv[:hostname]
|
1572
|
+
# TODO: Delete any other state which we are not using here too.
|
1573
|
+
synchronize do
|
1574
|
+
@pongs.synchronize do
|
1575
|
+
@pongs.each do |pong|
|
1576
|
+
pong.signal
|
1259
1577
|
end
|
1578
|
+
@pongs.clear
|
1579
|
+
end
|
1260
1580
|
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1581
|
+
# Try to write any pending flushes in case
|
1582
|
+
# we have a connection then close it.
|
1583
|
+
should_flush = (@pending_queue && @io && @io.socket && !@io.closed?)
|
1584
|
+
begin
|
1585
|
+
cmds = []
|
1586
|
+
cmds << @pending_queue.pop until @pending_queue.empty?
|
1267
1587
|
|
1268
|
-
#
|
1269
|
-
|
1270
|
-
rescue NoServersError => e
|
1271
|
-
raise e
|
1588
|
+
# FIXME: Fails when empty on TLS connection?
|
1589
|
+
@io.write(cmds.join) unless cmds.empty?
|
1272
1590
|
rescue => e
|
1273
|
-
# In case there was an error from the server check
|
1274
|
-
# to see whether need to take it out from rotation
|
1275
|
-
srv[:auth_required] ||= true if @server_info[:auth_required]
|
1276
|
-
server_pool << srv if can_reuse_server?(srv)
|
1277
|
-
|
1278
1591
|
@last_err = e
|
1592
|
+
err_cb_call(self, e, nil) if @err_cb
|
1593
|
+
end if should_flush
|
1279
1594
|
|
1280
|
-
|
1281
|
-
|
1595
|
+
# Destroy any remaining subscriptions.
|
1596
|
+
@subs.clear
|
1282
1597
|
|
1283
|
-
|
1284
|
-
|
1598
|
+
if do_cbs
|
1599
|
+
@disconnect_cb.call(@last_err) if @disconnect_cb
|
1600
|
+
@close_cb.call if @close_cb
|
1285
1601
|
end
|
1286
1602
|
|
1287
|
-
|
1288
|
-
@flush_queue.clear
|
1289
|
-
@pings_outstanding = 0
|
1290
|
-
@pongs_received = 0
|
1603
|
+
@status = conn_status
|
1291
1604
|
|
1292
|
-
#
|
1293
|
-
|
1294
|
-
|
1605
|
+
# Close the established connection in case
|
1606
|
+
# we still have it.
|
1607
|
+
if @io
|
1608
|
+
@io.close if @io.socket
|
1609
|
+
@io = nil
|
1295
1610
|
end
|
1611
|
+
end
|
1612
|
+
end
|
1296
1613
|
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1614
|
+
def start_threads!
|
1615
|
+
# Reading loop for gathering data
|
1616
|
+
@read_loop_thread = Thread.new { read_loop }
|
1617
|
+
@read_loop_thread.name = "nats:read_loop"
|
1618
|
+
@read_loop_thread.abort_on_exception = true
|
1619
|
+
|
1620
|
+
# Flusher loop for sending commands
|
1621
|
+
@flusher_thread = Thread.new { flusher_loop }
|
1622
|
+
@flusher_thread.name = "nats:flusher_loop"
|
1623
|
+
@flusher_thread.abort_on_exception = true
|
1624
|
+
|
1625
|
+
# Ping interval handling for keeping alive the connection
|
1626
|
+
@ping_interval_thread = Thread.new { ping_interval_loop }
|
1627
|
+
@ping_interval_thread.name = "nats:ping_loop"
|
1628
|
+
@ping_interval_thread.abort_on_exception = true
|
1629
|
+
|
1630
|
+
# Subscription handling thread pool
|
1631
|
+
@subscription_executor = Concurrent::ThreadPoolExecutor.new(
|
1632
|
+
name: 'nats:subscription', # threads will be given names like nats:subscription-worker-1
|
1633
|
+
max_threads: NATS::IO::DEFAULT_TOTAL_SUB_CONCURRENCY,
|
1634
|
+
)
|
1635
|
+
end
|
1308
1636
|
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1637
|
+
# Prepares requests subscription that handles the responses
|
1638
|
+
# for the new style request response.
|
1639
|
+
def start_resp_mux_sub!
|
1640
|
+
@resp_sub_prefix = new_inbox
|
1641
|
+
@resp_map = Hash.new { |h,k| h[k] = { }}
|
1642
|
+
|
1643
|
+
@resp_sub = Subscription.new
|
1644
|
+
@resp_sub.subject = "#{@resp_sub_prefix}.*"
|
1645
|
+
@resp_sub.received = 0
|
1646
|
+
@resp_sub.nc = self
|
1647
|
+
|
1648
|
+
# FIXME: Allow setting pending limits for responses mux subscription.
|
1649
|
+
@resp_sub.pending_msgs_limit = NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
|
1650
|
+
@resp_sub.pending_bytes_limit = NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
|
1651
|
+
@resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
|
1652
|
+
@resp_sub.callback = proc do |msg|
|
1653
|
+
# Pick the token and signal the request under the mutex
|
1654
|
+
# from the subscription itself.
|
1655
|
+
token = msg.subject.split('.').last
|
1656
|
+
future = nil
|
1657
|
+
synchronize do
|
1658
|
+
future = @resp_map[token][:future]
|
1659
|
+
@resp_map[token][:response] = msg
|
1660
|
+
end
|
1312
1661
|
|
1313
|
-
#
|
1314
|
-
#
|
1315
|
-
@
|
1662
|
+
# Signal back that the response has arrived
|
1663
|
+
# in case the future has not been yet delete.
|
1664
|
+
@resp_sub.synchronize do
|
1665
|
+
future.signal if future
|
1666
|
+
end
|
1316
1667
|
end
|
1317
1668
|
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
end
|
1669
|
+
sid = (@ssid += 1)
|
1670
|
+
@resp_sub.sid = sid
|
1671
|
+
@subs[sid] = @resp_sub
|
1672
|
+
send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
|
1673
|
+
@flush_queue << :sub
|
1674
|
+
end
|
1325
1675
|
|
1326
|
-
|
1327
|
-
|
1328
|
-
Thread.pass
|
1676
|
+
def can_reuse_server?(server)
|
1677
|
+
return false if server.nil?
|
1329
1678
|
|
1330
|
-
|
1331
|
-
|
1332
|
-
if @ping_interval_thread and @ping_interval_thread.alive?
|
1333
|
-
@ping_interval_thread.exit
|
1334
|
-
end
|
1679
|
+
# We can always reuse servers with infinite reconnects settings
|
1680
|
+
return true if @options[:max_reconnect_attempts] < 0
|
1335
1681
|
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1682
|
+
# In case of hard errors like authorization errors, drop the server
|
1683
|
+
# already since won't be able to connect.
|
1684
|
+
return false if server[:error_received]
|
1685
|
+
|
1686
|
+
# We will retry a number of times to reconnect to a server.
|
1687
|
+
return server[:reconnect_attempts] <= @options[:max_reconnect_attempts]
|
1688
|
+
end
|
1689
|
+
|
1690
|
+
def should_delay_connect?(server)
|
1691
|
+
server[:was_connected] && server[:reconnect_attempts] >= 0
|
1692
|
+
end
|
1339
1693
|
|
1340
|
-
|
1341
|
-
|
1694
|
+
def should_not_reconnect?
|
1695
|
+
!@options[:reconnect]
|
1696
|
+
end
|
1697
|
+
|
1698
|
+
def should_reconnect?
|
1699
|
+
@options[:reconnect]
|
1700
|
+
end
|
1701
|
+
|
1702
|
+
def create_socket
|
1703
|
+
socket_class = case @uri.scheme
|
1704
|
+
when "nats", "tls"
|
1705
|
+
NATS::IO::Socket
|
1706
|
+
when "ws", "wss"
|
1707
|
+
require_relative 'websocket'
|
1708
|
+
NATS::IO::WebSocket
|
1709
|
+
else
|
1710
|
+
raise NotImplementedError, "#{@uri.scheme} protocol is not supported, check NATS cluster URL spelling"
|
1342
1711
|
end
|
1343
1712
|
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
@pongs.clear
|
1351
|
-
end
|
1713
|
+
socket_class.new(
|
1714
|
+
uri: @uri,
|
1715
|
+
tls: { context: tls_context, hostname: @hostname },
|
1716
|
+
connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT,
|
1717
|
+
)
|
1718
|
+
end
|
1352
1719
|
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
# FIXME: Fails when empty on TLS connection?
|
1361
|
-
@io.write(cmds.join) unless cmds.empty?
|
1362
|
-
rescue => e
|
1363
|
-
@last_err = e
|
1364
|
-
@err_cb.call(e) if @err_cb
|
1365
|
-
end if should_flush
|
1366
|
-
|
1367
|
-
# Destroy any remaining subscriptions.
|
1368
|
-
@subs.each do |_, sub|
|
1369
|
-
if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
|
1370
|
-
sub.wait_for_msgs_t.exit
|
1371
|
-
sub.pending_queue.clear
|
1372
|
-
end
|
1373
|
-
end
|
1374
|
-
@subs.clear
|
1720
|
+
def setup_nkeys_connect
|
1721
|
+
begin
|
1722
|
+
require 'nkeys'
|
1723
|
+
require 'base64'
|
1724
|
+
rescue LoadError
|
1725
|
+
raise(Error, "nkeys is not installed")
|
1726
|
+
end
|
1375
1727
|
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1728
|
+
case
|
1729
|
+
when @nkeys_seed
|
1730
|
+
@user_nkey_cb = nkey_cb_for_nkey_file(@nkeys_seed)
|
1731
|
+
@signature_cb = signature_cb_for_nkey_file(@nkeys_seed)
|
1732
|
+
when @user_credentials
|
1733
|
+
# When the credentials are within a single decorated file.
|
1734
|
+
@user_jwt_cb = jwt_cb_for_creds_file(@user_credentials)
|
1735
|
+
@signature_cb = signature_cb_for_creds_file(@user_credentials)
|
1736
|
+
end
|
1737
|
+
end
|
1380
1738
|
|
1381
|
-
|
1739
|
+
def signature_cb_for_nkey_file(nkey)
|
1740
|
+
proc { |nonce|
|
1741
|
+
seed = File.read(nkey).chomp
|
1742
|
+
kp = NKEYS::from_seed(seed)
|
1743
|
+
raw_signed = kp.sign(nonce)
|
1744
|
+
kp.wipe!
|
1745
|
+
encoded = Base64.urlsafe_encode64(raw_signed)
|
1746
|
+
encoded.gsub('=', '')
|
1747
|
+
}
|
1748
|
+
end
|
1749
|
+
|
1750
|
+
def nkey_cb_for_nkey_file(nkey)
|
1751
|
+
proc {
|
1752
|
+
seed = File.read(nkey).chomp
|
1753
|
+
kp = NKEYS::from_seed(seed)
|
1754
|
+
|
1755
|
+
# Take a copy since original will be gone with the wipe.
|
1756
|
+
pub_key = kp.public_key.dup
|
1757
|
+
kp.wipe!
|
1758
|
+
|
1759
|
+
pub_key
|
1760
|
+
}
|
1761
|
+
end
|
1762
|
+
|
1763
|
+
def jwt_cb_for_creds_file(creds)
|
1764
|
+
proc {
|
1765
|
+
jwt_start = "BEGIN NATS USER JWT".freeze
|
1766
|
+
found = false
|
1767
|
+
jwt = nil
|
1382
1768
|
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1769
|
+
File.readlines(creds).each do |line|
|
1770
|
+
case
|
1771
|
+
when found
|
1772
|
+
jwt = line.chomp
|
1773
|
+
break
|
1774
|
+
when line.include?(jwt_start)
|
1775
|
+
found = true
|
1388
1776
|
end
|
1389
1777
|
end
|
1390
|
-
end
|
1391
1778
|
|
1392
|
-
|
1393
|
-
# Reading loop for gathering data
|
1394
|
-
@read_loop_thread = Thread.new { read_loop }
|
1395
|
-
@read_loop_thread.abort_on_exception = true
|
1396
|
-
|
1397
|
-
# Flusher loop for sending commands
|
1398
|
-
@flusher_thread = Thread.new { flusher_loop }
|
1399
|
-
@flusher_thread.abort_on_exception = true
|
1400
|
-
|
1401
|
-
# Ping interval handling for keeping alive the connection
|
1402
|
-
@ping_interval_thread = Thread.new { ping_interval_loop }
|
1403
|
-
@ping_interval_thread.abort_on_exception = true
|
1404
|
-
end
|
1405
|
-
|
1406
|
-
# Prepares requests subscription that handles the responses
|
1407
|
-
# for the new style request response.
|
1408
|
-
def start_resp_mux_sub!
|
1409
|
-
@resp_sub_prefix = "_INBOX.#{@nuid.next}"
|
1410
|
-
@resp_map = Hash.new { |h,k| h[k] = { }}
|
1411
|
-
|
1412
|
-
@resp_sub = Subscription.new
|
1413
|
-
@resp_sub.subject = "#{@resp_sub_prefix}.*"
|
1414
|
-
@resp_sub.received = 0
|
1415
|
-
|
1416
|
-
# FIXME: Allow setting pending limits for responses mux subscription.
|
1417
|
-
@resp_sub.pending_msgs_limit = DEFAULT_SUB_PENDING_MSGS_LIMIT
|
1418
|
-
@resp_sub.pending_bytes_limit = DEFAULT_SUB_PENDING_BYTES_LIMIT
|
1419
|
-
@resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
|
1420
|
-
@resp_sub.wait_for_msgs_t = Thread.new do
|
1421
|
-
loop do
|
1422
|
-
msg = @resp_sub.pending_queue.pop
|
1423
|
-
@resp_sub.pending_size -= msg.data.size
|
1424
|
-
|
1425
|
-
# Pick the token and signal the request under the mutex
|
1426
|
-
# from the subscription itself.
|
1427
|
-
token = msg.subject.split('.').last
|
1428
|
-
future = nil
|
1429
|
-
synchronize do
|
1430
|
-
future = @resp_map[token][:future]
|
1431
|
-
@resp_map[token][:response] = msg
|
1432
|
-
end
|
1779
|
+
raise(Error, "No JWT found in #{creds}") if not found
|
1433
1780
|
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1781
|
+
jwt
|
1782
|
+
}
|
1783
|
+
end
|
1784
|
+
|
1785
|
+
def signature_cb_for_creds_file(creds)
|
1786
|
+
proc { |nonce|
|
1787
|
+
seed_start = "BEGIN USER NKEY SEED".freeze
|
1788
|
+
found = false
|
1789
|
+
seed = nil
|
1790
|
+
|
1791
|
+
File.readlines(creds).each do |line|
|
1792
|
+
case
|
1793
|
+
when found
|
1794
|
+
seed = line.chomp
|
1795
|
+
break
|
1796
|
+
when line.include?(seed_start)
|
1797
|
+
found = true
|
1438
1798
|
end
|
1439
1799
|
end
|
1440
1800
|
|
1441
|
-
|
1442
|
-
@subs[sid] = @resp_sub
|
1443
|
-
send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
|
1444
|
-
@flush_queue << :sub
|
1445
|
-
end
|
1801
|
+
raise(Error, "No nkey user seed found in #{creds}") if not found
|
1446
1802
|
|
1447
|
-
|
1448
|
-
|
1803
|
+
kp = NKEYS::from_seed(seed)
|
1804
|
+
raw_signed = kp.sign(nonce)
|
1449
1805
|
|
1450
|
-
#
|
1451
|
-
|
1806
|
+
# seed is a reference so also cleared when doing wipe,
|
1807
|
+
# which can be done since Ruby strings are mutable.
|
1808
|
+
kp.wipe
|
1809
|
+
encoded = Base64.urlsafe_encode64(raw_signed)
|
1452
1810
|
|
1453
|
-
#
|
1454
|
-
|
1455
|
-
|
1811
|
+
# Remove padding
|
1812
|
+
encoded.gsub('=', '')
|
1813
|
+
}
|
1814
|
+
end
|
1456
1815
|
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1816
|
+
def process_uri(uris)
|
1817
|
+
uris.split(',').map do |uri|
|
1818
|
+
opts = {}
|
1460
1819
|
|
1461
|
-
|
1462
|
-
|
1463
|
-
end
|
1820
|
+
# Scheme
|
1821
|
+
uri = "nats://#{uri}" if !uri.include?("://")
|
1464
1822
|
|
1465
|
-
|
1466
|
-
!@options[:reconnect]
|
1467
|
-
end
|
1823
|
+
uri_object = URI(uri)
|
1468
1824
|
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1825
|
+
# Host and Port
|
1826
|
+
uri_object.hostname ||= "localhost"
|
1827
|
+
uri_object.port ||= DEFAULT_PORT.fetch(uri_object.scheme.to_sym, DEFAULT_PORT[:nats])
|
1472
1828
|
|
1473
|
-
|
1474
|
-
NATS::IO::Socket.new({
|
1475
|
-
uri: @uri,
|
1476
|
-
connect_timeout: DEFAULT_CONNECT_TIMEOUT
|
1477
|
-
})
|
1829
|
+
uri_object
|
1478
1830
|
end
|
1831
|
+
end
|
1832
|
+
end
|
1479
1833
|
|
1480
|
-
|
1481
|
-
|
1482
|
-
require 'nkeys'
|
1483
|
-
require 'base64'
|
1484
|
-
rescue LoadError
|
1485
|
-
raise(Error, "nkeys is not installed")
|
1486
|
-
end
|
1834
|
+
module IO
|
1835
|
+
include Status
|
1487
1836
|
|
1488
|
-
|
1489
|
-
|
1490
|
-
@user_nkey_cb = proc {
|
1491
|
-
seed = File.read(@nkeys_seed).chomp
|
1492
|
-
kp = NKEYS::from_seed(seed)
|
1493
|
-
|
1494
|
-
# Take a copy since original will be gone with the wipe.
|
1495
|
-
pub_key = kp.public_key.dup
|
1496
|
-
kp.wipe!
|
1497
|
-
|
1498
|
-
pub_key
|
1499
|
-
}
|
1500
|
-
|
1501
|
-
@signature_cb = proc { |nonce|
|
1502
|
-
seed = File.read(@nkeys_seed).chomp
|
1503
|
-
kp = NKEYS::from_seed(seed)
|
1504
|
-
raw_signed = kp.sign(nonce)
|
1505
|
-
kp.wipe!
|
1506
|
-
encoded = Base64.urlsafe_encode64(raw_signed)
|
1507
|
-
encoded.gsub('=', '')
|
1508
|
-
}
|
1509
|
-
when @user_credentials
|
1510
|
-
# When the credentials are within a single decorated file.
|
1511
|
-
@user_jwt_cb = proc {
|
1512
|
-
jwt_start = "BEGIN NATS USER JWT".freeze
|
1513
|
-
found = false
|
1514
|
-
jwt = nil
|
1515
|
-
File.readlines(@user_credentials).each do |line|
|
1516
|
-
case
|
1517
|
-
when found
|
1518
|
-
jwt = line.chomp
|
1519
|
-
break
|
1520
|
-
when line.include?(jwt_start)
|
1521
|
-
found = true
|
1522
|
-
end
|
1523
|
-
end
|
1524
|
-
raise(Error, "No JWT found in #{@user_credentials}") if not found
|
1525
|
-
|
1526
|
-
jwt
|
1527
|
-
}
|
1528
|
-
|
1529
|
-
@signature_cb = proc { |nonce|
|
1530
|
-
seed_start = "BEGIN USER NKEY SEED".freeze
|
1531
|
-
found = false
|
1532
|
-
seed = nil
|
1533
|
-
File.readlines(@user_credentials).each do |line|
|
1534
|
-
case
|
1535
|
-
when found
|
1536
|
-
seed = line.chomp
|
1537
|
-
break
|
1538
|
-
when line.include?(seed_start)
|
1539
|
-
found = true
|
1540
|
-
end
|
1541
|
-
end
|
1542
|
-
raise(Error, "No nkey user seed found in #{@user_credentials}") if not found
|
1837
|
+
# Client creates a connection to the NATS Server.
|
1838
|
+
Client = ::NATS::Client
|
1543
1839
|
|
1544
|
-
|
1545
|
-
|
1840
|
+
MAX_RECONNECT_ATTEMPTS = 10
|
1841
|
+
RECONNECT_TIME_WAIT = 2
|
1546
1842
|
|
1547
|
-
|
1548
|
-
|
1549
|
-
kp.wipe
|
1550
|
-
encoded = Base64.urlsafe_encode64(raw_signed)
|
1843
|
+
# Maximum accumulated pending commands bytesize before forcing a flush.
|
1844
|
+
MAX_PENDING_SIZE = 32768
|
1551
1845
|
|
1552
|
-
|
1553
|
-
|
1554
|
-
}
|
1555
|
-
end
|
1556
|
-
end
|
1846
|
+
# Maximum number of flush kicks that can be queued up before we block.
|
1847
|
+
MAX_FLUSH_KICK_SIZE = 1024
|
1557
1848
|
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
opts = {}
|
1849
|
+
# Maximum number of bytes which we will be gathering on a single read.
|
1850
|
+
# TODO: Make dynamic?
|
1851
|
+
MAX_SOCKET_READ_BYTES = 32768
|
1562
1852
|
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
opts[:scheme] = scheme
|
1567
|
-
else
|
1568
|
-
opts[:scheme] = 'nats'
|
1569
|
-
end
|
1853
|
+
# Ping intervals
|
1854
|
+
DEFAULT_PING_INTERVAL = 120
|
1855
|
+
DEFAULT_PING_MAX = 2
|
1570
1856
|
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
opts[:userinfo] = userinfo
|
1576
|
-
else
|
1577
|
-
host, port = uri.split(":")
|
1578
|
-
end
|
1857
|
+
# Default IO timeouts
|
1858
|
+
DEFAULT_CONNECT_TIMEOUT = 2
|
1859
|
+
DEFAULT_READ_WRITE_TIMEOUT = 2
|
1860
|
+
DEFAULT_DRAIN_TIMEOUT = 30
|
1579
1861
|
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1862
|
+
# Default Pending Limits
|
1863
|
+
DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
|
1864
|
+
DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
|
1583
1865
|
|
1584
|
-
|
1585
|
-
|
1586
|
-
connect_uris
|
1587
|
-
end
|
1588
|
-
end
|
1866
|
+
DEFAULT_TOTAL_SUB_CONCURRENCY = 24
|
1867
|
+
DEFAULT_SINGLE_SUB_CONCURRENCY = 1
|
1589
1868
|
|
1590
1869
|
# Implementation adapted from https://github.com/redis/redis-rb
|
1591
1870
|
class Socket
|
@@ -1597,10 +1876,11 @@ module NATS
|
|
1597
1876
|
@write_timeout = options[:write_timeout]
|
1598
1877
|
@read_timeout = options[:read_timeout]
|
1599
1878
|
@socket = nil
|
1879
|
+
@tls = options[:tls]
|
1600
1880
|
end
|
1601
1881
|
|
1602
1882
|
def connect
|
1603
|
-
addrinfo = ::Socket.getaddrinfo(@uri.
|
1883
|
+
addrinfo = ::Socket.getaddrinfo(@uri.hostname, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM)
|
1604
1884
|
addrinfo.each_with_index do |ai, i|
|
1605
1885
|
begin
|
1606
1886
|
@socket = connect_addrinfo(ai, @uri.port, @connect_timeout)
|
@@ -1615,10 +1895,26 @@ module NATS
|
|
1615
1895
|
@socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
1616
1896
|
end
|
1617
1897
|
|
1898
|
+
# (Re-)connect using secure connection if server and client agreed on using it.
|
1899
|
+
def setup_tls!
|
1900
|
+
# Setup TLS connection by rewrapping the socket
|
1901
|
+
tls_socket = OpenSSL::SSL::SSLSocket.new(@socket, @tls.fetch(:context))
|
1902
|
+
|
1903
|
+
# Close TCP socket after closing TLS socket as well.
|
1904
|
+
tls_socket.sync_close = true
|
1905
|
+
|
1906
|
+
# Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
|
1907
|
+
# https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
|
1908
|
+
tls_socket.hostname = @tls[:hostname]
|
1909
|
+
|
1910
|
+
tls_socket.connect
|
1911
|
+
@socket = tls_socket
|
1912
|
+
end
|
1913
|
+
|
1618
1914
|
def read_line(deadline=nil)
|
1619
1915
|
# FIXME: Should accumulate and read in a non blocking way instead
|
1620
1916
|
unless ::IO.select([@socket], nil, nil, deadline)
|
1621
|
-
raise SocketTimeoutError
|
1917
|
+
raise NATS::IO::SocketTimeoutError
|
1622
1918
|
end
|
1623
1919
|
@socket.gets
|
1624
1920
|
end
|
@@ -1631,13 +1927,13 @@ module NATS
|
|
1631
1927
|
if ::IO.select([@socket], nil, nil, deadline)
|
1632
1928
|
retry
|
1633
1929
|
else
|
1634
|
-
raise SocketTimeoutError
|
1930
|
+
raise NATS::IO::SocketTimeoutError
|
1635
1931
|
end
|
1636
1932
|
rescue ::IO::WaitWritable
|
1637
1933
|
if ::IO.select(nil, [@socket], nil, deadline)
|
1638
1934
|
retry
|
1639
1935
|
else
|
1640
|
-
raise SocketTimeoutError
|
1936
|
+
raise NATS::IO::SocketTimeoutError
|
1641
1937
|
end
|
1642
1938
|
end
|
1643
1939
|
rescue EOFError => e
|
@@ -1665,13 +1961,13 @@ module NATS
|
|
1665
1961
|
if ::IO.select(nil, [@socket], nil, deadline)
|
1666
1962
|
retry
|
1667
1963
|
else
|
1668
|
-
raise SocketTimeoutError
|
1964
|
+
raise NATS::IO::SocketTimeoutError
|
1669
1965
|
end
|
1670
1966
|
rescue ::IO::WaitReadable
|
1671
1967
|
if ::IO.select([@socket], nil, nil, deadline)
|
1672
1968
|
retry
|
1673
1969
|
else
|
1674
|
-
raise SocketTimeoutError
|
1970
|
+
raise NATS::IO::SocketTimeoutError
|
1675
1971
|
end
|
1676
1972
|
end
|
1677
1973
|
end
|
@@ -1698,7 +1994,7 @@ module NATS
|
|
1698
1994
|
sock.connect_nonblock(sockaddr)
|
1699
1995
|
rescue Errno::EINPROGRESS, Errno::EALREADY, ::IO::WaitWritable
|
1700
1996
|
unless ::IO.select(nil, [sock], nil, @connect_timeout)
|
1701
|
-
raise SocketTimeoutError
|
1997
|
+
raise NATS::IO::SocketTimeoutError
|
1702
1998
|
end
|
1703
1999
|
|
1704
2000
|
# Confirm that connection was established
|
@@ -1714,39 +2010,11 @@ module NATS
|
|
1714
2010
|
end
|
1715
2011
|
end
|
1716
2012
|
|
1717
|
-
|
1718
|
-
|
1719
|
-
class Subscription
|
1720
|
-
include MonitorMixin
|
1721
|
-
|
1722
|
-
attr_accessor :subject, :queue, :future, :callback, :response, :received, :max, :pending
|
1723
|
-
attr_accessor :pending_queue, :pending_size, :wait_for_msgs_t, :is_slow_consumer
|
1724
|
-
attr_accessor :pending_msgs_limit, :pending_bytes_limit
|
1725
|
-
|
1726
|
-
def initialize
|
1727
|
-
super # required to initialize monitor
|
1728
|
-
@subject = ''
|
1729
|
-
@queue = nil
|
1730
|
-
@future = nil
|
1731
|
-
@callback = nil
|
1732
|
-
@response = nil
|
1733
|
-
@received = 0
|
1734
|
-
@max = nil
|
1735
|
-
@pending = nil
|
1736
|
-
|
1737
|
-
# State from async subscriber messages delivery
|
1738
|
-
@pending_queue = nil
|
1739
|
-
@pending_size = 0
|
1740
|
-
@pending_msgs_limit = nil
|
1741
|
-
@pending_bytes_limit = nil
|
1742
|
-
@wait_for_msgs_t = nil
|
1743
|
-
@is_slow_consumer = false
|
1744
|
-
end
|
1745
|
-
end
|
2013
|
+
NANOSECONDS = 1_000_000_000
|
1746
2014
|
|
1747
|
-
# Implementation of MonotonicTime adapted from
|
1748
|
-
# https://github.com/ruby-concurrency/concurrent-ruby/
|
1749
2015
|
class MonotonicTime
|
2016
|
+
# Implementation of MonotonicTime adapted from
|
2017
|
+
# https://github.com/ruby-concurrency/concurrent-ruby/
|
1750
2018
|
class << self
|
1751
2019
|
case
|
1752
2020
|
when defined?(Process::CLOCK_MONOTONIC)
|
@@ -1763,6 +2031,20 @@ module NATS
|
|
1763
2031
|
::Time.now.to_f
|
1764
2032
|
end
|
1765
2033
|
end
|
2034
|
+
|
2035
|
+
def with_nats_timeout(timeout)
|
2036
|
+
start_time = now
|
2037
|
+
yield
|
2038
|
+
end_time = now
|
2039
|
+
duration = end_time - start_time
|
2040
|
+
if duration > timeout
|
2041
|
+
raise NATS::Timeout.new("nats: timeout")
|
2042
|
+
end
|
2043
|
+
end
|
2044
|
+
|
2045
|
+
def since(t0)
|
2046
|
+
now - t0
|
2047
|
+
end
|
1766
2048
|
end
|
1767
2049
|
end
|
1768
2050
|
end
|