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