nats-pure 0.7.2 → 2.0.0.pre.alpha

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