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