nats-pure 2.2.0 → 2.3.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -0
  3. data/README.md +251 -0
  4. data/lib/nats/io/client.rb +226 -151
  5. data/lib/nats/io/errors.rb +6 -0
  6. data/lib/nats/io/jetstream/api.rb +305 -0
  7. data/lib/nats/io/jetstream/errors.rb +104 -0
  8. data/lib/nats/io/jetstream/js/config.rb +26 -0
  9. data/lib/nats/io/jetstream/js/header.rb +31 -0
  10. data/lib/nats/io/jetstream/js/status.rb +27 -0
  11. data/lib/nats/io/jetstream/js/sub.rb +30 -0
  12. data/lib/nats/io/jetstream/js.rb +93 -0
  13. data/lib/nats/io/jetstream/manager.rb +284 -0
  14. data/lib/nats/io/jetstream/msg/ack.rb +57 -0
  15. data/lib/nats/io/jetstream/msg/ack_methods.rb +111 -0
  16. data/lib/nats/io/jetstream/msg/metadata.rb +37 -0
  17. data/lib/nats/io/jetstream/msg.rb +26 -0
  18. data/lib/nats/io/jetstream/pull_subscription.rb +260 -0
  19. data/lib/nats/io/jetstream/push_subscription.rb +42 -0
  20. data/lib/nats/io/jetstream.rb +269 -0
  21. data/lib/nats/io/kv/api.rb +39 -0
  22. data/lib/nats/io/kv/bucket_status.rb +38 -0
  23. data/lib/nats/io/kv/errors.rb +60 -0
  24. data/lib/nats/io/kv/manager.rb +89 -0
  25. data/lib/nats/io/kv.rb +5 -157
  26. data/lib/nats/io/msg.rb +4 -2
  27. data/lib/nats/io/rails.rb +29 -0
  28. data/lib/nats/io/subscription.rb +70 -5
  29. data/lib/nats/io/version.rb +1 -1
  30. data/lib/nats/io/websocket.rb +75 -0
  31. data/sig/nats/io/client.rbs +304 -0
  32. data/sig/nats/io/errors.rbs +54 -0
  33. data/sig/nats/io/jetstream/api.rbs +35 -0
  34. data/sig/nats/io/jetstream/errors.rbs +54 -0
  35. data/sig/nats/io/jetstream/js/config.rbs +11 -0
  36. data/sig/nats/io/jetstream/js/header.rbs +17 -0
  37. data/sig/nats/io/jetstream/js/status.rbs +13 -0
  38. data/sig/nats/io/jetstream/js/sub.rbs +14 -0
  39. data/sig/nats/io/jetstream/js.rbs +27 -0
  40. data/sig/nats/io/jetstream/manager.rbs +33 -0
  41. data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
  42. data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
  43. data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
  44. data/sig/nats/io/jetstream/msg.rbs +6 -0
  45. data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
  46. data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
  47. data/sig/nats/io/jetstream.rbs +15 -0
  48. data/sig/nats/io/kv/api.rbs +8 -0
  49. data/sig/nats/io/kv/bucket_status.rbs +17 -0
  50. data/sig/nats/io/kv/errors.rbs +30 -0
  51. data/sig/nats/io/kv/manager.rbs +11 -0
  52. data/sig/nats/io/kv.rbs +39 -0
  53. data/sig/nats/io/msg.rbs +14 -0
  54. data/sig/nats/io/parser.rbs +32 -0
  55. data/sig/nats/io/subscription.rbs +33 -0
  56. data/sig/nats/io/version.rbs +9 -0
  57. data/sig/nats/nuid.rbs +32 -0
  58. metadata +68 -5
  59. data/lib/nats/io/js.rb +0 -1434
@@ -17,7 +17,8 @@ require_relative 'version'
17
17
  require_relative 'errors'
18
18
  require_relative 'msg'
19
19
  require_relative 'subscription'
20
- require_relative 'js'
20
+ require_relative 'jetstream'
21
+ require_relative "rails" if defined?(::Rails::Engine)
21
22
 
22
23
  require 'nats/nuid'
23
24
  require 'thread'
@@ -26,6 +27,7 @@ require 'json'
26
27
  require 'monitor'
27
28
  require 'uri'
28
29
  require 'securerandom'
30
+ require 'concurrent'
29
31
 
30
32
  begin
31
33
  require "openssl"
@@ -81,15 +83,28 @@ module NATS
81
83
  DRAINING_PUBS = 6
82
84
  end
83
85
 
86
+ # Fork Detection handling
87
+ # Based from similar approach as mperham/connection_pool: https://github.com/mperham/connection_pool/pull/166
88
+ if Process.respond_to?(:fork) && Process.respond_to?(:_fork) # MRI 3.1+
89
+ module ForkTracker
90
+ def _fork
91
+ super.tap do |pid|
92
+ Client.after_fork if pid.zero? # in the child process
93
+ end
94
+ end
95
+ end
96
+ Process.singleton_class.prepend(ForkTracker)
97
+ end
98
+
84
99
  # Client creates a connection to the NATS Server.
85
100
  class Client
86
101
  include MonitorMixin
87
102
  include Status
88
103
 
89
- attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri
104
+ attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri, :subscription_executor, :reloader
90
105
 
91
- DEFAULT_PORT = 4222
92
- DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT}".freeze)
106
+ DEFAULT_PORT = { nats: 4222, ws: 80, wss: 443 }.freeze
107
+ DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT[:nats]}".freeze)
93
108
 
94
109
  CR_LF = ("\r\n".freeze)
95
110
  CR_LF_SIZE = (CR_LF.bytesize)
@@ -106,9 +121,39 @@ module NATS
106
121
  SUB_OP = ('SUB'.freeze)
107
122
  EMPTY_MSG = (''.freeze)
108
123
 
109
- def initialize
110
- super # required to initialize monitor
111
- @options = nil
124
+ INSTANCES = ObjectSpace::WeakMap.new # tracks all alive client instances
125
+ private_constant :INSTANCES
126
+
127
+ class << self
128
+ # Reloader should free resources managed by external framework
129
+ # that were implicitly acquired in subscription callbacks.
130
+ attr_writer :default_reloader
131
+
132
+ def default_reloader
133
+ @default_reloader ||= proc { |&block| block.call }.tap { |r| Ractor.make_shareable(r) if defined? Ractor }
134
+ end
135
+
136
+ # Re-establish connection in a new process after forking to start new threads.
137
+ def after_fork
138
+ INSTANCES.each do |client|
139
+ if client.options[:reconnect]
140
+ was_connected = !client.disconnected?
141
+ client.send(:close_connection, Status::DISCONNECTED, true)
142
+ client.connect if was_connected
143
+ else
144
+ client.send(:err_cb_call, self, NATS::IO::ForkDetectedError, nil)
145
+ client.close
146
+ end
147
+ rescue => e
148
+ warn "nats: Error during handling after_fork callback: #{e}" # TODO: Report as async error via error callback?
149
+ end
150
+ end
151
+ end
152
+
153
+ def initialize(uri = nil, opts = {})
154
+ super() # required to initialize monitor
155
+ @initial_uri = uri
156
+ @initial_options = opts
112
157
 
113
158
  # Read/Write IO
114
159
  @io = nil
@@ -132,7 +177,7 @@ module NATS
132
177
  @uri = nil
133
178
  @server_pool = []
134
179
 
135
- @status = DISCONNECTED
180
+ @status = nil
136
181
 
137
182
  # Subscriptions
138
183
  @subs = { }
@@ -194,29 +239,63 @@ module NATS
194
239
 
195
240
  # Draining
196
241
  @drain_t = nil
242
+
243
+ # Prepare for calling connect or automatic delayed connection
244
+ parse_and_validate_options if uri || opts.any?
245
+
246
+ # Keep track of all client instances to handle them after process forking in Ruby 3.1+
247
+ INSTANCES[self] = self if !defined?(Ractor) || Ractor.current == Ractor.main # Ractors doesn't work in forked processes
248
+
249
+ @reloader = opts.fetch(:reloader, self.class.default_reloader)
197
250
  end
198
251
 
199
- # Establishes a connection to NATS.
252
+ # Prepare connecting to NATS, but postpone real connection until first usage.
200
253
  def connect(uri=nil, opts={})
254
+ if uri || opts.any?
255
+ @initial_uri = uri
256
+ @initial_options = opts
257
+ end
258
+
201
259
  synchronize do
202
260
  # In case it has been connected already, then do not need to call this again.
203
261
  return if @connect_called
204
262
  @connect_called = true
205
263
  end
206
264
 
265
+ parse_and_validate_options
266
+ establish_connection!
267
+
268
+ self
269
+ end
270
+
271
+ private def parse_and_validate_options
272
+ # Reset these in case we have reconnected via fork.
273
+ @server_pool = []
274
+ @resp_sub = nil
275
+ @resp_map = nil
276
+ @resp_sub_prefix = nil
277
+ @nuid = NATS::NUID.new
278
+ @stats = {
279
+ in_msgs: 0,
280
+ out_msgs: 0,
281
+ in_bytes: 0,
282
+ out_bytes: 0,
283
+ reconnects: 0
284
+ }
285
+ @status = DISCONNECTED
286
+
207
287
  # Convert URI to string if needed.
288
+ uri = @initial_uri.dup
208
289
  uri = uri.to_s if uri.is_a?(URI)
209
290
 
291
+ opts = @initial_options.dup
292
+
210
293
  case uri
211
294
  when String
212
295
  # Initialize TLS defaults in case any url is using it.
213
296
  srvs = opts[:servers] = process_uri(uri)
214
- if srvs.any? {|u| u.scheme == 'tls'} and !opts[:tls]
215
- tls_context = OpenSSL::SSL::SSLContext.new
216
- tls_context.set_params
217
- opts[:tls] = {
218
- context: tls_context
219
- }
297
+ if srvs.any? {|u| %w[tls wss].include? u.scheme } and !opts[:tls]
298
+ opts[:tls] = { context: tls_context }
220
299
  end
221
300
  @single_url_connect_used = true if srvs.size == 1
222
301
  when Hash
@@ -287,11 +366,25 @@ module NATS
287
366
 
288
367
  validate_settings!
289
368
 
369
+ self
370
+ end
371
+
372
+ private def establish_connection!
373
+ @ruby_pid = Process.pid # For fork detection
374
+
290
375
  srv = nil
291
376
  begin
292
377
  srv = select_next_server
293
378
 
294
- # Create TCP socket connection to NATS
379
+ # Use the hostname from the server for TLS hostname verification.
380
+ if client_using_secure_connection? and single_url_connect_used?
381
+ # Always reuse the original hostname used to connect.
382
+ @hostname ||= srv[:hostname]
383
+ else
384
+ @hostname = srv[:hostname]
385
+ end
386
+
387
+ # Create TCP socket connection to NATS.
295
388
  @io = create_socket
296
389
  @io.connect
297
390
 
@@ -302,14 +395,6 @@ module NATS
302
395
  # Connection established and now in process of sending CONNECT to NATS
303
396
  @status = CONNECTING
304
397
 
305
- # Use the hostname from the server for TLS hostname verification.
306
- if client_using_secure_connection? and single_url_connect_used?
307
- # Always reuse the original hostname used to connect.
308
- @hostname ||= srv[:hostname]
309
- else
310
- @hostname = srv[:hostname]
311
- end
312
-
313
398
  # Established TCP connection successfully so can start connect
314
399
  process_connect_init
315
400
 
@@ -341,8 +426,8 @@ module NATS
341
426
  # triggering the disconnection/closed callbacks.
342
427
  close_connection(DISCONNECTED, false)
343
428
 
344
- # always sleep here to safe guard against errors before current[:was_connected]
345
- # is set for the first time
429
+ # Always sleep here to safe guard against errors before current[:was_connected]
430
+ # is set for the first time.
346
431
  sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
347
432
 
348
433
  # Continue retrying until there are no options left in the server pool
@@ -434,6 +519,7 @@ module NATS
434
519
  sub.pending_msgs_limit = opts[:pending_msgs_limit]
435
520
  sub.pending_bytes_limit = opts[:pending_bytes_limit]
436
521
  sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
522
+ sub.processing_concurrency = opts[:processing_concurrency] if opts.key?(:processing_concurrency)
437
523
 
438
524
  send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
439
525
  @flush_queue << :sub
@@ -446,41 +532,6 @@ module NATS
446
532
  sub.wait_for_msgs_cond = cond
447
533
  end
448
534
 
449
- # Async subscriptions each own a single thread for the
450
- # delivery of messages.
451
- # FIXME: Support shared thread pool with configurable limits
452
- # to better support case of having a lot of subscriptions.
453
- sub.wait_for_msgs_t = Thread.new do
454
- loop do
455
- msg = sub.pending_queue.pop
456
-
457
- cb = nil
458
- sub.synchronize do
459
-
460
- # Decrease pending size since consumed already
461
- sub.pending_size -= msg.data.size
462
- cb = sub.callback
463
- end
464
-
465
- begin
466
- # Note: Keep some of the alternative arity versions to slightly
467
- # improve backwards compatibility. Eventually fine to deprecate
468
- # since recommended version would be arity of 1 to get a NATS::Msg.
469
- case cb.arity
470
- when 0 then cb.call
471
- when 1 then cb.call(msg)
472
- when 2 then cb.call(msg.data, msg.reply)
473
- when 3 then cb.call(msg.data, msg.reply, msg.subject)
474
- else cb.call(msg.data, msg.reply, msg.subject, msg.header)
475
- end
476
- rescue => e
477
- synchronize do
478
- err_cb_call(self, e, sub) if @err_cb
479
- end
480
- end
481
- end
482
- end if callback
483
-
484
535
  sub
485
536
  end
486
537
 
@@ -709,6 +760,10 @@ module NATS
709
760
  connected? ? @uri : nil
710
761
  end
711
762
 
763
+ def disconnected?
764
+ !@status or @status == DISCONNECTED
765
+ end
766
+
712
767
  def connected?
713
768
  @status == CONNECTED
714
769
  end
@@ -812,7 +867,8 @@ module NATS
812
867
  if !@options[:ignore_discovered_urls] && connect_urls
813
868
  srvs = []
814
869
  connect_urls.each do |url|
815
- scheme = client_using_secure_connection? ? "tls" : "nats"
870
+ # Use the same scheme as the currently in use URI.
871
+ scheme = @uri.scheme
816
872
  u = URI.parse("#{scheme}://#{url}")
817
873
 
818
874
  # Skip in case it is the current server which we already know
@@ -854,14 +910,18 @@ module NATS
854
910
  hdr = {}
855
911
  lines = header.lines
856
912
 
857
- # Check if it is an inline status and description.
858
- if lines.count <= 2
913
+ # Check if the first line has an inline status and description.
914
+ if lines.count > 0
859
915
  status_hdr = lines.first.rstrip
860
- hdr[STATUS_HDR] = status_hdr.slice(NATS_HDR_LINE_SIZE-1, STATUS_MSG_LEN)
916
+ status = status_hdr.slice(NATS_HDR_LINE_SIZE-1, STATUS_MSG_LEN)
917
+
918
+ if status and !status.empty?
919
+ hdr[STATUS_HDR] = status
861
920
 
862
- if NATS_HDR_LINE_SIZE+2 < status_hdr.bytesize
863
- desc = status_hdr.slice(NATS_HDR_LINE_SIZE+STATUS_MSG_LEN, status_hdr.bytesize)
864
- hdr[DESC_HDR] = desc unless desc.empty?
921
+ if NATS_HDR_LINE_SIZE+2 < status_hdr.bytesize
922
+ desc = status_hdr.slice(NATS_HDR_LINE_SIZE+STATUS_MSG_LEN, status_hdr.bytesize)
923
+ hdr[DESC_HDR] = desc unless desc.empty?
924
+ end
865
925
  end
866
926
  end
867
927
  begin
@@ -967,12 +1027,8 @@ module NATS
967
1027
  # Only dispatch message when sure that it would not block
968
1028
  # the main read loop from the parser.
969
1029
  msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
970
- sub.pending_queue << msg
971
-
972
- # For sync subscribers, signal that there is a new message.
973
- sub.wait_for_msgs_cond.signal if sub.wait_for_msgs_cond
974
1030
 
975
- sub.pending_size += data.size
1031
+ sub.dispatch(msg)
976
1032
  end
977
1033
  end
978
1034
  end
@@ -1012,11 +1068,32 @@ module NATS
1012
1068
  @uri.scheme == "tls" || @tls
1013
1069
  end
1014
1070
 
1071
+ def tls_context
1072
+ return nil unless @tls
1073
+
1074
+ # Allow prepared context and customizations via :tls opts
1075
+ return @tls[:context] if @tls[:context]
1076
+
1077
+ @tls_context ||= OpenSSL::SSL::SSLContext.new.tap do |tls_context|
1078
+ # Use the default verification options from Ruby:
1079
+ # https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
1080
+ #
1081
+ # Insecure TLS versions not supported already:
1082
+ # https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
1083
+ #
1084
+ tls_context.set_params
1085
+ end
1086
+ end
1087
+
1015
1088
  def single_url_connect_used?
1016
1089
  @single_url_connect_used
1017
1090
  end
1018
1091
 
1019
1092
  def send_command(command)
1093
+ raise NATS::IO::ConnectionClosedError if closed?
1094
+
1095
+ establish_connection! if !status || (disconnected? && should_reconnect?)
1096
+
1020
1097
  @pending_size += command.bytesize
1021
1098
  @pending_queue << command
1022
1099
 
@@ -1043,12 +1120,6 @@ module NATS
1043
1120
  synchronize do
1044
1121
  sub.max = opt_max
1045
1122
  @subs.delete(sid) unless (sub.max && (sub.received < sub.max))
1046
-
1047
- # Stop messages delivery thread for async subscribers
1048
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1049
- sub.wait_for_msgs_t.exit
1050
- sub.pending_queue.clear
1051
- end
1052
1123
  end
1053
1124
 
1054
1125
  sub.synchronize do
@@ -1103,11 +1174,6 @@ module NATS
1103
1174
 
1104
1175
  to_delete.each do |sub|
1105
1176
  @subs.delete(sub.sid)
1106
- # Stop messages delivery thread for async subscribers
1107
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1108
- sub.wait_for_msgs_t.exit
1109
- sub.pending_queue.clear
1110
- end
1111
1177
  end
1112
1178
  to_delete.clear
1113
1179
 
@@ -1122,6 +1188,9 @@ module NATS
1122
1188
  end
1123
1189
  end
1124
1190
 
1191
+ subscription_executor.shutdown
1192
+ subscription_executor.wait_for_termination(@options[:drain_timeout])
1193
+
1125
1194
  if MonotonicTime::now > drain_timeout
1126
1195
  e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
1127
1196
  err_cb_call(self, e, nil) if @err_cb
@@ -1283,7 +1352,7 @@ module NATS
1283
1352
  @flush_queue.pop
1284
1353
 
1285
1354
  should_bail = synchronize do
1286
- @status != CONNECTED || @status == CONNECTING
1355
+ (@status != CONNECTED && !draining? ) || @status == CONNECTING
1287
1356
  end
1288
1357
  return if should_bail
1289
1358
 
@@ -1338,6 +1407,7 @@ module NATS
1338
1407
  end
1339
1408
 
1340
1409
  def process_connect_init
1410
+ # FIXME: Can receive PING as well here in recent versions.
1341
1411
  line = @io.read_line(options[:connect_timeout])
1342
1412
  if !line or line.empty?
1343
1413
  raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not received")
@@ -1352,39 +1422,13 @@ module NATS
1352
1422
 
1353
1423
  case
1354
1424
  when (server_using_secure_connection? and client_using_secure_connection?)
1355
- tls_context = nil
1356
-
1357
- if @tls
1358
- # Allow prepared context and customizations via :tls opts
1359
- tls_context = @tls[:context] if @tls[:context]
1360
- else
1361
- # Defaults
1362
- tls_context = OpenSSL::SSL::SSLContext.new
1363
-
1364
- # Use the default verification options from Ruby:
1365
- # https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
1366
- #
1367
- # Insecure TLS versions not supported already:
1368
- # https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
1369
- #
1370
- tls_context.set_params
1371
- end
1372
-
1373
- # Setup TLS connection by rewrapping the socket
1374
- tls_socket = OpenSSL::SSL::SSLSocket.new(@io.socket, tls_context)
1375
-
1376
- # Close TCP socket after closing TLS socket as well.
1377
- tls_socket.sync_close = true
1378
-
1379
- # Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
1380
- # https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
1381
- tls_socket.hostname = @hostname
1382
-
1383
- tls_socket.connect
1384
- @io.socket = tls_socket
1385
- when (server_using_secure_connection? and !client_using_secure_connection?)
1425
+ @io.setup_tls!
1426
+ # Server > v2.9.19 returns tls_required regardless of no_tls for WebSocket config being used so need to check URI.
1427
+ when (server_using_secure_connection? and !client_using_secure_connection? and @uri.scheme != "ws")
1386
1428
  raise NATS::IO::ConnectError.new('TLS/SSL required by server')
1387
- when (client_using_secure_connection? and !server_using_secure_connection?)
1429
+ # Server < v2.9.19 requiring TLS/SSL over websocket but not requiring it over standard protocol
1430
+ # doesn't send `tls_required` in its INFO so we need to check the URI scheme for WebSocket.
1431
+ when (client_using_secure_connection? and !server_using_secure_connection? and @uri.scheme != "wss")
1388
1432
  raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1389
1433
  else
1390
1434
  # Otherwise, use a regular connection.
@@ -1429,11 +1473,6 @@ module NATS
1429
1473
  begin
1430
1474
  srv = select_next_server
1431
1475
 
1432
- # Establish TCP connection with new server
1433
- @io = create_socket
1434
- @io.connect
1435
- @stats[:reconnects] += 1
1436
-
1437
1476
  # Set hostname to use for TLS hostname verification
1438
1477
  if client_using_secure_connection? and single_url_connect_used?
1439
1478
  # Reuse original hostname name in case of using TLS.
@@ -1442,6 +1481,11 @@ module NATS
1442
1481
  @hostname = srv[:hostname]
1443
1482
  end
1444
1483
 
1484
+ # Establish TCP connection with new server
1485
+ @io = create_socket
1486
+ @io.connect
1487
+ @stats[:reconnects] += 1
1488
+
1445
1489
  # Established TCP connection successfully so can start connect
1446
1490
  process_connect_init
1447
1491
 
@@ -1501,6 +1545,7 @@ module NATS
1501
1545
 
1502
1546
  def close_connection(conn_status, do_cbs=true)
1503
1547
  synchronize do
1548
+ @connect_called = false
1504
1549
  if @status == CLOSED
1505
1550
  @status = conn_status
1506
1551
  return
@@ -1549,12 +1594,6 @@ module NATS
1549
1594
  end if should_flush
1550
1595
 
1551
1596
  # Destroy any remaining subscriptions.
1552
- @subs.each do |_, sub|
1553
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1554
- sub.wait_for_msgs_t.exit
1555
- sub.pending_queue.clear
1556
- end
1557
- end
1558
1597
  @subs.clear
1559
1598
 
1560
1599
  if do_cbs
@@ -1576,15 +1615,24 @@ module NATS
1576
1615
  def start_threads!
1577
1616
  # Reading loop for gathering data
1578
1617
  @read_loop_thread = Thread.new { read_loop }
1618
+ @read_loop_thread.name = "nats:read_loop"
1579
1619
  @read_loop_thread.abort_on_exception = true
1580
1620
 
1581
1621
  # Flusher loop for sending commands
1582
1622
  @flusher_thread = Thread.new { flusher_loop }
1623
+ @flusher_thread.name = "nats:flusher_loop"
1583
1624
  @flusher_thread.abort_on_exception = true
1584
1625
 
1585
1626
  # Ping interval handling for keeping alive the connection
1586
1627
  @ping_interval_thread = Thread.new { ping_interval_loop }
1628
+ @ping_interval_thread.name = "nats:ping_loop"
1587
1629
  @ping_interval_thread.abort_on_exception = true
1630
+
1631
+ # Subscription handling thread pool
1632
+ @subscription_executor = Concurrent::ThreadPoolExecutor.new(
1633
+ name: 'nats:subscription', # threads will be given names like nats:subscription-worker-1
1634
+ max_threads: NATS::IO::DEFAULT_TOTAL_SUB_CONCURRENCY,
1635
+ )
1588
1636
  end
1589
1637
 
1590
1638
  # Prepares requests subscription that handles the responses
@@ -1602,29 +1650,25 @@ module NATS
1602
1650
  @resp_sub.pending_msgs_limit = NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
1603
1651
  @resp_sub.pending_bytes_limit = NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
1604
1652
  @resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
1605
- @resp_sub.wait_for_msgs_t = Thread.new do
1606
- loop do
1607
- msg = @resp_sub.pending_queue.pop
1608
- @resp_sub.pending_size -= msg.data.size
1609
-
1610
- # Pick the token and signal the request under the mutex
1611
- # from the subscription itself.
1612
- token = msg.subject.split('.').last
1613
- future = nil
1614
- synchronize do
1615
- future = @resp_map[token][:future]
1616
- @resp_map[token][:response] = msg
1617
- end
1653
+ @resp_sub.callback = proc do |msg|
1654
+ # Pick the token and signal the request under the mutex
1655
+ # from the subscription itself.
1656
+ token = msg.subject.split('.').last
1657
+ future = nil
1658
+ synchronize do
1659
+ future = @resp_map[token][:future]
1660
+ @resp_map[token][:response] = msg
1661
+ end
1618
1662
 
1619
- # Signal back that the response has arrived
1620
- # in case the future has not been yet delete.
1621
- @resp_sub.synchronize do
1622
- future.signal if future
1623
- end
1663
+ # Signal back that the response has arrived
1664
+ # in case the future has not been yet delete.
1665
+ @resp_sub.synchronize do
1666
+ future.signal if future
1624
1667
  end
1625
1668
  end
1626
1669
 
1627
1670
  sid = (@ssid += 1)
1671
+ @resp_sub.sid = sid
1628
1672
  @subs[sid] = @resp_sub
1629
1673
  send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
1630
1674
  @flush_queue << :sub
@@ -1657,10 +1701,21 @@ module NATS
1657
1701
  end
1658
1702
 
1659
1703
  def create_socket
1660
- NATS::IO::Socket.new({
1661
- uri: @uri,
1662
- connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT
1663
- })
1704
+ socket_class = case @uri.scheme
1705
+ when "nats", "tls"
1706
+ NATS::IO::Socket
1707
+ when "ws", "wss"
1708
+ require_relative 'websocket'
1709
+ NATS::IO::WebSocket
1710
+ else
1711
+ raise NotImplementedError, "#{@uri.scheme} protocol is not supported, check NATS cluster URL spelling"
1712
+ end
1713
+
1714
+ socket_class.new(
1715
+ uri: @uri,
1716
+ tls: { context: tls_context, hostname: @hostname },
1717
+ connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT,
1718
+ )
1664
1719
  end
1665
1720
 
1666
1721
  def setup_nkeys_connect
@@ -1770,7 +1825,7 @@ module NATS
1770
1825
 
1771
1826
  # Host and Port
1772
1827
  uri_object.hostname ||= "localhost"
1773
- uri_object.port ||= DEFAULT_PORT
1828
+ uri_object.port ||= DEFAULT_PORT.fetch(uri.scheme.to_sym, DEFAULT_PORT[:nats])
1774
1829
 
1775
1830
  uri_object
1776
1831
  end
@@ -1809,6 +1864,9 @@ module NATS
1809
1864
  DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
1810
1865
  DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
1811
1866
 
1867
+ DEFAULT_TOTAL_SUB_CONCURRENCY = 24
1868
+ DEFAULT_SINGLE_SUB_CONCURRENCY = 1
1869
+
1812
1870
  # Implementation adapted from https://github.com/redis/redis-rb
1813
1871
  class Socket
1814
1872
  attr_accessor :socket
@@ -1819,6 +1877,7 @@ module NATS
1819
1877
  @write_timeout = options[:write_timeout]
1820
1878
  @read_timeout = options[:read_timeout]
1821
1879
  @socket = nil
1880
+ @tls = options[:tls]
1822
1881
  end
1823
1882
 
1824
1883
  def connect
@@ -1837,6 +1896,22 @@ module NATS
1837
1896
  @socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
1838
1897
  end
1839
1898
 
1899
+ # (Re-)connect using secure connection if server and client agreed on using it.
1900
+ def setup_tls!
1901
+ # Setup TLS connection by rewrapping the socket
1902
+ tls_socket = OpenSSL::SSL::SSLSocket.new(@socket, @tls.fetch(:context))
1903
+
1904
+ # Close TCP socket after closing TLS socket as well.
1905
+ tls_socket.sync_close = true
1906
+
1907
+ # Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
1908
+ # https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
1909
+ tls_socket.hostname = @tls[:hostname]
1910
+
1911
+ tls_socket.connect
1912
+ @socket = tls_socket
1913
+ end
1914
+
1840
1915
  def read_line(deadline=nil)
1841
1916
  # FIXME: Should accumulate and read in a non blocking way instead
1842
1917
  unless ::IO.select([@socket], nil, nil, deadline)
@@ -58,6 +58,12 @@ module NATS
58
58
 
59
59
  # When drain takes too long to complete.
60
60
  class DrainTimeoutError < Error; end
61
+
62
+ # When a fork is detected, but the client is not configured to re-connect automatically.
63
+ class ForkDetectedError < Error; end
64
+
65
+ # When tried to send command after connection has been closed.
66
+ class ConnectionClosedError < Error; end
61
67
  end
62
68
 
63
69
  # Timeout is raised when the client gives up waiting for a response from a service.