nats-pure 0.7.2 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +201 -0
  3. data/README.md +251 -0
  4. data/lib/nats/client.rb +16 -0
  5. data/lib/nats/io/client.rb +1559 -1277
  6. data/lib/nats/io/errors.rb +74 -0
  7. data/lib/nats/io/jetstream/api.rb +309 -0
  8. data/lib/nats/io/jetstream/errors.rb +104 -0
  9. data/lib/nats/io/jetstream/js/config.rb +26 -0
  10. data/lib/nats/io/jetstream/js/header.rb +31 -0
  11. data/lib/nats/io/jetstream/js/status.rb +27 -0
  12. data/lib/nats/io/jetstream/js/sub.rb +30 -0
  13. data/lib/nats/io/jetstream/js.rb +93 -0
  14. data/lib/nats/io/jetstream/manager.rb +303 -0
  15. data/lib/nats/io/jetstream/msg/ack.rb +57 -0
  16. data/lib/nats/io/jetstream/msg/ack_methods.rb +111 -0
  17. data/lib/nats/io/jetstream/msg/metadata.rb +37 -0
  18. data/lib/nats/io/jetstream/msg.rb +26 -0
  19. data/lib/nats/io/jetstream/pull_subscription.rb +260 -0
  20. data/lib/nats/io/jetstream/push_subscription.rb +42 -0
  21. data/lib/nats/io/jetstream.rb +344 -0
  22. data/lib/nats/io/kv/api.rb +39 -0
  23. data/lib/nats/io/kv/bucket_status.rb +38 -0
  24. data/lib/nats/io/kv/errors.rb +60 -0
  25. data/lib/nats/io/kv/manager.rb +89 -0
  26. data/lib/nats/io/kv.rb +178 -0
  27. data/lib/nats/io/msg.rb +58 -0
  28. data/lib/nats/io/parser.rb +7 -7
  29. data/lib/nats/io/rails.rb +29 -0
  30. data/lib/nats/io/subscription.rb +157 -0
  31. data/lib/nats/io/version.rb +8 -4
  32. data/lib/nats/io/websocket.rb +75 -0
  33. data/lib/nats/nuid.rb +3 -1
  34. data/lib/nats.rb +39 -0
  35. data/sig/nats/io/client.rbs +304 -0
  36. data/sig/nats/io/errors.rbs +54 -0
  37. data/sig/nats/io/jetstream/api.rbs +35 -0
  38. data/sig/nats/io/jetstream/errors.rbs +54 -0
  39. data/sig/nats/io/jetstream/js/config.rbs +11 -0
  40. data/sig/nats/io/jetstream/js/header.rbs +17 -0
  41. data/sig/nats/io/jetstream/js/status.rbs +13 -0
  42. data/sig/nats/io/jetstream/js/sub.rbs +14 -0
  43. data/sig/nats/io/jetstream/js.rbs +27 -0
  44. data/sig/nats/io/jetstream/manager.rbs +33 -0
  45. data/sig/nats/io/jetstream/msg/ack.rbs +35 -0
  46. data/sig/nats/io/jetstream/msg/ack_methods.rbs +25 -0
  47. data/sig/nats/io/jetstream/msg/metadata.rbs +15 -0
  48. data/sig/nats/io/jetstream/msg.rbs +6 -0
  49. data/sig/nats/io/jetstream/pull_subscription.rbs +14 -0
  50. data/sig/nats/io/jetstream/push_subscription.rbs +7 -0
  51. data/sig/nats/io/jetstream.rbs +15 -0
  52. data/sig/nats/io/kv/api.rbs +8 -0
  53. data/sig/nats/io/kv/bucket_status.rbs +17 -0
  54. data/sig/nats/io/kv/errors.rbs +30 -0
  55. data/sig/nats/io/kv/manager.rbs +11 -0
  56. data/sig/nats/io/kv.rbs +39 -0
  57. data/sig/nats/io/msg.rbs +14 -0
  58. data/sig/nats/io/parser.rbs +32 -0
  59. data/sig/nats/io/subscription.rbs +33 -0
  60. data/sig/nats/io/version.rbs +9 -0
  61. data/sig/nats/nuid.rbs +32 -0
  62. metadata +74 -4
@@ -12,8 +12,13 @@
12
12
  # limitations under the License.
13
13
  #
14
14
 
15
- require 'nats/io/parser'
16
- require 'nats/io/version'
15
+ require_relative 'parser'
16
+ require_relative 'version'
17
+ require_relative 'errors'
18
+ require_relative 'msg'
19
+ require_relative 'subscription'
20
+ require_relative 'jetstream'
21
+
17
22
  require 'nats/nuid'
18
23
  require 'thread'
19
24
  require 'socket'
@@ -21,6 +26,7 @@ require 'json'
21
26
  require 'monitor'
22
27
  require 'uri'
23
28
  require 'securerandom'
29
+ require 'concurrent'
24
30
 
25
31
  begin
26
32
  require "openssl"
@@ -29,43 +35,75 @@ end
29
35
 
30
36
  module NATS
31
37
  class << self
38
+ # NATS.connect creates a connection to the NATS Server.
39
+ # @param uri [String] URL endpoint of the NATS Server or cluster.
40
+ # @param opts [Hash] Options to customize the NATS connection.
41
+ # @return [NATS::Client]
42
+ #
43
+ # @example
44
+ # require 'nats'
45
+ # nc = NATS.connect("demo.nats.io")
46
+ # nc.publish("hello", "world")
47
+ # nc.close
48
+ #
32
49
  def connect(uri=nil, opts={})
33
- nc = NATS::IO::Client.new
50
+ nc = NATS::Client.new
34
51
  nc.connect(uri, opts)
35
52
 
36
53
  nc
37
54
  end
38
55
  end
39
56
 
40
- module IO
57
+ # Status represents the different states from a NATS connection.
58
+ # A client starts from the DISCONNECTED state to CONNECTING during
59
+ # the initial connect, then CONNECTED. If the connection is reset
60
+ # then it goes from DISCONNECTED to RECONNECTING until it is back to
61
+ # the CONNECTED state. In case the client gives up reconnecting or
62
+ # the connection is manually closed then it will reach the CLOSED
63
+ # connection state after which it will not reconnect again.
64
+ module Status
65
+ # When the client is not actively connected.
66
+ DISCONNECTED = 0
41
67
 
42
- DEFAULT_PORT = 4222
43
- DEFAULT_URI = "nats://localhost:#{DEFAULT_PORT}".freeze
68
+ # When the client is connected.
69
+ CONNECTED = 1
44
70
 
45
- MAX_RECONNECT_ATTEMPTS = 10
46
- RECONNECT_TIME_WAIT = 2
71
+ # When the client will no longer attempt to connect to a NATS Server.
72
+ CLOSED = 2
47
73
 
48
- # Maximum accumulated pending commands bytesize before forcing a flush.
49
- MAX_PENDING_SIZE = 32768
74
+ # When the client has disconnected and is attempting to reconnect.
75
+ RECONNECTING = 3
50
76
 
51
- # Maximum number of flush kicks that can be queued up before we block.
52
- MAX_FLUSH_KICK_SIZE = 1024
77
+ # When the client is attempting to connect to a NATS Server for the first time.
78
+ CONNECTING = 4
53
79
 
54
- # Maximum number of bytes which we will be gathering on a single read.
55
- # TODO: Make dynamic?
56
- MAX_SOCKET_READ_BYTES = 32768
80
+ # When the client is draining a connection before closing.
81
+ DRAINING_SUBS = 5
82
+ DRAINING_PUBS = 6
83
+ end
57
84
 
58
- # Ping intervals
59
- DEFAULT_PING_INTERVAL = 120
60
- DEFAULT_PING_MAX = 2
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
61
97
 
62
- # Default IO timeouts
63
- DEFAULT_CONNECT_TIMEOUT = 2
64
- DEFAULT_READ_WRITE_TIMEOUT = 2
98
+ # Client creates a connection to the NATS Server.
99
+ class Client
100
+ include MonitorMixin
101
+ include Status
65
102
 
66
- # Default Pending Limits
67
- DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
68
- DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
103
+ attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri, :subscription_executor, :reloader
104
+
105
+ DEFAULT_PORT = { nats: 4222, ws: 80, wss: 443 }.freeze
106
+ DEFAULT_URI = ("nats://localhost:#{DEFAULT_PORT[:nats]}".freeze)
69
107
 
70
108
  CR_LF = ("\r\n".freeze)
71
109
  CR_LF_SIZE = (CR_LF.bytesize)
@@ -82,1510 +120,1751 @@ module NATS
82
120
  SUB_OP = ('SUB'.freeze)
83
121
  EMPTY_MSG = (''.freeze)
84
122
 
85
- # Connection States
86
- DISCONNECTED = 0
87
- CONNECTED = 1
88
- CLOSED = 2
89
- RECONNECTING = 3
90
- CONNECTING = 4
91
-
92
- class Error < StandardError; end
123
+ INSTANCES = ObjectSpace::WeakMap.new # tracks all alive client instances
124
+ private_constant :INSTANCES
93
125
 
94
- # When the NATS server sends us an 'ERR' message.
95
- class ServerError < Error; end
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
96
130
 
97
- # When we detect error on the client side.
98
- class ClientError < Error; end
131
+ def default_reloader
132
+ @default_reloader ||= proc { |&block| block.call }.tap { |r| Ractor.make_shareable(r) if defined? Ractor }
133
+ end
99
134
 
100
- # When we cannot connect to the server (either initially or after a reconnect).
101
- class ConnectError < Error; end
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
102
151
 
103
- # When we cannot connect to the server because authorization failed.
104
- class AuthError < ConnectError; end
152
+ def initialize(uri = nil, opts = {})
153
+ super() # required to initialize monitor
154
+ @initial_uri = uri
155
+ @initial_options = opts
105
156
 
106
- # When we cannot connect since there are no servers available.
107
- class NoServersError < ConnectError; end
157
+ # Read/Write IO
158
+ @io = nil
108
159
 
109
- # When there are no subscribers available to respond.
110
- class NoRespondersError < ConnectError; end
160
+ # Queues for coalescing writes of commands we need to send to server.
161
+ @flush_queue = nil
162
+ @pending_queue = nil
111
163
 
112
- # When the connection exhausts max number of pending pings replies.
113
- class StaleConnectionError < Error; end
164
+ # Parser with state
165
+ @parser = NATS::Protocol::Parser.new(self)
114
166
 
115
- # When we do not get a result within a specified time.
116
- class Timeout < Error; end
167
+ # Threads for both reading and flushing command
168
+ @flusher_thread = nil
169
+ @read_loop_thread = nil
170
+ @ping_interval_thread = nil
117
171
 
118
- # When there is an i/o timeout with the socket.
119
- class SocketTimeoutError < Error; end
172
+ # Info that we get from the server
173
+ @server_info = { }
120
174
 
121
- # When we use an invalid subject.
122
- class BadSubject < Error; end
175
+ # URI from server to which we are currently connected
176
+ @uri = nil
177
+ @server_pool = []
123
178
 
124
- # When a subscription hits the pending messages limit.
125
- class SlowConsumer < Error; end
179
+ @status = nil
126
180
 
127
- class Client
128
- include MonitorMixin
181
+ # Subscriptions
182
+ @subs = { }
183
+ @ssid = 0
129
184
 
130
- attr_reader :status, :server_info, :server_pool, :options, :connected_server, :stats, :uri
185
+ # Ping interval
186
+ @pings_outstanding = 0
187
+ @pongs_received = 0
188
+ @pongs = []
189
+ @pongs.extend(MonitorMixin)
131
190
 
132
- def initialize
133
- super # required to initialize monitor
134
- @options = nil
191
+ # Accounting
192
+ @pending_size = 0
193
+ @stats = {
194
+ in_msgs: 0,
195
+ out_msgs: 0,
196
+ in_bytes: 0,
197
+ out_bytes: 0,
198
+ reconnects: 0
199
+ }
135
200
 
136
- # Read/Write IO
137
- @io = nil
201
+ # Sticky error
202
+ @last_err = nil
138
203
 
139
- # Queues for coalescing writes of commands we need to send to server.
140
- @flush_queue = nil
141
- @pending_queue = nil
204
+ # Async callbacks, no ops by default.
205
+ @err_cb = proc { }
206
+ @close_cb = proc { }
207
+ @disconnect_cb = proc { }
208
+ @reconnect_cb = proc { }
142
209
 
143
- # Parser with state
144
- @parser = NATS::Protocol::Parser.new(self)
210
+ # Secure TLS options
211
+ @tls = nil
145
212
 
146
- # Threads for both reading and flushing command
147
- @flusher_thread = nil
148
- @read_loop_thread = nil
149
- @ping_interval_thread = nil
213
+ # Hostname of current server; used for when TLS host
214
+ # verification is enabled.
215
+ @hostname = nil
216
+ @single_url_connect_used = false
150
217
 
151
- # Info that we get from the server
152
- @server_info = { }
218
+ # Track whether connect has been already been called.
219
+ @connect_called = false
153
220
 
154
- # URI from server to which we are currently connected
155
- @uri = nil
156
- @server_pool = []
221
+ # New style request/response implementation.
222
+ @resp_sub = nil
223
+ @resp_map = nil
224
+ @resp_sub_prefix = nil
225
+ @nuid = NATS::NUID.new
157
226
 
158
- @status = DISCONNECTED
227
+ # NKEYS
228
+ @user_credentials = nil
229
+ @nkeys_seed = nil
230
+ @user_nkey_cb = nil
231
+ @user_jwt_cb = nil
232
+ @signature_cb = nil
159
233
 
160
- # Subscriptions
161
- @subs = { }
162
- @ssid = 0
163
-
164
- # Ping interval
165
- @pings_outstanding = 0
166
- @pongs_received = 0
167
- @pongs = []
168
- @pongs.extend(MonitorMixin)
169
-
170
- # Accounting
171
- @pending_size = 0
172
- @stats = {
173
- in_msgs: 0,
174
- out_msgs: 0,
175
- in_bytes: 0,
176
- out_bytes: 0,
177
- reconnects: 0
178
- }
234
+ # Tokens
235
+ @auth_token = nil
179
236
 
180
- # Sticky error
181
- @last_err = nil
237
+ @inbox_prefix = "_INBOX"
182
238
 
183
- # Async callbacks, no ops by default.
184
- @err_cb = proc { }
185
- @close_cb = proc { }
186
- @disconnect_cb = proc { }
187
- @reconnect_cb = proc { }
239
+ # Draining
240
+ @drain_t = nil
188
241
 
189
- # Secure TLS options
190
- @tls = nil
242
+ # Prepare for calling connect or automatic delayed connection
243
+ parse_and_validate_options if uri || opts.any?
191
244
 
192
- # Hostname of current server; used for when TLS host
193
- # verification is enabled.
194
- @hostname = nil
195
- @single_url_connect_used = false
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
196
247
 
197
- # Track whether connect has been already been called.
198
- @connect_called = false
248
+ @reloader = opts.fetch(:reloader, self.class.default_reloader)
249
+ end
199
250
 
200
- # New style request/response implementation.
201
- @resp_sub = nil
202
- @resp_map = nil
203
- @resp_sub_prefix = nil
204
- @nuid = NATS::NUID.new
251
+ # Prepare connecting to NATS, but postpone real connection until first usage.
252
+ def connect(uri=nil, opts={})
253
+ if uri || opts.any?
254
+ @initial_uri = uri
255
+ @initial_options = opts
256
+ end
205
257
 
206
- # NKEYS
207
- @user_credentials = nil
208
- @nkeys_seed = nil
209
- @user_nkey_cb = nil
210
- @user_jwt_cb = nil
211
- @signature_cb = nil
258
+ synchronize do
259
+ # In case it has been connected already, then do not need to call this again.
260
+ return if @connect_called
261
+ @connect_called = true
212
262
  end
213
263
 
214
- # Establishes connection to NATS.
215
- def connect(uri=nil, opts={})
216
- synchronize do
217
- # In case it has been connected already, then do not need to call this again.
218
- return if @connect_called
219
- @connect_called = true
220
- end
264
+ parse_and_validate_options
265
+ establish_connection!
221
266
 
222
- # Convert URI to string if needed.
223
- uri = uri.to_s if uri.is_a?(URI)
224
-
225
- case uri
226
- when String
227
- # Initialize TLS defaults in case any url is using it.
228
- srvs = opts[:servers] = process_uri(uri)
229
- if srvs.any? {|u| u.scheme == 'tls'} and !opts[:tls]
230
- tls_context = OpenSSL::SSL::SSLContext.new
231
- tls_context.set_params
232
- opts[:tls] = {
233
- context: tls_context
234
- }
235
- end
236
- @single_url_connect_used = true if srvs.size == 1
237
- when Hash
238
- opts = uri
239
- end
267
+ self
268
+ end
240
269
 
241
- opts[:verbose] = false if opts[:verbose].nil?
242
- opts[:pedantic] = false if opts[:pedantic].nil?
243
- opts[:reconnect] = true if opts[:reconnect].nil?
244
- opts[:old_style_request] = false if opts[:old_style_request].nil?
245
- opts[:reconnect_time_wait] = RECONNECT_TIME_WAIT if opts[:reconnect_time_wait].nil?
246
- opts[:max_reconnect_attempts] = MAX_RECONNECT_ATTEMPTS if opts[:max_reconnect_attempts].nil?
247
- opts[:ping_interval] = DEFAULT_PING_INTERVAL if opts[:ping_interval].nil?
248
- opts[:max_outstanding_pings] = DEFAULT_PING_MAX if opts[:max_outstanding_pings].nil?
249
-
250
- # Override with ENV
251
- opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil?
252
- opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil?
253
- opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil?
254
- opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil?
255
- opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil?
256
- opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil?
257
- opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil?
258
- opts[:connect_timeout] ||= DEFAULT_CONNECT_TIMEOUT
259
- @options = opts
260
-
261
- # Process servers in the NATS cluster and pick one to connect
262
- uris = opts[:servers] || [DEFAULT_URI]
263
- uris.shuffle! unless @options[:dont_randomize_servers]
264
- uris.each do |u|
265
- nats_uri = case u
266
- when URI
267
- u.dup
268
- else
269
- URI.parse(u)
270
- end
271
- @server_pool << {
272
- :uri => nats_uri,
273
- :hostname => nats_uri.host
274
- }
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
+
286
+ # Convert URI to string if needed.
287
+ uri = @initial_uri.dup
288
+ uri = uri.to_s if uri.is_a?(URI)
289
+
290
+ opts = @initial_options.dup
291
+
292
+ case uri
293
+ when String
294
+ # Initialize TLS defaults in case any url is using it.
295
+ srvs = opts[:servers] = process_uri(uri)
296
+ if srvs.any? {|u| %w[tls wss].include? u.scheme } and !opts[:tls]
297
+ opts[:tls] = { context: tls_context }
275
298
  end
299
+ @single_url_connect_used = true if srvs.size == 1
300
+ when Hash
301
+ opts = uri
302
+ end
276
303
 
277
- if @options[:old_style_request]
278
- # Replace for this instance the implementation
279
- # of request to use the old_request style.
280
- class << self; alias_method :request, :old_request; end
281
- end
304
+ opts[:verbose] = false if opts[:verbose].nil?
305
+ opts[:pedantic] = false if opts[:pedantic].nil?
306
+ opts[:reconnect] = true if opts[:reconnect].nil?
307
+ opts[:old_style_request] = false if opts[:old_style_request].nil?
308
+ opts[:ignore_discovered_urls] = false if opts[:ignore_discovered_urls].nil?
309
+ opts[:reconnect_time_wait] = NATS::IO::RECONNECT_TIME_WAIT if opts[:reconnect_time_wait].nil?
310
+ opts[:max_reconnect_attempts] = NATS::IO::MAX_RECONNECT_ATTEMPTS if opts[:max_reconnect_attempts].nil?
311
+ opts[:ping_interval] = NATS::IO::DEFAULT_PING_INTERVAL if opts[:ping_interval].nil?
312
+ opts[:max_outstanding_pings] = NATS::IO::DEFAULT_PING_MAX if opts[:max_outstanding_pings].nil?
313
+
314
+ # Override with ENV
315
+ opts[:verbose] = ENV['NATS_VERBOSE'].downcase == 'true' unless ENV['NATS_VERBOSE'].nil?
316
+ opts[:pedantic] = ENV['NATS_PEDANTIC'].downcase == 'true' unless ENV['NATS_PEDANTIC'].nil?
317
+ opts[:reconnect] = ENV['NATS_RECONNECT'].downcase == 'true' unless ENV['NATS_RECONNECT'].nil?
318
+ opts[:reconnect_time_wait] = ENV['NATS_RECONNECT_TIME_WAIT'].to_i unless ENV['NATS_RECONNECT_TIME_WAIT'].nil?
319
+ opts[:ignore_discovered_urls] = ENV['NATS_IGNORE_DISCOVERED_URLS'].downcase == 'true' unless ENV['NATS_IGNORE_DISCOVERED_URLS'].nil?
320
+ opts[:max_reconnect_attempts] = ENV['NATS_MAX_RECONNECT_ATTEMPTS'].to_i unless ENV['NATS_MAX_RECONNECT_ATTEMPTS'].nil?
321
+ opts[:ping_interval] = ENV['NATS_PING_INTERVAL'].to_i unless ENV['NATS_PING_INTERVAL'].nil?
322
+ opts[:max_outstanding_pings] = ENV['NATS_MAX_OUTSTANDING_PINGS'].to_i unless ENV['NATS_MAX_OUTSTANDING_PINGS'].nil?
323
+ opts[:connect_timeout] ||= NATS::IO::DEFAULT_CONNECT_TIMEOUT
324
+ opts[:drain_timeout] ||= NATS::IO::DEFAULT_DRAIN_TIMEOUT
325
+ @options = opts
326
+
327
+ # Process servers in the NATS cluster and pick one to connect
328
+ uris = opts[:servers] || [DEFAULT_URI]
329
+ uris.shuffle! unless @options[:dont_randomize_servers]
330
+ uris.each do |u|
331
+ nats_uri = case u
332
+ when URI
333
+ u.dup
334
+ else
335
+ URI.parse(u)
336
+ end
337
+ @server_pool << {
338
+ :uri => nats_uri,
339
+ :hostname => nats_uri.hostname
340
+ }
341
+ end
282
342
 
283
- # NKEYS
284
- @user_credentials ||= opts[:user_credentials]
285
- @nkeys_seed ||= opts[:nkeys_seed]
286
- setup_nkeys_connect if @user_credentials or @nkeys_seed
343
+ if @options[:old_style_request]
344
+ # Replace for this instance the implementation
345
+ # of request to use the old_request style.
346
+ class << self; alias_method :request, :old_request; end
347
+ end
287
348
 
288
- # Check for TLS usage
289
- @tls = @options[:tls]
349
+ # NKEYS
350
+ @signature_cb ||= opts[:user_signature_cb]
351
+ @user_jwt_cb ||= opts[:user_jwt_cb]
352
+ @user_nkey_cb ||= opts[:user_nkey_cb]
353
+ @user_credentials ||= opts[:user_credentials]
354
+ @nkeys_seed ||= opts[:nkeys_seed]
290
355
 
291
- srv = nil
292
- begin
293
- srv = select_next_server
356
+ setup_nkeys_connect if @user_credentials or @nkeys_seed
294
357
 
295
- # Create TCP socket connection to NATS
296
- @io = create_socket
297
- @io.connect
358
+ # Tokens, if set will take preference over the user@server uri token
359
+ @auth_token ||= opts[:auth_token]
298
360
 
299
- # Capture state that we have had a TCP connection established against
300
- # this server and could potentially be used for reconnecting.
301
- srv[:was_connected] = true
361
+ # Check for TLS usage
362
+ @tls = @options[:tls]
302
363
 
303
- # Connection established and now in process of sending CONNECT to NATS
304
- @status = CONNECTING
364
+ @inbox_prefix = opts.fetch(:custom_inbox_prefix, @inbox_prefix)
305
365
 
306
- # Use the hostname from the server for TLS hostname verification.
307
- if client_using_secure_connection? and single_url_connect_used?
308
- # Always reuse the original hostname used to connect.
309
- @hostname ||= srv[:hostname]
310
- else
311
- @hostname = srv[:hostname]
312
- end
366
+ validate_settings!
313
367
 
314
- # Established TCP connection successfully so can start connect
315
- process_connect_init
368
+ self
369
+ end
316
370
 
317
- # Reset reconnection attempts if connection is valid
318
- srv[:reconnect_attempts] = 0
319
- srv[:auth_required] ||= true if @server_info[:auth_required]
371
+ private def establish_connection!
372
+ @ruby_pid = Process.pid # For fork detection
320
373
 
321
- # Add back to rotation since successfully connected
322
- server_pool << srv
323
- rescue NoServersError => e
324
- @disconnect_cb.call(e) if @disconnect_cb
325
- raise @last_err || e
326
- rescue => e
327
- # Capture sticky error
328
- synchronize do
329
- @last_err = e
330
- srv[:auth_required] ||= true if @server_info[:auth_required]
331
- server_pool << srv if can_reuse_server?(srv)
332
- end
374
+ srv = nil
375
+ begin
376
+ srv = select_next_server
333
377
 
334
- @err_cb.call(e) if @err_cb
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
335
385
 
336
- if should_not_reconnect?
337
- @disconnect_cb.call(e) if @disconnect_cb
338
- raise e
339
- end
386
+ # Create TCP socket connection to NATS.
387
+ @io = create_socket
388
+ @io.connect
340
389
 
341
- # Clean up any connecting state and close connection without
342
- # triggering the disconnection/closed callbacks.
343
- close_connection(DISCONNECTED, false)
390
+ # Capture state that we have had a TCP connection established against
391
+ # this server and could potentially be used for reconnecting.
392
+ srv[:was_connected] = true
344
393
 
345
- # always sleep here to safe guard against errors before current[:was_connected]
346
- # is set for the first time
347
- sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
394
+ # Connection established and now in process of sending CONNECT to NATS
395
+ @status = CONNECTING
348
396
 
349
- # Continue retrying until there are no options left in the server pool
350
- retry
397
+ # Established TCP connection successfully so can start connect
398
+ process_connect_init
399
+
400
+ # Reset reconnection attempts if connection is valid
401
+ srv[:reconnect_attempts] = 0
402
+ srv[:auth_required] ||= true if @server_info[:auth_required]
403
+
404
+ # Add back to rotation since successfully connected
405
+ server_pool << srv
406
+ rescue NATS::IO::NoServersError => e
407
+ @disconnect_cb.call(e) if @disconnect_cb
408
+ raise @last_err || e
409
+ rescue => e
410
+ # Capture sticky error
411
+ synchronize do
412
+ @last_err = e
413
+ srv[:auth_required] ||= true if @server_info[:auth_required]
414
+ server_pool << srv if can_reuse_server?(srv)
351
415
  end
352
416
 
353
- # Initialize queues and loops for message dispatching and processing engine
354
- @flush_queue = SizedQueue.new(MAX_FLUSH_KICK_SIZE)
355
- @pending_queue = SizedQueue.new(MAX_PENDING_SIZE)
356
- @pings_outstanding = 0
357
- @pongs_received = 0
358
- @pending_size = 0
417
+ err_cb_call(self, e, nil) if @err_cb
359
418
 
360
- # Server roundtrip went ok so consider to be connected at this point
361
- @status = CONNECTED
419
+ if should_not_reconnect?
420
+ @disconnect_cb.call(e) if @disconnect_cb
421
+ raise e
422
+ end
423
+
424
+ # Clean up any connecting state and close connection without
425
+ # triggering the disconnection/closed callbacks.
426
+ close_connection(DISCONNECTED, false)
362
427
 
363
- # Connected to NATS so Ready to start parser loop, flusher and ping interval
364
- start_threads!
428
+ # Always sleep here to safe guard against errors before current[:was_connected]
429
+ # is set for the first time.
430
+ sleep @options[:reconnect_time_wait] if @options[:reconnect_time_wait]
431
+
432
+ # Continue retrying until there are no options left in the server pool
433
+ retry
365
434
  end
366
435
 
367
- def publish(subject, msg=EMPTY_MSG, opt_reply=nil, &blk)
368
- raise BadSubject if !subject or subject.empty?
369
- msg_size = msg.bytesize
436
+ # Initialize queues and loops for message dispatching and processing engine
437
+ @flush_queue = SizedQueue.new(NATS::IO::MAX_FLUSH_KICK_SIZE)
438
+ @pending_queue = SizedQueue.new(NATS::IO::MAX_PENDING_SIZE)
439
+ @pings_outstanding = 0
440
+ @pongs_received = 0
441
+ @pending_size = 0
442
+
443
+ # Server roundtrip went ok so consider to be connected at this point
444
+ @status = CONNECTED
370
445
 
371
- # Accounting
372
- @stats[:out_msgs] += 1
373
- @stats[:out_bytes] += msg_size
446
+ # Connected to NATS so Ready to start parser loop, flusher and ping interval
447
+ start_threads!
374
448
 
375
- send_command("PUB #{subject} #{opt_reply} #{msg_size}\r\n#{msg}\r\n")
376
- @flush_queue << :pub if @flush_queue.empty?
449
+ self
450
+ end
451
+
452
+ def publish(subject, msg=EMPTY_MSG, opt_reply=nil, **options, &blk)
453
+ raise NATS::IO::BadSubject if !subject or subject.empty?
454
+ if options[:header]
455
+ return publish_msg(NATS::Msg.new(subject: subject, data: msg, reply: opt_reply, header: options[:header]))
377
456
  end
378
457
 
379
- # Publishes a NATS::Msg that may include headers.
380
- def publish_msg(msg)
381
- raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
382
- raise BadSubject if !msg.subject or msg.subject.empty?
458
+ # Accounting
459
+ msg_size = msg.bytesize
460
+ @stats[:out_msgs] += 1
461
+ @stats[:out_bytes] += msg_size
383
462
 
384
- msg.reply ||= ''
385
- msg.data ||= ''
386
- msg_size = msg.data.bytesize
463
+ send_command("PUB #{subject} #{opt_reply} #{msg_size}\r\n#{msg}\r\n")
464
+ @flush_queue << :pub if @flush_queue.empty?
465
+ end
387
466
 
388
- # Accounting
389
- @stats[:out_msgs] += 1
390
- @stats[:out_bytes] += msg_size
467
+ # Publishes a NATS::Msg that may include headers.
468
+ def publish_msg(msg)
469
+ raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
470
+ raise NATS::IO::BadSubject if !msg.subject or msg.subject.empty?
391
471
 
392
- if msg.header
393
- hdr = ''
394
- hdr << NATS_HDR_LINE
395
- msg.header.each do |k, v|
396
- hdr << "#{k}: #{v}#{CR_LF}"
397
- end
398
- hdr << CR_LF
399
- hdr_len = hdr.bytesize
400
- total_size = msg_size + hdr_len
401
- send_command("HPUB #{msg.subject} #{msg.reply} #{hdr_len} #{total_size}\r\n#{hdr}#{msg.data}\r\n")
402
- else
403
- send_command("PUB #{msg.subject} #{msg.reply} #{msg_size}\r\n#{msg.data}\r\n")
404
- end
472
+ msg.reply ||= ''
473
+ msg.data ||= ''
474
+ msg_size = msg.data.bytesize
405
475
 
406
- @flush_queue << :pub if @flush_queue.empty?
407
- end
476
+ # Accounting
477
+ @stats[:out_msgs] += 1
478
+ @stats[:out_bytes] += msg_size
408
479
 
409
- # Create subscription which is dispatched asynchronously
410
- # messages to a callback.
411
- def subscribe(subject, opts={}, &callback)
412
- sid = nil
413
- sub = nil
414
- synchronize do
415
- sid = (@ssid += 1)
416
- sub = @subs[sid] = Subscription.new
480
+ if msg.header
481
+ hdr = ''
482
+ hdr << NATS_HDR_LINE
483
+ msg.header.each do |k, v|
484
+ hdr << "#{k}: #{v}#{CR_LF}"
417
485
  end
418
- opts[:pending_msgs_limit] ||= DEFAULT_SUB_PENDING_MSGS_LIMIT
419
- opts[:pending_bytes_limit] ||= DEFAULT_SUB_PENDING_BYTES_LIMIT
420
-
421
- sub.subject = subject
422
- sub.callback = callback
423
- sub.received = 0
424
- sub.queue = opts[:queue] if opts[:queue]
425
- sub.max = opts[:max] if opts[:max]
426
- sub.pending_msgs_limit = opts[:pending_msgs_limit]
427
- sub.pending_bytes_limit = opts[:pending_bytes_limit]
428
- sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
429
-
430
- send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
431
- @flush_queue << :sub
432
-
433
- # Setup server support for auto-unsubscribe when receiving enough messages
434
- unsubscribe(sid, opts[:max]) if opts[:max]
435
-
436
- # Async subscriptions each own a single thread for the
437
- # delivery of messages.
438
- # FIXME: Support shared thread pool with configurable limits
439
- # to better support case of having a lot of subscriptions.
440
- sub.wait_for_msgs_t = Thread.new do
441
- loop do
442
- msg = sub.pending_queue.pop
443
-
444
- cb = nil
445
- sub.synchronize do
446
- # Decrease pending size since consumed already
447
- sub.pending_size -= msg.data.size
448
- cb = sub.callback
449
- end
486
+ hdr << CR_LF
487
+ hdr_len = hdr.bytesize
488
+ total_size = msg_size + hdr_len
489
+ send_command("HPUB #{msg.subject} #{msg.reply} #{hdr_len} #{total_size}\r\n#{hdr}#{msg.data}\r\n")
490
+ else
491
+ send_command("PUB #{msg.subject} #{msg.reply} #{msg_size}\r\n#{msg.data}\r\n")
492
+ end
450
493
 
451
- begin
452
- case cb.arity
453
- when 0 then cb.call
454
- when 1 then cb.call(msg.data)
455
- when 2 then cb.call(msg.data, msg.reply)
456
- when 3 then cb.call(msg.data, msg.reply, msg.subject)
457
- else cb.call(msg.data, msg.reply, msg.subject, msg.header)
458
- end
459
- rescue => e
460
- synchronize do
461
- @err_cb.call(e) if @err_cb
462
- end
463
- end
464
- end
465
- end
494
+ @flush_queue << :pub if @flush_queue.empty?
495
+ end
466
496
 
467
- sid
497
+ # Create subscription which is dispatched asynchronously
498
+ # messages to a callback.
499
+ def subscribe(subject, opts={}, &callback)
500
+ raise NATS::IO::ConnectionDrainingError.new("nats: connection draining") if draining?
501
+
502
+ sid = nil
503
+ sub = nil
504
+ synchronize do
505
+ sid = (@ssid += 1)
506
+ sub = @subs[sid] = Subscription.new
507
+ sub.nc = self
508
+ sub.sid = sid
509
+ end
510
+ opts[:pending_msgs_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
511
+ opts[:pending_bytes_limit] ||= NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
512
+
513
+ sub.subject = subject
514
+ sub.callback = callback
515
+ sub.received = 0
516
+ sub.queue = opts[:queue] if opts[:queue]
517
+ sub.max = opts[:max] if opts[:max]
518
+ sub.pending_msgs_limit = opts[:pending_msgs_limit]
519
+ sub.pending_bytes_limit = opts[:pending_bytes_limit]
520
+ sub.pending_queue = SizedQueue.new(sub.pending_msgs_limit)
521
+ sub.processing_concurrency = opts[:processing_concurrency] if opts.key?(:processing_concurrency)
522
+
523
+ send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}")
524
+ @flush_queue << :sub
525
+
526
+ # Setup server support for auto-unsubscribe when receiving enough messages
527
+ sub.unsubscribe(opts[:max]) if opts[:max]
528
+
529
+ unless callback
530
+ cond = sub.new_cond
531
+ sub.wait_for_msgs_cond = cond
468
532
  end
469
533
 
470
- # Sends a request using expecting a single response using a
471
- # single subscription per connection for receiving the responses.
472
- # It times out in case the request is not retrieved within the
473
- # specified deadline.
474
- # If given a callback, then the request happens asynchronously.
475
- def request(subject, payload="", opts={}, &blk)
476
- raise BadSubject if !subject or subject.empty?
534
+ sub
535
+ end
477
536
 
478
- # If a block was given then fallback to method using auto unsubscribe.
479
- return old_request(subject, payload, opts, &blk) if blk
480
- return old_request(subject, payload, opts) if opts[:old_style]
537
+ # Sends a request using expecting a single response using a
538
+ # single subscription per connection for receiving the responses.
539
+ # It times out in case the request is not retrieved within the
540
+ # specified deadline.
541
+ # If given a callback, then the request happens asynchronously.
542
+ def request(subject, payload="", **opts, &blk)
543
+ raise NATS::IO::BadSubject if !subject or subject.empty?
481
544
 
482
- token = nil
483
- inbox = nil
484
- future = nil
485
- response = nil
486
- timeout = opts[:timeout] ||= 0.5
487
- synchronize do
488
- start_resp_mux_sub! unless @resp_sub_prefix
545
+ # If a block was given then fallback to method using auto unsubscribe.
546
+ return old_request(subject, payload, opts, &blk) if blk
547
+ return old_request(subject, payload, opts) if opts[:old_style]
489
548
 
490
- # Create token for this request.
491
- token = @nuid.next
492
- inbox = "#{@resp_sub_prefix}.#{token}"
549
+ if opts[:header]
550
+ return request_msg(NATS::Msg.new(subject: subject, data: payload, header: opts[:header]), **opts)
551
+ end
493
552
 
494
- # Create the a future for the request that will
495
- # get signaled when it receives the request.
496
- future = @resp_sub.new_cond
497
- @resp_map[token][:future] = future
498
- end
553
+ token = nil
554
+ inbox = nil
555
+ future = nil
556
+ response = nil
557
+ timeout = opts[:timeout] ||= 0.5
558
+ synchronize do
559
+ start_resp_mux_sub! unless @resp_sub_prefix
560
+
561
+ # Create token for this request.
562
+ token = @nuid.next
563
+ inbox = "#{@resp_sub_prefix}.#{token}"
564
+
565
+ # Create the a future for the request that will
566
+ # get signaled when it receives the request.
567
+ future = @resp_sub.new_cond
568
+ @resp_map[token][:future] = future
569
+ end
499
570
 
500
- # Publish request and wait for reply.
501
- publish(subject, payload, inbox)
502
- begin
503
- with_nats_timeout(timeout) do
504
- @resp_sub.synchronize do
505
- future.wait(timeout)
506
- end
571
+ # Publish request and wait for reply.
572
+ publish(subject, payload, inbox)
573
+ begin
574
+ MonotonicTime::with_nats_timeout(timeout) do
575
+ @resp_sub.synchronize do
576
+ future.wait(timeout)
507
577
  end
508
- rescue NATS::IO::Timeout => e
509
- synchronize { @resp_map.delete(token) }
510
- raise e
511
578
  end
579
+ rescue NATS::Timeout => e
580
+ synchronize { @resp_map.delete(token) }
581
+ raise e
582
+ end
512
583
 
513
- # Check if there is a response already.
514
- synchronize do
515
- result = @resp_map[token]
516
- response = result[:response]
517
- @resp_map.delete(token)
584
+ # Check if there is a response already.
585
+ synchronize do
586
+ result = @resp_map[token]
587
+ response = result[:response]
588
+ @resp_map.delete(token)
589
+ end
590
+
591
+ if response and response.header
592
+ status = response.header[STATUS_HDR]
593
+ raise NATS::IO::NoRespondersError if status == "503"
594
+ end
595
+
596
+ response
597
+ end
598
+
599
+ # request_msg makes a NATS request using a NATS::Msg that may include headers.
600
+ def request_msg(msg, **opts)
601
+ raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
602
+ raise NATS::IO::BadSubject if !msg.subject or msg.subject.empty?
603
+
604
+ token = nil
605
+ inbox = nil
606
+ future = nil
607
+ response = nil
608
+ timeout = opts[:timeout] ||= 0.5
609
+ synchronize do
610
+ start_resp_mux_sub! unless @resp_sub_prefix
611
+
612
+ # Create token for this request.
613
+ token = @nuid.next
614
+ inbox = "#{@resp_sub_prefix}.#{token}"
615
+
616
+ # Create the a future for the request that will
617
+ # get signaled when it receives the request.
618
+ future = @resp_sub.new_cond
619
+ @resp_map[token][:future] = future
620
+ end
621
+ msg.reply = inbox
622
+ msg.data ||= ''
623
+ msg_size = msg.data.bytesize
624
+
625
+ # Publish request and wait for reply.
626
+ publish_msg(msg)
627
+ begin
628
+ MonotonicTime::with_nats_timeout(timeout) do
629
+ @resp_sub.synchronize do
630
+ future.wait(timeout)
631
+ end
518
632
  end
633
+ rescue NATS::Timeout => e
634
+ synchronize { @resp_map.delete(token) }
635
+ raise e
636
+ end
637
+
638
+ # Check if there is a response already.
639
+ synchronize do
640
+ result = @resp_map[token]
641
+ response = result[:response]
642
+ @resp_map.delete(token)
643
+ end
644
+
645
+ if response and response.header
646
+ status = response.header[STATUS_HDR]
647
+ raise NATS::IO::NoRespondersError if status == "503"
648
+ end
649
+
650
+ response
651
+ end
519
652
 
520
- if response and response.header
521
- status = response.header[STATUS_HDR]
522
- raise NoRespondersError if status == "503"
653
+ # Sends a request creating an ephemeral subscription for the request,
654
+ # expecting a single response or raising a timeout in case the request
655
+ # is not retrieved within the specified deadline.
656
+ # If given a callback, then the request happens asynchronously.
657
+ def old_request(subject, payload, opts={}, &blk)
658
+ return unless subject
659
+ inbox = new_inbox
660
+
661
+ # If a callback was passed, then have it process
662
+ # the messages asynchronously and return the sid.
663
+ if blk
664
+ opts[:max] ||= 1
665
+ s = subscribe(inbox, opts) do |msg|
666
+ case blk.arity
667
+ when 0 then blk.call
668
+ when 1 then blk.call(msg)
669
+ when 2 then blk.call(msg.data, msg.reply)
670
+ when 3 then blk.call(msg.data, msg.reply, msg.subject)
671
+ else blk.call(msg.data, msg.reply, msg.subject, msg.header)
672
+ end
523
673
  end
674
+ publish(subject, payload, inbox)
524
675
 
525
- response
676
+ return s
526
677
  end
527
678
 
528
- # Makes a NATS request using a NATS::Msg that may include headers.
529
- def request_msg(msg, opts={})
530
- raise TypeError, "nats: expected NATS::Msg, got #{msg.class.name}" unless msg.is_a?(Msg)
531
- raise BadSubject if !msg.subject or msg.subject.empty?
679
+ # In case block was not given, handle synchronously
680
+ # with a timeout and only allow a single response.
681
+ timeout = opts[:timeout] ||= 0.5
682
+ opts[:max] = 1
532
683
 
533
- token = nil
534
- inbox = nil
535
- future = nil
536
- response = nil
537
- timeout = opts[:timeout] ||= 0.5
538
- synchronize do
539
- start_resp_mux_sub! unless @resp_sub_prefix
684
+ sub = Subscription.new
685
+ sub.subject = inbox
686
+ sub.received = 0
687
+ future = sub.new_cond
688
+ sub.future = future
689
+ sub.nc = self
540
690
 
541
- # Create token for this request.
542
- token = @nuid.next
543
- inbox = "#{@resp_sub_prefix}.#{token}"
691
+ sid = nil
692
+ synchronize do
693
+ sid = (@ssid += 1)
694
+ sub.sid = sid
695
+ @subs[sid] = sub
696
+ end
544
697
 
545
- # Create the a future for the request that will
546
- # get signaled when it receives the request.
547
- future = @resp_sub.new_cond
548
- @resp_map[token][:future] = future
549
- end
550
- msg.reply = inbox
551
- msg.data ||= ''
552
- msg_size = msg.data.bytesize
698
+ send_command("SUB #{inbox} #{sid}#{CR_LF}")
699
+ @flush_queue << :sub
700
+ unsubscribe(sub, 1)
553
701
 
554
- # Publish request and wait for reply.
555
- publish_msg(msg)
556
- begin
557
- with_nats_timeout(timeout) do
558
- @resp_sub.synchronize do
559
- future.wait(timeout)
560
- end
561
- end
562
- rescue NATS::IO::Timeout => e
563
- synchronize { @resp_map.delete(token) }
564
- raise e
565
- end
702
+ sub.synchronize do
703
+ # Publish the request and then wait for the response...
704
+ publish(subject, payload, inbox)
566
705
 
567
- # Check if there is a response already.
568
- synchronize do
569
- result = @resp_map[token]
570
- response = result[:response]
571
- @resp_map.delete(token)
706
+ MonotonicTime::with_nats_timeout(timeout) do
707
+ future.wait(timeout)
572
708
  end
709
+ end
710
+ response = sub.response
573
711
 
574
- if response and response.header
575
- status = response.header[STATUS_HDR]
576
- raise NoRespondersError if status == "503"
577
- end
712
+ if response and response.header
713
+ status = response.header[STATUS_HDR]
714
+ raise NATS::IO::NoRespondersError if status == "503"
715
+ end
578
716
 
579
- response
580
- end
581
-
582
- # Sends a request creating an ephemeral subscription for the request,
583
- # expecting a single response or raising a timeout in case the request
584
- # is not retrieved within the specified deadline.
585
- # If given a callback, then the request happens asynchronously.
586
- def old_request(subject, payload, opts={}, &blk)
587
- return unless subject
588
- inbox = new_inbox
589
-
590
- # If a callback was passed, then have it process
591
- # the messages asynchronously and return the sid.
592
- if blk
593
- opts[:max] ||= 1
594
- s = subscribe(inbox, opts) do |msg, reply, subject, header|
595
- case blk.arity
596
- when 0 then blk.call
597
- when 1 then blk.call(msg)
598
- when 2 then blk.call(msg, reply)
599
- when 3 then blk.call(msg, reply, subject)
600
- else blk.call(msg, reply, subject, header)
601
- end
602
- end
603
- publish(subject, payload, inbox)
717
+ response
718
+ end
604
719
 
605
- return s
720
+ # Send a ping and wait for a pong back within a timeout.
721
+ def flush(timeout=10)
722
+ # Schedule sending a PING, and block until we receive PONG back,
723
+ # or raise a timeout in case the response is past the deadline.
724
+ pong = @pongs.new_cond
725
+ @pongs.synchronize do
726
+ @pongs << pong
727
+
728
+ # Flush once pong future has been prepared
729
+ @pending_queue << PING_REQUEST
730
+ @flush_queue << :ping
731
+ MonotonicTime::with_nats_timeout(timeout) do
732
+ pong.wait(timeout)
606
733
  end
734
+ end
735
+ end
607
736
 
608
- # In case block was not given, handle synchronously
609
- # with a timeout and only allow a single response.
610
- timeout = opts[:timeout] ||= 0.5
611
- opts[:max] = 1
737
+ alias :servers :server_pool
612
738
 
613
- sub = Subscription.new
614
- sub.subject = inbox
615
- sub.received = 0
616
- future = sub.new_cond
617
- sub.future = future
739
+ # discovered_servers returns the NATS Servers that have been discovered
740
+ # via INFO protocol updates.
741
+ def discovered_servers
742
+ servers.select {|s| s[:discovered] }
743
+ end
618
744
 
619
- sid = nil
620
- synchronize do
621
- sid = (@ssid += 1)
622
- @subs[sid] = sub
623
- end
745
+ # Close connection to NATS, flushing in case connection is alive
746
+ # and there are any pending messages, should not be used while
747
+ # holding the lock.
748
+ def close
749
+ close_connection(CLOSED, true)
750
+ end
624
751
 
625
- send_command("SUB #{inbox} #{sid}#{CR_LF}")
626
- @flush_queue << :sub
627
- unsubscribe(sid, 1)
752
+ # new_inbox returns a unique inbox used for subscriptions.
753
+ # @return [String]
754
+ def new_inbox
755
+ "#{@inbox_prefix}.#{@nuid.next}"
756
+ end
628
757
 
629
- sub.synchronize do
630
- # Publish the request and then wait for the response...
631
- publish(subject, payload, inbox)
758
+ def connected_server
759
+ connected? ? @uri : nil
760
+ end
632
761
 
633
- with_nats_timeout(timeout) do
634
- future.wait(timeout)
635
- end
636
- end
637
- response = sub.response
762
+ def disconnected?
763
+ !@status or @status == DISCONNECTED
764
+ end
638
765
 
639
- if response and response.header
640
- status = response.header[STATUS_HDR]
641
- raise NoRespondersError if status == "503"
642
- end
766
+ def connected?
767
+ @status == CONNECTED
768
+ end
643
769
 
644
- response
645
- end
770
+ def connecting?
771
+ @status == CONNECTING
772
+ end
646
773
 
647
- # Auto unsubscribes the server by sending UNSUB command and throws away
648
- # subscription in case already present and has received enough messages.
649
- def unsubscribe(sid, opt_max=nil)
650
- opt_max_str = " #{opt_max}" unless opt_max.nil?
651
- send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}")
652
- @flush_queue << :unsub
774
+ def reconnecting?
775
+ @status == RECONNECTING
776
+ end
653
777
 
654
- return unless sub = @subs[sid]
655
- synchronize do
656
- sub.max = opt_max
657
- @subs.delete(sid) unless (sub.max && (sub.received < sub.max))
778
+ def closed?
779
+ @status == CLOSED
780
+ end
658
781
 
659
- # Stop messages delivery thread for async subscribers
660
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
661
- sub.wait_for_msgs_t.exit
662
- sub.pending_queue.clear
663
- end
664
- end
782
+ def draining?
783
+ if @status == DRAINING_PUBS or @status == DRAINING_SUBS
784
+ return true
665
785
  end
666
786
 
667
- # Send a ping and wait for a pong back within a timeout.
668
- def flush(timeout=60)
669
- # Schedule sending a PING, and block until we receive PONG back,
670
- # or raise a timeout in case the response is past the deadline.
671
- pong = @pongs.new_cond
672
- @pongs.synchronize do
673
- @pongs << pong
674
-
675
- # Flush once pong future has been prepared
676
- @pending_queue << PING_REQUEST
677
- @flush_queue << :ping
678
- with_nats_timeout(timeout) do
679
- pong.wait(timeout)
680
- end
681
- end
787
+ is_draining = false
788
+ synchronize do
789
+ is_draining = true if @drain_t
682
790
  end
683
791
 
684
- alias :servers :server_pool
792
+ is_draining
793
+ end
685
794
 
686
- def discovered_servers
687
- servers.select {|s| s[:discovered] }
688
- end
795
+ def on_error(&callback)
796
+ @err_cb = callback
797
+ end
689
798
 
690
- # Methods only used by the parser
799
+ def on_disconnect(&callback)
800
+ @disconnect_cb = callback
801
+ end
691
802
 
692
- def process_pong
693
- # Take first pong wait and signal any flush in case there was one
694
- @pongs.synchronize do
695
- pong = @pongs.pop
696
- pong.signal unless pong.nil?
697
- end
698
- @pings_outstanding -= 1
699
- @pongs_received += 1
803
+ def on_reconnect(&callback)
804
+ @reconnect_cb = callback
805
+ end
806
+
807
+ def on_close(&callback)
808
+ @close_cb = callback
809
+ end
810
+
811
+ def last_error
812
+ synchronize do
813
+ @last_err
700
814
  end
815
+ end
701
816
 
702
- # Received a ping so respond back with a pong
703
- def process_ping
704
- @pending_queue << PONG_RESPONSE
705
- @flush_queue << :ping
706
- pong = @pongs.new_cond
707
- @pongs.synchronize { @pongs << pong }
817
+ # drain will put a connection into a drain state. All subscriptions will
818
+ # immediately be put into a drain state. Upon completion, the publishers
819
+ # will be drained and can not publish any additional messages. Upon draining
820
+ # of the publishers, the connection will be closed. Use the `on_close`
821
+ # callback option to know when the connection has moved from draining to closed.
822
+ def drain
823
+ return if draining?
824
+
825
+ synchronize do
826
+ @drain_t ||= Thread.new { do_drain }
708
827
  end
828
+ end
709
829
 
710
- # Handles protocol errors being sent by the server.
711
- def process_err(err)
712
- # In case of permissions violation then dispatch the error callback
713
- # while holding the lock.
714
- e = synchronize do
715
- current = server_pool.first
716
- case
717
- when err =~ /'Stale Connection'/
718
- @last_err = NATS::IO::StaleConnectionError.new(err)
719
- when current && current[:auth_required]
720
- # We cannot recover from auth errors so mark it to avoid
721
- # retrying to unecessarily next time.
722
- current[:error_received] = true
723
- @last_err = NATS::IO::AuthError.new(err)
724
- else
725
- @last_err = NATS::IO::ServerError.new(err)
726
- end
830
+ # Create a JetStream context.
831
+ # @param opts [Hash] Options to customize the JetStream context.
832
+ # @option params [String] :prefix JetStream API prefix to use for the requests.
833
+ # @option params [String] :domain JetStream Domain to use for the requests.
834
+ # @option params [Float] :timeout Default timeout to use for JS requests.
835
+ # @return [NATS::JetStream]
836
+ def jetstream(opts={})
837
+ ::NATS::JetStream.new(self, opts)
838
+ end
839
+ alias_method :JetStream, :jetstream
840
+ alias_method :jsm, :jetstream
841
+
842
+ private
843
+
844
+ def validate_settings!
845
+ raise(NATS::IO::ClientError, "custom inbox may not include '>'") if @inbox_prefix.include?(">")
846
+ raise(NATS::IO::ClientError, "custom inbox may not include '*'") if @inbox_prefix.include?("*")
847
+ raise(NATS::IO::ClientError, "custom inbox may not end in '.'") if @inbox_prefix.end_with?(".")
848
+ raise(NATS::IO::ClientError, "custom inbox may not begin with '.'") if @inbox_prefix.start_with?(".")
849
+ end
850
+
851
+ def process_info(line)
852
+ parsed_info = JSON.parse(line)
853
+
854
+ # INFO can be received asynchronously too,
855
+ # so has to be done under the lock.
856
+ synchronize do
857
+ # Symbolize keys from parsed info line
858
+ @server_info = parsed_info.reduce({}) do |info, (k,v)|
859
+ info[k.to_sym] = v
860
+
861
+ info
727
862
  end
728
- process_op_error(e)
729
- end
730
863
 
731
- def process_msg(subject, sid, reply, data, header)
732
- @stats[:in_msgs] += 1
733
- @stats[:in_bytes] += data.size
734
-
735
- # Throw away in case we no longer manage the subscription
736
- sub = nil
737
- synchronize { sub = @subs[sid] }
738
- return unless sub
739
-
740
- err = nil
741
- sub.synchronize do
742
- sub.received += 1
743
-
744
- # Check for auto_unsubscribe
745
- if sub.max
746
- case
747
- when sub.received > sub.max
748
- # Client side support in case server did not receive unsubscribe
749
- unsubscribe(sid)
750
- return
751
- when sub.received == sub.max
752
- # Cleanup here if we have hit the max..
753
- synchronize { @subs.delete(sid) }
864
+ # Detect any announced server that we might not be aware of...
865
+ connect_urls = @server_info[:connect_urls]
866
+ if !@options[:ignore_discovered_urls] && connect_urls
867
+ srvs = []
868
+ connect_urls.each do |url|
869
+ # Use the same scheme as the currently in use URI.
870
+ scheme = @uri.scheme
871
+ u = URI.parse("#{scheme}://#{url}")
872
+
873
+ # Skip in case it is the current server which we already know
874
+ next if @uri.hostname == u.hostname && @uri.port == u.port
875
+
876
+ present = server_pool.detect do |srv|
877
+ srv[:uri].hostname == u.hostname && srv[:uri].port == u.port
754
878
  end
755
- end
756
879
 
757
- # In case of a request which requires a future
758
- # do so here already while holding the lock and return
759
- if sub.future
760
- future = sub.future
761
- hdr = process_hdr(header)
762
- sub.response = Msg.new(subject: subject, reply: reply, data: data, header: hdr)
763
- future.signal
880
+ if not present
881
+ # Let explicit user and pass options set the credentials.
882
+ u.user = options[:user] if options[:user]
883
+ u.password = options[:pass] if options[:pass]
764
884
 
765
- return
766
- elsif sub.pending_queue
767
- # Async subscribers use a sized queue for processing
768
- # and should be able to consume messages in parallel.
769
- if sub.pending_queue.size >= sub.pending_msgs_limit \
770
- or sub.pending_size >= sub.pending_bytes_limit then
771
- err = SlowConsumer.new("nats: slow consumer, messages dropped")
772
- else
773
- hdr = process_hdr(header)
885
+ # Use creds from the current server if not set explicitly.
886
+ if @uri
887
+ u.user ||= @uri.user if @uri.user
888
+ u.password ||= @uri.password if @uri.password
889
+ end
774
890
 
775
- # Only dispatch message when sure that it would not block
776
- # the main read loop from the parser.
777
- msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr)
778
- sub.pending_queue << msg
779
- sub.pending_size += data.size
891
+ # NOTE: Auto discovery won't work here when TLS host verification is enabled.
892
+ srv = { :uri => u, :reconnect_attempts => 0, :discovered => true, :hostname => u.hostname }
893
+ srvs << srv
780
894
  end
781
895
  end
782
- end
896
+ srvs.shuffle! unless @options[:dont_randomize_servers]
783
897
 
784
- synchronize do
785
- @last_err = err
786
- @err_cb.call(err) if @err_cb
787
- end if err
898
+ # Include in server pool but keep current one as the first one.
899
+ server_pool.push(*srvs)
900
+ end
788
901
  end
789
902
 
790
- def process_hdr(header)
791
- hdr = nil
792
- if header
793
- hdr = {}
794
- lines = header.lines
903
+ @server_info
904
+ end
905
+
906
+ def process_hdr(header)
907
+ hdr = nil
908
+ if header
909
+ hdr = {}
910
+ lines = header.lines
795
911
 
796
- # Check if it is an inline status and description.
797
- if lines.count <= 2
798
- status_hdr = lines.first.rstrip
799
- hdr[STATUS_HDR] = status_hdr.slice(NATS_HDR_LINE_SIZE-1, STATUS_MSG_LEN)
912
+ # Check if the first line has an inline status and description.
913
+ if lines.count > 0
914
+ status_hdr = lines.first.rstrip
915
+ status = status_hdr.slice(NATS_HDR_LINE_SIZE-1, STATUS_MSG_LEN)
916
+
917
+ if status and !status.empty?
918
+ hdr[STATUS_HDR] = status
800
919
 
801
920
  if NATS_HDR_LINE_SIZE+2 < status_hdr.bytesize
802
921
  desc = status_hdr.slice(NATS_HDR_LINE_SIZE+STATUS_MSG_LEN, status_hdr.bytesize)
803
922
  hdr[DESC_HDR] = desc unless desc.empty?
804
923
  end
805
924
  end
806
- begin
807
- lines.slice(1, header.size).each do |line|
808
- line.rstrip!
809
- next if line.empty?
810
- key, value = line.strip.split(/\s*:\s*/, 2)
811
- hdr[key] = value
812
- end
813
- rescue => e
814
- err = e
925
+ end
926
+ begin
927
+ lines.slice(1, header.size).each do |line|
928
+ line.rstrip!
929
+ next if line.empty?
930
+ key, value = line.strip.split(/\s*:\s*/, 2)
931
+ hdr[key] = value
815
932
  end
933
+ rescue => e
934
+ err = e
816
935
  end
817
-
818
- hdr
819
936
  end
820
937
 
821
- def process_info(line)
822
- parsed_info = JSON.parse(line)
938
+ hdr
939
+ end
823
940
 
824
- # INFO can be received asynchronously too,
825
- # so has to be done under the lock.
826
- synchronize do
827
- # Symbolize keys from parsed info line
828
- @server_info = parsed_info.reduce({}) do |info, (k,v)|
829
- info[k.to_sym] = v
941
+ # Methods only used by the parser
830
942
 
831
- info
832
- end
943
+ def process_pong
944
+ # Take first pong wait and signal any flush in case there was one
945
+ @pongs.synchronize do
946
+ pong = @pongs.pop
947
+ pong.signal unless pong.nil?
948
+ end
949
+ @pings_outstanding -= 1
950
+ @pongs_received += 1
951
+ end
952
+
953
+ # Received a ping so respond back with a pong
954
+ def process_ping
955
+ @pending_queue << PONG_RESPONSE
956
+ @flush_queue << :ping
957
+ pong = @pongs.new_cond
958
+ @pongs.synchronize { @pongs << pong }
959
+ end
960
+
961
+ # Handles protocol errors being sent by the server.
962
+ def process_err(err)
963
+ # In case of permissions violation then dispatch the error callback
964
+ # while holding the lock.
965
+ e = synchronize do
966
+ current = server_pool.first
967
+ case
968
+ when err =~ /'Stale Connection'/
969
+ @last_err = NATS::IO::StaleConnectionError.new(err)
970
+ when current && current[:auth_required]
971
+ # We cannot recover from auth errors so mark it to avoid
972
+ # retrying to unecessarily next time.
973
+ current[:error_received] = true
974
+ @last_err = NATS::IO::AuthError.new(err)
975
+ else
976
+ @last_err = NATS::IO::ServerError.new(err)
977
+ end
978
+ end
979
+ process_op_error(e)
980
+ end
833
981
 
834
- # Detect any announced server that we might not be aware of...
835
- connect_urls = @server_info[:connect_urls]
836
- if connect_urls
837
- srvs = []
838
- connect_urls.each do |url|
839
- scheme = client_using_secure_connection? ? "tls" : "nats"
840
- u = URI.parse("#{scheme}://#{url}")
982
+ def process_msg(subject, sid, reply, data, header)
983
+ @stats[:in_msgs] += 1
984
+ @stats[:in_bytes] += data.size
841
985
 
842
- # Skip in case it is the current server which we already know
843
- next if @uri.host == u.host && @uri.port == u.port
986
+ # Throw away in case we no longer manage the subscription
987
+ sub = nil
988
+ synchronize { sub = @subs[sid] }
989
+ return unless sub
844
990
 
845
- present = server_pool.detect do |srv|
846
- srv[:uri].host == u.host && srv[:uri].port == u.port
847
- end
991
+ err = nil
992
+ sub.synchronize do
993
+ sub.received += 1
848
994
 
849
- if not present
850
- # Let explicit user and pass options set the credentials.
851
- u.user = options[:user] if options[:user]
852
- u.password = options[:pass] if options[:pass]
995
+ # Check for auto_unsubscribe
996
+ if sub.max
997
+ case
998
+ when sub.received > sub.max
999
+ # Client side support in case server did not receive unsubscribe
1000
+ unsubscribe(sid)
1001
+ return
1002
+ when sub.received == sub.max
1003
+ # Cleanup here if we have hit the max..
1004
+ synchronize { @subs.delete(sid) }
1005
+ end
1006
+ end
853
1007
 
854
- # Use creds from the current server if not set explicitly.
855
- if @uri
856
- u.user ||= @uri.user if @uri.user
857
- u.password ||= @uri.password if @uri.password
858
- end
1008
+ # In case of a request which requires a future
1009
+ # do so here already while holding the lock and return
1010
+ if sub.future
1011
+ future = sub.future
1012
+ hdr = process_hdr(header)
1013
+ sub.response = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
1014
+ future.signal
1015
+
1016
+ return
1017
+ elsif sub.pending_queue
1018
+ # Async subscribers use a sized queue for processing
1019
+ # and should be able to consume messages in parallel.
1020
+ if sub.pending_queue.size >= sub.pending_msgs_limit \
1021
+ or sub.pending_size >= sub.pending_bytes_limit then
1022
+ err = NATS::IO::SlowConsumer.new("nats: slow consumer, messages dropped")
1023
+ else
1024
+ hdr = process_hdr(header)
859
1025
 
860
- # NOTE: Auto discovery won't work here when TLS host verification is enabled.
861
- srv = { :uri => u, :reconnect_attempts => 0, :discovered => true, :hostname => u.host }
862
- srvs << srv
863
- end
864
- end
865
- srvs.shuffle! unless @options[:dont_randomize_servers]
1026
+ # Only dispatch message when sure that it would not block
1027
+ # the main read loop from the parser.
1028
+ msg = Msg.new(subject: subject, reply: reply, data: data, header: hdr, nc: self, sub: sub)
866
1029
 
867
- # Include in server pool but keep current one as the first one.
868
- server_pool.push(*srvs)
1030
+ sub.dispatch(msg)
869
1031
  end
870
1032
  end
1033
+ end
1034
+
1035
+ synchronize do
1036
+ @last_err = err
1037
+ err_cb_call(self, err, sub) if @err_cb
1038
+ end if err
1039
+ end
1040
+
1041
+ def select_next_server
1042
+ raise NATS::IO::NoServersError.new("nats: No servers available") if server_pool.empty?
1043
+
1044
+ # Pick next from head of the list
1045
+ srv = server_pool.shift
1046
+
1047
+ # Track connection attempts to this server
1048
+ srv[:reconnect_attempts] ||= 0
1049
+ srv[:reconnect_attempts] += 1
1050
+
1051
+ # Back off in case we are reconnecting to it and have been connected
1052
+ sleep @options[:reconnect_time_wait] if should_delay_connect?(srv)
871
1053
 
872
- @server_info
1054
+ # Set url of the server to which we would be connected
1055
+ @uri = srv[:uri]
1056
+ @uri.user = @options[:user] if @options[:user]
1057
+ @uri.password = @options[:pass] if @options[:pass]
1058
+
1059
+ srv
1060
+ end
1061
+
1062
+ def server_using_secure_connection?
1063
+ @server_info[:ssl_required] || @server_info[:tls_required]
1064
+ end
1065
+
1066
+ def client_using_secure_connection?
1067
+ @uri.scheme == "tls" || @tls
1068
+ end
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
873
1084
  end
1085
+ end
874
1086
 
875
- # Close connection to NATS, flushing in case connection is alive
876
- # and there are any pending messages, should not be used while
877
- # holding the lock.
878
- def close
879
- close_connection(CLOSED, true)
1087
+ def single_url_connect_used?
1088
+ @single_url_connect_used
1089
+ end
1090
+
1091
+ def send_command(command)
1092
+ raise NATS::IO::ConnectionClosedError if closed?
1093
+
1094
+ establish_connection! if !status || (disconnected? && should_reconnect?)
1095
+
1096
+ @pending_size += command.bytesize
1097
+ @pending_queue << command
1098
+
1099
+ # TODO: kick flusher here in case pending_size growing large
1100
+ end
1101
+
1102
+ # Auto unsubscribes the server by sending UNSUB command and throws away
1103
+ # subscription in case already present and has received enough messages.
1104
+ def unsubscribe(sub, opt_max=nil)
1105
+ sid = nil
1106
+ closed = nil
1107
+ sub.synchronize do
1108
+ sid = sub.sid
1109
+ closed = sub.closed
880
1110
  end
1111
+ raise NATS::IO::BadSubscription.new("nats: invalid subscription") if closed
881
1112
 
882
- def new_inbox
883
- "_INBOX.#{SecureRandom.hex(13)}"
1113
+ opt_max_str = " #{opt_max}" unless opt_max.nil?
1114
+ send_command("UNSUB #{sid}#{opt_max_str}#{CR_LF}")
1115
+ @flush_queue << :unsub
1116
+
1117
+ synchronize { sub = @subs[sid] }
1118
+ return unless sub
1119
+ synchronize do
1120
+ sub.max = opt_max
1121
+ @subs.delete(sid) unless (sub.max && (sub.received < sub.max))
884
1122
  end
885
1123
 
886
- def connected_server
887
- connected? ? @uri : nil
1124
+ sub.synchronize do
1125
+ sub.closed = true
888
1126
  end
1127
+ end
889
1128
 
890
- def connected?
891
- @status == CONNECTED
1129
+ def drain_sub(sub)
1130
+ sid = nil
1131
+ closed = nil
1132
+ sub.synchronize do
1133
+ sid = sub.sid
1134
+ closed = sub.closed
892
1135
  end
1136
+ return if closed
1137
+
1138
+ send_command("UNSUB #{sid}#{CR_LF}")
1139
+ @flush_queue << :drain
1140
+
1141
+ synchronize { sub = @subs[sid] }
1142
+ return unless sub
1143
+ end
1144
+
1145
+ def do_drain
1146
+ synchronize { @status = DRAINING_SUBS }
1147
+
1148
+ # Do unsubscribe protocol for all the susbcriptions, then have a single thread
1149
+ # waiting until all subs are done or drain timeout error reported to async error cb.
1150
+ subs = []
1151
+ @subs.each do |_, sub|
1152
+ next if sub == @resp_sub
1153
+ drain_sub(sub)
1154
+ subs << sub
1155
+ end
1156
+ force_flush!
1157
+
1158
+ # Wait until all subs have no pending messages.
1159
+ drain_timeout = MonotonicTime::now + @options[:drain_timeout]
1160
+ to_delete = []
893
1161
 
894
- def connecting?
895
- @status == CONNECTING
1162
+ loop do
1163
+ break if MonotonicTime::now > drain_timeout
1164
+ sleep 0.1
1165
+
1166
+ # Wait until all subs are done.
1167
+ @subs.each do |_, sub|
1168
+ if sub != @resp_sub and sub.pending_queue.size == 0
1169
+ to_delete << sub
1170
+ end
1171
+ end
1172
+ next if to_delete.empty?
1173
+
1174
+ to_delete.each do |sub|
1175
+ @subs.delete(sub.sid)
1176
+ end
1177
+ to_delete.clear
1178
+
1179
+ # Wait until only the resp mux is remaining or there are no subscriptions.
1180
+ if @subs.count == 1
1181
+ sid, sub = @subs.first
1182
+ if sub == @resp_sub
1183
+ break
1184
+ end
1185
+ elsif @subs.count == 0
1186
+ break
1187
+ end
896
1188
  end
897
1189
 
898
- def reconnecting?
899
- @status == RECONNECTING
900
- end
1190
+ subscription_executor.shutdown
1191
+ subscription_executor.wait_for_termination(@options[:drain_timeout])
901
1192
 
902
- def closed?
903
- @status == CLOSED
1193
+ if MonotonicTime::now > drain_timeout
1194
+ e = NATS::IO::DrainTimeoutError.new("nats: draining connection timed out")
1195
+ err_cb_call(self, e, nil) if @err_cb
904
1196
  end
1197
+ synchronize { @status = DRAINING_PUBS }
905
1198
 
906
- def on_error(&callback)
907
- @err_cb = callback
908
- end
1199
+ # Remove resp mux handler in case there is one.
1200
+ unsubscribe(@resp_sub) if @resp_sub
1201
+ close
1202
+ end
909
1203
 
910
- def on_disconnect(&callback)
911
- @disconnect_cb = callback
912
- end
1204
+ def send_flush_queue(s)
1205
+ @flush_queue << s
1206
+ end
913
1207
 
914
- def on_reconnect(&callback)
915
- @reconnect_cb = callback
916
- end
1208
+ def delete_sid(sid)
1209
+ @subs.delete(sid)
1210
+ end
917
1211
 
918
- def on_close(&callback)
919
- @close_cb = callback
920
- end
1212
+ def err_cb_call(nc, e, sub)
1213
+ return unless @err_cb
921
1214
 
922
- def last_error
923
- synchronize do
924
- @last_err
925
- end
1215
+ cb = @err_cb
1216
+ case cb.arity
1217
+ when 0 then cb.call
1218
+ when 1 then cb.call(e)
1219
+ when 2 then cb.call(e, sub)
1220
+ else cb.call(nc, e, sub)
926
1221
  end
1222
+ end
927
1223
 
928
- private
1224
+ def auth_connection?
1225
+ !@uri.user.nil?
1226
+ end
929
1227
 
930
- def select_next_server
931
- raise NoServersError.new("nats: No servers available") if server_pool.empty?
1228
+ def connect_command
1229
+ cs = {
1230
+ :verbose => @options[:verbose],
1231
+ :pedantic => @options[:pedantic],
1232
+ :lang => NATS::IO::LANG,
1233
+ :version => NATS::IO::VERSION,
1234
+ :protocol => NATS::IO::PROTOCOL
1235
+ }
1236
+ cs[:name] = @options[:name] if @options[:name]
932
1237
 
933
- # Pick next from head of the list
934
- srv = server_pool.shift
1238
+ case
1239
+ when auth_connection?
1240
+ if @uri.password
1241
+ cs[:user] = @uri.user
1242
+ cs[:pass] = @uri.password
1243
+ else
1244
+ cs[:auth_token] = @uri.user
1245
+ end
1246
+ when @user_jwt_cb && @signature_cb
1247
+ nonce = @server_info[:nonce]
1248
+ cs[:jwt] = @user_jwt_cb.call
1249
+ cs[:sig] = @signature_cb.call(nonce)
1250
+ when @user_nkey_cb && @signature_cb
1251
+ nonce = @server_info[:nonce]
1252
+ cs[:nkey] = @user_nkey_cb.call
1253
+ cs[:sig] = @signature_cb.call(nonce)
1254
+ end
935
1255
 
936
- # Track connection attempts to this server
937
- srv[:reconnect_attempts] ||= 0
938
- srv[:reconnect_attempts] += 1
1256
+ cs[:auth_token] = @auth_token if @auth_token
939
1257
 
940
- # Back off in case we are reconnecting to it and have been connected
941
- sleep @options[:reconnect_time_wait] if should_delay_connect?(srv)
1258
+ if @server_info[:headers]
1259
+ cs[:headers] = @server_info[:headers]
1260
+ cs[:no_responders] = if @options[:no_responders] == false
1261
+ @options[:no_responders]
1262
+ else
1263
+ @server_info[:headers]
1264
+ end
1265
+ end
942
1266
 
943
- # Set url of the server to which we would be connected
944
- @uri = srv[:uri]
945
- @uri.user = @options[:user] if @options[:user]
946
- @uri.password = @options[:pass] if @options[:pass]
1267
+ "CONNECT #{cs.to_json}#{CR_LF}"
1268
+ end
947
1269
 
948
- srv
1270
+ # Handles errors from reading, parsing the protocol or stale connection.
1271
+ # the lock should not be held entering this function.
1272
+ def process_op_error(e)
1273
+ should_bail = synchronize do
1274
+ connecting? || closed? || reconnecting?
949
1275
  end
1276
+ return if should_bail
950
1277
 
951
- def server_using_secure_connection?
952
- @server_info[:ssl_required] || @server_info[:tls_required]
953
- end
1278
+ synchronize do
1279
+ @last_err = e
1280
+ err_cb_call(self, e, nil) if @err_cb
954
1281
 
955
- def client_using_secure_connection?
956
- @uri.scheme == "tls" || @tls
957
- end
1282
+ # If we were connected and configured to reconnect,
1283
+ # then trigger disconnect and start reconnection logic
1284
+ if connected? and should_reconnect?
1285
+ @status = RECONNECTING
1286
+ @io.close if @io
1287
+ @io = nil
958
1288
 
959
- def single_url_connect_used?
960
- @single_url_connect_used
961
- end
1289
+ # TODO: Reconnecting pending buffer?
962
1290
 
963
- def send_command(command)
964
- @pending_size += command.bytesize
965
- @pending_queue << command
1291
+ # Do reconnect under a different thread than the one
1292
+ # in which we got the error.
1293
+ Thread.new do
1294
+ begin
1295
+ # Abort currently running reads in case they're around
1296
+ # FIXME: There might be more graceful way here...
1297
+ @read_loop_thread.exit if @read_loop_thread.alive?
1298
+ @flusher_thread.exit if @flusher_thread.alive?
1299
+ @ping_interval_thread.exit if @ping_interval_thread.alive?
1300
+
1301
+ attempt_reconnect
1302
+ rescue NATS::IO::NoServersError => e
1303
+ @last_err = e
1304
+ close
1305
+ end
1306
+ end
966
1307
 
967
- # TODO: kick flusher here in case pending_size growing large
968
- end
1308
+ Thread.exit
1309
+ return
1310
+ end
969
1311
 
970
- def auth_connection?
971
- !@uri.user.nil?
1312
+ # Otherwise, stop trying to reconnect and close the connection
1313
+ @status = DISCONNECTED
972
1314
  end
973
1315
 
974
- def connect_command
975
- cs = {
976
- :verbose => @options[:verbose],
977
- :pedantic => @options[:pedantic],
978
- :lang => NATS::IO::LANG,
979
- :version => NATS::IO::VERSION,
980
- :protocol => NATS::IO::PROTOCOL
981
- }
982
- cs[:name] = @options[:name] if @options[:name]
1316
+ # Otherwise close the connection to NATS
1317
+ close
1318
+ end
983
1319
 
984
- case
985
- when auth_connection?
986
- if @uri.password
987
- cs[:user] = @uri.user
988
- cs[:pass] = @uri.password
989
- else
990
- cs[:auth_token] = @uri.user
1320
+ # Gathers data from the socket and sends it to the parser.
1321
+ def read_loop
1322
+ loop do
1323
+ begin
1324
+ should_bail = synchronize do
1325
+ # FIXME: In case of reconnect as well?
1326
+ @status == CLOSED or @status == RECONNECTING
1327
+ end
1328
+ if !@io or @io.closed? or should_bail
1329
+ return
991
1330
  end
992
- when @user_credentials
993
- nonce = @server_info[:nonce]
994
- cs[:jwt] = @user_jwt_cb.call
995
- cs[:sig] = @signature_cb.call(nonce)
996
- when @nkeys_seed
997
- nonce = @server_info[:nonce]
998
- cs[:nkey] = @user_nkey_cb.call
999
- cs[:sig] = @signature_cb.call(nonce)
1000
- end
1001
1331
 
1002
- if @server_info[:headers]
1003
- cs[:headers] = @server_info[:headers]
1004
- cs[:no_responders] = if @options[:no_responders] == false
1005
- @options[:no_responders]
1006
- else
1007
- @server_info[:headers]
1008
- end
1332
+ # TODO: Remove timeout and just wait to be ready
1333
+ data = @io.read(NATS::IO::MAX_SOCKET_READ_BYTES)
1334
+ @parser.parse(data) if data
1335
+ rescue Errno::ETIMEDOUT
1336
+ # FIXME: We do not really need a timeout here...
1337
+ retry
1338
+ rescue => e
1339
+ # In case of reading/parser errors, trigger
1340
+ # reconnection logic in case desired.
1341
+ process_op_error(e)
1009
1342
  end
1010
-
1011
- "CONNECT #{cs.to_json}#{CR_LF}"
1012
1343
  end
1344
+ end
1013
1345
 
1014
- def with_nats_timeout(timeout)
1015
- start_time = MonotonicTime.now
1016
- yield
1017
- end_time = MonotonicTime.now
1018
- duration = end_time - start_time
1019
- raise NATS::IO::Timeout.new("nats: timeout") if duration > timeout
1020
- end
1346
+ # Waits for client to notify the flusher that it will be
1347
+ # it is sending a command.
1348
+ def flusher_loop
1349
+ loop do
1350
+ # Blocks waiting for the flusher to be kicked...
1351
+ @flush_queue.pop
1021
1352
 
1022
- # Handles errors from reading, parsing the protocol or stale connection.
1023
- # the lock should not be held entering this function.
1024
- def process_op_error(e)
1025
1353
  should_bail = synchronize do
1026
- connecting? || closed? || reconnecting?
1354
+ (@status != CONNECTED && !draining? ) || @status == CONNECTING
1027
1355
  end
1028
1356
  return if should_bail
1029
1357
 
1030
- synchronize do
1031
- @last_err = e
1032
- @err_cb.call(e) if @err_cb
1033
-
1034
- # If we were connected and configured to reconnect,
1035
- # then trigger disconnect and start reconnection logic
1036
- if connected? and should_reconnect?
1037
- @status = RECONNECTING
1038
- @io.close if @io
1039
- @io = nil
1040
-
1041
- # TODO: Reconnecting pending buffer?
1042
-
1043
- # Do reconnect under a different thread than the one
1044
- # in which we got the error.
1045
- Thread.new do
1046
- begin
1047
- # Abort currently running reads in case they're around
1048
- # FIXME: There might be more graceful way here...
1049
- @read_loop_thread.exit if @read_loop_thread.alive?
1050
- @flusher_thread.exit if @flusher_thread.alive?
1051
- @ping_interval_thread.exit if @ping_interval_thread.alive?
1052
-
1053
- attempt_reconnect
1054
- rescue NoServersError => e
1055
- @last_err = e
1056
- close
1057
- end
1058
- end
1358
+ # Skip in case nothing remains pending already.
1359
+ next if @pending_queue.empty?
1059
1360
 
1060
- Thread.exit
1061
- return
1062
- end
1361
+ force_flush!
1063
1362
 
1064
- # Otherwise, stop trying to reconnect and close the connection
1065
- @status = DISCONNECTED
1363
+ synchronize do
1364
+ @pending_size = 0
1066
1365
  end
1067
-
1068
- # Otherwise close the connection to NATS
1069
- close
1070
1366
  end
1367
+ end
1071
1368
 
1072
- # Gathers data from the socket and sends it to the parser.
1073
- def read_loop
1074
- loop do
1075
- begin
1076
- should_bail = synchronize do
1077
- # FIXME: In case of reconnect as well?
1078
- @status == CLOSED or @status == RECONNECTING
1079
- end
1080
- if !@io or @io.closed? or should_bail
1081
- return
1082
- end
1083
-
1084
- # TODO: Remove timeout and just wait to be ready
1085
- data = @io.read(MAX_SOCKET_READ_BYTES)
1086
- @parser.parse(data) if data
1087
- rescue Errno::ETIMEDOUT
1088
- # FIXME: We do not really need a timeout here...
1089
- retry
1090
- rescue => e
1091
- # In case of reading/parser errors, trigger
1092
- # reconnection logic in case desired.
1093
- process_op_error(e)
1094
- end
1369
+ def force_flush!
1370
+ # FIXME: should limit how many commands to take at once
1371
+ # since producers could be adding as many as possible
1372
+ # until reaching the max pending queue size.
1373
+ cmds = []
1374
+ cmds << @pending_queue.pop until @pending_queue.empty?
1375
+ begin
1376
+ @io.write(cmds.join) unless cmds.empty?
1377
+ rescue => e
1378
+ synchronize do
1379
+ @last_err = e
1380
+ err_cb_call(self, e, nil) if @err_cb
1095
1381
  end
1096
- end
1097
1382
 
1098
- # Waits for client to notify the flusher that it will be
1099
- # it is sending a command.
1100
- def flusher_loop
1101
- loop do
1102
- # Blocks waiting for the flusher to be kicked...
1103
- @flush_queue.pop
1383
+ process_op_error(e)
1384
+ return
1385
+ end if @io
1386
+ end
1104
1387
 
1105
- should_bail = synchronize do
1106
- @status != CONNECTED || @status == CONNECTING
1107
- end
1108
- return if should_bail
1388
+ def ping_interval_loop
1389
+ loop do
1390
+ sleep @options[:ping_interval]
1109
1391
 
1110
- # Skip in case nothing remains pending already.
1111
- next if @pending_queue.empty?
1392
+ # Skip ping interval until connected
1393
+ next if !connected?
1112
1394
 
1113
- # FIXME: should limit how many commands to take at once
1114
- # since producers could be adding as many as possible
1115
- # until reaching the max pending queue size.
1116
- cmds = []
1117
- cmds << @pending_queue.pop until @pending_queue.empty?
1118
- begin
1119
- @io.write(cmds.join) unless cmds.empty?
1120
- rescue => e
1121
- synchronize do
1122
- @last_err = e
1123
- @err_cb.call(e) if @err_cb
1124
- end
1395
+ if @pings_outstanding >= @options[:max_outstanding_pings]
1396
+ process_op_error(NATS::IO::StaleConnectionError.new("nats: stale connection"))
1397
+ return
1398
+ end
1125
1399
 
1126
- process_op_error(e)
1127
- return
1128
- end if @io
1400
+ @pings_outstanding += 1
1401
+ send_command(PING_REQUEST)
1402
+ @flush_queue << :ping
1403
+ end
1404
+ rescue => e
1405
+ process_op_error(e)
1406
+ end
1129
1407
 
1130
- synchronize do
1131
- @pending_size = 0
1132
- end
1133
- end
1408
+ def process_connect_init
1409
+ # FIXME: Can receive PING as well here in recent versions.
1410
+ line = @io.read_line(options[:connect_timeout])
1411
+ if !line or line.empty?
1412
+ raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not received")
1134
1413
  end
1135
1414
 
1136
- def ping_interval_loop
1137
- loop do
1138
- sleep @options[:ping_interval]
1415
+ if match = line.match(NATS::Protocol::INFO)
1416
+ info_json = match.captures.first
1417
+ process_info(info_json)
1418
+ else
1419
+ raise NATS::IO::ConnectError.new("nats: protocol exception, INFO not valid")
1420
+ end
1139
1421
 
1140
- # Skip ping interval until connected
1141
- next if !connected?
1422
+ case
1423
+ 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")
1427
+ raise NATS::IO::ConnectError.new('TLS/SSL required by server')
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")
1431
+ raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1432
+ else
1433
+ # Otherwise, use a regular connection.
1434
+ end
1142
1435
 
1143
- if @pings_outstanding >= @options[:max_outstanding_pings]
1144
- process_op_error(StaleConnectionError.new("nats: stale connection"))
1145
- return
1146
- end
1436
+ # Send connect and process synchronously. If using TLS,
1437
+ # it should have handled upgrading at this point.
1438
+ @io.write(connect_command)
1147
1439
 
1148
- @pings_outstanding += 1
1149
- send_command(PING_REQUEST)
1150
- @flush_queue << :ping
1151
- end
1152
- rescue => e
1153
- process_op_error(e)
1440
+ # Send ping/pong after connect
1441
+ @io.write(PING_REQUEST)
1442
+
1443
+ next_op = @io.read_line(options[:connect_timeout])
1444
+ if @options[:verbose]
1445
+ # Need to get another command here if verbose
1446
+ raise NATS::IO::ConnectError.new("expected to receive +OK") unless next_op =~ NATS::Protocol::OK
1447
+ next_op = @io.read_line(options[:connect_timeout])
1154
1448
  end
1155
1449
 
1156
- def process_connect_init
1157
- line = @io.read_line(options[:connect_timeout])
1158
- if !line or line.empty?
1159
- raise ConnectError.new("nats: protocol exception, INFO not received")
1450
+ case next_op
1451
+ when NATS::Protocol::PONG
1452
+ when NATS::Protocol::ERR
1453
+ if @server_info[:auth_required]
1454
+ raise NATS::IO::AuthError.new($1)
1455
+ else
1456
+ raise NATS::IO::ServerError.new($1)
1160
1457
  end
1458
+ else
1459
+ raise NATS::IO::ConnectError.new("expected PONG, got #{next_op}")
1460
+ end
1461
+ end
1462
+
1463
+ # Reconnect logic, this is done while holding the lock.
1464
+ def attempt_reconnect
1465
+ @disconnect_cb.call(@last_err) if @disconnect_cb
1161
1466
 
1162
- if match = line.match(NATS::Protocol::INFO)
1163
- info_json = match.captures.first
1164
- process_info(info_json)
1467
+ # Clear sticky error
1468
+ @last_err = nil
1469
+
1470
+ # Do reconnect
1471
+ srv = nil
1472
+ begin
1473
+ srv = select_next_server
1474
+
1475
+ # Set hostname to use for TLS hostname verification
1476
+ if client_using_secure_connection? and single_url_connect_used?
1477
+ # Reuse original hostname name in case of using TLS.
1478
+ @hostname ||= srv[:hostname]
1165
1479
  else
1166
- raise ConnectError.new("nats: protocol exception, INFO not valid")
1480
+ @hostname = srv[:hostname]
1167
1481
  end
1168
1482
 
1169
- case
1170
- when (server_using_secure_connection? and client_using_secure_connection?)
1171
- tls_context = nil
1483
+ # Establish TCP connection with new server
1484
+ @io = create_socket
1485
+ @io.connect
1486
+ @stats[:reconnects] += 1
1172
1487
 
1173
- if @tls
1174
- # Allow prepared context and customizations via :tls opts
1175
- tls_context = @tls[:context] if @tls[:context]
1176
- else
1177
- # Defaults
1178
- tls_context = OpenSSL::SSL::SSLContext.new
1179
-
1180
- # Use the default verification options from Ruby:
1181
- # https://github.com/ruby/ruby/blob/96db72ce38b27799dd8e80ca00696e41234db6ba/ext/openssl/lib/openssl/ssl.rb#L19-L29
1182
- #
1183
- # Insecure TLS versions not supported already:
1184
- # https://github.com/ruby/openssl/commit/3e5a009966bd7f806f7180d82cf830a04be28986
1185
- #
1186
- tls_context.set_params
1187
- end
1488
+ # Established TCP connection successfully so can start connect
1489
+ process_connect_init
1188
1490
 
1189
- # Setup TLS connection by rewrapping the socket
1190
- tls_socket = OpenSSL::SSL::SSLSocket.new(@io.socket, tls_context)
1491
+ # Reset reconnection attempts if connection is valid
1492
+ srv[:reconnect_attempts] = 0
1493
+ srv[:auth_required] ||= true if @server_info[:auth_required]
1191
1494
 
1192
- # Close TCP socket after closing TLS socket as well.
1193
- tls_socket.sync_close = true
1495
+ # Add back to rotation since successfully connected
1496
+ server_pool << srv
1497
+ rescue NATS::IO::NoServersError => e
1498
+ raise e
1499
+ rescue => e
1500
+ # In case there was an error from the server check
1501
+ # to see whether need to take it out from rotation
1502
+ srv[:auth_required] ||= true if @server_info[:auth_required]
1503
+ server_pool << srv if can_reuse_server?(srv)
1194
1504
 
1195
- # Required to enable hostname verification if Ruby runtime supports it (>= 2.4):
1196
- # https://github.com/ruby/openssl/commit/028e495734e9e6aa5dba1a2e130b08f66cf31a21
1197
- tls_socket.hostname = @hostname
1505
+ @last_err = e
1198
1506
 
1199
- tls_socket.connect
1200
- @io.socket = tls_socket
1201
- when (server_using_secure_connection? and !client_using_secure_connection?)
1202
- raise NATS::IO::ConnectError.new('TLS/SSL required by server')
1203
- when (client_using_secure_connection? and !server_using_secure_connection?)
1204
- raise NATS::IO::ConnectError.new('TLS/SSL not supported by server')
1205
- else
1206
- # Otherwise, use a regular connection.
1207
- end
1507
+ # Trigger async error handler
1508
+ err_cb_call(self, e, nil) if @err_cb
1208
1509
 
1209
- # Send connect and process synchronously. If using TLS,
1210
- # it should have handled upgrading at this point.
1211
- @io.write(connect_command)
1510
+ # Continue retrying until there are no options left in the server pool
1511
+ retry
1512
+ end
1212
1513
 
1213
- # Send ping/pong after connect
1214
- @io.write(PING_REQUEST)
1514
+ # Clear pending flush calls and reset state before restarting loops
1515
+ @flush_queue.clear
1516
+ @pings_outstanding = 0
1517
+ @pongs_received = 0
1215
1518
 
1216
- next_op = @io.read_line(options[:connect_timeout])
1217
- if @options[:verbose]
1218
- # Need to get another command here if verbose
1219
- raise NATS::IO::ConnectError.new("expected to receive +OK") unless next_op =~ NATS::Protocol::OK
1220
- next_op = @io.read_line(options[:connect_timeout])
1221
- end
1519
+ # Replay all subscriptions
1520
+ @subs.each_pair do |sid, sub|
1521
+ @io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
1522
+ end
1222
1523
 
1223
- case next_op
1224
- when NATS::Protocol::PONG
1225
- when NATS::Protocol::ERR
1226
- if @server_info[:auth_required]
1227
- raise NATS::IO::AuthError.new($1)
1228
- else
1229
- raise NATS::IO::ServerError.new($1)
1230
- end
1231
- else
1232
- raise NATS::IO::ConnectError.new("expected PONG, got #{next_op}")
1524
+ # Flush anything which was left pending, in case of errors during flush
1525
+ # then we should raise error then retry the reconnect logic
1526
+ cmds = []
1527
+ cmds << @pending_queue.pop until @pending_queue.empty?
1528
+ @io.write(cmds.join) unless cmds.empty?
1529
+ @status = CONNECTED
1530
+ @pending_size = 0
1531
+
1532
+ # Reset parser state here to avoid unknown protocol errors
1533
+ # on reconnect...
1534
+ @parser.reset!
1535
+
1536
+ # Now connected to NATS, and we can restart parser loop, flusher
1537
+ # and ping interval
1538
+ start_threads!
1539
+
1540
+ # Dispatch the reconnected callback while holding lock
1541
+ # which we should have already
1542
+ @reconnect_cb.call if @reconnect_cb
1543
+ end
1544
+
1545
+ def close_connection(conn_status, do_cbs=true)
1546
+ synchronize do
1547
+ @connect_called = false
1548
+ if @status == CLOSED
1549
+ @status = conn_status
1550
+ return
1233
1551
  end
1234
1552
  end
1235
1553
 
1236
- # Reconnect logic, this is done while holding the lock.
1237
- def attempt_reconnect
1238
- @disconnect_cb.call(@last_err) if @disconnect_cb
1554
+ # Kick the flusher so it bails due to closed state
1555
+ @flush_queue << :fallout if @flush_queue
1556
+ Thread.pass
1239
1557
 
1240
- # Clear sticky error
1241
- @last_err = nil
1558
+ # FIXME: More graceful way of handling the following?
1559
+ # Ensure ping interval and flusher are not running anymore
1560
+ if @ping_interval_thread and @ping_interval_thread.alive?
1561
+ @ping_interval_thread.exit
1562
+ end
1242
1563
 
1243
- # Do reconnect
1244
- srv = nil
1245
- begin
1246
- srv = select_next_server
1564
+ if @flusher_thread and @flusher_thread.alive?
1565
+ @flusher_thread.exit
1566
+ end
1247
1567
 
1248
- # Establish TCP connection with new server
1249
- @io = create_socket
1250
- @io.connect
1251
- @stats[:reconnects] += 1
1568
+ if @read_loop_thread and @read_loop_thread.alive?
1569
+ @read_loop_thread.exit
1570
+ end
1252
1571
 
1253
- # Set hostname to use for TLS hostname verification
1254
- if client_using_secure_connection? and single_url_connect_used?
1255
- # Reuse original hostname name in case of using TLS.
1256
- @hostname ||= srv[:hostname]
1257
- else
1258
- @hostname = srv[:hostname]
1572
+ # TODO: Delete any other state which we are not using here too.
1573
+ synchronize do
1574
+ @pongs.synchronize do
1575
+ @pongs.each do |pong|
1576
+ pong.signal
1259
1577
  end
1578
+ @pongs.clear
1579
+ end
1260
1580
 
1261
- # Established TCP connection successfully so can start connect
1262
- process_connect_init
1263
-
1264
- # Reset reconnection attempts if connection is valid
1265
- srv[:reconnect_attempts] = 0
1266
- srv[:auth_required] ||= true if @server_info[:auth_required]
1581
+ # Try to write any pending flushes in case
1582
+ # we have a connection then close it.
1583
+ should_flush = (@pending_queue && @io && @io.socket && !@io.closed?)
1584
+ begin
1585
+ cmds = []
1586
+ cmds << @pending_queue.pop until @pending_queue.empty?
1267
1587
 
1268
- # Add back to rotation since successfully connected
1269
- server_pool << srv
1270
- rescue NoServersError => e
1271
- raise e
1588
+ # FIXME: Fails when empty on TLS connection?
1589
+ @io.write(cmds.join) unless cmds.empty?
1272
1590
  rescue => e
1273
- # In case there was an error from the server check
1274
- # to see whether need to take it out from rotation
1275
- srv[:auth_required] ||= true if @server_info[:auth_required]
1276
- server_pool << srv if can_reuse_server?(srv)
1277
-
1278
1591
  @last_err = e
1592
+ err_cb_call(self, e, nil) if @err_cb
1593
+ end if should_flush
1279
1594
 
1280
- # Trigger async error handler
1281
- @err_cb.call(e) if @err_cb
1595
+ # Destroy any remaining subscriptions.
1596
+ @subs.clear
1282
1597
 
1283
- # Continue retrying until there are no options left in the server pool
1284
- retry
1598
+ if do_cbs
1599
+ @disconnect_cb.call(@last_err) if @disconnect_cb
1600
+ @close_cb.call if @close_cb
1285
1601
  end
1286
1602
 
1287
- # Clear pending flush calls and reset state before restarting loops
1288
- @flush_queue.clear
1289
- @pings_outstanding = 0
1290
- @pongs_received = 0
1603
+ @status = conn_status
1291
1604
 
1292
- # Replay all subscriptions
1293
- @subs.each_pair do |sid, sub|
1294
- @io.write("SUB #{sub.subject} #{sub.queue} #{sid}#{CR_LF}")
1605
+ # Close the established connection in case
1606
+ # we still have it.
1607
+ if @io
1608
+ @io.close if @io.socket
1609
+ @io = nil
1295
1610
  end
1611
+ end
1612
+ end
1296
1613
 
1297
- # Flush anything which was left pending, in case of errors during flush
1298
- # then we should raise error then retry the reconnect logic
1299
- cmds = []
1300
- cmds << @pending_queue.pop until @pending_queue.empty?
1301
- @io.write(cmds.join) unless cmds.empty?
1302
- @status = CONNECTED
1303
- @pending_size = 0
1304
-
1305
- # Reset parser state here to avoid unknown protocol errors
1306
- # on reconnect...
1307
- @parser.reset!
1614
+ def start_threads!
1615
+ # Reading loop for gathering data
1616
+ @read_loop_thread = Thread.new { read_loop }
1617
+ @read_loop_thread.name = "nats:read_loop"
1618
+ @read_loop_thread.abort_on_exception = true
1619
+
1620
+ # Flusher loop for sending commands
1621
+ @flusher_thread = Thread.new { flusher_loop }
1622
+ @flusher_thread.name = "nats:flusher_loop"
1623
+ @flusher_thread.abort_on_exception = true
1624
+
1625
+ # Ping interval handling for keeping alive the connection
1626
+ @ping_interval_thread = Thread.new { ping_interval_loop }
1627
+ @ping_interval_thread.name = "nats:ping_loop"
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
+ )
1635
+ end
1308
1636
 
1309
- # Now connected to NATS, and we can restart parser loop, flusher
1310
- # and ping interval
1311
- start_threads!
1637
+ # Prepares requests subscription that handles the responses
1638
+ # for the new style request response.
1639
+ def start_resp_mux_sub!
1640
+ @resp_sub_prefix = new_inbox
1641
+ @resp_map = Hash.new { |h,k| h[k] = { }}
1642
+
1643
+ @resp_sub = Subscription.new
1644
+ @resp_sub.subject = "#{@resp_sub_prefix}.*"
1645
+ @resp_sub.received = 0
1646
+ @resp_sub.nc = self
1647
+
1648
+ # FIXME: Allow setting pending limits for responses mux subscription.
1649
+ @resp_sub.pending_msgs_limit = NATS::IO::DEFAULT_SUB_PENDING_MSGS_LIMIT
1650
+ @resp_sub.pending_bytes_limit = NATS::IO::DEFAULT_SUB_PENDING_BYTES_LIMIT
1651
+ @resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
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
1312
1661
 
1313
- # Dispatch the reconnected callback while holding lock
1314
- # which we should have already
1315
- @reconnect_cb.call if @reconnect_cb
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
1666
+ end
1316
1667
  end
1317
1668
 
1318
- def close_connection(conn_status, do_cbs=true)
1319
- synchronize do
1320
- if @status == CLOSED
1321
- @status = conn_status
1322
- return
1323
- end
1324
- end
1669
+ sid = (@ssid += 1)
1670
+ @resp_sub.sid = sid
1671
+ @subs[sid] = @resp_sub
1672
+ send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
1673
+ @flush_queue << :sub
1674
+ end
1325
1675
 
1326
- # Kick the flusher so it bails due to closed state
1327
- @flush_queue << :fallout if @flush_queue
1328
- Thread.pass
1676
+ def can_reuse_server?(server)
1677
+ return false if server.nil?
1329
1678
 
1330
- # FIXME: More graceful way of handling the following?
1331
- # Ensure ping interval and flusher are not running anymore
1332
- if @ping_interval_thread and @ping_interval_thread.alive?
1333
- @ping_interval_thread.exit
1334
- end
1679
+ # We can always reuse servers with infinite reconnects settings
1680
+ return true if @options[:max_reconnect_attempts] < 0
1335
1681
 
1336
- if @flusher_thread and @flusher_thread.alive?
1337
- @flusher_thread.exit
1338
- end
1682
+ # In case of hard errors like authorization errors, drop the server
1683
+ # already since won't be able to connect.
1684
+ return false if server[:error_received]
1685
+
1686
+ # We will retry a number of times to reconnect to a server.
1687
+ return server[:reconnect_attempts] <= @options[:max_reconnect_attempts]
1688
+ end
1689
+
1690
+ def should_delay_connect?(server)
1691
+ server[:was_connected] && server[:reconnect_attempts] >= 0
1692
+ end
1339
1693
 
1340
- if @read_loop_thread and @read_loop_thread.alive?
1341
- @read_loop_thread.exit
1694
+ def should_not_reconnect?
1695
+ !@options[:reconnect]
1696
+ end
1697
+
1698
+ def should_reconnect?
1699
+ @options[:reconnect]
1700
+ end
1701
+
1702
+ def create_socket
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"
1342
1711
  end
1343
1712
 
1344
- # TODO: Delete any other state which we are not using here too.
1345
- synchronize do
1346
- @pongs.synchronize do
1347
- @pongs.each do |pong|
1348
- pong.signal
1349
- end
1350
- @pongs.clear
1351
- end
1713
+ socket_class.new(
1714
+ uri: @uri,
1715
+ tls: { context: tls_context, hostname: @hostname },
1716
+ connect_timeout: NATS::IO::DEFAULT_CONNECT_TIMEOUT,
1717
+ )
1718
+ end
1352
1719
 
1353
- # Try to write any pending flushes in case
1354
- # we have a connection then close it.
1355
- should_flush = (@pending_queue && @io && @io.socket && !@io.closed?)
1356
- begin
1357
- cmds = []
1358
- cmds << @pending_queue.pop until @pending_queue.empty?
1359
-
1360
- # FIXME: Fails when empty on TLS connection?
1361
- @io.write(cmds.join) unless cmds.empty?
1362
- rescue => e
1363
- @last_err = e
1364
- @err_cb.call(e) if @err_cb
1365
- end if should_flush
1366
-
1367
- # Destroy any remaining subscriptions.
1368
- @subs.each do |_, sub|
1369
- if sub.wait_for_msgs_t && sub.wait_for_msgs_t.alive?
1370
- sub.wait_for_msgs_t.exit
1371
- sub.pending_queue.clear
1372
- end
1373
- end
1374
- @subs.clear
1720
+ def setup_nkeys_connect
1721
+ begin
1722
+ require 'nkeys'
1723
+ require 'base64'
1724
+ rescue LoadError
1725
+ raise(Error, "nkeys is not installed")
1726
+ end
1375
1727
 
1376
- if do_cbs
1377
- @disconnect_cb.call(@last_err) if @disconnect_cb
1378
- @close_cb.call if @close_cb
1379
- end
1728
+ case
1729
+ when @nkeys_seed
1730
+ @user_nkey_cb = nkey_cb_for_nkey_file(@nkeys_seed)
1731
+ @signature_cb = signature_cb_for_nkey_file(@nkeys_seed)
1732
+ when @user_credentials
1733
+ # When the credentials are within a single decorated file.
1734
+ @user_jwt_cb = jwt_cb_for_creds_file(@user_credentials)
1735
+ @signature_cb = signature_cb_for_creds_file(@user_credentials)
1736
+ end
1737
+ end
1380
1738
 
1381
- @status = conn_status
1739
+ def signature_cb_for_nkey_file(nkey)
1740
+ proc { |nonce|
1741
+ seed = File.read(nkey).chomp
1742
+ kp = NKEYS::from_seed(seed)
1743
+ raw_signed = kp.sign(nonce)
1744
+ kp.wipe!
1745
+ encoded = Base64.urlsafe_encode64(raw_signed)
1746
+ encoded.gsub('=', '')
1747
+ }
1748
+ end
1749
+
1750
+ def nkey_cb_for_nkey_file(nkey)
1751
+ proc {
1752
+ seed = File.read(nkey).chomp
1753
+ kp = NKEYS::from_seed(seed)
1754
+
1755
+ # Take a copy since original will be gone with the wipe.
1756
+ pub_key = kp.public_key.dup
1757
+ kp.wipe!
1758
+
1759
+ pub_key
1760
+ }
1761
+ end
1762
+
1763
+ def jwt_cb_for_creds_file(creds)
1764
+ proc {
1765
+ jwt_start = "BEGIN NATS USER JWT".freeze
1766
+ found = false
1767
+ jwt = nil
1382
1768
 
1383
- # Close the established connection in case
1384
- # we still have it.
1385
- if @io
1386
- @io.close if @io.socket
1387
- @io = nil
1769
+ File.readlines(creds).each do |line|
1770
+ case
1771
+ when found
1772
+ jwt = line.chomp
1773
+ break
1774
+ when line.include?(jwt_start)
1775
+ found = true
1388
1776
  end
1389
1777
  end
1390
- end
1391
1778
 
1392
- def start_threads!
1393
- # Reading loop for gathering data
1394
- @read_loop_thread = Thread.new { read_loop }
1395
- @read_loop_thread.abort_on_exception = true
1396
-
1397
- # Flusher loop for sending commands
1398
- @flusher_thread = Thread.new { flusher_loop }
1399
- @flusher_thread.abort_on_exception = true
1400
-
1401
- # Ping interval handling for keeping alive the connection
1402
- @ping_interval_thread = Thread.new { ping_interval_loop }
1403
- @ping_interval_thread.abort_on_exception = true
1404
- end
1405
-
1406
- # Prepares requests subscription that handles the responses
1407
- # for the new style request response.
1408
- def start_resp_mux_sub!
1409
- @resp_sub_prefix = "_INBOX.#{@nuid.next}"
1410
- @resp_map = Hash.new { |h,k| h[k] = { }}
1411
-
1412
- @resp_sub = Subscription.new
1413
- @resp_sub.subject = "#{@resp_sub_prefix}.*"
1414
- @resp_sub.received = 0
1415
-
1416
- # FIXME: Allow setting pending limits for responses mux subscription.
1417
- @resp_sub.pending_msgs_limit = DEFAULT_SUB_PENDING_MSGS_LIMIT
1418
- @resp_sub.pending_bytes_limit = DEFAULT_SUB_PENDING_BYTES_LIMIT
1419
- @resp_sub.pending_queue = SizedQueue.new(@resp_sub.pending_msgs_limit)
1420
- @resp_sub.wait_for_msgs_t = Thread.new do
1421
- loop do
1422
- msg = @resp_sub.pending_queue.pop
1423
- @resp_sub.pending_size -= msg.data.size
1424
-
1425
- # Pick the token and signal the request under the mutex
1426
- # from the subscription itself.
1427
- token = msg.subject.split('.').last
1428
- future = nil
1429
- synchronize do
1430
- future = @resp_map[token][:future]
1431
- @resp_map[token][:response] = msg
1432
- end
1779
+ raise(Error, "No JWT found in #{creds}") if not found
1433
1780
 
1434
- # Signal back that the response has arrived.
1435
- @resp_sub.synchronize do
1436
- future.signal
1437
- end
1781
+ jwt
1782
+ }
1783
+ end
1784
+
1785
+ def signature_cb_for_creds_file(creds)
1786
+ proc { |nonce|
1787
+ seed_start = "BEGIN USER NKEY SEED".freeze
1788
+ found = false
1789
+ seed = nil
1790
+
1791
+ File.readlines(creds).each do |line|
1792
+ case
1793
+ when found
1794
+ seed = line.chomp
1795
+ break
1796
+ when line.include?(seed_start)
1797
+ found = true
1438
1798
  end
1439
1799
  end
1440
1800
 
1441
- sid = (@ssid += 1)
1442
- @subs[sid] = @resp_sub
1443
- send_command("SUB #{@resp_sub.subject} #{sid}#{CR_LF}")
1444
- @flush_queue << :sub
1445
- end
1801
+ raise(Error, "No nkey user seed found in #{creds}") if not found
1446
1802
 
1447
- def can_reuse_server?(server)
1448
- return false if server.nil?
1803
+ kp = NKEYS::from_seed(seed)
1804
+ raw_signed = kp.sign(nonce)
1449
1805
 
1450
- # We can always reuse servers with infinite reconnects settings
1451
- return true if @options[:max_reconnect_attempts] < 0
1806
+ # seed is a reference so also cleared when doing wipe,
1807
+ # which can be done since Ruby strings are mutable.
1808
+ kp.wipe
1809
+ encoded = Base64.urlsafe_encode64(raw_signed)
1452
1810
 
1453
- # In case of hard errors like authorization errors, drop the server
1454
- # already since won't be able to connect.
1455
- return false if server[:error_received]
1811
+ # Remove padding
1812
+ encoded.gsub('=', '')
1813
+ }
1814
+ end
1456
1815
 
1457
- # We will retry a number of times to reconnect to a server.
1458
- return server[:reconnect_attempts] <= @options[:max_reconnect_attempts]
1459
- end
1816
+ def process_uri(uris)
1817
+ uris.split(',').map do |uri|
1818
+ opts = {}
1460
1819
 
1461
- def should_delay_connect?(server)
1462
- server[:was_connected] && server[:reconnect_attempts] >= 0
1463
- end
1820
+ # Scheme
1821
+ uri = "nats://#{uri}" if !uri.include?("://")
1464
1822
 
1465
- def should_not_reconnect?
1466
- !@options[:reconnect]
1467
- end
1823
+ uri_object = URI(uri)
1468
1824
 
1469
- def should_reconnect?
1470
- @options[:reconnect]
1471
- end
1825
+ # Host and Port
1826
+ uri_object.hostname ||= "localhost"
1827
+ uri_object.port ||= DEFAULT_PORT.fetch(uri_object.scheme.to_sym, DEFAULT_PORT[:nats])
1472
1828
 
1473
- def create_socket
1474
- NATS::IO::Socket.new({
1475
- uri: @uri,
1476
- connect_timeout: DEFAULT_CONNECT_TIMEOUT
1477
- })
1829
+ uri_object
1478
1830
  end
1831
+ end
1832
+ end
1479
1833
 
1480
- def setup_nkeys_connect
1481
- begin
1482
- require 'nkeys'
1483
- require 'base64'
1484
- rescue LoadError
1485
- raise(Error, "nkeys is not installed")
1486
- end
1834
+ module IO
1835
+ include Status
1487
1836
 
1488
- case
1489
- when @nkeys_seed
1490
- @user_nkey_cb = proc {
1491
- seed = File.read(@nkeys_seed).chomp
1492
- kp = NKEYS::from_seed(seed)
1493
-
1494
- # Take a copy since original will be gone with the wipe.
1495
- pub_key = kp.public_key.dup
1496
- kp.wipe!
1497
-
1498
- pub_key
1499
- }
1500
-
1501
- @signature_cb = proc { |nonce|
1502
- seed = File.read(@nkeys_seed).chomp
1503
- kp = NKEYS::from_seed(seed)
1504
- raw_signed = kp.sign(nonce)
1505
- kp.wipe!
1506
- encoded = Base64.urlsafe_encode64(raw_signed)
1507
- encoded.gsub('=', '')
1508
- }
1509
- when @user_credentials
1510
- # When the credentials are within a single decorated file.
1511
- @user_jwt_cb = proc {
1512
- jwt_start = "BEGIN NATS USER JWT".freeze
1513
- found = false
1514
- jwt = nil
1515
- File.readlines(@user_credentials).each do |line|
1516
- case
1517
- when found
1518
- jwt = line.chomp
1519
- break
1520
- when line.include?(jwt_start)
1521
- found = true
1522
- end
1523
- end
1524
- raise(Error, "No JWT found in #{@user_credentials}") if not found
1525
-
1526
- jwt
1527
- }
1528
-
1529
- @signature_cb = proc { |nonce|
1530
- seed_start = "BEGIN USER NKEY SEED".freeze
1531
- found = false
1532
- seed = nil
1533
- File.readlines(@user_credentials).each do |line|
1534
- case
1535
- when found
1536
- seed = line.chomp
1537
- break
1538
- when line.include?(seed_start)
1539
- found = true
1540
- end
1541
- end
1542
- raise(Error, "No nkey user seed found in #{@user_credentials}") if not found
1837
+ # Client creates a connection to the NATS Server.
1838
+ Client = ::NATS::Client
1543
1839
 
1544
- kp = NKEYS::from_seed(seed)
1545
- raw_signed = kp.sign(nonce)
1840
+ MAX_RECONNECT_ATTEMPTS = 10
1841
+ RECONNECT_TIME_WAIT = 2
1546
1842
 
1547
- # seed is a reference so also cleared when doing wipe,
1548
- # which can be done since Ruby strings are mutable.
1549
- kp.wipe
1550
- encoded = Base64.urlsafe_encode64(raw_signed)
1843
+ # Maximum accumulated pending commands bytesize before forcing a flush.
1844
+ MAX_PENDING_SIZE = 32768
1551
1845
 
1552
- # Remove padding
1553
- encoded.gsub('=', '')
1554
- }
1555
- end
1556
- end
1846
+ # Maximum number of flush kicks that can be queued up before we block.
1847
+ MAX_FLUSH_KICK_SIZE = 1024
1557
1848
 
1558
- def process_uri(uris)
1559
- connect_uris = []
1560
- uris.split(',').each do |uri|
1561
- opts = {}
1849
+ # Maximum number of bytes which we will be gathering on a single read.
1850
+ # TODO: Make dynamic?
1851
+ MAX_SOCKET_READ_BYTES = 32768
1562
1852
 
1563
- # Scheme
1564
- if uri.include?("://")
1565
- scheme, uri = uri.split("://")
1566
- opts[:scheme] = scheme
1567
- else
1568
- opts[:scheme] = 'nats'
1569
- end
1853
+ # Ping intervals
1854
+ DEFAULT_PING_INTERVAL = 120
1855
+ DEFAULT_PING_MAX = 2
1570
1856
 
1571
- # UserInfo
1572
- if uri.include?("@")
1573
- userinfo, endpoint = uri.split("@")
1574
- host, port = endpoint.split(":")
1575
- opts[:userinfo] = userinfo
1576
- else
1577
- host, port = uri.split(":")
1578
- end
1857
+ # Default IO timeouts
1858
+ DEFAULT_CONNECT_TIMEOUT = 2
1859
+ DEFAULT_READ_WRITE_TIMEOUT = 2
1860
+ DEFAULT_DRAIN_TIMEOUT = 30
1579
1861
 
1580
- # Host and Port
1581
- opts[:host] = host || "localhost"
1582
- opts[:port] = port || DEFAULT_PORT
1862
+ # Default Pending Limits
1863
+ DEFAULT_SUB_PENDING_MSGS_LIMIT = 65536
1864
+ DEFAULT_SUB_PENDING_BYTES_LIMIT = 65536 * 1024
1583
1865
 
1584
- connect_uris << URI::Generic.build(opts)
1585
- end
1586
- connect_uris
1587
- end
1588
- end
1866
+ DEFAULT_TOTAL_SUB_CONCURRENCY = 24
1867
+ DEFAULT_SINGLE_SUB_CONCURRENCY = 1
1589
1868
 
1590
1869
  # Implementation adapted from https://github.com/redis/redis-rb
1591
1870
  class Socket
@@ -1597,10 +1876,11 @@ module NATS
1597
1876
  @write_timeout = options[:write_timeout]
1598
1877
  @read_timeout = options[:read_timeout]
1599
1878
  @socket = nil
1879
+ @tls = options[:tls]
1600
1880
  end
1601
1881
 
1602
1882
  def connect
1603
- addrinfo = ::Socket.getaddrinfo(@uri.host, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM)
1883
+ addrinfo = ::Socket.getaddrinfo(@uri.hostname, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM)
1604
1884
  addrinfo.each_with_index do |ai, i|
1605
1885
  begin
1606
1886
  @socket = connect_addrinfo(ai, @uri.port, @connect_timeout)
@@ -1615,10 +1895,26 @@ module NATS
1615
1895
  @socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
1616
1896
  end
1617
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
+
1618
1914
  def read_line(deadline=nil)
1619
1915
  # FIXME: Should accumulate and read in a non blocking way instead
1620
1916
  unless ::IO.select([@socket], nil, nil, deadline)
1621
- raise SocketTimeoutError
1917
+ raise NATS::IO::SocketTimeoutError
1622
1918
  end
1623
1919
  @socket.gets
1624
1920
  end
@@ -1631,13 +1927,13 @@ module NATS
1631
1927
  if ::IO.select([@socket], nil, nil, deadline)
1632
1928
  retry
1633
1929
  else
1634
- raise SocketTimeoutError
1930
+ raise NATS::IO::SocketTimeoutError
1635
1931
  end
1636
1932
  rescue ::IO::WaitWritable
1637
1933
  if ::IO.select(nil, [@socket], nil, deadline)
1638
1934
  retry
1639
1935
  else
1640
- raise SocketTimeoutError
1936
+ raise NATS::IO::SocketTimeoutError
1641
1937
  end
1642
1938
  end
1643
1939
  rescue EOFError => e
@@ -1665,13 +1961,13 @@ module NATS
1665
1961
  if ::IO.select(nil, [@socket], nil, deadline)
1666
1962
  retry
1667
1963
  else
1668
- raise SocketTimeoutError
1964
+ raise NATS::IO::SocketTimeoutError
1669
1965
  end
1670
1966
  rescue ::IO::WaitReadable
1671
1967
  if ::IO.select([@socket], nil, nil, deadline)
1672
1968
  retry
1673
1969
  else
1674
- raise SocketTimeoutError
1970
+ raise NATS::IO::SocketTimeoutError
1675
1971
  end
1676
1972
  end
1677
1973
  end
@@ -1698,7 +1994,7 @@ module NATS
1698
1994
  sock.connect_nonblock(sockaddr)
1699
1995
  rescue Errno::EINPROGRESS, Errno::EALREADY, ::IO::WaitWritable
1700
1996
  unless ::IO.select(nil, [sock], nil, @connect_timeout)
1701
- raise SocketTimeoutError
1997
+ raise NATS::IO::SocketTimeoutError
1702
1998
  end
1703
1999
 
1704
2000
  # Confirm that connection was established
@@ -1714,39 +2010,11 @@ module NATS
1714
2010
  end
1715
2011
  end
1716
2012
 
1717
- Msg = Struct.new(:subject, :reply, :data, :header, keyword_init: true)
1718
-
1719
- class Subscription
1720
- include MonitorMixin
1721
-
1722
- attr_accessor :subject, :queue, :future, :callback, :response, :received, :max, :pending
1723
- attr_accessor :pending_queue, :pending_size, :wait_for_msgs_t, :is_slow_consumer
1724
- attr_accessor :pending_msgs_limit, :pending_bytes_limit
1725
-
1726
- def initialize
1727
- super # required to initialize monitor
1728
- @subject = ''
1729
- @queue = nil
1730
- @future = nil
1731
- @callback = nil
1732
- @response = nil
1733
- @received = 0
1734
- @max = nil
1735
- @pending = nil
1736
-
1737
- # State from async subscriber messages delivery
1738
- @pending_queue = nil
1739
- @pending_size = 0
1740
- @pending_msgs_limit = nil
1741
- @pending_bytes_limit = nil
1742
- @wait_for_msgs_t = nil
1743
- @is_slow_consumer = false
1744
- end
1745
- end
2013
+ NANOSECONDS = 1_000_000_000
1746
2014
 
1747
- # Implementation of MonotonicTime adapted from
1748
- # https://github.com/ruby-concurrency/concurrent-ruby/
1749
2015
  class MonotonicTime
2016
+ # Implementation of MonotonicTime adapted from
2017
+ # https://github.com/ruby-concurrency/concurrent-ruby/
1750
2018
  class << self
1751
2019
  case
1752
2020
  when defined?(Process::CLOCK_MONOTONIC)
@@ -1763,6 +2031,20 @@ module NATS
1763
2031
  ::Time.now.to_f
1764
2032
  end
1765
2033
  end
2034
+
2035
+ def with_nats_timeout(timeout)
2036
+ start_time = now
2037
+ yield
2038
+ end_time = now
2039
+ duration = end_time - start_time
2040
+ if duration > timeout
2041
+ raise NATS::Timeout.new("nats: timeout")
2042
+ end
2043
+ end
2044
+
2045
+ def since(t0)
2046
+ now - t0
2047
+ end
1766
2048
  end
1767
2049
  end
1768
2050
  end