nats-pure 0.7.2 → 2.4.0

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