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.
@@ -12,8 +12,13 @@
12
12
  # limitations under the License.
13
13
  #
14
14
 
15
- require 'nats/io/parser'
16
- require 'nats/io/version'
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::IO::Client.new
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
- module IO
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
- DEFAULT_PORT = 4222
43
- DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze
67
+ # When the client is connected.
68
+ CONNECTED = 1
44
69
 
45
- MAX_RECONNECT_ATTEMPTS = 10
46
- RECONNECT_TIME_WAIT = 2
70
+ # When the client will no longer attempt to connect to a NATS Server.
71
+ CLOSED = 2
47
72
 
48
- # Maximum accumulated pending commands bytesize before forcing a flush.
49
- MAX_PENDING_SIZE = 32768
73
+ # When the client has disconnected and is attempting to reconnect.
74
+ RECONNECTING = 3
50
75
 
51
- # Maximum number of flush kicks that can be queued up before we block.
52
- MAX_FLUSH_KICK_SIZE = 1024
76
+ # When the client is attempting to connect to a NATS Server for the first time.
77
+ CONNECTING = 4
53
78
 
54
- # Maximum number of bytes which we will be gathering on a single read.
55
- # TODO: Make dynamic?
56
- MAX_SOCKET_READ_BYTES = 32768
79
+ # When the client is draining a connection before closing.
80
+ DRAINING_SUBS = 5
81
+ DRAINING_PUBS = 6
82
+ end
57
83
 
58
- # Ping intervals
59
- DEFAULT_PING_INTERVAL = 120
60
- DEFAULT_PING_MAX = 2
84
+ # Client creates a connection to the NATS Server.
85
+ class Client
86
+ include MonitorMixin
87
+ include Status
61
88
 
62
- # Default IO timeouts
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
- # Default Pending Limits
67
- DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
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
- # Connection States
86
- DISCONNECTED = 0
87
- CONNECTED = 1
88
- CLOSED = 2
89
- RECONNECTING = 3
90
- CONNECTING = 4
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
- class Error < StandardError; end
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
- # When the NATS server sends us an 'ERR' message.
95
- class ServerError < Error; end
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
- # When we detect error on the client side.
98
- class ClientError < Error; end
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
- # When we cannot connect to the server (either initially or after a reconnect).
101
- class ConnectError < Error; end
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
- # When we cannot connect to the server because authorization failed.
104
- class AuthError < ConnectError; end
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
- # When we cannot connect since there are no servers available.
107
- class NoServersError < ConnectError; end
276
+ setup_nkeys_connect if @user_credentials or @nkeys_seed
108
277
 
109
- # When there are no subscribers available to respond.
110
- class NoRespondersError < ConnectError; end
278
+ # Tokens, if set will take preference over the user@server uri token
279
+ @auth_token ||= opts[:auth_token]
111
280
 
112
- # When the connection exhausts max number of pending pings replies.
113
- class StaleConnectionError < Error; end
281
+ # Check for TLS usage
282
+ @tls = @options[:tls]
114
283
 
115
- # When we do not get a result within a specified time.
116
- class Timeout < Error; end
284
+ @inbox_prefix = opts.fetch(:custom_inbox_prefix, @inbox_prefix)
117
285
 
118
- # When there is an i/o timeout with the socket.
119
- class SocketTimeoutError < Error; end
286
+ validate_settings!
120
287
 
121
- # When we use an invalid subject.
122
- class BadSubject < Error; end
288
+ srv = nil
289
+ begin
290
+ srv = select_next_server
123
291
 
124
- # When a subscription hits the pending messages limit.
125
- class SlowConsumer < Error; end
292
+ # Create TCP socket connection to NATS
293
+ @io = create_socket
294
+ @io.connect
126
295
 
127
- class Client
128
- include MonitorMixin
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
- attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri
300
+ # Connection established and now in process of sending CONNECT to NATS
301
+ @status = CONNECTING
131
302
 
132
- def initialize
133
- super # required to initialize monitor
134
- @options = nil
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
- # Read/Write IO
137
- @io = nil
311
+ # Established TCP connection successfully so can start connect
312
+ process_connect_init
138
313
 
139
- # Queues for coalescing writes of commands we need to send to server.
140
- @flush_queue = nil
141
- @pending_queue = nil
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
- # Parser with state
144
- @parser = NATS::Protocol::Parser.new(self)
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
- # Threads for both reading and flushing command
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
- # Info that we get from the server
152
- @server_info = { }
333
+ if should_not_reconnect?
334
+ @disconnect_cb.call(e) if @disconnect_cb
335
+ raise e
336
+ end
153
337
 
154
- # URI from server to which we are currently connected
155
- @uri = nil
156
- @server_pool = []
338
+ # Clean up any connecting state and close connection without
339
+ # triggering the disconnection/closed callbacks.
340
+ close_connection(DISCONNECTED, false)
157
341
 
158
- @status = DISCONNECTED
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
- # Subscriptions
161
- @subs = { }
162
- @ssid = 0
163
-
164
- # Ping interval
165
- @pings_outstanding = 0
166
- @pongs_received = 0
167
- @pongs = []
168
- @pongs.extend(MonitorMixin)
169
-
170
- # Accounting
171
- @pending_size = 0
172
- @stats = {
173
- in_msgs: 0,
174
- out_msgs: 0,
175
- in_bytes: 0,
176
- out_bytes: 0,
177
- reconnects: 0
178
- }
346
+ # Continue retrying until there are no options left in the server pool
347
+ retry
348
+ end
179
349
 
180
- # Sticky error
181
- @last_err = nil
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
- # Async callbacks, no ops by default.
184
- @err_cb = proc { }
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
- # Secure TLS options
190
- @tls = nil
360
+ # Connected to NATS so Ready to start parser loop, flusher and ping interval
361
+ start_threads!
191
362
 
192
- # Hostname of current server; used for when TLS host
193
- # verification is enabled.
194
- @hostname = nil
195
- @single_url_connect_used = false
363
+ self
364
+ end
196
365
 
197
- # Track whether connect has been already been called.
198
- @connect_called = false
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
- # New style request/response implementation.
201
- @resp_sub = nil
202
- @resp_map = nil
203
- @resp_sub_prefix = nil
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
- # NKEYS
207
- @user_credentials = nil
208
- @nkeys_seed = nil
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
- # Establishes connection to NATS.
215
- def connect(uri=nil, opts={})
216
- synchronize do
217
- # In case it has been connected already, then do not need to call this again.
218
- return if @connect_called
219
- @connect_called = true
220
- end
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
- # Convert URI to string if needed.
223
- uri = uri.to_s if uri.is_a?(URI)
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
- opts[:verbose] = false if opts[:verbose].nil?
242
- opts[:pedantic] = false if opts[:pedantic].nil?
243
- opts[:reconnect] = true if opts[:reconnect].nil?
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
- if @options[:old_style_request]
278
- # Replace for this instance the implementation
279
- # of request to use the old_request style.
280
- class << self; alias_method :request, :old_request; end
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
- # NKEYS
284
- @user_credentials ||= opts[:user_credentials]
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
- # Check for TLS usage
289
- @tls = @options[:tls]
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
- srv = nil
292
- begin
293
- srv = select_next_server
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
- # Create TCP socket connection to NATS
296
- @io = create_socket
297
- @io.connect
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
- # Capture state that we have had a TCP connection established against
300
- # this server and could potentially be used for reconnecting.
301
- srv[:was_connected] = true
455
+ cb = nil
456
+ sub.synchronize do
302
457
 
303
- # Connection established and now in process of sending CONNECT to NATS
304
- @status = CONNECTING
458
+ # Decrease pending size since consumed already
459
+ sub.pending_size -= msg.data.size
460
+ cb = sub.callback
461
+ end
305
462
 
306
- # Use the hostname from the server for TLS hostname verification.
307
- if client_using_secure_connection? and single_url_connect_used?
308
- # Always reuse the original hostname used to connect.
309
- @hostname ||= srv[:hostname]
310
- else
311
- @hostname = srv[:hostname]
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
- # Established TCP connection successfully so can start connect
315
- process_connect_init
482
+ sub
483
+ end
316
484
 
317
- # Reset reconnection attempts if connection is valid
318
- srv[:reconnect_attempts] = 0
319
- srv[:auth_required] ||= true if @server_info[:auth_required]
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
- # Add back to rotation since successfully connected
322
- server_pool << srv
323
- rescue NoServersError => e
324
- @disconnect_cb.call(e) if @disconnect_cb
325
- raise @last_err || e
326
- rescue => e
327
- # Capture sticky error
328
- synchronize do
329
- @last_err = e
330
- srv[:auth_required] ||= true if @server_info[:auth_required]
331
- server_pool << srv if can_reuse_server?(srv)
332
- end
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
- @err_cb.call(e) if @err_cb
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
- if should_not_reconnect?
337
- @disconnect_cb.call(e) if @disconnect_cb
338
- raise e
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
- # Clean up any connecting state and close connection without
342
- # triggering the disconnection/closed callbacks.
343
- close_connection(DISCONNECTED, false)
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
- # always sleep here to safe guard against errors before current[:was_connected]
346
- # is set for the first time
347
- sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
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
- # Continue retrying until there are no options left in the server pool
350
- retry
351
- end
544
+ response
545
+ end
352
546
 
353
- # Initialize queues and loops for message dispatching and processing engine
354
- @flush_queue = SizedQueue.new(MAX_FLUSH_KICK_SIZE)
355
- @pending_queue = SizedQueue.new(MAX_PENDING_SIZE)
356
- @pings_outstanding = 0
357
- @pongs_received = 0
358
- @pending_size = 0
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
- # Server roundtrip went ok so consider to be connected at this point
361
- @status = CONNECTED
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
- # Connected to NATS so Ready to start parser loop, flusher and ping interval
364
- start_threads!
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
- def publish(subject, msg=EMPTY_MSG, opt_reply=nil, &blk)
368
- raise BadSubject if !subject or subject.empty?
369
- msg_size = msg.bytesize
598
+ response
599
+ end
370
600
 
371
- # Accounting
372
- @stats[:out_msgs] += 1
373
- @stats[:out_bytes] += msg_size
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
- send_command("PUB #{subject} #{opt_reply} #{msg_size}\r\n#{msg}\r\n")
376
- @flush_queue << :pub if @flush_queue.empty?
624
+ return s
377
625
  end
378
626
 
379
- # Publishes a NATS::Msg that may include headers.
380
- def publish_msg(msg)
381
- raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
382
- raise BadSubject if !msg.subject or msg.subject.empty?
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
- msg.reply ||= ''
385
- msg.data ||= ''
386
- msg_size = msg.data.bytesize
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
- # Accounting
389
- @stats[:out_msgs] += 1
390
- @stats[:out_bytes] += msg_size
639
+ sid = nil
640
+ synchronize do
641
+ sid = (@ssid += 1)
642
+ sub.sid = sid
643
+ @subs[sid] = sub
644
+ end
391
645
 
392
- if msg.header
393
- hdr = ''
394
- hdr << NATS_HDR_LINE
395
- msg.header.each do |k, v|
396
- hdr << "#{k}: #{v}#{CR_LF}"
397
- end
398
- hdr << CR_LF
399
- hdr_len = hdr.bytesize
400
- total_size = msg_size + hdr_len
401
- send_command("HPUB #{msg.subject} #{msg.reply} #{hdr_len} #{total_size}\r\n#{hdr}#{msg.data}\r\n")
402
- else
403
- send_command("PUB #{msg.subject} #{msg.reply} #{msg_size}\r\n#{msg.data}\r\n")
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
- @flush_queue << :pub if @flush_queue.empty?
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
- # Create subscription which is dispatched asynchronously
410
- # messages to a callback.
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
- begin
452
- case cb.arity
453
- when 0 then cb.call
454
- when 1 then cb.call(msg.data)
455
- when 2 then cb.call(msg.data, msg.reply)
456
- when 3 then cb.call(msg.data, msg.reply, msg.subject)
457
- else cb.call(msg.data, msg.reply, msg.subject, msg.header)
458
- end
459
- rescue => e
460
- synchronize do
461
- @err_cb.call(e) if @err_cb
462
- end
463
- end
464
- end
465
- end
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
- sid
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
- # Sends a request using expecting a single response using a
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
- # If a block was given then fallback to method using auto unsubscribe.
479
- return old_request(subject, payload, opts, &blk) if blk
480
- return old_request(subject, payload, opts) if opts[:old_style]
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
- token = nil
483
- inbox = nil
484
- future = nil
485
- response = nil
486
- timeout = opts[:timeout] ||= 0.5
487
- synchronize do
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
- # Create token for this request.
491
- token = @nuid.next
492
- inbox = "#{@resp_sub_prefix}.#{token}"
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
- # Create the a future for the request that will
495
- # get signaled when it receives the request.
496
- future = @resp_sub.new_cond
497
- @resp_map[token][:future] = future
498
- end
706
+ def connected_server
707
+ connected? ? @uri : nil
708
+ end
499
709
 
500
- # Publish request and wait for reply.
501
- publish(subject, payload, inbox)
502
- begin
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
- # Check if there is a response already.
514
- synchronize do
515
- result = @resp_map[token]
516
- response = result[:response]
517
- @resp_map.delete(token)
518
- end
714
+ def connecting?
715
+ @status == CONNECTING
716
+ end
519
717
 
520
- if response and response.header
521
- status = response.header[STATUS_HDR]
522
- raise NoRespondersError if status == "503"
523
- end
718
+ def reconnecting?
719
+ @status == RECONNECTING
720
+ end
524
721
 
525
- response
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
- # Makes a NATS request using a NATS::Msg that may include headers.
529
- def request_msg(msg, opts={})
530
- raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
531
- raise BadSubject if !msg.subject or msg.subject.empty?
731
+ is_draining = false
732
+ synchronize do
733
+ is_draining = true if @drain_t
734
+ end
532
735
 
533
- token = nil
534
- inbox = nil
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
- # Create token for this request.
542
- token = @nuid.next
543
- inbox = "#{@resp_sub_prefix}.#{token}"
739
+ def on_error(&callback)
740
+ @err_cb = callback
741
+ end
544
742
 
545
- # Create the a future for the request that will
546
- # get signaled when it receives the request.
547
- future = @resp_sub.new_cond
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
- # Publish request and wait for reply.
555
- publish_msg(msg)
556
- begin
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
- # Check if there is a response already.
568
- synchronize do
569
- result = @resp_map[token]
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
- if response and response.header
575
- status = response.header[STATUS_HDR]
576
- raise NoRespondersError if status == "503"
577
- end
755
+ def last_error
756
+ synchronize do
757
+ @last_err
758
+ end
759
+ end
578
760
 
579
- response
580
- end
581
-
582
- # Sends a request creating an ephemeral subscription for the request,
583
- # expecting a single response or raising a timeout in case the request
584
- # is not retrieved within the specified deadline.
585
- # If given a callback, then the request happens asynchronously.
586
- def old_request(subject, payload, opts={}, &blk)
587
- return unless subject
588
- inbox = new_inbox
589
-
590
- # If a callback was passed, then have it process
591
- # the messages asynchronously and return the sid.
592
- if blk
593
- opts[:max] ||= 1
594
- s = subscribe(inbox, opts) do |msg, reply, subject, header|
595
- case blk.arity
596
- when 0 then blk.call
597
- when 1 then blk.call(msg)
598
- when 2 then blk.call(msg, reply)
599
- when 3 then blk.call(msg, reply, subject)
600
- else blk.call(msg, reply, subject, header)
601
- end
602
- end
603
- publish(subject, payload, inbox)
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
- return s
606
- end
769
+ synchronize do
770
+ @drain_t ||= Thread.new { do_drain }
771
+ end
772
+ end
607
773
 
608
- # In case block was not given, handle synchronously
609
- # with a timeout and only allow a single response.
610
- timeout = opts[:timeout] ||= 0.5
611
- opts[:max] = 1
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
- sub = Subscription.new
614
- sub.subject = inbox
615
- sub.received = 0
616
- future = sub.new_cond
617
- sub.future = future
786
+ private
618
787
 
619
- sid = nil
620
- synchronize do
621
- sid = (@ssid += 1)
622
- @subs[sid] = sub
623
- end
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
- send_command("SUB #{inbox} #{sid}#{CR_LF}")
626
- @flush_queue << :sub
627
- unsubscribe(sid, 1)
795
+ def process_info(line)
796
+ parsed_info = JSON.parse(line)
628
797
 
629
- sub.synchronize do
630
- # Publish the request and then wait for the response...
631
- publish(subject, payload, inbox)
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
- with_nats_timeout(timeout) do
634
- future.wait(timeout)
635
- end
805
+ info
636
806
  end
637
- response = sub.response
638
807
 
639
- if response and response.header
640
- status = response.header[STATUS_HDR]
641
- raise NoRespondersError if status == "503"
642
- end
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
- response
645
- end
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
- # Auto unsubscribes the server by sending UNSUB command and throws away
648
- # subscription in case already present and has received enough messages.
649
- def unsubscribe(sid, opt_max=nil)
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
- return unless sub = @subs[sid]
655
- synchronize do
656
- sub.max = opt_max
657
- @subs.delete(sid) unless (sub.max && (sub.received < sub.max))
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
- # Stop messages delivery thread for async subscribers
660
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
661
- sub.wait_for_msgs_t.exit
662
- sub.pending_queue.clear
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
- # Send a ping and wait for a pong back within a timeout.
668
- def flush(timeout=60)
669
- # Schedule sending a PING, and block until we receive PONG back,
670
- # or raise a timeout in case the response is past the deadline.
671
- pong = @pongs.new_cond
672
- @pongs.synchronize do
673
- @pongs << pong
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
- # Flush once pong future has been prepared
676
- @pending_queue << PING_REQUEST
677
- @flush_queue << :ping
678
- with_nats_timeout(timeout) do
679
- pong.wait(timeout)
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
- alias :servers :server_pool
877
+ hdr
878
+ end
879
+
880
+ # Methods only used by the parser
685
881
 
686
- def discovered_servers
687
- servers.select {|s| s[:discovered] }
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
- # Methods only used by the parser
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
- def process_pong
693
- # Take first pong wait and signal any flush in case there was one
694
- @pongs.synchronize do
695
- pong = @pongs.pop
696
- pong.signal unless pong.nil?
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
- # Received a ping so respond back with a pong
703
- def process_ping
704
- @pending_queue << PONG_RESPONSE
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
- # Handles protocol errors being sent by the server.
711
- def process_err(err)
712
- # In case of permissions violation then dispatch the error callback
713
- # while holding the lock.
714
- e = synchronize do
715
- current = server_pool.first
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 err =~ /'Stale Connection'/
718
- @last_err = NATS::IO::StaleConnectionError.new(err)
719
- when current && current[:auth_required]
720
- # We cannot recover from auth errors so mark it to avoid
721
- # retrying to unecessarily next time.
722
- current[:error_received] = true
723
- @last_err = NATS::IO::AuthError.new(err)
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
- # In case of a request which requires a future
758
- # do so here already while holding the lock and return
759
- if sub.future
760
- future = sub.future
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
- return
766
- elsif sub.pending_queue
767
- # Async subscribers use a sized queue for processing
768
- # and should be able to consume messages in parallel.
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
- # Only dispatch message when sure that it would not block
776
- # the main read loop from the parser.
777
- msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr)
778
- sub.pending_queue << msg
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
- def process_hdr(header)
791
- hdr = nil
792
- if header
793
- hdr = {}
794
- lines = header.lines
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
- # Check if it is an inline status and description.
797
- if lines.count <= 2
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
- if NATS_HDR_LINE_SIZE+2 < status_hdr.bytesize
802
- desc = status_hdr.slice(NATS_HDR_LINE_SIZE+STATUS_MSG_LEN, status_hdr.bytesize)
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
- hdr
819
- end
990
+ # Track connection attempts to this server
991
+ srv[:reconnect_attempts] ||= 0
992
+ srv[:reconnect_attempts] += 1
820
993
 
821
- def process_info(line)
822
- parsed_info = JSON.parse(line)
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
- # INFO can be received asynchronously too,
825
- # so has to be done under the lock.
826
- synchronize do
827
- # Symbolize keys from parsed info line
828
- @server_info = parsed_info.reduce({}) do |info, (k,v)|
829
- info[k.to_sym] = v
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
- info
832
- end
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
- # Skip in case it is the current server which we already know
843
- next if @uri.host == u.host && @uri.port == u.port
1005
+ def server_using_secure_connection?
1006
+ @server_info[:ssl_required] || @server_info[:tls_required]
1007
+ end
844
1008
 
845
- present = server_pool.detect do |srv|
846
- srv[:uri].host == u.host && srv[:uri].port == u.port
847
- end
1009
+ def client_using_secure_connection?
1010
+ @uri.scheme == "tls" || @tls
1011
+ end
848
1012
 
849
- if not present
850
- # Let explicit user and pass options set the credentials.
851
- u.user = options[:user] if options[:user]
852
- u.password = options[:pass] if options[:pass]
1013
+ def single_url_connect_used?
1014
+ @single_url_connect_used
1015
+ end
853
1016
 
854
- # Use creds from the current server if not set explicitly.
855
- if @uri
856
- u.user ||= @uri.user if @uri.user
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
- # NOTE: Auto discovery won't work here when TLS host verification is enabled.
861
- srv = { :uri => u, :reconnect_attempts => 0, :discovered => true, :hostname => u.host }
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
- # Include in server pool but keep current one as the first one.
868
- server_pool.push(*srvs)
869
- end
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
- # Close connection to NATS, flushing in case connection is alive
876
- # and there are any pending messages, should not be used while
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
- def new_inbox
883
- "_INBOX.#{SecureRandom.hex(13)}"
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
- def connected_server
887
- connected? ? @uri : nil
888
- end
1066
+ send_command("UNSUB #{sid}#{CR_LF}")
1067
+ @flush_queue << :drain
889
1068
 
890
- def connected?
891
- @status == CONNECTED
892
- end
1069
+ synchronize { sub = @subs[sid] }
1070
+ return unless sub
1071
+ end
893
1072
 
894
- def connecting?
895
- @status == CONNECTING
896
- end
1073
+ def do_drain
1074
+ synchronize { @status = DRAINING_SUBS }
897
1075
 
898
- def reconnecting?
899
- @status == RECONNECTING
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
- def closed?
903
- @status == CLOSED
904
- end
1086
+ # Wait until all subs have no pending messages.
1087
+ drain_timeout = MonotonicTime::now + @options[:drain_timeout]
1088
+ to_delete = []
905
1089
 
906
- def on_error(&callback)
907
- @err_cb = callback
908
- end
1090
+ loop do
1091
+ break if MonotonicTime::now > drain_timeout
1092
+ sleep 0.1
909
1093
 
910
- def on_disconnect(&callback)
911
- @disconnect_cb = callback
912
- end
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
- def on_reconnect(&callback)
915
- @reconnect_cb = callback
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
- def on_close(&callback)
919
- @close_cb = callback
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
- def last_error
923
- synchronize do
924
- @last_err
925
- end
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
- private
1154
+ def auth_connection?
1155
+ !@uri.user.nil?
1156
+ end
929
1157
 
930
- def select_next_server
931
- raise NoServersError.new("nats: No servers available") if server_pool.empty?
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
- # Pick next from head of the list
934
- srv = server_pool.shift
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
- # Track connection attempts to this server
937
- srv[:reconnect_attempts] ||= 0
938
- srv[:reconnect_attempts] += 1
1186
+ cs[:auth_token] = @auth_token if @auth_token
939
1187
 
940
- # Back off in case we are reconnecting to it and have been connected
941
- sleep @options[:reconnect_time_wait] if should_delay_connect?(srv)
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
- # Set url of the server to which we would be connected
944
- @uri = srv[:uri]
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
- srv
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
- def server_using_secure_connection?
952
- @server_info[:ssl_required] || @server_info[:tls_required]
953
- end
1208
+ synchronize do
1209
+ @last_err = e
1210
+ err_cb_call(self, e, nil) if @err_cb
954
1211
 
955
- def client_using_secure_connection?
956
- @uri.scheme == "tls" || @tls
957
- end
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
- def single_url_connect_used?
960
- @single_url_connect_used
961
- end
1219
+ # TODO: Reconnecting pending buffer?
962
1220
 
963
- def send_command(command)
964
- @pending_size += command.bytesize
965
- @pending_queue << command
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
- # TODO: kick flusher here in case pending_size growing large
968
- end
1238
+ Thread.exit
1239
+ return
1240
+ end
969
1241
 
970
- def auth_connection?
971
- !@uri.user.nil?
1242
+ # Otherwise, stop trying to reconnect and close the connection
1243
+ @status = DISCONNECTED
972
1244
  end
973
1245
 
974
- def connect_command
975
- cs = {
976
- :verbose => @options[:verbose],
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
- case
985
- when auth_connection?
986
- if @uri.password
987
- cs[:user] = @uri.user
988
- cs[:pass] = @uri.password
989
- else
990
- cs[:auth_token] = @uri.user
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
- if @server_info[:headers]
1003
- cs[:headers] = @server_info[:headers]
1004
- cs[:no_responders] = if @options[:no_responders] == false
1005
- @options[:no_responders]
1006
- else
1007
- @server_info[:headers]
1008
- end
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
- def with_nats_timeout(timeout)
1015
- start_time = MonotonicTime.now
1016
- yield
1017
- end_time = MonotonicTime.now
1018
- duration = end_time - start_time
1019
- raise NATS::IO::Timeout.new("nats: timeout") if duration > timeout
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
- connecting? || closed? || reconnecting?
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
- @err_cb.call(e) if @err_cb
1033
-
1034
- # If we were connected and configured to reconnect,
1035
- # then trigger disconnect and start reconnection logic
1036
- if connected? and should_reconnect?
1037
- @status = RECONNECTING
1038
- @io.close if @io
1039
- @io = nil
1040
-
1041
- # TODO: Reconnecting pending buffer?
1042
-
1043
- # Do reconnect under a different thread than the one
1044
- # in which we got the error.
1045
- Thread.new do
1046
- begin
1047
- # Abort currently running reads in case they're around
1048
- # FIXME: There might be more graceful way here...
1049
- @read_loop_thread.exit if @read_loop_thread.alive?
1050
- @flusher_thread.exit if @flusher_thread.alive?
1051
- @ping_interval_thread.exit if @ping_interval_thread.alive?
1052
-
1053
- attempt_reconnect
1054
- rescue NoServersError => e
1055
- @last_err = e
1056
- close
1057
- end
1058
- end
1310
+ err_cb_call(self, e, nil) if @err_cb
1311
+ end
1059
1312
 
1060
- Thread.exit
1061
- return
1062
- end
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
- # Otherwise, stop trying to reconnect and close the connection
1065
- @status = DISCONNECTED
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
- # Otherwise close the connection to NATS
1069
- close
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
- # Gathers data from the socket and sends it to the parser.
1073
- def read_loop
1074
- loop do
1075
- begin
1076
- should_bail = synchronize do
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
- # TODO: Remove timeout and just wait to be ready
1085
- data = @io.read(MAX_SOCKET_READ_BYTES)
1086
- @parser.parse(data) if data
1087
- rescue Errno::ETIMEDOUT
1088
- # FIXME: We do not really need a timeout here...
1089
- retry
1090
- rescue => e
1091
- # In case of reading/parser errors, trigger
1092
- # reconnection logic in case desired.
1093
- process_op_error(e)
1094
- end
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
- # Waits for client to notify the flusher that it will be
1099
- # it is sending a command.
1100
- def flusher_loop
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
- should_bail = synchronize do
1106
- @status != CONNECTED || @status == CONNECTING
1107
- end
1108
- return if should_bail
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
- # Skip in case nothing remains pending already.
1111
- next if @pending_queue.empty?
1371
+ # Setup TLS connection by rewrapping the socket
1372
+ tls_socket = OpenSSL::SSL::SSLSocket.new(@io.socket, tls_context)
1112
1373
 
1113
- # FIXME: should limit how many commands to take at once
1114
- # since producers could be adding as many as possible
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
- process_op_error(e)
1127
- return
1128
- end if @io
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
- synchronize do
1131
- @pending_size = 0
1132
- end
1133
- end
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
- def ping_interval_loop
1137
- loop do
1138
- sleep @options[:ping_interval]
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
- # Skip ping interval until connected
1141
- next if !connected?
1395
+ # Send ping/pong after connect
1396
+ @io.write(PING_REQUEST)
1142
1397
 
1143
- if @pings_outstanding >= @options[:max_outstanding_pings]
1144
- process_op_error(StaleConnectionError.new("nats: stale connection"))
1145
- return
1146
- end
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
- @pings_outstanding += 1
1149
- send_command(PING_REQUEST)
1150
- @flush_queue << :ping
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
- rescue => e
1153
- process_op_error(e)
1413
+ else
1414
+ raise NATS::IO::ConnectError.new("expected PONG, got #{next_op}")
1154
1415
  end
1416
+ end
1155
1417
 
1156
- def process_connect_init
1157
- line = @io.read_line(options[:connect_timeout])
1158
- if !line or line.empty?
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
- if match = line.match(NATS::Protocol::INFO)
1163
- info_json = match.captures.first
1164
- process_info(info_json)
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
- raise ConnectError.new("nats: protocol exception, INFO not valid")
1440
+ @hostname = srv[:hostname]
1167
1441
  end
1168
1442
 
1169
- case
1170
- when (server_using_secure_connection? and client_using_secure_connection?)
1171
- tls_context = nil
1443
+ # Established TCP connection successfully so can start connect
1444
+ process_connect_init
1172
1445
 
1173
- if @tls
1174
- # Allow prepared context and customizations via :tls opts
1175
- tls_context = @tls[:context] if @tls[:context]
1176
- else
1177
- # Defaults
1178
- tls_context = OpenSSL::SSL::SSLContext.new
1179
-
1180
- # Use the default verification options from Ruby:
1181
- # https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
1182
- #
1183
- # Insecure TLS versions not supported already:
1184
- # https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
1185
- #
1186
- tls_context.set_params
1187
- end
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
- # Setup TLS connection by rewrapping the socket
1190
- tls_socket = OpenSSL::SSL::SSLSocket.new(@io.socket, tls_context)
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
- # Close TCP socket after closing TLS socket as well.
1193
- tls_socket.sync_close = true
1460
+ @last_err = e
1194
1461
 
1195
- # Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
1196
- # https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
1197
- tls_socket.hostname = @hostname
1462
+ # Trigger async error handler
1463
+ err_cb_call(self, e, nil) if @err_cb
1198
1464
 
1199
- tls_socket.connect
1200
- @io.socket = tls_socket
1201
- when (server_using_secure_connection? and !client_using_secure_connection?)
1202
- raise NATS::IO::ConnectError.new('TLS/SSL required by server')
1203
- when (client_using_secure_connection? and !server_using_secure_connection?)
1204
- raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1205
- else
1206
- # Otherwise, use a regular connection.
1207
- end
1465
+ # Continue retrying until there are no options left in the server pool
1466
+ retry
1467
+ end
1208
1468
 
1209
- # Send connect and process synchronously. If using TLS,
1210
- # it should have handled upgrading at this point.
1211
- @io.write(connect_command)
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
- # Send ping/pong after connect
1214
- @io.write(PING_REQUEST)
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
- next_op = @io.read_line(options[:connect_timeout])
1217
- if @options[:verbose]
1218
- # Need to get another command here if verbose
1219
- raise NATS::IO::ConnectError.new("expected to receive +OK") unless next_op =~ NATS::Protocol::OK
1220
- next_op = @io.read_line(options[:connect_timeout])
1221
- end
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
- case next_op
1224
- when NATS::Protocol::PONG
1225
- when NATS::Protocol::ERR
1226
- if @server_info[:auth_required]
1227
- raise NATS::IO::AuthError.new($1)
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
- # Reconnect logic, this is done while holding the lock.
1237
- def attempt_reconnect
1238
- @disconnect_cb.call(@last_err) if @disconnect_cb
1508
+ # Kick the flusher so it bails due to closed state
1509
+ @flush_queue << :fallout if @flush_queue
1510
+ Thread.pass
1239
1511
 
1240
- # Clear sticky error
1241
- @last_err = nil
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
- # Do reconnect
1244
- srv = nil
1245
- begin
1246
- srv = select_next_server
1518
+ if @flusher_thread and @flusher_thread.alive?
1519
+ @flusher_thread.exit
1520
+ end
1247
1521
 
1248
- # Establish TCP connection with new server
1249
- @io = create_socket
1250
- @io.connect
1251
- @stats[:reconnects] += 1
1522
+ if @read_loop_thread and @read_loop_thread.alive?
1523
+ @read_loop_thread.exit
1524
+ end
1252
1525
 
1253
- # Set hostname to use for TLS hostname verification
1254
- if client_using_secure_connection? and single_url_connect_used?
1255
- # Reuse original hostname name in case of using TLS.
1256
- @hostname ||= srv[:hostname]
1257
- else
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
- # Established TCP connection successfully so can start connect
1262
- process_connect_init
1263
-
1264
- # Reset reconnection attempts if connection is valid
1265
- srv[:reconnect_attempts] = 0
1266
- srv[:auth_required] ||= true if @server_info[:auth_required]
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
- # Add back to rotation since successfully connected
1269
- server_pool << srv
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
- # Trigger async error handler
1281
- @err_cb.call(e) if @err_cb
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
- # Continue retrying until there are no options left in the server pool
1284
- retry
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
- # Clear pending flush calls and reset state before restarting loops
1288
- @flush_queue.clear
1289
- @pings_outstanding = 0
1290
- @pongs_received = 0
1563
+ @status = conn_status
1291
1564
 
1292
- # Replay all subscriptions
1293
- @subs.each_pair do |sid, sub|
1294
- @io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
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
- # Flush anything which was left pending, in case of errors during flush
1298
- # then we should raise error then retry the reconnect logic
1299
- cmds = []
1300
- cmds << @pending_queue.pop until @pending_queue.empty?
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
- # Reset parser state here to avoid unknown protocol errors
1306
- # on reconnect...
1307
- @parser.reset!
1579
+ # Flusher loop for sending commands
1580
+ @flusher_thread = Thread.new { flusher_loop }
1581
+ @flusher_thread.abort_on_exception = true
1308
1582
 
1309
- # Now connected to NATS, and we can restart parser loop, flusher
1310
- # and ping interval
1311
- start_threads!
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
- # Dispatch the reconnected callback while holding lock
1314
- # which we should have already
1315
- @reconnect_cb.call if @reconnect_cb
1316
- end
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
- def close_connection(conn_status, do_cbs=true)
1319
- synchronize do
1320
- if @status == CLOSED
1321
- @status = conn_status
1322
- return
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
- # Kick the flusher so it bails due to closed state
1327
- @flush_queue << :fallout if @flush_queue
1328
- Thread.pass
1329
-
1330
- # FIXME: More graceful way of handling the following?
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
- if @flusher_thread and @flusher_thread.alive?
1337
- @flusher_thread.exit
1338
- end
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
- if @read_loop_thread and @read_loop_thread.alive?
1341
- @read_loop_thread.exit
1342
- end
1631
+ def can_reuse_server?(server)
1632
+ return false if server.nil?
1343
1633
 
1344
- # TODO: Delete any other state which we are not using here too.
1345
- synchronize do
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
- # Try to write any pending flushes in case
1354
- # we have a connection then close it.
1355
- should_flush = (@pending_queue && @io && @io.socket && !@io.closed?)
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
- # FIXME: Fails when empty on TLS connection?
1361
- @io.write(cmds.join) unless cmds.empty?
1362
- rescue => e
1363
- @last_err = e
1364
- @err_cb.call(e) if @err_cb
1365
- end if should_flush
1366
-
1367
- # Destroy any remaining subscriptions.
1368
- @subs.each do |_, sub|
1369
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1370
- sub.wait_for_msgs_t.exit
1371
- sub.pending_queue.clear
1372
- end
1373
- end
1374
- @subs.clear
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
- if do_cbs
1377
- @disconnect_cb.call(@last_err) if @disconnect_cb
1378
- @close_cb.call if @close_cb
1379
- end
1645
+ def should_delay_connect?(server)
1646
+ server[:was_connected] && server[:reconnect_attempts] >= 0
1647
+ end
1380
1648
 
1381
- @status = conn_status
1649
+ def should_not_reconnect?
1650
+ !@options[:reconnect]
1651
+ end
1382
1652
 
1383
- # Close the established connection in case
1384
- # we still have it.
1385
- if @io
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
- def start_threads!
1393
- # Reading loop for gathering data
1394
- @read_loop_thread = Thread.new { read_loop }
1395
- @read_loop_thread.abort_on_exception = true
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
- # Flusher loop for sending commands
1398
- @flusher_thread = Thread.new { flusher_loop }
1399
- @flusher_thread.abort_on_exception = true
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
- # Ping interval handling for keeping alive the connection
1402
- @ping_interval_thread = Thread.new { ping_interval_loop }
1403
- @ping_interval_thread.abort_on_exception = true
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
- # Prepares requests subscription that handles the responses
1407
- # for the new style request response.
1408
- def start_resp_mux_sub!
1409
- @resp_sub_prefix = "_INBOX.#{@nuid.next}"
1410
- @resp_map = Hash.new { |h,k| h[k] = { }}
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
- @resp_sub = Subscription.new
1413
- @resp_sub.subject = "#{@resp_sub_prefix}.*"
1414
- @resp_sub.received = 0
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
- # FIXME: Allow setting pending limits for responses mux subscription.
1417
- @resp_sub.pending_msgs_limit = DEFAULT_SUB_PENDING_MSGS_LIMIT
1418
- @resp_sub.pending_bytes_limit = DEFAULT_SUB_PENDING_BYTES_LIMIT
1419
- @resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
1420
- @resp_sub.wait_for_msgs_t = Thread.new do
1421
- loop do
1422
- msg = @resp_sub.pending_queue.pop
1423
- @resp_sub.pending_size -= msg.data.size
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
- # Pick the token and signal the request under the mutex
1426
- # from the subscription itself.
1427
- token = msg.subject.split('.').last
1428
- future = nil
1429
- synchronize do
1430
- future = @resp_map[token][:future]
1431
- @resp_map[token][:response] = msg
1432
- end
1703
+ pub_key
1704
+ }
1705
+ end
1433
1706
 
1434
- # Signal back that the response has arrived.
1435
- @resp_sub.synchronize do
1436
- future.signal
1437
- end
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
- sid = (@ssid += 1)
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
- def can_reuse_server?(server)
1448
- return false if server.nil?
1725
+ jwt
1726
+ }
1727
+ end
1449
1728
 
1450
- # We can always reuse servers with infinite reconnects settings
1451
- return true if @options[:max_reconnect_attempts] < 0
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
- # In case of hard errors like authorization errors, drop the server
1454
- # already since won't be able to connect.
1455
- return false if server[:error_received]
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
- # We will retry a number of times to reconnect to a server.
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
- def should_delay_connect?(server)
1462
- server[:was_connected] && server[:reconnect_attempts] >= 0
1463
- end
1747
+ kp = NKEYS::from_seed(seed)
1748
+ raw_signed = kp.sign(nonce)
1464
1749
 
1465
- def should_not_reconnect?
1466
- !@options[:reconnect]
1467
- end
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
- def should_reconnect?
1470
- @options[:reconnect]
1471
- end
1755
+ # Remove padding
1756
+ encoded.gsub('=', '')
1757
+ }
1758
+ end
1472
1759
 
1473
- def create_socket
1474
- NATS::IO::Socket.new({
1475
- uri: @uri,
1476
- connect_timeout: DEFAULT_CONNECT_TIMEOUT
1477
- })
1478
- end
1760
+ def process_uri(uris)
1761
+ connect_uris = []
1762
+ uris.split(',').each do |uri|
1763
+ opts = {}
1479
1764
 
1480
- def setup_nkeys_connect
1481
- begin
1482
- require 'nkeys'
1483
- require 'base64'
1484
- rescue LoadError
1485
- raise(Error, "nkeys is not installed")
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
- case
1489
- when @nkeys_seed
1490
- @user_nkey_cb = proc {
1491
- seed = File.read(@nkeys_seed).chomp
1492
- kp = NKEYS::from_seed(seed)
1493
-
1494
- # Take a copy since original will be gone with the wipe.
1495
- pub_key = kp.public_key.dup
1496
- kp.wipe!
1497
-
1498
- pub_key
1499
- }
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
- @signature_cb = proc { |nonce|
1502
- seed = File.read(@nkeys_seed).chomp
1503
- kp = NKEYS::from_seed(seed)
1504
- raw_signed = kp.sign(nonce)
1505
- kp.wipe!
1506
- encoded = Base64.urlsafe_encode64(raw_signed)
1507
- encoded.gsub('=', '')
1508
- }
1509
- when @user_credentials
1510
- # When the credentials are within a single decorated file.
1511
- @user_jwt_cb = proc {
1512
- jwt_start = "BEGIN NATS USER JWT".freeze
1513
- found = false
1514
- jwt = nil
1515
- File.readlines(@user_credentials).each do |line|
1516
- case
1517
- when found
1518
- jwt = line.chomp
1519
- break
1520
- when line.include?(jwt_start)
1521
- found = true
1522
- end
1523
- end
1524
- raise(Error, "No JWT found in #{@user_credentials}") if not found
1782
+ # Host and Port
1783
+ opts[:host] = host || "localhost"
1784
+ opts[:port] = port || DEFAULT_PORT
1525
1785
 
1526
- jwt
1527
- }
1786
+ connect_uris << URI::Generic.build(opts)
1787
+ end
1788
+ connect_uris
1789
+ end
1790
+ end
1528
1791
 
1529
- @signature_cb = proc { |nonce|
1530
- seed_start = "BEGIN USER NKEY SEED".freeze
1531
- found = false
1532
- seed = nil
1533
- File.readlines(@user_credentials).each do |line|
1534
- case
1535
- when found
1536
- seed = line.chomp
1537
- break
1538
- when line.include?(seed_start)
1539
- found = true
1540
- end
1541
- end
1542
- raise(Error, "No nkey user seed found in #{@user_credentials}") if not found
1792
+ module IO
1793
+ include Status
1543
1794
 
1544
- kp = NKEYS::from_seed(seed)
1545
- raw_signed = kp.sign(nonce)
1795
+ # Client creates a connection to the NATS Server.
1796
+ Client = ::NATS::Client
1546
1797
 
1547
- # seed is a reference so also cleared when doing wipe,
1548
- # which can be done since Ruby strings are mutable.
1549
- kp.wipe
1550
- encoded = Base64.urlsafe_encode64(raw_signed)
1798
+ MAX_RECONNECT_ATTEMPTS = 10
1799
+ RECONNECT_TIME_WAIT = 2
1551
1800
 
1552
- # Remove padding
1553
- encoded.gsub('=', '')
1554
- }
1555
- end
1556
- end
1801
+ # Maximum accumulated pending commands bytesize before forcing a flush.
1802
+ MAX_PENDING_SIZE = 32768
1557
1803
 
1558
- def process_uri(uris)
1559
- connect_uris = []
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
- # Scheme
1564
- if uri.include?("://")
1565
- scheme, uri = uri.split("://")
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
- # UserInfo
1572
- if uri.include?("@")
1573
- userinfo, endpoint = uri.split("@")
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
- # Host and Port
1581
- opts[:host] = host || "localhost"
1582
- opts[:port] = port || DEFAULT_PORT
1815
+ # Default IO timeouts
1816
+ DEFAULT_CONNECT_TIMEOUT = 2
1817
+ DEFAULT_READ_WRITE_TIMEOUT = 2
1818
+ DEFAULT_DRAIN_TIMEOUT = 30
1583
1819
 
1584
- connect_uris << URI::Generic.build(opts)
1585
- end
1586
- connect_uris
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