nats-pure 0.6.2 → 2.0.0.pre.rc1

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