nats-pure 0.7.0 → 2.0.0.pre.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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