georgi-nsq-ruby 2.1.0
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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +405 -0
- data/lib/nsq.rb +13 -0
- data/lib/nsq/client_base.rb +120 -0
- data/lib/nsq/connection.rb +461 -0
- data/lib/nsq/consumer.rb +98 -0
- data/lib/nsq/discovery.rb +98 -0
- data/lib/nsq/exceptions.rb +5 -0
- data/lib/nsq/frames/error.rb +6 -0
- data/lib/nsq/frames/frame.rb +16 -0
- data/lib/nsq/frames/message.rb +33 -0
- data/lib/nsq/frames/response.rb +6 -0
- data/lib/nsq/logger.rb +38 -0
- data/lib/nsq/producer.rb +94 -0
- data/lib/version.rb +9 -0
- metadata +116 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require_relative 'discovery'
|
|
2
|
+
require_relative 'connection'
|
|
3
|
+
require_relative 'logger'
|
|
4
|
+
|
|
5
|
+
module Nsq
|
|
6
|
+
class ClientBase
|
|
7
|
+
include Nsq::AttributeLogger
|
|
8
|
+
@@log_attributes = [:topic]
|
|
9
|
+
|
|
10
|
+
attr_reader :topic
|
|
11
|
+
attr_reader :connections
|
|
12
|
+
|
|
13
|
+
def connected?
|
|
14
|
+
@connections.values.any?(&:connected?)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def terminate
|
|
19
|
+
@discovery_thread.kill if @discovery_thread
|
|
20
|
+
drop_all_connections
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# discovers nsqds from an nsqlookupd repeatedly
|
|
27
|
+
#
|
|
28
|
+
# opts:
|
|
29
|
+
# nsqlookups: ['127.0.0.1:4161'],
|
|
30
|
+
# topic: 'topic-to-find-nsqds-for',
|
|
31
|
+
# interval: 60
|
|
32
|
+
#
|
|
33
|
+
def discover_repeatedly(opts = {})
|
|
34
|
+
@discovery_thread = Thread.new do
|
|
35
|
+
|
|
36
|
+
@discovery = Discovery.new(opts[:nsqlookupds])
|
|
37
|
+
|
|
38
|
+
loop do
|
|
39
|
+
begin
|
|
40
|
+
nsqds = nsqds_from_lookupd(opts[:topic])
|
|
41
|
+
drop_and_add_connections(nsqds)
|
|
42
|
+
rescue DiscoveryException
|
|
43
|
+
# We can't connect to any nsqlookupds. That's okay, we'll just
|
|
44
|
+
# leave our current nsqd connections alone and try again later.
|
|
45
|
+
warn 'Could not connect to any nsqlookupd instances in discovery loop'
|
|
46
|
+
end
|
|
47
|
+
sleep opts[:interval]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@discovery_thread.abort_on_exception = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def nsqds_from_lookupd(topic = nil)
|
|
57
|
+
if topic
|
|
58
|
+
@discovery.nsqds_for_topic(topic)
|
|
59
|
+
else
|
|
60
|
+
@discovery.nsqds
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def drop_and_add_connections(nsqds)
|
|
66
|
+
# drop nsqd connections that are no longer in lookupd
|
|
67
|
+
missing_nsqds = @connections.keys - nsqds
|
|
68
|
+
missing_nsqds.each do |nsqd|
|
|
69
|
+
drop_connection(nsqd)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# add new ones
|
|
73
|
+
new_nsqds = nsqds - @connections.keys
|
|
74
|
+
new_nsqds.each do |nsqd|
|
|
75
|
+
begin
|
|
76
|
+
add_connection(nsqd)
|
|
77
|
+
rescue Exception => ex
|
|
78
|
+
error "Failed to connect to nsqd @ #{nsqd}: #{ex}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# balance RDY state amongst the connections
|
|
83
|
+
connections_changed
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def add_connection(nsqd, options = {})
|
|
88
|
+
info "+ Adding connection #{nsqd}"
|
|
89
|
+
host, port = nsqd.split(':')
|
|
90
|
+
connection = Connection.new({
|
|
91
|
+
host: host,
|
|
92
|
+
port: port,
|
|
93
|
+
ssl_context: @ssl_context,
|
|
94
|
+
tls_options: @tls_options,
|
|
95
|
+
tls_v1: @tls_v1
|
|
96
|
+
}.merge(options))
|
|
97
|
+
@connections[nsqd] = connection
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def drop_connection(nsqd)
|
|
102
|
+
info "- Dropping connection #{nsqd}"
|
|
103
|
+
connection = @connections.delete(nsqd)
|
|
104
|
+
connection.close if connection
|
|
105
|
+
connections_changed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def drop_all_connections
|
|
110
|
+
@connections.keys.each do |nsqd|
|
|
111
|
+
drop_connection(nsqd)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# optional subclass hook
|
|
117
|
+
def connections_changed
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
require_relative 'frames/error'
|
|
7
|
+
require_relative 'frames/message'
|
|
8
|
+
require_relative 'frames/response'
|
|
9
|
+
require_relative 'logger'
|
|
10
|
+
|
|
11
|
+
module Nsq
|
|
12
|
+
class Connection
|
|
13
|
+
include Nsq::AttributeLogger
|
|
14
|
+
@@log_attributes = [:host, :port]
|
|
15
|
+
|
|
16
|
+
attr_reader :host
|
|
17
|
+
attr_reader :port
|
|
18
|
+
attr_accessor :max_in_flight
|
|
19
|
+
attr_reader :presumed_in_flight
|
|
20
|
+
|
|
21
|
+
USER_AGENT = "nsq-ruby/#{Nsq::Version::STRING}"
|
|
22
|
+
RESPONSE_HEARTBEAT = '_heartbeat_'
|
|
23
|
+
RESPONSE_OK = 'OK'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def initialize(opts = {})
|
|
27
|
+
@host = opts[:host] || (raise ArgumentError, 'host is required')
|
|
28
|
+
@port = opts[:port] || (raise ArgumentError, 'port is required')
|
|
29
|
+
@queue = opts[:queue]
|
|
30
|
+
@topic = opts[:topic]
|
|
31
|
+
@channel = opts[:channel]
|
|
32
|
+
@msg_timeout = opts[:msg_timeout] || 60_000 # 60s
|
|
33
|
+
@max_in_flight = opts[:max_in_flight] || 1
|
|
34
|
+
@tls_options = opts[:tls_options]
|
|
35
|
+
if opts[:ssl_context]
|
|
36
|
+
if @tls_options
|
|
37
|
+
warn 'ssl_context and tls_options both set. Using tls_options. Ignoring ssl_context.'
|
|
38
|
+
else
|
|
39
|
+
@tls_options = opts[:ssl_context]
|
|
40
|
+
warn 'ssl_context will be deprecated nsq-ruby version 3. Please use tls_options instead.'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
@tls_v1 = !!opts[:tls_v1]
|
|
44
|
+
|
|
45
|
+
if @tls_options
|
|
46
|
+
if @tls_v1
|
|
47
|
+
validate_tls_options!
|
|
48
|
+
else
|
|
49
|
+
warn 'tls_options was provided, but tls_v1 is false. Skipping validation of tls_options.'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if @msg_timeout < 1000
|
|
54
|
+
raise ArgumentError, 'msg_timeout cannot be less than 1000. it\'s in milliseconds.'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# for outgoing communication
|
|
58
|
+
@write_queue = Queue.new
|
|
59
|
+
|
|
60
|
+
# For indicating that the connection has died.
|
|
61
|
+
# We use a Queue so we don't have to poll. Used to communicate across
|
|
62
|
+
# threads (from write_loop and read_loop to connect_and_monitor).
|
|
63
|
+
@death_queue = Queue.new
|
|
64
|
+
|
|
65
|
+
@connected = false
|
|
66
|
+
@presumed_in_flight = 0
|
|
67
|
+
|
|
68
|
+
open_connection
|
|
69
|
+
start_monitoring_connection
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def connected?
|
|
74
|
+
@connected
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# close the connection and don't try to re-open it
|
|
79
|
+
def close
|
|
80
|
+
stop_monitoring_connection
|
|
81
|
+
close_connection
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def sub(topic, channel)
|
|
86
|
+
write "SUB #{topic} #{channel}\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def rdy(count)
|
|
91
|
+
write "RDY #{count}\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def fin(message_id)
|
|
96
|
+
write "FIN #{message_id}\n"
|
|
97
|
+
decrement_in_flight
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def req(message_id, timeout)
|
|
102
|
+
write "REQ #{message_id} #{timeout}\n"
|
|
103
|
+
decrement_in_flight
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def touch(message_id)
|
|
108
|
+
write "TOUCH #{message_id}\n"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def pub(topic, message)
|
|
113
|
+
write ["PUB #{topic}\n", message.bytesize, message].pack('a*l>a*')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def dpub(topic, delay_in_ms, message)
|
|
117
|
+
write ["DPUB #{topic} #{delay_in_ms}\n", message.bytesize, message].pack('a*l>a*')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def mpub(topic, messages)
|
|
121
|
+
body = messages.map do |message|
|
|
122
|
+
[message.bytesize, message].pack('l>a*')
|
|
123
|
+
end.join
|
|
124
|
+
|
|
125
|
+
write ["MPUB #{topic}\n", body.bytesize, messages.size, body].pack('a*l>l>a*')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Tell the server we are ready for more messages!
|
|
130
|
+
def re_up_ready
|
|
131
|
+
rdy(@max_in_flight)
|
|
132
|
+
# assume these messages are coming our way. yes, this might not be the
|
|
133
|
+
# case, but it's much easier to manage our RDY state with the server if
|
|
134
|
+
# we treat things this way.
|
|
135
|
+
@presumed_in_flight = @max_in_flight
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def cls
|
|
142
|
+
write "CLS\n"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def nop
|
|
147
|
+
write "NOP\n"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def write(raw)
|
|
152
|
+
@write_queue.push(raw)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def write_to_socket(raw)
|
|
157
|
+
debug ">>> #{raw.inspect}"
|
|
158
|
+
@socket.write(raw)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def identify
|
|
163
|
+
hostname = Socket.gethostname
|
|
164
|
+
metadata = {
|
|
165
|
+
client_id: hostname,
|
|
166
|
+
hostname: hostname,
|
|
167
|
+
feature_negotiation: true,
|
|
168
|
+
heartbeat_interval: 30_000, # 30 seconds
|
|
169
|
+
output_buffer: 16_000, # 16kb
|
|
170
|
+
output_buffer_timeout: 250, # 250ms
|
|
171
|
+
tls_v1: @tls_v1,
|
|
172
|
+
snappy: false,
|
|
173
|
+
deflate: false,
|
|
174
|
+
sample_rate: 0, # disable sampling
|
|
175
|
+
user_agent: USER_AGENT,
|
|
176
|
+
msg_timeout: @msg_timeout
|
|
177
|
+
}.to_json
|
|
178
|
+
write_to_socket ["IDENTIFY\n", metadata.length, metadata].pack('a*l>a*')
|
|
179
|
+
|
|
180
|
+
# Now wait for the response!
|
|
181
|
+
frame = receive_frame
|
|
182
|
+
server = JSON.parse(frame.data)
|
|
183
|
+
|
|
184
|
+
if @max_in_flight > server['max_rdy_count']
|
|
185
|
+
raise "max_in_flight is set to #{@max_in_flight}, server only supports #{server['max_rdy_count']}"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
@server_version = server['version']
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def handle_response(frame)
|
|
193
|
+
if frame.data == RESPONSE_HEARTBEAT
|
|
194
|
+
debug 'Received heartbeat'
|
|
195
|
+
nop
|
|
196
|
+
elsif frame.data == RESPONSE_OK
|
|
197
|
+
debug 'Received OK'
|
|
198
|
+
else
|
|
199
|
+
die "Received response we don't know how to handle: #{frame.data}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def receive_frame
|
|
205
|
+
if buffer = @socket.read(8)
|
|
206
|
+
size, type = buffer.unpack('l>l>')
|
|
207
|
+
size -= 4 # we want the size of the data part and type already took up 4 bytes
|
|
208
|
+
data = @socket.read(size)
|
|
209
|
+
frame_class = frame_class_for_type(type)
|
|
210
|
+
return frame_class.new(data, self)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
FRAME_CLASSES = [Response, Error, Message]
|
|
216
|
+
def frame_class_for_type(type)
|
|
217
|
+
raise "Bad frame type specified: #{type}" if type > FRAME_CLASSES.length - 1
|
|
218
|
+
[Response, Error, Message][type]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def decrement_in_flight
|
|
223
|
+
@presumed_in_flight -= 1
|
|
224
|
+
|
|
225
|
+
if server_needs_rdy_re_ups?
|
|
226
|
+
# now that we're less than @max_in_flight we might need to re-up our RDY state
|
|
227
|
+
threshold = (@max_in_flight * 0.2).ceil
|
|
228
|
+
re_up_ready if @presumed_in_flight <= threshold
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def start_read_loop
|
|
234
|
+
@read_loop_thread ||= Thread.new{read_loop}
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def stop_read_loop
|
|
239
|
+
@read_loop_thread.kill if @read_loop_thread
|
|
240
|
+
@read_loop_thread = nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def read_loop
|
|
245
|
+
loop do
|
|
246
|
+
frame = receive_frame
|
|
247
|
+
if frame.is_a?(Response)
|
|
248
|
+
handle_response(frame)
|
|
249
|
+
elsif frame.is_a?(Error)
|
|
250
|
+
error "Error received: #{frame.data}"
|
|
251
|
+
elsif frame.is_a?(Message)
|
|
252
|
+
debug "<<< #{frame.body}"
|
|
253
|
+
@queue.push(frame) if @queue
|
|
254
|
+
else
|
|
255
|
+
raise 'No data from socket'
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
rescue Exception => ex
|
|
259
|
+
die(ex)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def start_write_loop
|
|
264
|
+
@write_loop_thread ||= Thread.new{write_loop}
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def stop_write_loop
|
|
269
|
+
if @write_loop_thread
|
|
270
|
+
@write_queue.push(:stop_write_loop)
|
|
271
|
+
@write_loop_thread.join
|
|
272
|
+
end
|
|
273
|
+
@write_loop_thread = nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def write_loop
|
|
278
|
+
data = nil
|
|
279
|
+
loop do
|
|
280
|
+
data = @write_queue.pop
|
|
281
|
+
break if data == :stop_write_loop
|
|
282
|
+
write_to_socket(data)
|
|
283
|
+
end
|
|
284
|
+
rescue Exception => ex
|
|
285
|
+
# requeue PUB and MPUB commands
|
|
286
|
+
if data =~ /^M?PUB/
|
|
287
|
+
debug "Requeueing to write_queue: #{data.inspect}"
|
|
288
|
+
@write_queue.push(data)
|
|
289
|
+
end
|
|
290
|
+
die(ex)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Waits for death of connection
|
|
295
|
+
def start_monitoring_connection
|
|
296
|
+
@connection_monitor_thread ||= Thread.new{monitor_connection}
|
|
297
|
+
@connection_monitor_thread.abort_on_exception = true
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def stop_monitoring_connection
|
|
302
|
+
@connection_monitor_thread.kill if @connection_monitor_thread
|
|
303
|
+
@connection_monitor = nil
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def monitor_connection
|
|
308
|
+
loop do
|
|
309
|
+
# wait for death, hopefully it never comes
|
|
310
|
+
cause_of_death = @death_queue.pop
|
|
311
|
+
warn "Died from: #{cause_of_death}"
|
|
312
|
+
|
|
313
|
+
debug 'Reconnecting...'
|
|
314
|
+
reconnect
|
|
315
|
+
debug 'Reconnected!'
|
|
316
|
+
|
|
317
|
+
# clear all death messages, since we're now reconnected.
|
|
318
|
+
# we don't want to complete this loop and immediately reconnect again.
|
|
319
|
+
@death_queue.clear
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# close the connection if it's not already closed and try to reconnect
|
|
325
|
+
# over and over until we succeed!
|
|
326
|
+
def reconnect
|
|
327
|
+
close_connection
|
|
328
|
+
with_retries do
|
|
329
|
+
open_connection
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def open_connection
|
|
335
|
+
@socket = TCPSocket.new(@host, @port)
|
|
336
|
+
# write the version and IDENTIFY directly to the socket to make sure
|
|
337
|
+
# it gets to nsqd ahead of anything in the `@write_queue`
|
|
338
|
+
write_to_socket ' V2'
|
|
339
|
+
identify
|
|
340
|
+
upgrade_to_ssl_socket if @tls_v1
|
|
341
|
+
|
|
342
|
+
start_read_loop
|
|
343
|
+
start_write_loop
|
|
344
|
+
@connected = true
|
|
345
|
+
|
|
346
|
+
# we need to re-subscribe if there's a topic specified
|
|
347
|
+
if @topic
|
|
348
|
+
debug "Subscribing to #{@topic}"
|
|
349
|
+
sub(@topic, @channel)
|
|
350
|
+
re_up_ready
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# closes the connection and stops listening for messages
|
|
356
|
+
def close_connection
|
|
357
|
+
cls if connected?
|
|
358
|
+
stop_read_loop
|
|
359
|
+
stop_write_loop
|
|
360
|
+
@socket.close if @socket
|
|
361
|
+
@socket = nil
|
|
362
|
+
@connected = false
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# this is called when there's a connection error in the read or write loop
|
|
367
|
+
# it triggers `connect_and_monitor` to try to reconnect
|
|
368
|
+
def die(reason)
|
|
369
|
+
@connected = false
|
|
370
|
+
@death_queue.push(reason)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def upgrade_to_ssl_socket
|
|
375
|
+
ssl_opts = [@socket, openssl_context].compact
|
|
376
|
+
@socket = OpenSSL::SSL::SSLSocket.new(*ssl_opts)
|
|
377
|
+
@socket.sync_close = true
|
|
378
|
+
@socket.connect
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def openssl_context
|
|
383
|
+
return unless @tls_options
|
|
384
|
+
|
|
385
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
386
|
+
context.cert = OpenSSL::X509::Certificate.new(File.read(@tls_options[:certificate]))
|
|
387
|
+
context.key = OpenSSL::PKey::RSA.new(File.read(@tls_options[:key]))
|
|
388
|
+
if @tls_options[:ca_certificate]
|
|
389
|
+
context.ca_file = @tls_options[:ca_certificate]
|
|
390
|
+
end
|
|
391
|
+
context.verify_mode = @tls_options[:verify_mode] || OpenSSL::SSL::VERIFY_NONE
|
|
392
|
+
context
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Retry the supplied block with exponential backoff.
|
|
397
|
+
#
|
|
398
|
+
# Borrowed liberally from:
|
|
399
|
+
# https://github.com/ooyala/retries/blob/master/lib/retries.rb
|
|
400
|
+
def with_retries(&block)
|
|
401
|
+
base_sleep_seconds = 0.5
|
|
402
|
+
max_sleep_seconds = 300 # 5 minutes
|
|
403
|
+
|
|
404
|
+
# Let's do this thing
|
|
405
|
+
attempts = 0
|
|
406
|
+
|
|
407
|
+
begin
|
|
408
|
+
attempts += 1
|
|
409
|
+
return block.call(attempts)
|
|
410
|
+
|
|
411
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
|
|
412
|
+
Errno::ENETDOWN, Errno::ENETUNREACH, Errno::ETIMEDOUT, Timeout::Error => ex
|
|
413
|
+
|
|
414
|
+
raise ex if attempts >= 100
|
|
415
|
+
|
|
416
|
+
# The sleep time is an exponentially-increasing function of base_sleep_seconds.
|
|
417
|
+
# But, it never exceeds max_sleep_seconds.
|
|
418
|
+
sleep_seconds = [base_sleep_seconds * (2 ** (attempts - 1)), max_sleep_seconds].min
|
|
419
|
+
# Randomize to a random value in the range sleep_seconds/2 .. sleep_seconds
|
|
420
|
+
sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
|
|
421
|
+
# But never sleep less than base_sleep_seconds
|
|
422
|
+
sleep_seconds = [base_sleep_seconds, sleep_seconds].max
|
|
423
|
+
|
|
424
|
+
warn "Failed to connect: #{ex}. Retrying in #{sleep_seconds.round(1)} seconds."
|
|
425
|
+
|
|
426
|
+
snooze(sleep_seconds)
|
|
427
|
+
|
|
428
|
+
retry
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# Se we can stub for testing and reconnect in a tight loop
|
|
434
|
+
def snooze(t)
|
|
435
|
+
sleep(t)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def server_needs_rdy_re_ups?
|
|
440
|
+
# versions less than 0.3.0 need RDY re-ups
|
|
441
|
+
# see: https://github.com/bitly/nsq/blob/master/ChangeLog.md#030---2014-11-18
|
|
442
|
+
major, minor = @server_version.split('.').map(&:to_i)
|
|
443
|
+
major == 0 && minor <= 2
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def validate_tls_options!
|
|
448
|
+
[:key, :certificate].each do |key|
|
|
449
|
+
unless @tls_options.has_key?(key)
|
|
450
|
+
raise ArgumentError.new "@tls_options requires a :#{key}"
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
[:key, :certificate, :ca_certificate].each do |key|
|
|
455
|
+
if @tls_options[key] && !File.readable?(@tls_options[key])
|
|
456
|
+
raise LoadError.new "@tls_options :#{key} is unreadable"
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|