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.
- checksums.yaml +4 -4
- data/LICENSE +201 -0
- data/README.md +251 -0
- data/lib/nats/io/client.rb +215 -144
- data/lib/nats/io/errors.rb +6 -0
- data/lib/nats/io/jetstream/msg/ack_methods.rb +8 -4
- data/lib/nats/io/msg.rb +3 -1
- data/lib/nats/io/rails.rb +29 -0
- data/lib/nats/io/subscription.rb +70 -5
- data/lib/nats/io/version.rb +1 -1
- data/lib/nats/io/websocket.rb +75 -0
- data/sig/nats/io/client.rbs +304 -0
- data/sig/nats/io/errors.rbs +54 -0
- data/sig/nats/io/jetstream/api.rbs +35 -0
- data/sig/nats/io/jetstream/errors.rbs +54 -0
- data/sig/nats/io/jetstream/js/config.rbs +11 -0
- data/sig/nats/io/jetstream/js/header.rbs +17 -0
- data/sig/nats/io/jetstream/js/status.rbs +13 -0
- data/sig/nats/io/jetstream/js/sub.rbs +14 -0
- data/sig/nats/io/jetstream/js.rbs +27 -0
- data/sig/nats/io/jetstream/manager.rbs +33 -0
- data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
- data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
- data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
- data/sig/nats/io/jetstream/msg.rbs +6 -0
- data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
- data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
- data/sig/nats/io/jetstream.rbs +15 -0
- data/sig/nats/io/kv/api.rbs +8 -0
- data/sig/nats/io/kv/bucket_status.rbs +17 -0
- data/sig/nats/io/kv/errors.rbs +30 -0
- data/sig/nats/io/kv/manager.rbs +11 -0
- data/sig/nats/io/kv.rbs +39 -0
- data/sig/nats/io/msg.rbs +14 -0
- data/sig/nats/io/parser.rbs +32 -0
- data/sig/nats/io/subscription.rbs +33 -0
- data/sig/nats/io/version.rbs +9 -0
- data/sig/nats/nuid.rbs +32 -0
- metadata +49 -4
data/lib/nats/io/client.rb
CHANGED
@@ -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
|
-
|
110
|
-
|
111
|
-
|
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 =
|
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
|
-
#
|
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
|
215
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
|
1360
|
-
|
1361
|
-
|
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
|
-
|
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.
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
|
1616
|
-
token = msg
|
1617
|
-
|
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
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
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
|
-
|
1665
|
-
|
1666
|
-
|
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)
|
data/lib/nats/io/errors.rb
CHANGED
@@ -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,
|
50
|
+
@nc.request(@reply, payload, **params)
|
47
51
|
else
|
48
|
-
@nc.publish(@reply,
|
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
|
-
|
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
|