nats-pure 2.2.1 → 2.4.0

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