nats-pure 0.6.0 → 2.0.0.pre.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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