nats-pure 0.7.0 → 2.0.0.pre.rc2

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