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