nats-pure 0.7.2 → 2.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,8 +12,13 @@
12
12
  # limitations under the License.
13
13
  #
14
14
 
15
- require 'nats/io/parser'
16
- require 'nats/io/version'
15
+ require_relative 'parser'
16
+ require_relative 'version'
17
+ require_relative 'errors'
18
+ require_relative 'msg'
19
+ require_relative 'subscription'
20
+ require_relative 'js'
21
+
17
22
  require 'nats/nuid'
18
23
  require 'thread'
19
24
  require 'socket'
@@ -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