nats-pure 0.6.0 → 2.0.0.pre.alpha

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