consul-templaterb 1.0.3
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/.gitignore +25 -0
- data/.gitreview +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +43 -0
- data/.ruby_app +0 -0
- data/.travis.yml +13 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +201 -0
- data/README.md +270 -0
- data/Rakefile +6 -0
- data/bin/consul-templaterb +246 -0
- data/consul-templaterb.gemspec +37 -0
- data/lib/consul/async/consul_endpoint.rb +279 -0
- data/lib/consul/async/consul_template.rb +323 -0
- data/lib/consul/async/consul_template_engine.rb +57 -0
- data/lib/consul/async/consul_template_render.rb +80 -0
- data/lib/consul/async/process_handler.rb +64 -0
- data/lib/consul/async/utilities.rb +17 -0
- data/lib/consul/async/version.rb +5 -0
- data/samples/checks.html.erb +96 -0
- data/samples/common/footer.html.erb +10 -0
- data/samples/common/header.html.erb +51 -0
- data/samples/consul_template.html.erb +94 -0
- data/samples/consul_template.json.erb +77 -0
- data/samples/consul_template.txt.erb +45 -0
- data/samples/consul_template.xml.erb +70 -0
- data/samples/criteo/haproxy.cfg.erb +163 -0
- data/samples/criteo_choregraphies.html.erb +91 -0
- data/samples/ha_proxy.cfg.erb +127 -0
- data/samples/keys.html.erb +38 -0
- data/samples/nodes.html.erb +17 -0
- data/samples/services.html.erb +89 -0
- metadata +189 -0
data/Rakefile
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This script can be launched to get a uniq id for this instance
|
3
|
+
require 'consul/async/consul_template_engine'
|
4
|
+
require 'consul/async/process_handler'
|
5
|
+
require 'consul/async/version'
|
6
|
+
require 'optparse'
|
7
|
+
require 'optparse/uri'
|
8
|
+
|
9
|
+
def usage_text
|
10
|
+
"USAGE: #{__FILE__} [[options]]"
|
11
|
+
end
|
12
|
+
|
13
|
+
def compute_default_output(source)
|
14
|
+
dest = source.gsub(/\.erb$/, '')
|
15
|
+
raise "Source and destination cannot be the same in #{source}" if source == dest || dest.empty?
|
16
|
+
dest
|
17
|
+
end
|
18
|
+
|
19
|
+
options = {
|
20
|
+
consul: {
|
21
|
+
debug: {
|
22
|
+
network: false
|
23
|
+
},
|
24
|
+
base_url: ENV['CONSUL_HTTP_ADDR'] || 'http://locahost:8500',
|
25
|
+
token: nil,
|
26
|
+
retry_duration: 10, # On error, retry after n seconds
|
27
|
+
min_duration: 5, # On sucess and when differences are found
|
28
|
+
retry_on_non_diff: 3, # On success but when there are not differences
|
29
|
+
wait_duration: 600, # Delay to block in Consul
|
30
|
+
max_retry_duration: 600, # On consecutive errors, delay will increase, max value
|
31
|
+
missing_index_retry_time_on_diff: 15, # On endpoints without X-Consul-Index => next request
|
32
|
+
missing_index_retry_time_on_unchanged: 60, # Endpoints with X-Consul index and no diff
|
33
|
+
paths: {
|
34
|
+
'/v1/catalog/services': {
|
35
|
+
min_duration: 30, # Since services change a lot, refresh services every 30 seconds
|
36
|
+
},
|
37
|
+
'/v1/catalog/nodes': {
|
38
|
+
min_duration: 30, # Do not wake up before 30 seconds when node appear/disappear
|
39
|
+
},
|
40
|
+
'/v1/catalog/datacenters': {
|
41
|
+
min_duration: 60, # Datacenters are not added every minute, right?
|
42
|
+
},
|
43
|
+
'/v1/agent/metrics': {
|
44
|
+
min_duration: 60, # Refresh metrics only minute max
|
45
|
+
},
|
46
|
+
'/v1/agent/self': {
|
47
|
+
min_duration: 60, # Refresh self info every minute max
|
48
|
+
}
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
consul_engine = Consul::Async::ConsulTemplateEngine.new
|
53
|
+
@programs = {}
|
54
|
+
cur_sig_reload = 'HUP'.freeze
|
55
|
+
cur_sig_term = 'TERM'.freeze
|
56
|
+
|
57
|
+
optparse = OptionParser.new do |opts|
|
58
|
+
opts.banner = usage_text
|
59
|
+
|
60
|
+
opts.on('-h', '--help', 'Show help') do
|
61
|
+
STDERR.puts opts
|
62
|
+
exit 0
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on('-v', '--version', 'Show Version') do
|
66
|
+
STDERR.puts Consul::Async::VERSION
|
67
|
+
exit 0
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('-c', '--consul-addr=<address>', String, 'Address of Consul, eg: http://locahost:8500') do |consul_url|
|
71
|
+
options[:consul][:base_url] = consul_url
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on('-t', '--consul-token=<token>', String, 'Use a token to connect to Consul') do |consul_token|
|
75
|
+
options[:consul][:token] = consul_token
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on('-w', '--wait=<min_duration>', Float, 'Wait at least n seconds before each template generation') do |min_duration|
|
79
|
+
options[:consul][:min_duration] = min_duration
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on('-r', '--retry-delay=<min_duration>', Float, 'Min Retry delay on Error/Missing Consul Index') do |min_duration|
|
83
|
+
options[:consul][:min_duration] = min_duration
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on('-k', '--hot-reload=<behavior>', String, 'Control hot reload behaviour, one of :'\
|
87
|
+
'[die (kill daemon on hot reload failure), '\
|
88
|
+
'keep (on error, keep running), '\
|
89
|
+
'disable (hot reload disabled)] ') do |hot_reload_behaviour|
|
90
|
+
consul_engine.hot_reload_failure = hot_reload_behaviour == 'die' ? nil : hot_reload_behaviour
|
91
|
+
end
|
92
|
+
|
93
|
+
def compute_signal(val, none_value)
|
94
|
+
valid_signals = Signal.list.keys
|
95
|
+
raise "Please #{val} specifiy a signal: #{valid_signals.inspect}" unless val
|
96
|
+
return nil if val == none_value
|
97
|
+
raise "Invalid signal, valid signals: #{valid_signals.inspect}" unless valid_signals.include? val
|
98
|
+
val
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.on('-K', '--sig-term=kill_signal', String,
|
102
|
+
"Signal to sent to next --exec command on kill, default=#{cur_sig_term}") do |sig|
|
103
|
+
cur_sig_term = compute_signal(sig, nil)
|
104
|
+
end
|
105
|
+
|
106
|
+
opts.on('-R', '--sig-reload=reload_signal', String,
|
107
|
+
"Signal to sent to next --exec command on reload (NONE supported), default=#{cur_sig_reload}") do |sig|
|
108
|
+
cur_sig_reload = compute_signal(sig, 'NONE')
|
109
|
+
end
|
110
|
+
|
111
|
+
opts.on('-e', '--exec=<command>', String, 'Execute the following command') do |cmd|
|
112
|
+
sig_reload = cur_sig_reload
|
113
|
+
sig_term = cur_sig_term
|
114
|
+
consul_engine.add_template_callback do |all_ready, template_manager, results|
|
115
|
+
if all_ready
|
116
|
+
modified = results.reduce(false) { |a, e| a || (e.ready? && e.modified) }
|
117
|
+
if modified
|
118
|
+
if @programs[cmd].nil?
|
119
|
+
STDERR.puts "[EXEC] Starting process: #{cmd}... on_reload=#{sig_reload ? sig_reload : 'NONE'} on_term=#{sig_term}"
|
120
|
+
@programs[cmd] = Consul::Async::ProcessHandler.new(cmd, sig_reload: sig_reload, sig_term: sig_term)
|
121
|
+
@programs[cmd].start
|
122
|
+
else
|
123
|
+
@programs[cmd].reload
|
124
|
+
end
|
125
|
+
elsif !@programs[cmd].nil?
|
126
|
+
begin
|
127
|
+
@programs[cmd].process_status
|
128
|
+
rescue Consul::Async::ProcessDoesNotExist => e
|
129
|
+
STDERR.puts "[ERROR] The process is dead, aborting run: #{e.inspect}"
|
130
|
+
template_manager.terminate
|
131
|
+
EventMachine.stop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
opts.on('-d', '--debug-network-usage', 'Debug the network usage') do
|
139
|
+
options[:consul][:debug][:network] = true
|
140
|
+
consul_engine.add_template_callback do |all_ready, template_manager, results|
|
141
|
+
if all_ready
|
142
|
+
mod = false
|
143
|
+
results = results.map do |res|
|
144
|
+
mod ||= res.modified
|
145
|
+
STDERR.puts "[INFO] Hot reload of template #{res.template_file} with success" if res.hot_reloaded
|
146
|
+
"#{res.modified ? 'WRITTEN' : 'UNCHANGED'}[#{res.output_file}]"
|
147
|
+
end.join(' ')
|
148
|
+
if mod
|
149
|
+
STDERR.puts("[INFO] File written: #{results} #{template_manager.net_info.inspect}")
|
150
|
+
else
|
151
|
+
STDERR.print "[DBUG] Files not changed #{results} #{template_manager.net_info.inspect}\r"
|
152
|
+
end
|
153
|
+
else
|
154
|
+
STDERR.print "[DBUG] Still waiting for data #{template_manager.net_info.inspect}...\r"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
opts.on('-t', '--template erb_file:[output]:[command]', String, 'Add a erb template, its output and optional reload command') do |tpl|
|
160
|
+
splitted = tpl.split(':')
|
161
|
+
source = splitted[0]
|
162
|
+
dest = splitted[1]
|
163
|
+
unless dest
|
164
|
+
dest = compute_default_output(source)
|
165
|
+
STDERR.puts "-t --template #{tpl} : Since output has not been set, using #{dest}"
|
166
|
+
end
|
167
|
+
raise "Source and destination cannot be the same in #{tpl}" if source == dest || dest.empty?
|
168
|
+
command = splitted[2]
|
169
|
+
consul_engine.add_template(source, dest)
|
170
|
+
|
171
|
+
if command
|
172
|
+
consul_engine.add_template_callback do |_all_ready, _template_manager, results|
|
173
|
+
results.each do |res|
|
174
|
+
next unless res.ready? && res.modified && res.output_file == dest && res.template_file == source
|
175
|
+
# Our template has been fully rendered
|
176
|
+
system(command)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
opts.on(nil, '--once', 'Do not run the process as a daemon') do
|
183
|
+
consul_engine.add_template_callback do |all_ready, template_manager, _|
|
184
|
+
if all_ready
|
185
|
+
STDERR.puts '[INFO] Program ends since daemon mode has been disabled, file(s) has been written'
|
186
|
+
template_manager.terminate
|
187
|
+
EventMachine.stop
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def kill_program
|
194
|
+
@programs.each do |k, v|
|
195
|
+
STDERR.puts "Killing process #{k}..."
|
196
|
+
v.kill
|
197
|
+
end
|
198
|
+
@programs = {}
|
199
|
+
exit 0
|
200
|
+
end
|
201
|
+
|
202
|
+
optparse.parse!
|
203
|
+
|
204
|
+
# Find the max descriptors for our system
|
205
|
+
def find_max_descriptors(max_descripors)
|
206
|
+
i = max_descripors
|
207
|
+
max_size = 1024
|
208
|
+
while i != max_size && i > 1024
|
209
|
+
max_size = EM.set_descriptor_table_size i
|
210
|
+
i /= 2 if max_size < i
|
211
|
+
end
|
212
|
+
max_size
|
213
|
+
end
|
214
|
+
|
215
|
+
# Since we might be using a lots of descriptors, document this
|
216
|
+
new_size = find_max_descriptors(65_536)
|
217
|
+
STDERR.puts "Max number of descriptors set to #{new_size}" if options[:consul][:debug][:network]
|
218
|
+
|
219
|
+
# This is needed to avoid EM not to crash on some Linux Hosts
|
220
|
+
# When using a very large number of Consul Endpoints
|
221
|
+
# See https://github.com/eventmachine/eventmachine/issues/636#issuecomment-143313282
|
222
|
+
EM.epoll
|
223
|
+
|
224
|
+
consul_conf = Consul::Async::ConsulConfiguration.new(options[:consul])
|
225
|
+
template_manager = Consul::Async::ConsulEndPointsManager.new(consul_conf)
|
226
|
+
|
227
|
+
ARGV.each do |tpl|
|
228
|
+
dest = compute_default_output(tpl)
|
229
|
+
puts "Using #{dest} output for #{tpl}"
|
230
|
+
consul_engine.add_template(tpl, dest)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Ensure to kill child process if any
|
234
|
+
%w[INT PIPE TERM].each do |sig|
|
235
|
+
Signal.trap(sig) do
|
236
|
+
STDERR.puts "[KILL] received #{sig}, stopping myself"
|
237
|
+
kill_program
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
consul_engine.run(template_manager)
|
242
|
+
|
243
|
+
# Kill possible child process if consul_engine.run did stop
|
244
|
+
kill_program
|
245
|
+
|
246
|
+
exit 0
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'consul/async/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'consul-templaterb'
|
9
|
+
|
10
|
+
spec.version = Consul::Async::VERSION
|
11
|
+
spec.authors = ['SRE Core Services']
|
12
|
+
spec.email = ['sre-core-services@criteo.com']
|
13
|
+
|
14
|
+
spec.summary = 'Implementation of Consul template using Ruby and .erb templating language'
|
15
|
+
spec.homepage = 'https://github.com/criteo/consul-templaterb'
|
16
|
+
spec.description = 'A ruby implementation of Consul Template with support of erb templating'
|
17
|
+
|
18
|
+
spec.license = 'Apache v2'
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
21
|
+
f.match(%r{^(test|spec|features)/})
|
22
|
+
end
|
23
|
+
spec.bindir = 'bin'
|
24
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
spec.add_runtime_dependency 'em-http-request', '>= 1.1.5'
|
29
|
+
|
30
|
+
spec.add_development_dependency 'bundler', '>= 1.14'
|
31
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
32
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
33
|
+
spec.add_development_dependency 'rspec_junit_formatter'
|
34
|
+
spec.add_development_dependency 'rubocop', '0.49.0'
|
35
|
+
spec.add_development_dependency 'rubocop-junit-formatter'
|
36
|
+
spec.add_development_dependency 'webmock'
|
37
|
+
end
|
@@ -0,0 +1,279 @@
|
|
1
|
+
require 'consul/async/utilities'
|
2
|
+
require 'em-http'
|
3
|
+
require 'thread'
|
4
|
+
require 'json'
|
5
|
+
module Consul
|
6
|
+
module Async
|
7
|
+
class ConsulConfiguration
|
8
|
+
attr_reader :base_url, :token, :retry_duration, :min_duration, :wait_duration, :max_retry_duration, :retry_on_non_diff,
|
9
|
+
:missing_index_retry_time_on_diff, :missing_index_retry_time_on_unchanged, :debug
|
10
|
+
def initialize(base_url: 'http://locahost:8500',
|
11
|
+
debug: { network: false },
|
12
|
+
token: nil,
|
13
|
+
retry_duration: 10,
|
14
|
+
min_duration: 0.1,
|
15
|
+
retry_on_non_diff: 5,
|
16
|
+
wait_duration: 600,
|
17
|
+
max_retry_duration: 600,
|
18
|
+
missing_index_retry_time_on_diff: 15,
|
19
|
+
missing_index_retry_time_on_unchanged: 60,
|
20
|
+
paths: {})
|
21
|
+
@base_url = base_url
|
22
|
+
@token = token
|
23
|
+
@debug = debug
|
24
|
+
@retry_duration = retry_duration
|
25
|
+
@min_duration = min_duration
|
26
|
+
@wait_duration = wait_duration
|
27
|
+
@max_retry_duration = max_retry_duration
|
28
|
+
@retry_on_non_diff = retry_on_non_diff
|
29
|
+
@missing_index_retry_time_on_diff = missing_index_retry_time_on_diff
|
30
|
+
@missing_index_retry_time_on_unchanged = missing_index_retry_time_on_unchanged
|
31
|
+
@paths = paths
|
32
|
+
end
|
33
|
+
|
34
|
+
def ch(path, symbol)
|
35
|
+
sub = @paths[path.to_sym]
|
36
|
+
if sub && sub[symbol]
|
37
|
+
STDERR.puts "[INFO] Overriding #{symbol}:=#{sub[symbol]} for #{path}"
|
38
|
+
sub[symbol]
|
39
|
+
else
|
40
|
+
method(symbol).call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def create(path)
|
45
|
+
return self unless @paths[path.to_sym]
|
46
|
+
ConsulConfiguration.new(base_url: ch(path, :base_url),
|
47
|
+
debug: ch(path, :debug),
|
48
|
+
token: ch(path, :token),
|
49
|
+
retry_duration: ch(path, :retry_duration),
|
50
|
+
min_duration: ch(path, :min_duration),
|
51
|
+
retry_on_non_diff: ch(path, :retry_on_non_diff),
|
52
|
+
wait_duration: ch(path, :wait_duration),
|
53
|
+
max_retry_duration: ch(path, :max_retry_duration),
|
54
|
+
missing_index_retry_time_on_diff: ch(path, :missing_index_retry_time_on_diff),
|
55
|
+
missing_index_retry_time_on_unchanged: ch(path, :missing_index_retry_time_on_unchanged),
|
56
|
+
paths: @paths)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
class ConsulEndPointStats
|
60
|
+
attr_reader :successes, :errors, :start, :body_bytes
|
61
|
+
def initialize
|
62
|
+
@start = Time.now.utc
|
63
|
+
@successes = 0
|
64
|
+
@errors = 0
|
65
|
+
@body_bytes = 0
|
66
|
+
end
|
67
|
+
|
68
|
+
def on_reponse(res)
|
69
|
+
@successes += 1
|
70
|
+
@body_bytes = body_bytes + res.http.response.bytesize
|
71
|
+
end
|
72
|
+
|
73
|
+
def on_error(_http)
|
74
|
+
@errors += 1
|
75
|
+
end
|
76
|
+
|
77
|
+
def bytes_per_sec
|
78
|
+
diff = (Time.now.utc - start)
|
79
|
+
diff = 1 if diff < 1
|
80
|
+
(body_bytes / diff).round(0)
|
81
|
+
end
|
82
|
+
|
83
|
+
def bytes_per_sec_human
|
84
|
+
"#{Utilities.bytes_to_h(bytes_per_sec)}/s"
|
85
|
+
end
|
86
|
+
|
87
|
+
def body_bytes_human
|
88
|
+
Utilities.bytes_to_h(body_bytes)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
class ConsulResult
|
92
|
+
attr_reader :data, :http, :x_consul_index, :last_update, :stats, :retry_in
|
93
|
+
def initialize(data, modified, http, x_consul_index, stats, retry_in)
|
94
|
+
@data = data
|
95
|
+
@modified = modified
|
96
|
+
@http = http
|
97
|
+
@x_consul_index = x_consul_index
|
98
|
+
@last_update = Time.now.utc
|
99
|
+
@stats = stats
|
100
|
+
@retry_in = retry_in
|
101
|
+
end
|
102
|
+
|
103
|
+
def modified?
|
104
|
+
@modified
|
105
|
+
end
|
106
|
+
|
107
|
+
def mutate(new_data)
|
108
|
+
@data = new_data.dup
|
109
|
+
end
|
110
|
+
|
111
|
+
def json
|
112
|
+
@data_json = JSON.parse(data) if @data_json.nil?
|
113
|
+
@data_json
|
114
|
+
end
|
115
|
+
|
116
|
+
def next_retry_at
|
117
|
+
next_retry + last_update
|
118
|
+
end
|
119
|
+
end
|
120
|
+
class HttpResponse
|
121
|
+
attr_reader :response_header, :response, :error
|
122
|
+
def initialize(http, override_nil_response = nil)
|
123
|
+
if http.nil?
|
124
|
+
@response_header = nil
|
125
|
+
@response = override_nil_response
|
126
|
+
@error = 'Not initialized yet'
|
127
|
+
else
|
128
|
+
@response_header = http.response_header.nil? ? nil : http.response_header.dup.freeze
|
129
|
+
@response = http.response.nil? || http.response.empty? ? override_nil_response : http.response.dup.freeze
|
130
|
+
@error = http.error.nil? ? nil : http.error.dup.freeze
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
class ConsulEndpoint
|
135
|
+
attr_reader :conf, :path, :x_consul_index, :queue, :stats, :last_result, :enforce_json_200, :start_time, :default_value, :query_params
|
136
|
+
def initialize(conf, path, enforce_json_200 = true, query_params = {}, default_value = '[]')
|
137
|
+
@conf = conf.create(path)
|
138
|
+
@default_value = default_value
|
139
|
+
@path = path
|
140
|
+
@queue = EM::Queue.new
|
141
|
+
@x_consul_index = 0
|
142
|
+
@s_callbacks = []
|
143
|
+
@e_callbacks = []
|
144
|
+
@enforce_json_200 = enforce_json_200
|
145
|
+
@start_time = Time.now.utc
|
146
|
+
@consecutive_errors = 0
|
147
|
+
@query_params = query_params
|
148
|
+
@stopping = false
|
149
|
+
@stats = ConsulEndPointStats.new
|
150
|
+
@last_result = ConsulResult.new(default_value, false, HttpResponse.new(nil), 0, stats, 1)
|
151
|
+
on_response { |result| @stats.on_reponse result }
|
152
|
+
on_error { |http| @stats.on_error http }
|
153
|
+
_enable_network_debug if conf.debug && conf.debug[:network]
|
154
|
+
fetch
|
155
|
+
queue << 0
|
156
|
+
end
|
157
|
+
|
158
|
+
def _enable_network_debug
|
159
|
+
on_response do |result|
|
160
|
+
state = result.x_consul_index.to_i < 1 ? '[WARN]' : '[ OK ]'
|
161
|
+
stats = result.stats
|
162
|
+
STDERR.puts "[DBUG]#{state}#{result.modified? ? '[MODFIED]' : '[NO DIFF]'}" \
|
163
|
+
"[s:#{stats.successes},err:#{stats.errors}]" \
|
164
|
+
"[#{stats.body_bytes_human.ljust(8)}][#{stats.bytes_per_sec_human.ljust(9)}]"\
|
165
|
+
" #{path.ljust(48)} idx:#{result.x_consul_index}, next in #{result.retry_in} s"
|
166
|
+
end
|
167
|
+
on_error { |http| STDERR.puts "[ERROR]: #{path}: #{http.error}" }
|
168
|
+
end
|
169
|
+
|
170
|
+
def on_response(&block)
|
171
|
+
@s_callbacks << block
|
172
|
+
end
|
173
|
+
|
174
|
+
def on_error(&block)
|
175
|
+
@e_callbacks << block
|
176
|
+
end
|
177
|
+
|
178
|
+
def ready?
|
179
|
+
@ready
|
180
|
+
end
|
181
|
+
|
182
|
+
def terminate
|
183
|
+
@stopping = true
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def build_request(consul_index)
|
189
|
+
res = {
|
190
|
+
head: {
|
191
|
+
'Accept' => 'application/json',
|
192
|
+
'X-Consul-Index' => consul_index,
|
193
|
+
'X-Consul-Token' => conf.token
|
194
|
+
},
|
195
|
+
path: path,
|
196
|
+
query: {
|
197
|
+
wait: "#{conf.wait_duration}s",
|
198
|
+
index: consul_index,
|
199
|
+
stale: 'stale'
|
200
|
+
},
|
201
|
+
keepalive: true,
|
202
|
+
callback: method(:on_response)
|
203
|
+
}
|
204
|
+
@query_params.each_pair do |k, v|
|
205
|
+
res[:query][k] = v
|
206
|
+
end
|
207
|
+
res
|
208
|
+
end
|
209
|
+
|
210
|
+
def find_x_consul_token(http)
|
211
|
+
http.response_header['X_CONSUL_INDEX']
|
212
|
+
end
|
213
|
+
|
214
|
+
def _handle_error(http, consul_index)
|
215
|
+
retry_in = [600, conf.retry_duration + 2**@consecutive_errors].min
|
216
|
+
STDERR.puts "[ERROR][#{path}] X-Consul-Index:#{consul_index} - #{http.error} - Retry in #{retry_in}s #{stats.body_bytes_human}"
|
217
|
+
@consecutive_errors += 1
|
218
|
+
http_result = HttpResponse.new(http)
|
219
|
+
EventMachine.add_timer(retry_in) do
|
220
|
+
yield
|
221
|
+
queue.push(consul_index)
|
222
|
+
end
|
223
|
+
@e_callbacks.each { |c| c.call(http_result) }
|
224
|
+
end
|
225
|
+
|
226
|
+
def fetch
|
227
|
+
options = {
|
228
|
+
connect_timeout: 5, # default connection setup timeout
|
229
|
+
inactivity_timeout: conf.wait_duration + 1, # default connection inactivity (post-setup) timeout
|
230
|
+
}
|
231
|
+
connection = EventMachine::HttpRequest.new(conf.base_url, options)
|
232
|
+
cb = proc do |consul_index|
|
233
|
+
http = connection.get(build_request(consul_index))
|
234
|
+
http.callback do
|
235
|
+
# Dirty hack, but contrary to other path, when key is not present, Consul returns 404
|
236
|
+
is_kv_empty = path.start_with?('/v1/kv') && http.response_header.status == 404
|
237
|
+
if !is_kv_empty && enforce_json_200 && http.response_header.status != 200 && http.response_header['Content-Type'] != 'application/json'
|
238
|
+
_handle_error(http, consul_index) { connection = EventMachine::HttpRequest.new(conf.base_url, options) }
|
239
|
+
else
|
240
|
+
n_consul_index = find_x_consul_token(http)
|
241
|
+
@consecutive_errors = 0
|
242
|
+
http_result = if is_kv_empty
|
243
|
+
HttpResponse.new(http, default_value)
|
244
|
+
else
|
245
|
+
HttpResponse.new(http)
|
246
|
+
end
|
247
|
+
new_content = http_result.response.freeze
|
248
|
+
modified = @last_result.nil? ? true : @last_result.data != new_content
|
249
|
+
if n_consul_index == consul_index || n_consul_index.nil?
|
250
|
+
retry_in = modified ? conf.missing_index_retry_time_on_diff : conf.missing_index_retry_time_on_unchanged
|
251
|
+
n_consul_index = consul_index
|
252
|
+
else
|
253
|
+
retry_in = modified ? conf.min_duration : conf.retry_on_non_diff
|
254
|
+
end
|
255
|
+
retry_in = 0.1 if retry_in < 0.1
|
256
|
+
unless @stopping
|
257
|
+
EventMachine.add_timer(retry_in) do
|
258
|
+
queue.push(n_consul_index)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
result = ConsulResult.new(new_content, modified, http_result, n_consul_index, stats, retry_in)
|
262
|
+
@last_result = result
|
263
|
+
@ready = true
|
264
|
+
@s_callbacks.each { |c| c.call(result) }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
http.errback do
|
269
|
+
unless @stopping
|
270
|
+
_handle_error(http, consul_index) { connection = EventMachine::HttpRequest.new(conf.base_url, options) }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
queue.pop(&cb)
|
274
|
+
end
|
275
|
+
queue.pop(&cb)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|