nats-pure 0.6.2 → 2.0.0.pre.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- # Copyright 2016-2018 The NATS Authors
1
+ # Copyright 2016-2021 The NATS Authors
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -12,8 +12,13 @@
12
12
  # limitations under the License.
13
13
  #
14
14
 
15
- require 'nats/io/parser'
16
- require 'nats/io/version'
15
+ require_relative 'parser'
16
+ require_relative 'version'
17
+ require_relative 'errors'
18
+ require_relative 'msg'
19
+ require_relative 'subscription'
20
+ require_relative 'js'
21
+
17
22
  require 'nats/nuid'
18
23
  require 'thread'
19
24
  require 'socket'
@@ -28,35 +33,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