nats-pure 2.2.1 → 2.4.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -0
  3. data/README.md +251 -0
  4. data/lib/nats/client.rb +1 -0
  5. data/lib/nats/io/client.rb +214 -144
  6. data/lib/nats/io/errors.rb +6 -0
  7. data/lib/nats/io/jetstream/api.rb +4 -0
  8. data/lib/nats/io/jetstream/manager.rb +21 -2
  9. data/lib/nats/io/jetstream/msg/ack_methods.rb +8 -4
  10. data/lib/nats/io/jetstream.rb +82 -7
  11. data/lib/nats/io/msg.rb +3 -1
  12. data/lib/nats/io/rails.rb +29 -0
  13. data/lib/nats/io/subscription.rb +70 -5
  14. data/lib/nats/io/version.rb +1 -1
  15. data/lib/nats/io/websocket.rb +75 -0
  16. data/sig/nats/io/client.rbs +304 -0
  17. data/sig/nats/io/errors.rbs +54 -0
  18. data/sig/nats/io/jetstream/api.rbs +35 -0
  19. data/sig/nats/io/jetstream/errors.rbs +54 -0
  20. data/sig/nats/io/jetstream/js/config.rbs +11 -0
  21. data/sig/nats/io/jetstream/js/header.rbs +17 -0
  22. data/sig/nats/io/jetstream/js/status.rbs +13 -0
  23. data/sig/nats/io/jetstream/js/sub.rbs +14 -0
  24. data/sig/nats/io/jetstream/js.rbs +27 -0
  25. data/sig/nats/io/jetstream/manager.rbs +33 -0
  26. data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
  27. data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
  28. data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
  29. data/sig/nats/io/jetstream/msg.rbs +6 -0
  30. data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
  31. data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
  32. data/sig/nats/io/jetstream.rbs +15 -0
  33. data/sig/nats/io/kv/api.rbs +8 -0
  34. data/sig/nats/io/kv/bucket_status.rbs +17 -0
  35. data/sig/nats/io/kv/errors.rbs +30 -0
  36. data/sig/nats/io/kv/manager.rbs +11 -0
  37. data/sig/nats/io/kv.rbs +39 -0
  38. data/sig/nats/io/msg.rbs +14 -0
  39. data/sig/nats/io/parser.rbs +32 -0
  40. data/sig/nats/io/subscription.rbs +33 -0
  41. data/sig/nats/io/version.rbs +9 -0
  42. data/sig/nats/nuid.rbs +32 -0
  43. metadata +49 -4
@@ -26,6 +26,7 @@ require 'json'
26
26
  require 'monitor'
27
27
  require 'uri'
28
28
  require 'securerandom'
29
+ require 'concurrent'
29
30
 
30
31
  begin
31
32
  require "openssl"
@@ -81,15 +82,28 @@ module NATS
81
82
  DRAINING_PUBS = 6
82
83
  end
83
84
 
85
+ # Fork Detection handling
86
+ # Based from similar approach as mperham/connection_pool: https://github.com/mperham/connection_pool/pull/166
87
+ if Process.respond_to?(:fork) && Process.respond_to?(:_fork) # MRI 3.1+
88
+ module ForkTracker
89
+ def _fork
90
+ super.tap do |pid|
91
+ Client.after_fork if pid.zero? # in the child process
92
+ end
93
+ end
94
+ end
95
+ Process.singleton_class.prepend(ForkTracker)
96
+ end
97
+
84
98
  # Client creates a connection to the NATS Server.
85
99
  class Client
86
100
  include MonitorMixin
87
101
  include Status
88
102
 
89
- attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri
103
+ attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri, :subscription_executor, :reloader
90
104
 
91
- DEFAULT_PORT = 4222
92
- DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT}".freeze)
105
+ DEFAULT_PORT = { nats: 4222, ws: 80, wss: 443 }.freeze
106
+ DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT[:nats]}".freeze)
93
107
 
94
108
  CR_LF = ("\r\n".freeze)
95
109
  CR_LF_SIZE = (CR_LF.bytesize)
@@ -106,9 +120,39 @@ module NATS
106
120
  SUB_OP = ('SUB'.freeze)
107
121
  EMPTY_MSG = (''.freeze)
108
122
 
109
- def initialize
110
- super # required to initialize monitor
111
- @options = nil
123
+ INSTANCES = ObjectSpace::WeakMap.new # tracks all alive client instances
124
+ private_constant :INSTANCES
125
+
126
+ class << self
127
+ # Reloader should free resources managed by external framework
128
+ # that were implicitly acquired in subscription callbacks.
129
+ attr_writer :default_reloader
130
+
131
+ def default_reloader
132
+ @default_reloader ||= proc { |&block| block.call }.tap { |r| Ractor.make_shareable(r) if defined? Ractor }
133
+ end
134
+
135
+ # Re-establish connection in a new process after forking to start new threads.
136
+ def after_fork
137
+ INSTANCES.each do |client|
138
+ if client.options[:reconnect]
139
+ was_connected = !client.disconnected?
140
+ client.send(:close_connection, Status::DISCONNECTED, true)
141
+ client.connect if was_connected
142
+ else
143
+ client.send(:err_cb_call, self, NATS::IO::ForkDetectedError, nil)
144
+ client.close
145
+ end
146
+ rescue => e
147
+ warn "nats: Error during handling after_fork callback: #{e}" # TODO: Report as async error via error callback?
148
+ end
149
+ end
150
+ end
151
+
152
+ def initialize(uri = nil, opts = {})
153
+ super() # required to initialize monitor
154
+ @initial_uri = uri
155
+ @initial_options = opts
112
156
 
113
157
  # Read/Write IO
114
158
  @io = nil
@@ -132,7 +176,7 @@ module NATS
132
176
  @uri = nil
133
177
  @server_pool = []
134
178
 
135
- @status = DISCONNECTED
179
+ @status = nil
136
180
 
137
181
  # Subscriptions
138
182
  @subs = { }
@@ -194,29 +238,63 @@ module NATS
194
238
 
195
239
  # Draining
196
240
  @drain_t = nil
241
+
242
+ # Prepare for calling connect or automatic delayed connection
243
+ parse_and_validate_options if uri || opts.any?
244
+
245
+ # Keep track of all client instances to handle them after process forking in Ruby 3.1+
246
+ INSTANCES[self] = self if !defined?(Ractor) || Ractor.current == Ractor.main # Ractors doesn't work in forked processes
247
+
248
+ @reloader = opts.fetch(:reloader, self.class.default_reloader)
197
249
  end
198
250
 
199
- # Establishes a connection to NATS.
251
+ # Prepare connecting to NATS, but postpone real connection until first usage.
200
252
  def connect(uri=nil, opts={})
253
+ if uri || opts.any?
254
+ @initial_uri = uri
255
+ @initial_options = opts
256
+ end
257
+
201
258
  synchronize do
202
259
  # In case it has been connected already, then do not need to call this again.
203
260
  return if @connect_called
204
261
  @connect_called = true
205
262
  end
206
263
 
264
+ parse_and_validate_options
265
+ establish_connection!
266
+
267
+ self
268
+ end
269
+
270
+ private def parse_and_validate_options
271
+ # Reset these in case we have reconnected via fork.
272
+ @server_pool = []
273
+ @resp_sub = nil
274
+ @resp_map = nil
275
+ @resp_sub_prefix = nil
276
+ @nuid = NATS::NUID.new
277
+ @stats = {
278
+ in_msgs: 0,
279
+ out_msgs: 0,
280
+ in_bytes: 0,
281
+ out_bytes: 0,
282
+ reconnects: 0
283
+ }
284
+ @status = DISCONNECTED
285
+
207
286
  # Convert URI to string if needed.
287
+ uri = @initial_uri.dup
208
288
  uri = uri.to_s if uri.is_a?(URI)
209
289
 
290
+ opts = @initial_options.dup
291
+
210
292
  case uri
211
293
  when String
212
294
  # Initialize TLS defaults in case any url is using it.
213
295
  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
- }
296
+ if srvs.any? {|u| %w[tls wss].include? u.scheme } and !opts[:tls]
297
+ opts[:tls] = { context: tls_context }
220
298
  end
221
299
  @single_url_connect_used = true if srvs.size == 1
222
300
  when Hash
@@ -287,11 +365,25 @@ module NATS
287
365
 
288
366
  validate_settings!
289
367
 
368
+ self
369
+ end
370
+
371
+ private def establish_connection!
372
+ @ruby_pid = Process.pid # For fork detection
373
+
290
374
  srv = nil
291
375
  begin
292
376
  srv = select_next_server
293
377
 
294
- # Create TCP socket connection to NATS
378
+ # Use the hostname from the server for TLS hostname verification.
379
+ if client_using_secure_connection? and single_url_connect_used?
380
+ # Always reuse the original hostname used to connect.
381
+ @hostname ||= srv[:hostname]
382
+ else
383
+ @hostname = srv[:hostname]
384
+ end
385
+
386
+ # Create TCP socket connection to NATS.
295
387
  @io = create_socket
296
388
  @io.connect
297
389
 
@@ -302,14 +394,6 @@ module NATS
302
394
  # Connection established and now in process of sending CONNECT to NATS
303
395
  @status = CONNECTING
304
396
 
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
397
  # Established TCP connection successfully so can start connect
314
398
  process_connect_init
315
399
 
@@ -341,8 +425,8 @@ module NATS
341
425
  # triggering the disconnection/closed callbacks.
342
426
  close_connection(DISCONNECTED, false)
343
427
 
344
- # always sleep here to safe guard against errors before current[:was_connected]
345
- # is set for the first time
428
+ # Always sleep here to safe guard against errors before current[:was_connected]
429
+ # is set for the first time.
346
430
  sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
347
431
 
348
432
  # Continue retrying until there are no options left in the server pool
@@ -434,6 +518,7 @@ module NATS
434
518
  sub.pending_msgs_limit = opts[:pending_msgs_limit]
435
519
  sub.pending_bytes_limit = opts[:pending_bytes_limit]
436
520
  sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
521
+ sub.processing_concurrency = opts[:processing_concurrency] if opts.key?(:processing_concurrency)
437
522
 
438
523
  send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
439
524
  @flush_queue << :sub
@@ -446,41 +531,6 @@ module NATS
446
531
  sub.wait_for_msgs_cond = cond
447
532
  end
448
533
 
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
534
  sub
485
535
  end
486
536
 
@@ -709,6 +759,10 @@ module NATS
709
759
  connected? ? @uri : nil
710
760
  end
711
761
 
762
+ def disconnected?
763
+ !@status or @status == DISCONNECTED
764
+ end
765
+
712
766
  def connected?
713
767
  @status == CONNECTED
714
768
  end
@@ -812,7 +866,8 @@ module NATS
812
866
  if !@options[:ignore_discovered_urls] && connect_urls
813
867
  srvs = []
814
868
  connect_urls.each do |url|
815
- scheme = client_using_secure_connection? ? "tls" : "nats"
869
+ # Use the same scheme as the currently in use URI.
870
+ scheme = @uri.scheme
816
871
  u = URI.parse("#{scheme}://#{url}")
817
872
 
818
873
  # Skip in case it is the current server which we already know
@@ -971,12 +1026,8 @@ module NATS
971
1026
  # Only dispatch message when sure that it would not block
972
1027
  # the main read loop from the parser.
973
1028
  msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
974
- sub.pending_queue << msg
975
1029
 
976
- # For sync subscribers, signal that there is a new message.
977
- sub.wait_for_msgs_cond.signal if sub.wait_for_msgs_cond
978
-
979
- sub.pending_size += data.size
1030
+ sub.dispatch(msg)
980
1031
  end
981
1032
  end
982
1033
  end
@@ -1016,11 +1067,32 @@ module NATS
1016
1067
  @uri.scheme == "tls" || @tls
1017
1068
  end
1018
1069
 
1070
+ def tls_context
1071
+ return nil unless @tls
1072
+
1073
+ # Allow prepared context and customizations via :tls opts
1074
+ return @tls[:context] if @tls[:context]
1075
+
1076
+ @tls_context ||= OpenSSL::SSL::SSLContext.new.tap do |tls_context|
1077
+ # Use the default verification options from Ruby:
1078
+ # https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
1079
+ #
1080
+ # Insecure TLS versions not supported already:
1081
+ # https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
1082
+ #
1083
+ tls_context.set_params
1084
+ end
1085
+ end
1086
+
1019
1087
  def single_url_connect_used?
1020
1088
  @single_url_connect_used
1021
1089
  end
1022
1090
 
1023
1091
  def send_command(command)
1092
+ raise NATS::IO::ConnectionClosedError if closed?
1093
+
1094
+ establish_connection! if !status || (disconnected? && should_reconnect?)
1095
+
1024
1096
  @pending_size += command.bytesize
1025
1097
  @pending_queue << command
1026
1098
 
@@ -1047,12 +1119,6 @@ module NATS
1047
1119
  synchronize do
1048
1120
  sub.max = opt_max
1049
1121
  @subs.delete(sid) unless (sub.max && (sub.received < sub.max))
1050
-
1051
- # Stop messages delivery thread for async subscribers
1052
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1053
- sub.wait_for_msgs_t.exit
1054
- sub.pending_queue.clear
1055
- end
1056
1122
  end
1057
1123
 
1058
1124
  sub.synchronize do
@@ -1107,11 +1173,6 @@ module NATS
1107
1173
 
1108
1174
  to_delete.each do |sub|
1109
1175
  @subs.delete(sub.sid)
1110
- # Stop messages delivery thread for async subscribers
1111
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1112
- sub.wait_for_msgs_t.exit
1113
- sub.pending_queue.clear
1114
- end
1115
1176
  end
1116
1177
  to_delete.clear
1117
1178
 
@@ -1126,6 +1187,9 @@ module NATS
1126
1187
  end
1127
1188
  end
1128
1189
 
1190
+ subscription_executor.shutdown
1191
+ subscription_executor.wait_for_termination(@options[:drain_timeout])
1192
+
1129
1193
  if MonotonicTime::now > drain_timeout
1130
1194
  e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
1131
1195
  err_cb_call(self, e, nil) if @err_cb
@@ -1287,7 +1351,7 @@ module NATS
1287
1351
  @flush_queue.pop
1288
1352
 
1289
1353
  should_bail = synchronize do
1290
- @status != CONNECTED || @status == CONNECTING
1354
+ (@status != CONNECTED && !draining? ) || @status == CONNECTING
1291
1355
  end
1292
1356
  return if should_bail
1293
1357
 
@@ -1342,6 +1406,7 @@ module NATS
1342
1406
  end
1343
1407
 
1344
1408
  def process_connect_init
1409
+ # FIXME: Can receive PING as well here in recent versions.
1345
1410
  line = @io.read_line(options[:connect_timeout])
1346
1411
  if !line or line.empty?
1347
1412
  raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not received")
@@ -1356,39 +1421,13 @@ module NATS
1356
1421
 
1357
1422
  case
1358
1423
  when (server_using_secure_connection? and client_using_secure_connection?)
1359
- tls_context = nil
1360
-
1361
- if @tls
1362
- # Allow prepared context and customizations via :tls opts
1363
- tls_context = @tls[:context] if @tls[:context]
1364
- else
1365
- # Defaults
1366
- tls_context = OpenSSL::SSL::SSLContext.new
1367
-
1368
- # Use the default verification options from Ruby:
1369
- # https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
1370
- #
1371
- # Insecure TLS versions not supported already:
1372
- # https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
1373
- #
1374
- tls_context.set_params
1375
- end
1376
-
1377
- # Setup TLS connection by rewrapping the socket
1378
- tls_socket = OpenSSL::SSL::SSLSocket.new(@io.socket, tls_context)
1379
-
1380
- # Close TCP socket after closing TLS socket as well.
1381
- tls_socket.sync_close = true
1382
-
1383
- # Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
1384
- # https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
1385
- tls_socket.hostname = @hostname
1386
-
1387
- tls_socket.connect
1388
- @io.socket = tls_socket
1389
- when (server_using_secure_connection? and !client_using_secure_connection?)
1424
+ @io.setup_tls!
1425
+ # 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")
1390
1427
  raise NATS::IO::ConnectError.new('TLS/SSL required by server')
1391
- when (client_using_secure_connection? and !server_using_secure_connection?)
1428
+ # Server < v2.9.19 requiring TLS/SSL over websocket but not requiring it over standard protocol
1429
+ # 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")
1392
1431
  raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1393
1432
  else
1394
1433
  # Otherwise, use a regular connection.
@@ -1433,11 +1472,6 @@ module NATS
1433
1472
  begin
1434
1473
  srv = select_next_server
1435
1474
 
1436
- # Establish TCP connection with new server
1437
- @io = create_socket
1438
- @io.connect
1439
- @stats[:reconnects] += 1
1440
-
1441
1475
  # Set hostname to use for TLS hostname verification
1442
1476
  if client_using_secure_connection? and single_url_connect_used?
1443
1477
  # Reuse original hostname name in case of using TLS.
@@ -1446,6 +1480,11 @@ module NATS
1446
1480
  @hostname = srv[:hostname]
1447
1481
  end
1448
1482
 
1483
+ # Establish TCP connection with new server
1484
+ @io = create_socket
1485
+ @io.connect
1486
+ @stats[:reconnects] += 1
1487
+
1449
1488
  # Established TCP connection successfully so can start connect
1450
1489
  process_connect_init
1451
1490
 
@@ -1505,6 +1544,7 @@ module NATS
1505
1544
 
1506
1545
  def close_connection(conn_status, do_cbs=true)
1507
1546
  synchronize do
1547
+ @connect_called = false
1508
1548
  if @status == CLOSED
1509
1549
  @status = conn_status
1510
1550
  return
@@ -1553,12 +1593,6 @@ module NATS
1553
1593
  end if should_flush
1554
1594
 
1555
1595
  # Destroy any remaining subscriptions.
1556
- @subs.each do |_, sub|
1557
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1558
- sub.wait_for_msgs_t.exit
1559
- sub.pending_queue.clear
1560
- end
1561
- end
1562
1596
  @subs.clear
1563
1597
 
1564
1598
  if do_cbs
@@ -1580,15 +1614,24 @@ module NATS
1580
1614
  def start_threads!
1581
1615
  # Reading loop for gathering data
1582
1616
  @read_loop_thread = Thread.new { read_loop }
1617
+ @read_loop_thread.name = "nats:read_loop"
1583
1618
  @read_loop_thread.abort_on_exception = true
1584
1619
 
1585
1620
  # Flusher loop for sending commands
1586
1621
  @flusher_thread = Thread.new { flusher_loop }
1622
+ @flusher_thread.name = "nats:flusher_loop"
1587
1623
  @flusher_thread.abort_on_exception = true
1588
1624
 
1589
1625
  # Ping interval handling for keeping alive the connection
1590
1626
  @ping_interval_thread = Thread.new { ping_interval_loop }
1627
+ @ping_interval_thread.name = "nats:ping_loop"
1591
1628
  @ping_interval_thread.abort_on_exception = true
1629
+
1630
+ # Subscription handling thread pool
1631
+ @subscription_executor = Concurrent::ThreadPoolExecutor.new(
1632
+ name: 'nats:subscription', # threads will be given names like nats:subscription-worker-1
1633
+ max_threads: NATS::IO::DEFAULT_TOTAL_SUB_CONCURRENCY,
1634
+ )
1592
1635
  end
1593
1636
 
1594
1637
  # Prepares requests subscription that handles the responses
@@ -1606,29 +1649,25 @@ module NATS
1606
1649
  @resp_sub.pending_msgs_limit = NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
1607
1650
  @resp_sub.pending_bytes_limit = NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
1608
1651
  @resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
1609
- @resp_sub.wait_for_msgs_t = Thread.new do
1610
- loop do
1611
- msg = @resp_sub.pending_queue.pop
1612
- @resp_sub.pending_size -= msg.data.size
1613
-
1614
- # Pick the token and signal the request under the mutex
1615
- # from the subscription itself.
1616
- token = msg.subject.split('.').last
1617
- future = nil
1618
- synchronize do
1619
- future = @resp_map[token][:future]
1620
- @resp_map[token][:response] = msg
1621
- end
1652
+ @resp_sub.callback = proc do |msg|
1653
+ # Pick the token and signal the request under the mutex
1654
+ # from the subscription itself.
1655
+ token = msg.subject.split('.').last
1656
+ future = nil
1657
+ synchronize do
1658
+ future = @resp_map[token][:future]
1659
+ @resp_map[token][:response] = msg
1660
+ end
1622
1661
 
1623
- # Signal back that the response has arrived
1624
- # in case the future has not been yet delete.
1625
- @resp_sub.synchronize do
1626
- future.signal if future
1627
- end
1662
+ # Signal back that the response has arrived
1663
+ # in case the future has not been yet delete.
1664
+ @resp_sub.synchronize do
1665
+ future.signal if future
1628
1666
  end
1629
1667
  end
1630
1668
 
1631
1669
  sid = (@ssid += 1)
1670
+ @resp_sub.sid = sid
1632
1671
  @subs[sid] = @resp_sub
1633
1672
  send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
1634
1673
  @flush_queue << :sub
@@ -1661,10 +1700,21 @@ module NATS
1661
1700
  end
1662
1701
 
1663
1702
  def create_socket
1664
- NATS::IO::Socket.new({
1665
- uri: @uri,
1666
- connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT
1667
- })
1703
+ 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
1712
+
1713
+ socket_class.new(
1714
+ uri: @uri,
1715
+ tls: { context: tls_context, hostname: @hostname },
1716
+ connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT,
1717
+ )
1668
1718
  end
1669
1719
 
1670
1720
  def setup_nkeys_connect
@@ -1774,7 +1824,7 @@ module NATS
1774
1824
 
1775
1825
  # Host and Port
1776
1826
  uri_object.hostname ||= "localhost"
1777
- uri_object.port ||= DEFAULT_PORT
1827
+ uri_object.port ||= DEFAULT_PORT.fetch(uri_object.scheme.to_sym, DEFAULT_PORT[:nats])
1778
1828
 
1779
1829
  uri_object
1780
1830
  end
@@ -1813,6 +1863,9 @@ module NATS
1813
1863
  DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
1814
1864
  DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
1815
1865
 
1866
+ DEFAULT_TOTAL_SUB_CONCURRENCY = 24
1867
+ DEFAULT_SINGLE_SUB_CONCURRENCY = 1
1868
+
1816
1869
  # Implementation adapted from https://github.com/redis/redis-rb
1817
1870
  class Socket
1818
1871
  attr_accessor :socket
@@ -1823,6 +1876,7 @@ module NATS
1823
1876
  @write_timeout = options[:write_timeout]
1824
1877
  @read_timeout = options[:read_timeout]
1825
1878
  @socket = nil
1879
+ @tls = options[:tls]
1826
1880
  end
1827
1881
 
1828
1882
  def connect
@@ -1841,6 +1895,22 @@ module NATS
1841
1895
  @socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
1842
1896
  end
1843
1897
 
1898
+ # (Re-)connect using secure connection if server and client agreed on using it.
1899
+ def setup_tls!
1900
+ # Setup TLS connection by rewrapping the socket
1901
+ tls_socket = OpenSSL::SSL::SSLSocket.new(@socket, @tls.fetch(:context))
1902
+
1903
+ # Close TCP socket after closing TLS socket as well.
1904
+ tls_socket.sync_close = true
1905
+
1906
+ # Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
1907
+ # https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
1908
+ tls_socket.hostname = @tls[:hostname]
1909
+
1910
+ tls_socket.connect
1911
+ @socket = tls_socket
1912
+ end
1913
+
1844
1914
  def read_line(deadline=nil)
1845
1915
  # FIXME: Should accumulate and read in a non blocking way instead
1846
1916
  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.
@@ -119,6 +119,9 @@ module NATS
119
119
  :num_replicas,
120
120
  # Force memory storage
121
121
  :mem_storage,
122
+
123
+ # NATS v2.10 features
124
+ :metadata, :filter_subjects, :max_bytes,
122
125
  keyword_init: true) do
123
126
  def initialize(opts={})
124
127
  # Filter unrecognized fields just in case.
@@ -192,6 +195,7 @@ module NATS
192
195
  :republish,
193
196
  :allow_direct,
194
197
  :mirror_direct,
198
+ :metadata,
195
199
  keyword_init: true) do
196
200
  def initialize(opts={})
197
201
  # Filter unrecognized fields just in case.
@@ -106,18 +106,37 @@ module NATS
106
106
  else
107
107
  config
108
108
  end
109
-
109
+ config[:name] ||= config[:durable_name]
110
110
  req_subject = case
111
111
  when config[:name]
112
- # NOTE: Only supported after nats-server v2.9.0
112
+ ###############################################################################
113
+ # #
114
+ # Using names is the supported way of creating consumers (NATS +v2.9.0. #
115
+ # #
116
+ ###############################################################################
113
117
  if config[:filter_subject] && config[:filter_subject] != ">"
114
118
  "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}.#{config[:filter_subject]}"
115
119
  else
120
+ ##############################################################################
121
+ # #
122
+ # Endpoint to support creating ANY consumer with multi-filters (NATS +v2.10) #
123
+ # #
124
+ ##############################################################################
116
125
  "#{@prefix}.CONSUMER.CREATE.#{stream}.#{config[:name]}"
117
126
  end
118
127
  when config[:durable_name]
128
+ ###############################################################################
129
+ # #
130
+ # Endpoint to support creating DURABLES before NATS v2.9.0. #
131
+ # #
132
+ ###############################################################################
119
133
  "#{@prefix}.CONSUMER.DURABLE.CREATE.#{stream}.#{config[:durable_name]}"
120
134
  else
135
+ ###############################################################################
136
+ # #
137
+ # Endpoint to support creating EPHEMERALS before NATS v2.9.0. #
138
+ # #
139
+ ###############################################################################
121
140
  "#{@prefix}.CONSUMER.CREATE.#{stream}"
122
141
  end
123
142