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