nats-pure 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +10 -3
  4. data/lib/nats/client.rb +7 -3
  5. data/lib/nats/io/client.rb +464 -328
  6. data/lib/nats/io/errors.rb +2 -0
  7. data/lib/nats/io/jetstream/api.rb +53 -50
  8. data/lib/nats/io/jetstream/errors.rb +30 -14
  9. data/lib/nats/io/jetstream/js/config.rb +9 -3
  10. data/lib/nats/io/jetstream/js/header.rb +15 -9
  11. data/lib/nats/io/jetstream/js/status.rb +11 -5
  12. data/lib/nats/io/jetstream/js/sub.rb +4 -2
  13. data/lib/nats/io/jetstream/js.rb +10 -8
  14. data/lib/nats/io/jetstream/manager.rb +103 -101
  15. data/lib/nats/io/jetstream/msg/ack.rb +22 -12
  16. data/lib/nats/io/jetstream/msg/ack_methods.rb +30 -24
  17. data/lib/nats/io/jetstream/msg/metadata.rb +10 -7
  18. data/lib/nats/io/jetstream/msg.rb +11 -4
  19. data/lib/nats/io/jetstream/pull_subscription.rb +21 -10
  20. data/lib/nats/io/jetstream/push_subscription.rb +3 -1
  21. data/lib/nats/io/jetstream.rb +102 -106
  22. data/lib/nats/io/kv/api.rb +7 -3
  23. data/lib/nats/io/kv/bucket_status.rb +7 -5
  24. data/lib/nats/io/kv/errors.rb +25 -2
  25. data/lib/nats/io/kv/manager.rb +19 -10
  26. data/lib/nats/io/kv.rb +354 -22
  27. data/lib/nats/io/msg.rb +19 -19
  28. data/lib/nats/io/parser.rb +23 -23
  29. data/lib/nats/io/rails.rb +2 -0
  30. data/lib/nats/io/subscription.rb +33 -23
  31. data/lib/nats/io/version.rb +4 -2
  32. data/lib/nats/io/websocket.rb +40 -18
  33. data/lib/nats/nuid.rb +33 -22
  34. data/lib/nats/service/callbacks.rb +22 -0
  35. data/lib/nats/service/endpoint.rb +155 -0
  36. data/lib/nats/service/errors.rb +44 -0
  37. data/lib/nats/service/group.rb +37 -0
  38. data/lib/nats/service/monitoring.rb +108 -0
  39. data/lib/nats/service/stats.rb +52 -0
  40. data/lib/nats/service/status.rb +66 -0
  41. data/lib/nats/service/validator.rb +31 -0
  42. data/lib/nats/service.rb +121 -0
  43. data/lib/nats/utils/list.rb +26 -0
  44. data/lib/nats-pure.rb +5 -0
  45. data/lib/nats.rb +10 -6
  46. metadata +176 -8
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright 2016-2021 The NATS Authors
2
4
  # Licensed under the Apache License, Version 2.0 (the "License");
3
5
  # you may not use this file except in compliance with the License.
@@ -12,21 +14,20 @@
12
14
  # limitations under the License.
13
15
  #
14
16
 
15
- require_relative 'parser'
16
- require_relative 'version'
17
- require_relative 'errors'
18
- require_relative 'msg'
19
- require_relative 'subscription'
20
- require_relative 'jetstream'
21
-
22
- require 'nats/nuid'
23
- require 'thread'
24
- require 'socket'
25
- require 'json'
26
- require 'monitor'
27
- require 'uri'
28
- require 'securerandom'
29
- require 'concurrent'
17
+ require_relative "parser"
18
+ require_relative "version"
19
+ require_relative "errors"
20
+ require_relative "msg"
21
+ require_relative "subscription"
22
+ require_relative "jetstream"
23
+
24
+ require "nats/nuid"
25
+ require "socket"
26
+ require "json"
27
+ require "monitor"
28
+ require "uri"
29
+ require "securerandom"
30
+ require "concurrent"
30
31
 
31
32
  begin
32
33
  require "openssl"
@@ -46,7 +47,7 @@ module NATS
46
47
  # nc.publish("hello", "world")
47
48
  # nc.close
48
49
  #
49
- def connect(uri=nil, opts={})
50
+ def connect(uri = nil, opts = {})
50
51
  nc = NATS::Client.new
51
52
  nc.connect(uri, opts)
52
53
 
@@ -100,25 +101,25 @@ module NATS
100
101
  include MonitorMixin
101
102
  include Status
102
103
 
103
- attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri, :subscription_executor, :reloader
104
+ attr_reader :status, :server_info, :server_pool, :options, :stats, :uri, :subscription_executor, :reloader
104
105
 
105
- DEFAULT_PORT = { nats: 4222, ws: 80, wss: 443 }.freeze
106
- DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT[:nats]}".freeze)
106
+ DEFAULT_PORT = {nats: 4222, ws: 80, wss: 443}.freeze
107
+ DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT[:nats]}".freeze
107
108
 
108
- CR_LF = ("\r\n".freeze)
109
- CR_LF_SIZE = (CR_LF.bytesize)
109
+ CR_LF = "\r\n"
110
+ CR_LF_SIZE = CR_LF.bytesize
110
111
 
111
- PING_REQUEST = ("PING#{CR_LF}".freeze)
112
- PONG_RESPONSE = ("PONG#{CR_LF}".freeze)
112
+ PING_REQUEST = "PING#{CR_LF}".freeze
113
+ PONG_RESPONSE = "PONG#{CR_LF}".freeze
113
114
 
114
- NATS_HDR_LINE = ("NATS/1.0#{CR_LF}".freeze)
115
+ NATS_HDR_LINE = "NATS/1.0#{CR_LF}".freeze
115
116
  STATUS_MSG_LEN = 3
116
- STATUS_HDR = ("Status".freeze)
117
- DESC_HDR = ("Description".freeze)
118
- NATS_HDR_LINE_SIZE = (NATS_HDR_LINE.bytesize)
117
+ STATUS_HDR = "Status"
118
+ DESC_HDR = "Description"
119
+ NATS_HDR_LINE_SIZE = NATS_HDR_LINE.bytesize
119
120
 
120
- SUB_OP = ('SUB'.freeze)
121
- EMPTY_MSG = (''.freeze)
121
+ SUB_OP = "SUB"
122
+ EMPTY_MSG = ""
122
123
 
123
124
  INSTANCES = ObjectSpace::WeakMap.new # tracks all alive client instances
124
125
  private_constant :INSTANCES
@@ -135,6 +136,8 @@ module NATS
135
136
  # Re-establish connection in a new process after forking to start new threads.
136
137
  def after_fork
137
138
  INSTANCES.each do |client|
139
+ next if client.closed?
140
+
138
141
  if client.options[:reconnect]
139
142
  was_connected = !client.disconnected?
140
143
  client.send(:close_connection, Status::DISCONNECTED, true)
@@ -170,7 +173,7 @@ module NATS
170
173
  @ping_interval_thread = nil
171
174
 
172
175
  # Info that we get from the server
173
- @server_info = { }
176
+ @server_info = {}
174
177
 
175
178
  # URI from server to which we are currently connected
176
179
  @uri = nil
@@ -179,7 +182,7 @@ module NATS
179
182
  @status = nil
180
183
 
181
184
  # Subscriptions
182
- @subs = { }
185
+ @subs = {}
183
186
  @ssid = 0
184
187
 
185
188
  # Ping interval
@@ -202,10 +205,10 @@ module NATS
202
205
  @last_err = nil
203
206
 
204
207
  # Async callbacks, no ops by default.
205
- @err_cb = proc { }
206
- @close_cb = proc { }
207
- @disconnect_cb = proc { }
208
- @reconnect_cb = proc { }
208
+ @err_cb = proc {}
209
+ @close_cb = proc {}
210
+ @disconnect_cb = proc {}
211
+ @reconnect_cb = proc {}
209
212
 
210
213
  # Secure TLS options
211
214
  @tls = nil
@@ -218,6 +221,10 @@ module NATS
218
221
  # Track whether connect has been already been called.
219
222
  @connect_called = false
220
223
 
224
+ # Bumped by every close, so that a reconnect started earlier can tell
225
+ # that the connection was closed meanwhile.
226
+ @close_generation = 0
227
+
221
228
  # New style request/response implementation.
222
229
  @resp_sub = nil
223
230
  @resp_map = nil
@@ -239,6 +246,9 @@ module NATS
239
246
  # Draining
240
247
  @drain_t = nil
241
248
 
249
+ # Service API
250
+ @_services = nil
251
+
242
252
  # Prepare for calling connect or automatic delayed connection
243
253
  parse_and_validate_options if uri || opts.any?
244
254
 
@@ -249,7 +259,7 @@ module NATS
249
259
  end
250
260
 
251
261
  # Prepare connecting to NATS, but postpone real connection until first usage.
252
- def connect(uri=nil, opts={})
262
+ def connect(uri = nil, opts = {})
253
263
  if uri || opts.any?
254
264
  @initial_uri = uri
255
265
  @initial_options = opts
@@ -267,6 +277,19 @@ module NATS
267
277
  self
268
278
  end
269
279
 
280
+ def force_reconnect
281
+ synchronize do
282
+ return true if reconnecting?
283
+
284
+ if closed? || draining? || disconnected?
285
+ raise NATS::IO::ConnectionClosedError
286
+ end
287
+
288
+ initiate_reconnect
289
+ true
290
+ end
291
+ end
292
+
270
293
  private def parse_and_validate_options
271
294
  # Reset these in case we have reconnected via fork.
272
295
  @server_pool = []
@@ -293,8 +316,8 @@ module NATS
293
316
  when String
294
317
  # Initialize TLS defaults in case any url is using it.
295
318
  srvs = opts[:servers] = process_uri(uri)
296
- if srvs.any? {|u| %w[tls wss].include? u.scheme } and !opts[:tls]
297
- opts[:tls] = { context: tls_context }
319
+ if srvs.any? { |u| %w[tls wss].include? u.scheme } && !opts[:tls]
320
+ opts[:tls] = {context: tls_context}
298
321
  end
299
322
  @single_url_connect_used = true if srvs.size == 1
300
323
  when Hash
@@ -312,16 +335,17 @@ module NATS
312
335
  opts[:max_outstanding_pings] = NATS::IO::DEFAULT_PING_MAX if opts[:max_outstanding_pings].nil?
313
336
 
314
337
  # Override with ENV
315
- opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil?
316
- opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil?
317
- opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil?
318
- opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil?
319
- opts[:ignore_discovered_urls] = ENV['NATS_IGNORE_DISCOVERED_URLS'].downcase == 'true' unless ENV['NATS_IGNORE_DISCOVERED_URLS'].nil?
320
- opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil?
321
- opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil?
322
- opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil?
338
+ opts[:verbose] = ENV["NATS_VERBOSE"].downcase == "true" unless ENV["NATS_VERBOSE"].nil?
339
+ opts[:pedantic] = ENV["NATS_PEDANTIC"].downcase == "true" unless ENV["NATS_PEDANTIC"].nil?
340
+ opts[:reconnect] = ENV["NATS_RECONNECT"].downcase == "true" unless ENV["NATS_RECONNECT"].nil?
341
+ opts[:reconnect_time_wait] = ENV["NATS_RECONNECT_TIME_WAIT"].to_i unless ENV["NATS_RECONNECT_TIME_WAIT"].nil?
342
+ opts[:ignore_discovered_urls] = ENV["NATS_IGNORE_DISCOVERED_URLS"].downcase == "true" unless ENV["NATS_IGNORE_DISCOVERED_URLS"].nil?
343
+ opts[:max_reconnect_attempts] = ENV["NATS_MAX_RECONNECT_ATTEMPTS"].to_i unless ENV["NATS_MAX_RECONNECT_ATTEMPTS"].nil?
344
+ opts[:ping_interval] = ENV["NATS_PING_INTERVAL"].to_i unless ENV["NATS_PING_INTERVAL"].nil?
345
+ opts[:max_outstanding_pings] = ENV["NATS_MAX_OUTSTANDING_PINGS"].to_i unless ENV["NATS_MAX_OUTSTANDING_PINGS"].nil?
323
346
  opts[:connect_timeout] ||= NATS::IO::DEFAULT_CONNECT_TIMEOUT
324
347
  opts[:drain_timeout] ||= NATS::IO::DEFAULT_DRAIN_TIMEOUT
348
+ opts[:close_timeout] ||= NATS::IO::DEFAULT_CLOSE_TIMEOUT
325
349
  @options = opts
326
350
 
327
351
  # Process servers in the NATS cluster and pick one to connect
@@ -329,14 +353,14 @@ module NATS
329
353
  uris.shuffle! unless @options[:dont_randomize_servers]
330
354
  uris.each do |u|
331
355
  nats_uri = case u
332
- when URI
333
- u.dup
334
- else
335
- URI.parse(u)
336
- end
356
+ when URI
357
+ u.dup
358
+ else
359
+ URI.parse(u)
360
+ end
337
361
  @server_pool << {
338
- :uri => nats_uri,
339
- :hostname => nats_uri.hostname
362
+ uri: nats_uri,
363
+ hostname: nats_uri.hostname
340
364
  }
341
365
  end
342
366
 
@@ -353,7 +377,7 @@ module NATS
353
377
  @user_credentials ||= opts[:user_credentials]
354
378
  @nkeys_seed ||= opts[:nkeys_seed]
355
379
 
356
- setup_nkeys_connect if @user_credentials or @nkeys_seed
380
+ setup_nkeys_connect if @user_credentials || @nkeys_seed
357
381
 
358
382
  # Tokens, if set will take preference over the user@server uri token
359
383
  @auth_token ||= opts[:auth_token]
@@ -376,7 +400,7 @@ module NATS
376
400
  srv = select_next_server
377
401
 
378
402
  # Use the hostname from the server for TLS hostname verification.
379
- if client_using_secure_connection? and single_url_connect_used?
403
+ if client_using_secure_connection? && single_url_connect_used?
380
404
  # Always reuse the original hostname used to connect.
381
405
  @hostname ||= srv[:hostname]
382
406
  else
@@ -449,8 +473,8 @@ module NATS
449
473
  self
450
474
  end
451
475
 
452
- def publish(subject, msg=EMPTY_MSG, opt_reply=nil, **options, &blk)
453
- raise NATS::IO::BadSubject if !subject or subject.empty?
476
+ def publish(subject, msg = EMPTY_MSG, opt_reply = nil, **options, &blk)
477
+ raise NATS::IO::BadSubject if !subject || subject.empty?
454
478
  if options[:header]
455
479
  return publish_msg(NATS::Msg.new(subject: subject, data: msg, reply: opt_reply, header: options[:header]))
456
480
  end
@@ -467,10 +491,10 @@ module NATS
467
491
  # Publishes a NATS::Msg that may include headers.
468
492
  def publish_msg(msg)
469
493
  raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
470
- raise NATS::IO::BadSubject if !msg.subject or msg.subject.empty?
494
+ raise NATS::IO::BadSubject if !msg.subject || msg.subject.empty?
471
495
 
472
- msg.reply ||= ''
473
- msg.data ||= ''
496
+ msg.reply ||= "".dup
497
+ msg.data ||= "".dup
474
498
  msg_size = msg.data.bytesize
475
499
 
476
500
  # Accounting
@@ -478,7 +502,7 @@ module NATS
478
502
  @stats[:out_bytes] += msg_size
479
503
 
480
504
  if msg.header
481
- hdr = ''
505
+ hdr = "".dup
482
506
  hdr << NATS_HDR_LINE
483
507
  msg.header.each do |k, v|
484
508
  hdr << "#{k}: #{v}#{CR_LF}"
@@ -496,7 +520,7 @@ module NATS
496
520
 
497
521
  # Create subscription which is dispatched asynchronously
498
522
  # messages to a callback.
499
- def subscribe(subject, opts={}, &callback)
523
+ def subscribe(subject, opts = {}, &callback)
500
524
  raise NATS::IO::ConnectionDrainingError.new("nats: connection draining") if draining?
501
525
 
502
526
  sid = nil
@@ -507,7 +531,7 @@ module NATS
507
531
  sub.nc = self
508
532
  sub.sid = sid
509
533
  end
510
- opts[:pending_msgs_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
534
+ opts[:pending_msgs_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
511
535
  opts[:pending_bytes_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
512
536
 
513
537
  sub.subject = subject
@@ -515,7 +539,7 @@ module NATS
515
539
  sub.received = 0
516
540
  sub.queue = opts[:queue] if opts[:queue]
517
541
  sub.max = opts[:max] if opts[:max]
518
- sub.pending_msgs_limit = opts[:pending_msgs_limit]
542
+ sub.pending_msgs_limit = opts[:pending_msgs_limit]
519
543
  sub.pending_bytes_limit = opts[:pending_bytes_limit]
520
544
  sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
521
545
  sub.processing_concurrency = opts[:processing_concurrency] if opts.key?(:processing_concurrency)
@@ -539,8 +563,8 @@ module NATS
539
563
  # It times out in case the request is not retrieved within the
540
564
  # specified deadline.
541
565
  # If given a callback, then the request happens asynchronously.
542
- def request(subject, payload="", **opts, &blk)
543
- raise NATS::IO::BadSubject if !subject or subject.empty?
566
+ def request(subject, payload = "", **opts, &blk)
567
+ raise NATS::IO::BadSubject if !subject || subject.empty?
544
568
 
545
569
  # If a block was given then fallback to method using auto unsubscribe.
546
570
  return old_request(subject, payload, opts, &blk) if blk
@@ -571,7 +595,7 @@ module NATS
571
595
  # Publish request and wait for reply.
572
596
  publish(subject, payload, inbox)
573
597
  begin
574
- MonotonicTime::with_nats_timeout(timeout) do
598
+ MonotonicTime.with_nats_timeout(timeout) do
575
599
  @resp_sub.synchronize do
576
600
  future.wait(timeout)
577
601
  end
@@ -588,7 +612,7 @@ module NATS
588
612
  @resp_map.delete(token)
589
613
  end
590
614
 
591
- if response and response.header
615
+ if response&.header
592
616
  status = response.header[STATUS_HDR]
593
617
  raise NATS::IO::NoRespondersError if status == "503"
594
618
  end
@@ -599,7 +623,7 @@ module NATS
599
623
  # request_msg makes a NATS request using a NATS::Msg that may include headers.
600
624
  def request_msg(msg, **opts)
601
625
  raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
602
- raise NATS::IO::BadSubject if !msg.subject or msg.subject.empty?
626
+ raise NATS::IO::BadSubject if !msg.subject || msg.subject.empty?
603
627
 
604
628
  token = nil
605
629
  inbox = nil
@@ -619,13 +643,13 @@ module NATS
619
643
  @resp_map[token][:future] = future
620
644
  end
621
645
  msg.reply = inbox
622
- msg.data ||= ''
623
- msg_size = msg.data.bytesize
646
+ msg.data ||= ""
647
+ msg.data.bytesize
624
648
 
625
649
  # Publish request and wait for reply.
626
650
  publish_msg(msg)
627
651
  begin
628
- MonotonicTime::with_nats_timeout(timeout) do
652
+ MonotonicTime.with_nats_timeout(timeout) do
629
653
  @resp_sub.synchronize do
630
654
  future.wait(timeout)
631
655
  end
@@ -642,7 +666,7 @@ module NATS
642
666
  @resp_map.delete(token)
643
667
  end
644
668
 
645
- if response and response.header
669
+ if response&.header
646
670
  status = response.header[STATUS_HDR]
647
671
  raise NATS::IO::NoRespondersError if status == "503"
648
672
  end
@@ -654,7 +678,7 @@ module NATS
654
678
  # expecting a single response or raising a timeout in case the request
655
679
  # is not retrieved within the specified deadline.
656
680
  # If given a callback, then the request happens asynchronously.
657
- def old_request(subject, payload, opts={}, &blk)
681
+ def old_request(subject, payload, opts = {}, &blk)
658
682
  return unless subject
659
683
  inbox = new_inbox
660
684
 
@@ -703,13 +727,13 @@ module NATS
703
727
  # Publish the request and then wait for the response...
704
728
  publish(subject, payload, inbox)
705
729
 
706
- MonotonicTime::with_nats_timeout(timeout) do
730
+ MonotonicTime.with_nats_timeout(timeout) do
707
731
  future.wait(timeout)
708
732
  end
709
733
  end
710
734
  response = sub.response
711
735
 
712
- if response and response.header
736
+ if response&.header
713
737
  status = response.header[STATUS_HDR]
714
738
  raise NATS::IO::NoRespondersError if status == "503"
715
739
  end
@@ -718,7 +742,7 @@ module NATS
718
742
  end
719
743
 
720
744
  # Send a ping and wait for a pong back within a timeout.
721
- def flush(timeout=10)
745
+ def flush(timeout = 10)
722
746
  # Schedule sending a PING, and block until we receive PONG back,
723
747
  # or raise a timeout in case the response is past the deadline.
724
748
  pong = @pongs.new_cond
@@ -728,18 +752,18 @@ module NATS
728
752
  # Flush once pong future has been prepared
729
753
  @pending_queue << PING_REQUEST
730
754
  @flush_queue << :ping
731
- MonotonicTime::with_nats_timeout(timeout) do
755
+ MonotonicTime.with_nats_timeout(timeout) do
732
756
  pong.wait(timeout)
733
757
  end
734
758
  end
735
759
  end
736
760
 
737
- alias :servers :server_pool
761
+ alias_method :servers, :server_pool
738
762
 
739
763
  # discovered_servers returns the NATS Servers that have been discovered
740
764
  # via INFO protocol updates.
741
765
  def discovered_servers
742
- servers.select {|s| s[:discovered] }
766
+ servers.select { |s| s[:discovered] }
743
767
  end
744
768
 
745
769
  # Close connection to NATS, flushing in case connection is alive
@@ -780,7 +804,7 @@ module NATS
780
804
  end
781
805
 
782
806
  def draining?
783
- if @status == DRAINING_PUBS or @status == DRAINING_SUBS
807
+ if (@status == DRAINING_PUBS) || (@status == DRAINING_SUBS)
784
808
  return true
785
809
  end
786
810
 
@@ -833,12 +857,16 @@ module NATS
833
857
  # @option params [String] :domain JetStream Domain to use for the requests.
834
858
  # @option params [Float] :timeout Default timeout to use for JS requests.
835
859
  # @return [NATS::JetStream]
836
- def jetstream(opts={})
860
+ def jetstream(opts = {})
837
861
  ::NATS::JetStream.new(self, opts)
838
862
  end
839
863
  alias_method :JetStream, :jetstream
840
864
  alias_method :jsm, :jetstream
841
865
 
866
+ def services
867
+ synchronize { @_services ||= Services.new(self) }
868
+ end
869
+
842
870
  private
843
871
 
844
872
  def validate_settings!
@@ -855,10 +883,8 @@ module NATS
855
883
  # so has to be done under the lock.
856
884
  synchronize do
857
885
  # Symbolize keys from parsed info line
858
- @server_info = parsed_info.reduce({}) do |info, (k,v)|
886
+ @server_info = parsed_info.each_with_object({}) do |(k, v), info|
859
887
  info[k.to_sym] = v
860
-
861
- info
862
888
  end
863
889
 
864
890
  # Detect any announced server that we might not be aware of...
@@ -877,7 +903,7 @@ module NATS
877
903
  srv[:uri].hostname == u.hostname && srv[:uri].port == u.port
878
904
  end
879
905
 
880
- if not present
906
+ if !present
881
907
  # Let explicit user and pass options set the credentials.
882
908
  u.user = options[:user] if options[:user]
883
909
  u.password = options[:pass] if options[:pass]
@@ -889,7 +915,7 @@ module NATS
889
915
  end
890
916
 
891
917
  # NOTE: Auto discovery won't work here when TLS host verification is enabled.
892
- srv = { :uri => u, :reconnect_attempts => 0, :discovered => true, :hostname => u.hostname }
918
+ srv = {uri: u, reconnect_attempts: 0, discovered: true, hostname: u.hostname}
893
919
  srvs << srv
894
920
  end
895
921
  end
@@ -912,13 +938,13 @@ module NATS
912
938
  # Check if the first line has an inline status and description.
913
939
  if lines.count > 0
914
940
  status_hdr = lines.first.rstrip
915
- status = status_hdr.slice(NATS_HDR_LINE_SIZE-1, STATUS_MSG_LEN)
941
+ status = status_hdr.slice(NATS_HDR_LINE_SIZE - 1, STATUS_MSG_LEN)
916
942
 
917
- if status and !status.empty?
943
+ if status && !status.empty?
918
944
  hdr[STATUS_HDR] = status
919
945
 
920
- if NATS_HDR_LINE_SIZE+2 < status_hdr.bytesize
921
- desc = status_hdr.slice(NATS_HDR_LINE_SIZE+STATUS_MSG_LEN, status_hdr.bytesize)
946
+ if NATS_HDR_LINE_SIZE + 2 < status_hdr.bytesize
947
+ desc = status_hdr.slice(NATS_HDR_LINE_SIZE + STATUS_MSG_LEN, status_hdr.bytesize)
922
948
  hdr[DESC_HDR] = desc unless desc.empty?
923
949
  end
924
950
  end
@@ -931,7 +957,7 @@ module NATS
931
957
  hdr[key] = value
932
958
  end
933
959
  rescue => e
934
- err = e
960
+ e
935
961
  end
936
962
  end
937
963
 
@@ -944,7 +970,7 @@ module NATS
944
970
  # Take first pong wait and signal any flush in case there was one
945
971
  @pongs.synchronize do
946
972
  pong = @pongs.pop
947
- pong.signal unless pong.nil?
973
+ pong&.signal
948
974
  end
949
975
  @pings_outstanding -= 1
950
976
  @pongs_received += 1
@@ -964,10 +990,9 @@ module NATS
964
990
  # while holding the lock.
965
991
  e = synchronize do
966
992
  current = server_pool.first
967
- case
968
- when err =~ /'Stale Connection'/
993
+ if err =~ /'Stale Connection'/
969
994
  @last_err = NATS::IO::StaleConnectionError.new(err)
970
- when current && current[:auth_required]
995
+ elsif current && current[:auth_required]
971
996
  # We cannot recover from auth errors so mark it to avoid
972
997
  # retrying to unecessarily next time.
973
998
  current[:error_received] = true
@@ -1017,8 +1042,8 @@ module NATS
1017
1042
  elsif sub.pending_queue
1018
1043
  # Async subscribers use a sized queue for processing
1019
1044
  # and should be able to consume messages in parallel.
1020
- if sub.pending_queue.size >= sub.pending_msgs_limit \
1021
- or sub.pending_size >= sub.pending_bytes_limit then
1045
+ if (sub.pending_queue.size >= sub.pending_msgs_limit) \
1046
+ || (sub.pending_size >= sub.pending_bytes_limit)
1022
1047
  err = NATS::IO::SlowConsumer.new("nats: slow consumer, messages dropped")
1023
1048
  else
1024
1049
  hdr = process_hdr(header)
@@ -1032,10 +1057,12 @@ module NATS
1032
1057
  end
1033
1058
  end
1034
1059
 
1035
- synchronize do
1036
- @last_err = err
1037
- err_cb_call(self, err, sub) if @err_cb
1038
- end if err
1060
+ if err
1061
+ synchronize do
1062
+ @last_err = err
1063
+ err_cb_call(self, err, sub) if @err_cb
1064
+ end
1065
+ end
1039
1066
  end
1040
1067
 
1041
1068
  def select_next_server
@@ -1101,7 +1128,7 @@ module NATS
1101
1128
 
1102
1129
  # Auto unsubscribes the server by sending UNSUB command and throws away
1103
1130
  # subscription in case already present and has received enough messages.
1104
- def unsubscribe(sub, opt_max=nil)
1131
+ def unsubscribe(sub, opt_max = nil)
1105
1132
  sid = nil
1106
1133
  closed = nil
1107
1134
  sub.synchronize do
@@ -1118,7 +1145,7 @@ module NATS
1118
1145
  return unless sub
1119
1146
  synchronize do
1120
1147
  sub.max = opt_max
1121
- @subs.delete(sid) unless (sub.max && (sub.received < sub.max))
1148
+ @subs.delete(sid) unless sub.max && (sub.received < sub.max)
1122
1149
  end
1123
1150
 
1124
1151
  sub.synchronize do
@@ -1138,8 +1165,9 @@ module NATS
1138
1165
  send_command("UNSUB #{sid}#{CR_LF}")
1139
1166
  @flush_queue << :drain
1140
1167
 
1168
+ sub.synchronize { sub.drained = true }
1141
1169
  synchronize { sub = @subs[sid] }
1142
- return unless sub
1170
+ nil unless sub
1143
1171
  end
1144
1172
 
1145
1173
  def do_drain
@@ -1156,16 +1184,16 @@ module NATS
1156
1184
  force_flush!
1157
1185
 
1158
1186
  # Wait until all subs have no pending messages.
1159
- drain_timeout = MonotonicTime::now + @options[:drain_timeout]
1187
+ drain_timeout = MonotonicTime.now + @options[:drain_timeout]
1160
1188
  to_delete = []
1161
1189
 
1162
1190
  loop do
1163
- break if MonotonicTime::now > drain_timeout
1191
+ break if MonotonicTime.now > drain_timeout
1164
1192
  sleep 0.1
1165
1193
 
1166
1194
  # Wait until all subs are done.
1167
1195
  @subs.each do |_, sub|
1168
- if sub != @resp_sub and sub.pending_queue.size == 0
1196
+ if (sub != @resp_sub) && (sub.pending_queue.size == 0)
1169
1197
  to_delete << sub
1170
1198
  end
1171
1199
  end
@@ -1178,7 +1206,7 @@ module NATS
1178
1206
 
1179
1207
  # Wait until only the resp mux is remaining or there are no subscriptions.
1180
1208
  if @subs.count == 1
1181
- sid, sub = @subs.first
1209
+ _, sub = @subs.first
1182
1210
  if sub == @resp_sub
1183
1211
  break
1184
1212
  end
@@ -1190,7 +1218,7 @@ module NATS
1190
1218
  subscription_executor.shutdown
1191
1219
  subscription_executor.wait_for_termination(@options[:drain_timeout])
1192
1220
 
1193
- if MonotonicTime::now > drain_timeout
1221
+ if MonotonicTime.now > drain_timeout
1194
1222
  e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
1195
1223
  err_cb_call(self, e, nil) if @err_cb
1196
1224
  end
@@ -1227,27 +1255,26 @@ module NATS
1227
1255
 
1228
1256
  def connect_command
1229
1257
  cs = {
1230
- :verbose => @options[:verbose],
1231
- :pedantic => @options[:pedantic],
1232
- :lang => NATS::IO::LANG,
1233
- :version => NATS::IO::VERSION,
1234
- :protocol => NATS::IO::PROTOCOL
1258
+ verbose: @options[:verbose],
1259
+ pedantic: @options[:pedantic],
1260
+ lang: NATS::IO::LANG,
1261
+ version: NATS::IO::VERSION,
1262
+ protocol: NATS::IO::PROTOCOL
1235
1263
  }
1236
1264
  cs[:name] = @options[:name] if @options[:name]
1237
1265
 
1238
- case
1239
- when auth_connection?
1266
+ if auth_connection?
1240
1267
  if @uri.password
1241
1268
  cs[:user] = @uri.user
1242
1269
  cs[:pass] = @uri.password
1243
1270
  else
1244
1271
  cs[:auth_token] = @uri.user
1245
1272
  end
1246
- when @user_jwt_cb && @signature_cb
1273
+ elsif @user_jwt_cb && @signature_cb
1247
1274
  nonce = @server_info[:nonce]
1248
1275
  cs[:jwt] = @user_jwt_cb.call
1249
1276
  cs[:sig] = @signature_cb.call(nonce)
1250
- when @user_nkey_cb && @signature_cb
1277
+ elsif @user_nkey_cb && @signature_cb
1251
1278
  nonce = @server_info[:nonce]
1252
1279
  cs[:nkey] = @user_nkey_cb.call
1253
1280
  cs[:sig] = @signature_cb.call(nonce)
@@ -1258,10 +1285,10 @@ module NATS
1258
1285
  if @server_info[:headers]
1259
1286
  cs[:headers] = @server_info[:headers]
1260
1287
  cs[:no_responders] = if @options[:no_responders] == false
1261
- @options[:no_responders]
1262
- else
1263
- @server_info[:headers]
1264
- end
1288
+ @options[:no_responders]
1289
+ else
1290
+ @server_info[:headers]
1291
+ end
1265
1292
  end
1266
1293
 
1267
1294
  "CONNECT #{cs.to_json}#{CR_LF}"
@@ -1270,6 +1297,11 @@ module NATS
1270
1297
  # Handles errors from reading, parsing the protocol or stale connection.
1271
1298
  # the lock should not be held entering this function.
1272
1299
  def process_op_error(e)
1300
+ # A background thread being stopped by close or a reconnect sees
1301
+ # errors from that; it must not start another close or reconnect,
1302
+ # which would wait for the thread that is stopping it.
1303
+ return if stopping?
1304
+
1273
1305
  should_bail = synchronize do
1274
1306
  connecting? || closed? || reconnecting?
1275
1307
  end
@@ -1281,30 +1313,8 @@ module NATS
1281
1313
 
1282
1314
  # If we were connected and configured to reconnect,
1283
1315
  # then trigger disconnect and start reconnection logic
1284
- if connected? and should_reconnect?
1285
- @status = RECONNECTING
1286
- @io.close if @io
1287
- @io = nil
1288
-
1289
- # TODO: Reconnecting pending buffer?
1290
-
1291
- # Do reconnect under a different thread than the one
1292
- # in which we got the error.
1293
- Thread.new do
1294
- begin
1295
- # Abort currently running reads in case they're around
1296
- # FIXME: There might be more graceful way here...
1297
- @read_loop_thread.exit if @read_loop_thread.alive?
1298
- @flusher_thread.exit if @flusher_thread.alive?
1299
- @ping_interval_thread.exit if @ping_interval_thread.alive?
1300
-
1301
- attempt_reconnect
1302
- rescue NATS::IO::NoServersError => e
1303
- @last_err = e
1304
- close
1305
- end
1306
- end
1307
-
1316
+ if connected? && should_reconnect?
1317
+ initiate_reconnect
1308
1318
  Thread.exit
1309
1319
  return
1310
1320
  end
@@ -1317,29 +1327,63 @@ module NATS
1317
1327
  close
1318
1328
  end
1319
1329
 
1330
+ def initiate_reconnect
1331
+ @status = RECONNECTING
1332
+ old_io = @io
1333
+ @io = nil
1334
+
1335
+ # Wake a read loop blocked on the socket by shutting it down, and
1336
+ # close the socket only once the loop has stopped: on JRuby a close
1337
+ # right after the shutdown can leave the read blocked for good.
1338
+ if @read_loop_thread&.alive? && @read_loop_thread != Thread.current
1339
+ old_io&.shutdown_read
1340
+ end
1341
+
1342
+ # TODO: Reconnecting pending buffer?
1343
+
1344
+ generation = @close_generation
1345
+
1346
+ # Do reconnect under a different thread than the one
1347
+ # in which we got the error.
1348
+ Thread.new do
1349
+ stop_threads!
1350
+ old_io&.close
1351
+
1352
+ @subscription_executor.shutdown
1353
+
1354
+ attempt_reconnect(generation)
1355
+ rescue NATS::IO::NoServersError => e
1356
+ @last_err = e
1357
+ close
1358
+ end
1359
+ end
1360
+
1320
1361
  # Gathers data from the socket and sends it to the parser.
1321
1362
  def read_loop
1322
1363
  loop do
1323
- begin
1324
- should_bail = synchronize do
1325
- # FIXME: In case of reconnect as well?
1326
- @status == CLOSED or @status == RECONNECTING
1327
- end
1328
- if !@io or @io.closed? or should_bail
1329
- return
1330
- end
1364
+ return if stopping?
1331
1365
 
1332
- # TODO: Remove timeout and just wait to be ready
1333
- data = @io.read(NATS::IO::MAX_SOCKET_READ_BYTES)
1334
- @parser.parse(data) if data
1335
- rescue Errno::ETIMEDOUT
1336
- # FIXME: We do not really need a timeout here...
1337
- retry
1338
- rescue => e
1339
- # In case of reading/parser errors, trigger
1340
- # reconnection logic in case desired.
1341
- process_op_error(e)
1366
+ should_bail = synchronize do
1367
+ # FIXME: In case of reconnect as well?
1368
+ @status == CLOSED or @status == RECONNECTING
1369
+ end
1370
+ if !@io || @io.closed? || should_bail
1371
+ return
1342
1372
  end
1373
+
1374
+ # TODO: Remove timeout and just wait to be ready
1375
+ data = @io.read(NATS::IO::MAX_SOCKET_READ_BYTES)
1376
+ @parser.parse(data) if data
1377
+ rescue Errno::ETIMEDOUT
1378
+ # FIXME: We do not really need a timeout here...
1379
+ retry
1380
+ rescue => e
1381
+ # Asked to stop: the error comes from the socket being shut down.
1382
+ return if stopping?
1383
+
1384
+ # In case of reading/parser errors, trigger
1385
+ # reconnection logic in case desired.
1386
+ process_op_error(e)
1343
1387
  end
1344
1388
  end
1345
1389
 
@@ -1349,9 +1393,10 @@ module NATS
1349
1393
  loop do
1350
1394
  # Blocks waiting for the flusher to be kicked...
1351
1395
  @flush_queue.pop
1396
+ return if stopping?
1352
1397
 
1353
1398
  should_bail = synchronize do
1354
- (@status != CONNECTED && !draining? ) || @status == CONNECTING
1399
+ (@status != CONNECTED && !draining?) || @status == CONNECTING
1355
1400
  end
1356
1401
  return if should_bail
1357
1402
 
@@ -1372,22 +1417,24 @@ module NATS
1372
1417
  # until reaching the max pending queue size.
1373
1418
  cmds = []
1374
1419
  cmds << @pending_queue.pop until @pending_queue.empty?
1375
- begin
1376
- @io.write(cmds.join) unless cmds.empty?
1377
- rescue => e
1378
- synchronize do
1379
- @last_err = e
1380
- err_cb_call(self, e, nil) if @err_cb
1381
- end
1420
+ if @io
1421
+ begin
1422
+ @io.write(cmds.join) unless cmds.empty?
1423
+ rescue => e
1424
+ synchronize do
1425
+ @last_err = e
1426
+ err_cb_call(self, e, nil) if @err_cb
1427
+ end
1382
1428
 
1383
- process_op_error(e)
1384
- return
1385
- end if @io
1429
+ process_op_error(e)
1430
+ nil
1431
+ end
1432
+ end
1386
1433
  end
1387
1434
 
1388
- def ping_interval_loop
1435
+ def ping_interval_loop(stop)
1389
1436
  loop do
1390
- sleep @options[:ping_interval]
1437
+ return if stop.wait(@options[:ping_interval])
1391
1438
 
1392
1439
  # Skip ping interval until connected
1393
1440
  next if !connected?
@@ -1408,27 +1455,26 @@ module NATS
1408
1455
  def process_connect_init
1409
1456
  # FIXME: Can receive PING as well here in recent versions.
1410
1457
  line = @io.read_line(options[:connect_timeout])
1411
- if !line or line.empty?
1458
+ if !line || line.empty?
1412
1459
  raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not received")
1413
1460
  end
1414
1461
 
1415
- if match = line.match(NATS::Protocol::INFO)
1462
+ if (match = line.match(NATS::Protocol::INFO))
1416
1463
  info_json = match.captures.first
1417
1464
  process_info(info_json)
1418
1465
  else
1419
1466
  raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not valid")
1420
1467
  end
1421
1468
 
1422
- case
1423
- when (server_using_secure_connection? and client_using_secure_connection?)
1469
+ if server_using_secure_connection? && client_using_secure_connection?
1424
1470
  @io.setup_tls!
1425
1471
  # Server > v2.9.19 returns tls_required regardless of no_tls for WebSocket config being used so need to check URI.
1426
- when (server_using_secure_connection? and !client_using_secure_connection? and @uri.scheme != "ws")
1427
- raise NATS::IO::ConnectError.new('TLS/SSL required by server')
1472
+ elsif server_using_secure_connection? && !client_using_secure_connection? && (@uri.scheme != "ws")
1473
+ raise NATS::IO::ConnectError.new("TLS/SSL required by server")
1428
1474
  # Server < v2.9.19 requiring TLS/SSL over websocket but not requiring it over standard protocol
1429
1475
  # doesn't send `tls_required` in its INFO so we need to check the URI scheme for WebSocket.
1430
- when (client_using_secure_connection? and !server_using_secure_connection? and @uri.scheme != "wss")
1431
- raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1476
+ elsif client_using_secure_connection? && !server_using_secure_connection? && (@uri.scheme != "wss")
1477
+ raise NATS::IO::ConnectError.new("TLS/SSL not supported by server")
1432
1478
  else
1433
1479
  # Otherwise, use a regular connection.
1434
1480
  end
@@ -1449,6 +1495,7 @@ module NATS
1449
1495
 
1450
1496
  case next_op
1451
1497
  when NATS::Protocol::PONG
1498
+ # do nothing
1452
1499
  when NATS::Protocol::ERR
1453
1500
  if @server_info[:auth_required]
1454
1501
  raise NATS::IO::AuthError.new($1)
@@ -1460,9 +1507,12 @@ module NATS
1460
1507
  end
1461
1508
  end
1462
1509
 
1463
- # Reconnect logic, this is done while holding the lock.
1464
- def attempt_reconnect
1465
- @disconnect_cb.call(@last_err) if @disconnect_cb
1510
+ # Reconnect logic. generation is @close_generation from when the
1511
+ # reconnect started; a close since then cancels the reconnect.
1512
+ def attempt_reconnect(generation)
1513
+ return if closed_since?(generation)
1514
+
1515
+ @disconnect_cb&.call(@last_err)
1466
1516
 
1467
1517
  # Clear sticky error
1468
1518
  @last_err = nil
@@ -1470,10 +1520,12 @@ module NATS
1470
1520
  # Do reconnect
1471
1521
  srv = nil
1472
1522
  begin
1523
+ return if closed_since?(generation)
1524
+
1473
1525
  srv = select_next_server
1474
1526
 
1475
1527
  # Set hostname to use for TLS hostname verification
1476
- if client_using_secure_connection? and single_url_connect_used?
1528
+ if client_using_secure_connection? && single_url_connect_used?
1477
1529
  # Reuse original hostname name in case of using TLS.
1478
1530
  @hostname ||= srv[:hostname]
1479
1531
  else
@@ -1511,63 +1563,63 @@ module NATS
1511
1563
  retry
1512
1564
  end
1513
1565
 
1514
- # Clear pending flush calls and reset state before restarting loops
1515
- @flush_queue.clear
1516
- @pings_outstanding = 0
1517
- @pongs_received = 0
1566
+ # Under the lock, so that a concurrent close either cancels this or
1567
+ # finds the new connection in place and closes it.
1568
+ synchronize do
1569
+ if @close_generation != generation
1570
+ @io.close
1571
+ @io = nil
1572
+ return
1573
+ end
1518
1574
 
1519
- # Replay all subscriptions
1520
- @subs.each_pair do |sid, sub|
1521
- @io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
1522
- end
1575
+ # Clear pending flush calls and reset state before restarting loops
1576
+ @flush_queue.clear
1577
+ @pings_outstanding = 0
1578
+ @pongs_received = 0
1523
1579
 
1524
- # Flush anything which was left pending, in case of errors during flush
1525
- # then we should raise error then retry the reconnect logic
1526
- cmds = []
1527
- cmds << @pending_queue.pop until @pending_queue.empty?
1528
- @io.write(cmds.join) unless cmds.empty?
1529
- @status = CONNECTED
1530
- @pending_size = 0
1580
+ # Replay all subscriptions
1581
+ @subs.each_pair do |sid, sub|
1582
+ @io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
1583
+ end
1531
1584
 
1532
- # Reset parser state here to avoid unknown protocol errors
1533
- # on reconnect...
1534
- @parser.reset!
1585
+ # Flush anything which was left pending, in case of errors during flush
1586
+ # then we should raise error then retry the reconnect logic
1587
+ cmds = []
1588
+ cmds << @pending_queue.pop until @pending_queue.empty?
1589
+ @io.write(cmds.join) unless cmds.empty?
1590
+ @status = CONNECTED
1591
+ @pending_size = 0
1535
1592
 
1536
- # Now connected to NATS, and we can restart parser loop, flusher
1537
- # and ping interval
1538
- start_threads!
1593
+ # Reset parser state here to avoid unknown protocol errors
1594
+ # on reconnect...
1595
+ @parser.reset!
1539
1596
 
1540
- # Dispatch the reconnected callback while holding lock
1541
- # which we should have already
1542
- @reconnect_cb.call if @reconnect_cb
1597
+ # Now connected to NATS, and we can restart parser loop, flusher
1598
+ # and ping interval
1599
+ start_threads!
1600
+ end
1601
+
1602
+ @reconnect_cb&.call
1603
+ end
1604
+
1605
+ def closed_since?(generation)
1606
+ synchronize { @close_generation != generation }
1543
1607
  end
1544
1608
 
1545
- def close_connection(conn_status, do_cbs=true)
1609
+ def close_connection(conn_status, do_cbs = true)
1546
1610
  synchronize do
1547
1611
  @connect_called = false
1548
1612
  if @status == CLOSED
1549
1613
  @status = conn_status
1550
1614
  return
1551
1615
  end
1616
+ @close_generation += 1
1552
1617
  end
1553
1618
 
1554
- # Kick the flusher so it bails due to closed state
1555
- @flush_queue << :fallout if @flush_queue
1556
- Thread.pass
1619
+ stop_threads!
1557
1620
 
1558
- # FIXME: More graceful way of handling the following?
1559
- # Ensure ping interval and flusher are not running anymore
1560
- if @ping_interval_thread and @ping_interval_thread.alive?
1561
- @ping_interval_thread.exit
1562
- end
1563
-
1564
- if @flusher_thread and @flusher_thread.alive?
1565
- @flusher_thread.exit
1566
- end
1567
-
1568
- if @read_loop_thread and @read_loop_thread.alive?
1569
- @read_loop_thread.exit
1570
- end
1621
+ @subscription_executor&.shutdown
1622
+ @subscription_executor&.wait_for_termination(options[:close_timeout])
1571
1623
 
1572
1624
  # TODO: Delete any other state which we are not using here too.
1573
1625
  synchronize do
@@ -1580,24 +1632,26 @@ module NATS
1580
1632
 
1581
1633
  # Try to write any pending flushes in case
1582
1634
  # we have a connection then close it.
1583
- should_flush = (@pending_queue && @io && @io.socket && !@io.closed?)
1584
- begin
1585
- cmds = []
1586
- cmds << @pending_queue.pop until @pending_queue.empty?
1587
-
1588
- # FIXME: Fails when empty on TLS connection?
1589
- @io.write(cmds.join) unless cmds.empty?
1590
- rescue => e
1591
- @last_err = e
1592
- err_cb_call(self, e, nil) if @err_cb
1593
- end if should_flush
1635
+ should_flush = @pending_queue && @io && @io.socket && !@io.closed?
1636
+ if should_flush
1637
+ begin
1638
+ cmds = []
1639
+ cmds << @pending_queue.pop until @pending_queue.empty?
1640
+
1641
+ # FIXME: Fails when empty on TLS connection?
1642
+ @io.write(cmds.join) unless cmds.empty?
1643
+ rescue => e
1644
+ @last_err = e
1645
+ err_cb_call(self, e, nil) if @err_cb
1646
+ end
1647
+ end
1594
1648
 
1595
1649
  # Destroy any remaining subscriptions.
1596
1650
  @subs.clear
1597
1651
 
1598
1652
  if do_cbs
1599
- @disconnect_cb.call(@last_err) if @disconnect_cb
1600
- @close_cb.call if @close_cb
1653
+ @disconnect_cb&.call(@last_err)
1654
+ @close_cb&.call
1601
1655
  end
1602
1656
 
1603
1657
  @status = conn_status
@@ -1611,26 +1665,66 @@ module NATS
1611
1665
  end
1612
1666
  end
1613
1667
 
1668
+ # Asks the read loop, flusher and ping threads to stop, wakes them from
1669
+ # wherever they block, and waits for them to finish.
1670
+ #
1671
+ # They are never killed: Thread#exit on a thread that is waiting for
1672
+ # the client lock can swallow the lock's wakeup, so that the next
1673
+ # thread waiting for the lock sleeps forever on an unlocked lock (#183).
1674
+ def stop_threads!(timeout = NATS::IO::THREADS_STOP_TIMEOUT)
1675
+ threads = [@read_loop_thread, @flusher_thread, @ping_interval_thread].compact
1676
+ threads.each { |t| t[:nats_stop]&.stop! } # also wakes the ping thread
1677
+
1678
+ begin
1679
+ @flush_queue&.push(:fallout, true)
1680
+ rescue ThreadError
1681
+ # Queue is full, so the flusher is awake already.
1682
+ end
1683
+ # Wake the read loop if it is blocked on the socket. Only while it
1684
+ # runs: shutdown acts on the socket itself, which a forked child
1685
+ # (whose threads are gone) shares with the parent.
1686
+ if @read_loop_thread&.alive? && @read_loop_thread != Thread.current
1687
+ @io&.shutdown_read
1688
+ end
1689
+
1690
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
1691
+ threads.each do |t|
1692
+ next if t == Thread.current
1693
+
1694
+ t.join([deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max)
1695
+ end
1696
+ end
1697
+
1698
+ def stopping?
1699
+ Thread.current[:nats_stop]&.stopped?
1700
+ end
1701
+
1702
+ def start_thread(name, stop, &block)
1703
+ thread = Thread.new(&block)
1704
+ thread[:nats_stop] = stop
1705
+ thread.name = name
1706
+ thread.abort_on_exception = true
1707
+ thread
1708
+ end
1709
+
1614
1710
  def start_threads!
1615
1711
  # Reading loop for gathering data
1616
- @read_loop_thread = Thread.new { read_loop }
1617
- @read_loop_thread.name = "nats:read_loop"
1618
- @read_loop_thread.abort_on_exception = true
1712
+ @read_loop_thread = start_thread("nats:read_loop", NATS::IO::StopSignal.new) { read_loop }
1619
1713
 
1620
1714
  # Flusher loop for sending commands
1621
- @flusher_thread = Thread.new { flusher_loop }
1622
- @flusher_thread.name = "nats:flusher_loop"
1623
- @flusher_thread.abort_on_exception = true
1715
+ @flusher_thread = start_thread("nats:flusher_loop", NATS::IO::StopSignal.new) { flusher_loop }
1624
1716
 
1625
1717
  # Ping interval handling for keeping alive the connection
1626
- @ping_interval_thread = Thread.new { ping_interval_loop }
1627
- @ping_interval_thread.name = "nats:ping_loop"
1628
- @ping_interval_thread.abort_on_exception = true
1718
+ ping_stop = NATS::IO::StopSignal.new
1719
+ @ping_interval_thread = start_thread("nats:ping_loop", ping_stop) { ping_interval_loop(ping_stop) }
1629
1720
 
1630
1721
  # Subscription handling thread pool
1631
1722
  @subscription_executor = Concurrent::ThreadPoolExecutor.new(
1632
- name: 'nats:subscription', # threads will be given names like nats:subscription-worker-1
1723
+ name: "nats:subscription", # threads will be given names like nats:subscription-worker-1
1633
1724
  max_threads: NATS::IO::DEFAULT_TOTAL_SUB_CONCURRENCY,
1725
+ # JRuby has a bug on certain Java version of not creating new threads:
1726
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/864
1727
+ min_threads: defined?(JRUBY_VERSION) ? 2 : 0
1634
1728
  )
1635
1729
  end
1636
1730
 
@@ -1638,7 +1732,7 @@ module NATS
1638
1732
  # for the new style request response.
1639
1733
  def start_resp_mux_sub!
1640
1734
  @resp_sub_prefix = new_inbox
1641
- @resp_map = Hash.new { |h,k| h[k] = { }}
1735
+ @resp_map = Hash.new { |h, k| h[k] = {} }
1642
1736
 
1643
1737
  @resp_sub = Subscription.new
1644
1738
  @resp_sub.subject = "#{@resp_sub_prefix}.*"
@@ -1652,7 +1746,7 @@ module NATS
1652
1746
  @resp_sub.callback = proc do |msg|
1653
1747
  # Pick the token and signal the request under the mutex
1654
1748
  # from the subscription itself.
1655
- token = msg.subject.split('.').last
1749
+ token = msg.subject.split(".").last
1656
1750
  future = nil
1657
1751
  synchronize do
1658
1752
  future = @resp_map[token][:future]
@@ -1684,7 +1778,7 @@ module NATS
1684
1778
  return false if server[:error_received]
1685
1779
 
1686
1780
  # We will retry a number of times to reconnect to a server.
1687
- return server[:reconnect_attempts] <= @options[:max_reconnect_attempts]
1781
+ server[:reconnect_attempts] <= @options[:max_reconnect_attempts]
1688
1782
  end
1689
1783
 
1690
1784
  def should_delay_connect?(server)
@@ -1701,35 +1795,34 @@ module NATS
1701
1795
 
1702
1796
  def create_socket
1703
1797
  socket_class = case @uri.scheme
1704
- when "nats", "tls"
1705
- NATS::IO::Socket
1706
- when "ws", "wss"
1707
- require_relative 'websocket'
1708
- NATS::IO::WebSocket
1709
- else
1710
- raise NotImplementedError, "#{@uri.scheme} protocol is not supported, check NATS cluster URL spelling"
1711
- end
1798
+ when "nats", "tls"
1799
+ NATS::IO::Socket
1800
+ when "ws", "wss"
1801
+ require_relative "websocket"
1802
+ NATS::IO::WebSocket
1803
+ else
1804
+ raise NotImplementedError, "#{@uri.scheme} protocol is not supported, check NATS cluster URL spelling"
1805
+ end
1712
1806
 
1713
1807
  socket_class.new(
1714
1808
  uri: @uri,
1715
- tls: { context: tls_context, hostname: @hostname },
1716
- connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT,
1809
+ tls: {context: tls_context, hostname: @hostname},
1810
+ connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT
1717
1811
  )
1718
1812
  end
1719
1813
 
1720
1814
  def setup_nkeys_connect
1721
1815
  begin
1722
- require 'nkeys'
1723
- require 'base64'
1816
+ require "nkeys"
1817
+ require "base64"
1724
1818
  rescue LoadError
1725
1819
  raise(Error, "nkeys is not installed")
1726
1820
  end
1727
1821
 
1728
- case
1729
- when @nkeys_seed
1822
+ if @nkeys_seed
1730
1823
  @user_nkey_cb = nkey_cb_for_nkey_file(@nkeys_seed)
1731
1824
  @signature_cb = signature_cb_for_nkey_file(@nkeys_seed)
1732
- when @user_credentials
1825
+ elsif @user_credentials
1733
1826
  # When the credentials are within a single decorated file.
1734
1827
  @user_jwt_cb = jwt_cb_for_creds_file(@user_credentials)
1735
1828
  @signature_cb = signature_cb_for_creds_file(@user_credentials)
@@ -1739,18 +1832,18 @@ module NATS
1739
1832
  def signature_cb_for_nkey_file(nkey)
1740
1833
  proc { |nonce|
1741
1834
  seed = File.read(nkey).chomp
1742
- kp = NKEYS::from_seed(seed)
1835
+ kp = NKEYS.from_seed(seed)
1743
1836
  raw_signed = kp.sign(nonce)
1744
1837
  kp.wipe!
1745
1838
  encoded = Base64.urlsafe_encode64(raw_signed)
1746
- encoded.gsub('=', '')
1839
+ encoded.gsub("=", "")
1747
1840
  }
1748
1841
  end
1749
1842
 
1750
1843
  def nkey_cb_for_nkey_file(nkey)
1751
1844
  proc {
1752
1845
  seed = File.read(nkey).chomp
1753
- kp = NKEYS::from_seed(seed)
1846
+ kp = NKEYS.from_seed(seed)
1754
1847
 
1755
1848
  # Take a copy since original will be gone with the wipe.
1756
1849
  pub_key = kp.public_key.dup
@@ -1762,21 +1855,20 @@ module NATS
1762
1855
 
1763
1856
  def jwt_cb_for_creds_file(creds)
1764
1857
  proc {
1765
- jwt_start = "BEGIN NATS USER JWT".freeze
1858
+ jwt_start = "BEGIN NATS USER JWT"
1766
1859
  found = false
1767
1860
  jwt = nil
1768
1861
 
1769
1862
  File.readlines(creds).each do |line|
1770
- case
1771
- when found
1863
+ if found
1772
1864
  jwt = line.chomp
1773
1865
  break
1774
- when line.include?(jwt_start)
1866
+ elsif line.include?(jwt_start)
1775
1867
  found = true
1776
1868
  end
1777
1869
  end
1778
1870
 
1779
- raise(Error, "No JWT found in #{creds}") if not found
1871
+ raise(Error, "No JWT found in #{creds}") if !found
1780
1872
 
1781
1873
  jwt
1782
1874
  }
@@ -1784,23 +1876,22 @@ module NATS
1784
1876
 
1785
1877
  def signature_cb_for_creds_file(creds)
1786
1878
  proc { |nonce|
1787
- seed_start = "BEGIN USER NKEY SEED".freeze
1879
+ seed_start = "BEGIN USER NKEY SEED"
1788
1880
  found = false
1789
1881
  seed = nil
1790
1882
 
1791
1883
  File.readlines(creds).each do |line|
1792
- case
1793
- when found
1884
+ if found
1794
1885
  seed = line.chomp
1795
1886
  break
1796
- when line.include?(seed_start)
1887
+ elsif line.include?(seed_start)
1797
1888
  found = true
1798
1889
  end
1799
1890
  end
1800
1891
 
1801
- raise(Error, "No nkey user seed found in #{creds}") if not found
1892
+ raise(Error, "No nkey user seed found in #{creds}") if !found
1802
1893
 
1803
- kp = NKEYS::from_seed(seed)
1894
+ kp = NKEYS.from_seed(seed)
1804
1895
  raw_signed = kp.sign(nonce)
1805
1896
 
1806
1897
  # seed is a reference so also cleared when doing wipe,
@@ -1809,14 +1900,12 @@ module NATS
1809
1900
  encoded = Base64.urlsafe_encode64(raw_signed)
1810
1901
 
1811
1902
  # Remove padding
1812
- encoded.gsub('=', '')
1903
+ encoded.gsub("=", "")
1813
1904
  }
1814
1905
  end
1815
1906
 
1816
1907
  def process_uri(uris)
1817
- uris.split(',').map do |uri|
1818
- opts = {}
1819
-
1908
+ uris.gsub(/\s+/, "").split(",").map do |uri|
1820
1909
  # Scheme
1821
1910
  uri = "nats://#{uri}" if !uri.include?("://")
1822
1911
 
@@ -1858,9 +1947,48 @@ module NATS
1858
1947
  DEFAULT_CONNECT_TIMEOUT = 2
1859
1948
  DEFAULT_READ_WRITE_TIMEOUT = 2
1860
1949
  DEFAULT_DRAIN_TIMEOUT = 30
1950
+ DEFAULT_CLOSE_TIMEOUT = 30
1951
+
1952
+ # How long to wait for the read loop, flusher and ping threads to
1953
+ # stop on close or reconnect.
1954
+ THREADS_STOP_TIMEOUT = 5
1955
+
1956
+ # Asks a background thread to stop, and lets it sleep until then.
1957
+ class StopSignal
1958
+ def initialize
1959
+ @mutex = Mutex.new
1960
+ @cond = ConditionVariable.new
1961
+ @stopped = false
1962
+ end
1963
+
1964
+ def stop!
1965
+ @mutex.synchronize do
1966
+ @stopped = true
1967
+ @cond.broadcast
1968
+ end
1969
+ end
1970
+
1971
+ def stopped?
1972
+ @stopped
1973
+ end
1974
+
1975
+ # Sleeps up to timeout seconds; returns whether stop! was called.
1976
+ def wait(timeout)
1977
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
1978
+ @mutex.synchronize do
1979
+ until @stopped
1980
+ left = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
1981
+ break if left <= 0
1982
+
1983
+ @cond.wait(@mutex, left)
1984
+ end
1985
+ @stopped
1986
+ end
1987
+ end
1988
+ end
1861
1989
 
1862
1990
  # Default Pending Limits
1863
- DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
1991
+ DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
1864
1992
  DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
1865
1993
 
1866
1994
  DEFAULT_TOTAL_SUB_CONCURRENCY = 24
@@ -1870,7 +1998,7 @@ module NATS
1870
1998
  class Socket
1871
1999
  attr_accessor :socket
1872
2000
 
1873
- def initialize(options={})
2001
+ def initialize(options = {})
1874
2002
  @uri = options[:uri]
1875
2003
  @connect_timeout = options[:connect_timeout]
1876
2004
  @write_timeout = options[:write_timeout]
@@ -1882,13 +2010,11 @@ module NATS
1882
2010
  def connect
1883
2011
  addrinfo = ::Socket.getaddrinfo(@uri.hostname, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM)
1884
2012
  addrinfo.each_with_index do |ai, i|
1885
- begin
1886
- @socket = connect_addrinfo(ai, @uri.port, @connect_timeout)
1887
- break
1888
- rescue SystemCallError => e
1889
- # Give up if no more available
1890
- raise e if addrinfo.length == i+1
1891
- end
2013
+ @socket = connect_addrinfo(ai, @uri.port, @connect_timeout)
2014
+ break
2015
+ rescue SystemCallError => e
2016
+ # Give up if no more available
2017
+ raise e if addrinfo.length == i + 1
1892
2018
  end
1893
2019
 
1894
2020
  # Set TCP no delay by default
@@ -1911,7 +2037,7 @@ module NATS
1911
2037
  @socket = tls_socket
1912
2038
  end
1913
2039
 
1914
- def read_line(deadline=nil)
2040
+ def read_line(deadline = nil)
1915
2041
  # FIXME: Should accumulate and read in a non blocking way instead
1916
2042
  unless ::IO.select([@socket], nil, nil, deadline)
1917
2043
  raise NATS::IO::SocketTimeoutError
@@ -1919,10 +2045,9 @@ module NATS
1919
2045
  @socket.gets
1920
2046
  end
1921
2047
 
1922
- def read(max_bytes, deadline=nil)
1923
-
2048
+ def read(max_bytes, deadline = nil)
1924
2049
  begin
1925
- return @socket.read_nonblock(max_bytes)
2050
+ @socket.read_nonblock(max_bytes)
1926
2051
  rescue ::IO::WaitReadable
1927
2052
  if ::IO.select([@socket], nil, nil, deadline)
1928
2053
  retry
@@ -1937,7 +2062,7 @@ module NATS
1937
2062
  end
1938
2063
  end
1939
2064
  rescue EOFError => e
1940
- if RUBY_ENGINE == 'jruby' and e.message == 'No message available'
2065
+ if (RUBY_ENGINE == "jruby") && (e.message == "No message available")
1941
2066
  # FIXME: <EOFError: No message available> can happen in jruby
1942
2067
  # even though seems it is temporary and eventually possible
1943
2068
  # to read from socket.
@@ -1946,32 +2071,29 @@ module NATS
1946
2071
  raise Errno::ECONNRESET
1947
2072
  end
1948
2073
 
1949
- def write(data, deadline=nil)
2074
+ def write(data, deadline = nil)
1950
2075
  length = data.bytesize
1951
2076
  total_written = 0
1952
2077
 
1953
2078
  loop do
1954
- begin
1955
- written = @socket.write_nonblock(data)
1956
-
1957
- total_written += written
1958
- break total_written if total_written >= length
1959
- data = data.byteslice(written..-1)
1960
- rescue ::IO::WaitWritable
1961
- if ::IO.select(nil, [@socket], nil, deadline)
1962
- retry
1963
- else
1964
- raise NATS::IO::SocketTimeoutError
1965
- end
1966
- rescue ::IO::WaitReadable
1967
- if ::IO.select([@socket], nil, nil, deadline)
1968
- retry
1969
- else
1970
- raise NATS::IO::SocketTimeoutError
1971
- end
2079
+ written = @socket.write_nonblock(data)
2080
+
2081
+ total_written += written
2082
+ break total_written if total_written >= length
2083
+ data = data.byteslice(written..-1)
2084
+ rescue ::IO::WaitWritable
2085
+ if ::IO.select(nil, [@socket], nil, deadline)
2086
+ retry
2087
+ else
2088
+ raise NATS::IO::SocketTimeoutError
2089
+ end
2090
+ rescue ::IO::WaitReadable
2091
+ if ::IO.select([@socket], nil, nil, deadline)
2092
+ retry
2093
+ else
2094
+ raise NATS::IO::SocketTimeoutError
1972
2095
  end
1973
2096
  end
1974
-
1975
2097
  rescue EOFError
1976
2098
  raise Errno::ECONNRESET
1977
2099
  end
@@ -1980,6 +2102,15 @@ module NATS
1980
2102
  @socket.close
1981
2103
  end
1982
2104
 
2105
+ # Makes a read blocked on the socket in another thread return,
2106
+ # while writes keep working.
2107
+ def shutdown_read
2108
+ io = @socket.respond_to?(:to_io) ? @socket.to_io : @socket
2109
+ io.shutdown(::Socket::SHUT_RD)
2110
+ rescue IOError, SystemCallError
2111
+ # Already closed or not connected.
2112
+ end
2113
+
1983
2114
  def closed?
1984
2115
  @socket.closed?
1985
2116
  end
@@ -2006,6 +2137,12 @@ module NATS
2006
2137
  end
2007
2138
 
2008
2139
  sock
2140
+ rescue IOError => e
2141
+ # JRuby raises a plain IOError for a refused connection.
2142
+ if (RUBY_ENGINE == "jruby") && e.message.include?("Connection refused")
2143
+ raise Errno::ECONNREFUSED
2144
+ end
2145
+ raise
2009
2146
  end
2010
2147
  end
2011
2148
  end
@@ -2016,14 +2153,13 @@ module NATS
2016
2153
  # Implementation of MonotonicTime adapted from
2017
2154
  # https://github.com/ruby-concurrency/concurrent-ruby/
2018
2155
  class << self
2019
- case
2020
- when defined?(Process::CLOCK_MONOTONIC)
2156
+ if defined?(Process::CLOCK_MONOTONIC)
2021
2157
  def now
2022
2158
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
2023
2159
  end
2024
- when RUBY_ENGINE == 'jruby'
2160
+ elsif RUBY_ENGINE == "jruby"
2025
2161
  def now
2026
- java.lang.System.nanoTime() / 1_000_000_000.0
2162
+ java.lang.System.nanoTime / 1_000_000_000.0
2027
2163
  end
2028
2164
  else
2029
2165
  def now