nats-pure 2.2.0 → 2.3.0

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