nats-pure 2.2.1 → 2.3.0

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