nats-pure 2.2.1 → 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 (39) 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 +215 -144
  5. data/lib/nats/io/errors.rb +6 -0
  6. data/lib/nats/io/jetstream/msg/ack_methods.rb +8 -4
  7. data/lib/nats/io/msg.rb +3 -1
  8. data/lib/nats/io/rails.rb +29 -0
  9. data/lib/nats/io/subscription.rb +70 -5
  10. data/lib/nats/io/version.rb +1 -1
  11. data/lib/nats/io/websocket.rb +75 -0
  12. data/sig/nats/io/client.rbs +304 -0
  13. data/sig/nats/io/errors.rbs +54 -0
  14. data/sig/nats/io/jetstream/api.rbs +35 -0
  15. data/sig/nats/io/jetstream/errors.rbs +54 -0
  16. data/sig/nats/io/jetstream/js/config.rbs +11 -0
  17. data/sig/nats/io/jetstream/js/header.rbs +17 -0
  18. data/sig/nats/io/jetstream/js/status.rbs +13 -0
  19. data/sig/nats/io/jetstream/js/sub.rbs +14 -0
  20. data/sig/nats/io/jetstream/js.rbs +27 -0
  21. data/sig/nats/io/jetstream/manager.rbs +33 -0
  22. data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
  23. data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
  24. data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
  25. data/sig/nats/io/jetstream/msg.rbs +6 -0
  26. data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
  27. data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
  28. data/sig/nats/io/jetstream.rbs +15 -0
  29. data/sig/nats/io/kv/api.rbs +8 -0
  30. data/sig/nats/io/kv/bucket_status.rbs +17 -0
  31. data/sig/nats/io/kv/errors.rbs +30 -0
  32. data/sig/nats/io/kv/manager.rbs +11 -0
  33. data/sig/nats/io/kv.rbs +39 -0
  34. data/sig/nats/io/msg.rbs +14 -0
  35. data/sig/nats/io/parser.rbs +32 -0
  36. data/sig/nats/io/subscription.rbs +33 -0
  37. data/sig/nats/io/version.rbs +9 -0
  38. data/sig/nats/nuid.rbs +32 -0
  39. metadata +49 -4
@@ -18,6 +18,7 @@ require_relative 'errors'
18
18
  require_relative 'msg'
19
19
  require_relative 'subscription'
20
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
@@ -971,12 +1027,8 @@ module NATS
971
1027
  # Only dispatch message when sure that it would not block
972
1028
  # the main read loop from the parser.
973
1029
  msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
974
- sub.pending_queue << msg
975
1030
 
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
1031
+ sub.dispatch(msg)
980
1032
  end
981
1033
  end
982
1034
  end
@@ -1016,11 +1068,32 @@ module NATS
1016
1068
  @uri.scheme == "tls" || @tls
1017
1069
  end
1018
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
+
1019
1088
  def single_url_connect_used?
1020
1089
  @single_url_connect_used
1021
1090
  end
1022
1091
 
1023
1092
  def send_command(command)
1093
+ raise NATS::IO::ConnectionClosedError if closed?
1094
+
1095
+ establish_connection! if !status || (disconnected? && should_reconnect?)
1096
+
1024
1097
  @pending_size += command.bytesize
1025
1098
  @pending_queue << command
1026
1099
 
@@ -1047,12 +1120,6 @@ module NATS
1047
1120
  synchronize do
1048
1121
  sub.max = opt_max
1049
1122
  @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
1123
  end
1057
1124
 
1058
1125
  sub.synchronize do
@@ -1107,11 +1174,6 @@ module NATS
1107
1174
 
1108
1175
  to_delete.each do |sub|
1109
1176
  @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
1177
  end
1116
1178
  to_delete.clear
1117
1179
 
@@ -1126,6 +1188,9 @@ module NATS
1126
1188
  end
1127
1189
  end
1128
1190
 
1191
+ subscription_executor.shutdown
1192
+ subscription_executor.wait_for_termination(@options[:drain_timeout])
1193
+
1129
1194
  if MonotonicTime::now > drain_timeout
1130
1195
  e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
1131
1196
  err_cb_call(self, e, nil) if @err_cb
@@ -1287,7 +1352,7 @@ module NATS
1287
1352
  @flush_queue.pop
1288
1353
 
1289
1354
  should_bail = synchronize do
1290
- @status != CONNECTED || @status == CONNECTING
1355
+ (@status != CONNECTED && !draining? ) || @status == CONNECTING
1291
1356
  end
1292
1357
  return if should_bail
1293
1358
 
@@ -1342,6 +1407,7 @@ module NATS
1342
1407
  end
1343
1408
 
1344
1409
  def process_connect_init
1410
+ # FIXME: Can receive PING as well here in recent versions.
1345
1411
  line = @io.read_line(options[:connect_timeout])
1346
1412
  if !line or line.empty?
1347
1413
  raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not received")
@@ -1356,39 +1422,13 @@ module NATS
1356
1422
 
1357
1423
  case
1358
1424
  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?)
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")
1390
1428
  raise NATS::IO::ConnectError.new('TLS/SSL required by server')
1391
- 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")
1392
1432
  raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1393
1433
  else
1394
1434
  # Otherwise, use a regular connection.
@@ -1433,11 +1473,6 @@ module NATS
1433
1473
  begin
1434
1474
  srv = select_next_server
1435
1475
 
1436
- # Establish TCP connection with new server
1437
- @io = create_socket
1438
- @io.connect
1439
- @stats[:reconnects] += 1
1440
-
1441
1476
  # Set hostname to use for TLS hostname verification
1442
1477
  if client_using_secure_connection? and single_url_connect_used?
1443
1478
  # Reuse original hostname name in case of using TLS.
@@ -1446,6 +1481,11 @@ module NATS
1446
1481
  @hostname = srv[:hostname]
1447
1482
  end
1448
1483
 
1484
+ # Establish TCP connection with new server
1485
+ @io = create_socket
1486
+ @io.connect
1487
+ @stats[:reconnects] += 1
1488
+
1449
1489
  # Established TCP connection successfully so can start connect
1450
1490
  process_connect_init
1451
1491
 
@@ -1505,6 +1545,7 @@ module NATS
1505
1545
 
1506
1546
  def close_connection(conn_status, do_cbs=true)
1507
1547
  synchronize do
1548
+ @connect_called = false
1508
1549
  if @status == CLOSED
1509
1550
  @status = conn_status
1510
1551
  return
@@ -1553,12 +1594,6 @@ module NATS
1553
1594
  end if should_flush
1554
1595
 
1555
1596
  # 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
1597
  @subs.clear
1563
1598
 
1564
1599
  if do_cbs
@@ -1580,15 +1615,24 @@ module NATS
1580
1615
  def start_threads!
1581
1616
  # Reading loop for gathering data
1582
1617
  @read_loop_thread = Thread.new { read_loop }
1618
+ @read_loop_thread.name = "nats:read_loop"
1583
1619
  @read_loop_thread.abort_on_exception = true
1584
1620
 
1585
1621
  # Flusher loop for sending commands
1586
1622
  @flusher_thread = Thread.new { flusher_loop }
1623
+ @flusher_thread.name = "nats:flusher_loop"
1587
1624
  @flusher_thread.abort_on_exception = true
1588
1625
 
1589
1626
  # Ping interval handling for keeping alive the connection
1590
1627
  @ping_interval_thread = Thread.new { ping_interval_loop }
1628
+ @ping_interval_thread.name = "nats:ping_loop"
1591
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
+ )
1592
1636
  end
1593
1637
 
1594
1638
  # Prepares requests subscription that handles the responses
@@ -1606,29 +1650,25 @@ module NATS
1606
1650
  @resp_sub.pending_msgs_limit = NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
1607
1651
  @resp_sub.pending_bytes_limit = NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
1608
1652
  @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
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
1622
1662
 
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
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
1628
1667
  end
1629
1668
  end
1630
1669
 
1631
1670
  sid = (@ssid += 1)
1671
+ @resp_sub.sid = sid
1632
1672
  @subs[sid] = @resp_sub
1633
1673
  send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
1634
1674
  @flush_queue << :sub
@@ -1661,10 +1701,21 @@ module NATS
1661
1701
  end
1662
1702
 
1663
1703
  def create_socket
1664
- NATS::IO::Socket.new({
1665
- uri: @uri,
1666
- connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT
1667
- })
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
+ )
1668
1719
  end
1669
1720
 
1670
1721
  def setup_nkeys_connect
@@ -1774,7 +1825,7 @@ module NATS
1774
1825
 
1775
1826
  # Host and Port
1776
1827
  uri_object.hostname ||= "localhost"
1777
- uri_object.port ||= DEFAULT_PORT
1828
+ uri_object.port ||= DEFAULT_PORT.fetch(uri.scheme.to_sym, DEFAULT_PORT[:nats])
1778
1829
 
1779
1830
  uri_object
1780
1831
  end
@@ -1813,6 +1864,9 @@ module NATS
1813
1864
  DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
1814
1865
  DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
1815
1866
 
1867
+ DEFAULT_TOTAL_SUB_CONCURRENCY = 24
1868
+ DEFAULT_SINGLE_SUB_CONCURRENCY = 1
1869
+
1816
1870
  # Implementation adapted from https://github.com/redis/redis-rb
1817
1871
  class Socket
1818
1872
  attr_accessor :socket
@@ -1823,6 +1877,7 @@ module NATS
1823
1877
  @write_timeout = options[:write_timeout]
1824
1878
  @read_timeout = options[:read_timeout]
1825
1879
  @socket = nil
1880
+ @tls = options[:tls]
1826
1881
  end
1827
1882
 
1828
1883
  def connect
@@ -1841,6 +1896,22 @@ module NATS
1841
1896
  @socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
1842
1897
  end
1843
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
+
1844
1915
  def read_line(deadline=nil)
1845
1916
  # FIXME: Should accumulate and read in a non blocking way instead
1846
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.
@@ -41,11 +41,15 @@ module NATS
41
41
 
42
42
  def nak(**params)
43
43
  ensure_is_acked_once!
44
-
44
+ payload = if params[:delay]
45
+ payload = "#{Ack::Nak} #{{ delay: params[:delay] }.to_json}"
46
+ else
47
+ Ack::Nak
48
+ end
45
49
  resp = if params[:timeout]
46
- @nc.request(@reply, Ack::Nak, **params)
50
+ @nc.request(@reply, payload, **params)
47
51
  else
48
- @nc.publish(@reply, Ack::Nak)
52
+ @nc.publish(@reply, payload)
49
53
  end
50
54
  @sub.synchronize { @ackd = true }
51
55
 
@@ -104,4 +108,4 @@ module NATS
104
108
  end
105
109
  end
106
110
  end
107
- end
111
+ end
data/lib/nats/io/msg.rb CHANGED
@@ -50,7 +50,9 @@ module NATS
50
50
 
51
51
  def inspect
52
52
  hdr = ", header=#{@header}" if @header
53
- "#<NATS::Msg(subject: \"#{@subject}\", reply: \"#{@reply}\", data: #{@data.slice(0, 10).inspect}#{hdr})>"
53
+ dot = '...' if @data.length > 10
54
+ dat = "#{data.slice(0, 10)}#{dot}"
55
+ "#<NATS::Msg(subject: \"#{@subject}\", reply: \"#{@reply}\", data: #{dat.inspect}#{hdr})>"
54
56
  end
55
57
  end
56
58
  end
@@ -0,0 +1,29 @@
1
+ require "rails"
2
+
3
+ module NATS
4
+ class Rails < ::Rails::Engine
5
+ # This class is used to free resources managed by Rails (e.g. database connections)
6
+ # that were implicitly acquired in subscription callbacks
7
+ # Implementation is based on https://github.com/sidekiq/sidekiq/blob/5e1a77a6d03193dd977fbfe8961ab78df91bb392/lib/sidekiq/rails.rb
8
+ class Reloader
9
+ def initialize(app = ::Rails.application)
10
+ @app = app
11
+ end
12
+
13
+ def call
14
+ params = (::Rails::VERSION::STRING >= "7.1") ? {source: "gem.nats"} : {}
15
+ @app.reloader.wrap(**params) do
16
+ yield
17
+ end
18
+ end
19
+
20
+ def inspect
21
+ "#<NATS::Rails::Reloader @app=#{@app.class.name}>"
22
+ end
23
+ end
24
+
25
+ config.after_initialize do
26
+ NATS::Client.default_reloader = NATS::Rails::Reloader.new
27
+ end
28
+ end
29
+ end