nerve_pharmeasy 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.mailmap +2 -0
- data/.nerve.rc +2 -0
- data/.travis.yml +8 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +75 -0
- data/LICENSE.txt +22 -0
- data/README.md +116 -0
- data/Rakefile +7 -0
- data/Vagrantfile +121 -0
- data/bin/nerve +16 -0
- data/example/nerve.conf.json +54 -0
- data/example/nerve_services/etcd_service1.json +19 -0
- data/example/nerve_services/zookeeper_service1.json +18 -0
- data/lib/nerve/configuration_manager.rb +106 -0
- data/lib/nerve/log.rb +24 -0
- data/lib/nerve/reporter/base.rb +61 -0
- data/lib/nerve/reporter/etcd.rb +73 -0
- data/lib/nerve/reporter/zookeeper.rb +101 -0
- data/lib/nerve/reporter.rb +18 -0
- data/lib/nerve/ring_buffer.rb +30 -0
- data/lib/nerve/service_watcher/base.rb +65 -0
- data/lib/nerve/service_watcher/http.rb +70 -0
- data/lib/nerve/service_watcher/rabbitmq.rb +68 -0
- data/lib/nerve/service_watcher/tcp.rb +56 -0
- data/lib/nerve/service_watcher.rb +152 -0
- data/lib/nerve/utils.rb +17 -0
- data/lib/nerve/version.rb +3 -0
- data/lib/nerve.rb +249 -0
- data/nerve.conf.json +23 -0
- data/nerve.gemspec +33 -0
- data/spec/.gitkeep +0 -0
- data/spec/configuration_manager_spec.rb +31 -0
- data/spec/example_services_spec.rb +42 -0
- data/spec/factories/check.rb +16 -0
- data/spec/factories/service.rb +26 -0
- data/spec/lib/nerve/reporter_etcd_spec.rb +18 -0
- data/spec/lib/nerve/reporter_spec.rb +86 -0
- data/spec/lib/nerve/reporter_zookeeper_spec.rb +32 -0
- data/spec/lib/nerve/service_watcher_spec.rb +89 -0
- data/spec/lib/nerve_spec.rb +186 -0
- data/spec/spec_helper.rb +33 -0
- metadata +216 -0
data/lib/nerve/utils.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Nerve
|
2
|
+
module Utils
|
3
|
+
def safe_run(command)
|
4
|
+
res = `#{command}`.chomp
|
5
|
+
raise "command '#{command}' failed to run:\n#{res}" unless $?.success?
|
6
|
+
end
|
7
|
+
|
8
|
+
def responsive_sleep(seconds, tick=1, &should_exit)
|
9
|
+
nap_time = seconds
|
10
|
+
while nap_time > 0
|
11
|
+
break if (should_exit && should_exit.call)
|
12
|
+
sleep [nap_time, tick].min
|
13
|
+
nap_time -= tick
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/nerve.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'logger'
|
3
|
+
require 'json'
|
4
|
+
require 'timeout'
|
5
|
+
require 'socket'
|
6
|
+
|
7
|
+
require 'nerve/version'
|
8
|
+
require 'nerve/utils'
|
9
|
+
require 'nerve/log'
|
10
|
+
require 'nerve/ring_buffer'
|
11
|
+
require 'nerve/reporter'
|
12
|
+
require 'nerve/service_watcher'
|
13
|
+
|
14
|
+
require 'nerve/configuration_manager'
|
15
|
+
|
16
|
+
module Nerve
|
17
|
+
class Nerve
|
18
|
+
include Logging
|
19
|
+
include Utils
|
20
|
+
|
21
|
+
MAIN_LOOP_SLEEP_S = 10.freeze
|
22
|
+
LAUNCH_WAIT_FOR_REPORT_S = 30.freeze
|
23
|
+
|
24
|
+
def initialize(config_manager)
|
25
|
+
log.info 'nerve: setting up!'
|
26
|
+
@config = config_manager
|
27
|
+
|
28
|
+
# set global variable for exit signal
|
29
|
+
$EXIT = false
|
30
|
+
|
31
|
+
# State of currently running watchers according to Nerve
|
32
|
+
@watchers = {}
|
33
|
+
@watcher_versions = {}
|
34
|
+
|
35
|
+
# instance_id, heartbeat_path, and watchers_desired are populated by
|
36
|
+
# load_config! in the main loop from the configuration source
|
37
|
+
@instance_id = nil
|
38
|
+
@heartbeat_path = nil
|
39
|
+
@watchers_desired = {}
|
40
|
+
|
41
|
+
# Flag to indicate a config reload is required by the main loop
|
42
|
+
# This decoupling is required for gracefully reloading config on SIGHUP
|
43
|
+
# as one should do as little as possible in a signal handler
|
44
|
+
@config_to_load = true
|
45
|
+
|
46
|
+
Signal.trap("HUP") do
|
47
|
+
@config_to_load = true
|
48
|
+
end
|
49
|
+
|
50
|
+
log.debug 'nerve: completed init'
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_config!
|
54
|
+
log.info 'nerve: loading config'
|
55
|
+
@config_to_load = false
|
56
|
+
log.info @config_manager.class.name
|
57
|
+
|
58
|
+
@config.each do|key, value|
|
59
|
+
log.info key
|
60
|
+
log.info value
|
61
|
+
end
|
62
|
+
|
63
|
+
services = @config["services"]
|
64
|
+
|
65
|
+
services.each do|service, data|
|
66
|
+
log.info "service:"+service
|
67
|
+
host = data["host"]
|
68
|
+
log.info "host:"+data["host"]
|
69
|
+
if host == "localhost"
|
70
|
+
log.info local_ip
|
71
|
+
data["host"]=local_ip
|
72
|
+
end
|
73
|
+
end
|
74
|
+
#kedar @config_manager.reload!
|
75
|
+
#kedar config = @config_manager.config
|
76
|
+
|
77
|
+
@config_manager = ConfigurationManager.new()
|
78
|
+
@config_manager.setConfig(@config)
|
79
|
+
|
80
|
+
|
81
|
+
# required options
|
82
|
+
log.debug 'nerve: checking for required inputs'
|
83
|
+
%w{instance_id services}.each do |required|
|
84
|
+
raise ArgumentError, "you need to specify required argument #{required}" unless @config[required]
|
85
|
+
end
|
86
|
+
@instance_id = @config['instance_id']
|
87
|
+
@watchers_desired = @config['services']
|
88
|
+
@heartbeat_path = @config['heartbeat_path']
|
89
|
+
end
|
90
|
+
|
91
|
+
def local_ip
|
92
|
+
orig = Socket.do_not_reverse_lookup
|
93
|
+
Socket.do_not_reverse_lookup =true # turn off reverse DNS resolution temporarily
|
94
|
+
UDPSocket.open do |s|
|
95
|
+
s.connect '64.233.187.99', 1 #google
|
96
|
+
s.addr.last
|
97
|
+
end
|
98
|
+
ensure
|
99
|
+
Socket.do_not_reverse_lookup = orig
|
100
|
+
end
|
101
|
+
|
102
|
+
def run
|
103
|
+
log.info 'nerve: starting main run loop'
|
104
|
+
begin
|
105
|
+
until $EXIT
|
106
|
+
# Check if configuration needs to be reloaded and reconcile any new
|
107
|
+
# configuration of watchers with old configuration
|
108
|
+
if @config_to_load
|
109
|
+
load_config!
|
110
|
+
|
111
|
+
# Reap undesired service watchers
|
112
|
+
services_to_reap = @watchers.select{ |name, _|
|
113
|
+
!@watchers_desired.has_key?(name)
|
114
|
+
}.keys()
|
115
|
+
|
116
|
+
unless services_to_reap.empty?
|
117
|
+
log.info "nerve: reaping old watchers: #{services_to_reap}"
|
118
|
+
services_to_reap.each do |name|
|
119
|
+
reap_watcher(name)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Start new desired service watchers
|
124
|
+
services_to_launch = @watchers_desired.select{ |name, _|
|
125
|
+
!@watchers.has_key?(name)
|
126
|
+
}.keys()
|
127
|
+
|
128
|
+
unless services_to_launch.empty?
|
129
|
+
log.info "nerve: launching new watchers: #{services_to_launch}"
|
130
|
+
services_to_launch.each do |name|
|
131
|
+
launch_watcher(name, @watchers_desired[name])
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Detect and update existing service watchers which are in both
|
136
|
+
# the currently running state and the desired (config) watcher
|
137
|
+
# state but have different configurations
|
138
|
+
services_to_update = @watchers.select { |name, _|
|
139
|
+
@watchers_desired.has_key?(name) &&
|
140
|
+
merged_config(@watchers_desired[name], name).hash != @watcher_versions[name]
|
141
|
+
}.keys()
|
142
|
+
|
143
|
+
services_to_update.each do |name|
|
144
|
+
log.info "nerve: detected new config for #{name}"
|
145
|
+
# Keep the old watcher running until the replacement is launched
|
146
|
+
# This keeps the service registered while we change it over
|
147
|
+
# This also keeps connection pools active across diffs
|
148
|
+
temp_name = "#{name}_#{@watcher_versions[name]}"
|
149
|
+
@watchers[temp_name] = @watchers.delete(name)
|
150
|
+
@watcher_versions[temp_name] = @watcher_versions.delete(name)
|
151
|
+
log.info "nerve: launching new watcher for #{name}"
|
152
|
+
launch_watcher(name, @watchers_desired[name], :wait => true)
|
153
|
+
log.info "nerve: reaping old watcher #{temp_name}"
|
154
|
+
reap_watcher(temp_name)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# If this was a configuration check, bail out now
|
159
|
+
if @config_manager.options[:check_config]
|
160
|
+
log.info 'nerve: configuration check succeeded, exiting immediately'
|
161
|
+
break
|
162
|
+
end
|
163
|
+
|
164
|
+
# Check that watchers are still alive, auto-remediate if they
|
165
|
+
# are not. Sometimes zookeeper flakes out or connections are lost to
|
166
|
+
# remote datacenter zookeeper clusters, failing is not an option
|
167
|
+
relaunch = []
|
168
|
+
@watchers.each do |name, watcher|
|
169
|
+
unless watcher.alive?
|
170
|
+
relaunch << name
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
relaunch.each do |name|
|
175
|
+
begin
|
176
|
+
log.warn "nerve: watcher #{name} not alive; reaping and relaunching"
|
177
|
+
reap_watcher(name)
|
178
|
+
rescue => e
|
179
|
+
log.warn "nerve: could not reap #{name}, got #{e.inspect}"
|
180
|
+
end
|
181
|
+
launch_watcher(name, @watchers_desired[name])
|
182
|
+
end
|
183
|
+
|
184
|
+
# Indicate we've made progress
|
185
|
+
heartbeat()
|
186
|
+
|
187
|
+
responsive_sleep(MAIN_LOOP_SLEEP_S) { @config_to_load || $EXIT }
|
188
|
+
end
|
189
|
+
rescue => e
|
190
|
+
log.error "nerve: encountered unexpected exception #{e.inspect} in main thread"
|
191
|
+
raise e
|
192
|
+
ensure
|
193
|
+
$EXIT = true
|
194
|
+
log.warn 'nerve: reaping all watchers'
|
195
|
+
@watchers.each do |name, _|
|
196
|
+
reap_watcher(name)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
log.info 'nerve: exiting'
|
201
|
+
ensure
|
202
|
+
$EXIT = true
|
203
|
+
end
|
204
|
+
|
205
|
+
def heartbeat
|
206
|
+
unless @heartbeat_path.nil?
|
207
|
+
FileUtils.touch(@heartbeat_path)
|
208
|
+
end
|
209
|
+
log.debug 'nerve: heartbeat'
|
210
|
+
end
|
211
|
+
|
212
|
+
def merged_config(config, name)
|
213
|
+
# Get a deep copy so sub-hashes are properly handled
|
214
|
+
deep_copy = Marshal.load(Marshal.dump(config))
|
215
|
+
return deep_copy.merge({'instance_id' => @instance_id, 'name' => name})
|
216
|
+
end
|
217
|
+
|
218
|
+
def launch_watcher(name, config, opts = {})
|
219
|
+
wait = opts[:wait] || false
|
220
|
+
|
221
|
+
watcher_config = merged_config(config, name)
|
222
|
+
# The ServiceWatcher may mutate the configs, so record the version before
|
223
|
+
# passing the config to the ServiceWatcher
|
224
|
+
@watcher_versions[name] = watcher_config.hash
|
225
|
+
|
226
|
+
watcher = ServiceWatcher.new(watcher_config)
|
227
|
+
unless @config_manager.options[:check_config]
|
228
|
+
log.debug "nerve: launching service watcher #{name}"
|
229
|
+
watcher.start()
|
230
|
+
@watchers[name] = watcher
|
231
|
+
if wait
|
232
|
+
log.info "nerve: waiting for watcher thread #{name} to report"
|
233
|
+
responsive_sleep(LAUNCH_WAIT_FOR_REPORT_S) { !watcher.was_up.nil? || $EXIT }
|
234
|
+
log.info "nerve: watcher thread #{name} has reported!"
|
235
|
+
end
|
236
|
+
else
|
237
|
+
log.info "nerve: not launching #{name} due to --check-config option"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def reap_watcher(name)
|
242
|
+
watcher = @watchers.delete(name)
|
243
|
+
@watcher_versions.delete(name)
|
244
|
+
shutdown_status = watcher.stop()
|
245
|
+
log.info "nerve: stopped #{name}, clean shutdown? #{shutdown_status}"
|
246
|
+
shutdown_status
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
data/nerve.conf.json
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
{
|
2
|
+
"instance_id": "mymachine",
|
3
|
+
"services": {
|
4
|
+
"nodejs": {
|
5
|
+
"host": "localhost",
|
6
|
+
"port": 3000,
|
7
|
+
"reporter_type": "zookeeper",
|
8
|
+
"zk_hosts": ["localhost:2181"],
|
9
|
+
"zk_path": "/nerve/services/your_http_service/services",
|
10
|
+
"check_interval": 2,
|
11
|
+
"checks": [
|
12
|
+
{
|
13
|
+
"type": "http",
|
14
|
+
"uri": "/health",
|
15
|
+
"timeout": 0.2,
|
16
|
+
"rise": 3,
|
17
|
+
"fall": 2
|
18
|
+
}
|
19
|
+
]
|
20
|
+
}
|
21
|
+
|
22
|
+
}
|
23
|
+
}
|
data/nerve.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'nerve/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "nerve"
|
8
|
+
gem.version = Nerve::VERSION
|
9
|
+
gem.authors = ["Martin Rhoads", "Igor Serebryany", "Pierre Carrier"]
|
10
|
+
gem.email = ["martin.rhoads@airbnb.com", "igor.serebryany@airbnb.com"]
|
11
|
+
gem.description = "Nerve is a service registration daemon. It performs health "\
|
12
|
+
"checks on your service and then publishes success or failure "\
|
13
|
+
"into one of several registries (currently, zookeeper or etcd). "\
|
14
|
+
"Nerve is half or SmartStack, and is designed to be operated "\
|
15
|
+
"along with Synapse to provide a full service discovery framework"
|
16
|
+
gem.summary = %q{A service registration daemon}
|
17
|
+
gem.homepage = "https://github.com/airbnb/nerve"
|
18
|
+
|
19
|
+
gem.files = `git ls-files`.split($/)
|
20
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
|
24
|
+
gem.add_runtime_dependency "json"
|
25
|
+
gem.add_runtime_dependency "zk", "~> 1.9.2"
|
26
|
+
gem.add_runtime_dependency "bunny", "= 1.1.0"
|
27
|
+
gem.add_runtime_dependency "etcd", "~> 0.2.3"
|
28
|
+
|
29
|
+
gem.add_development_dependency "rake"
|
30
|
+
gem.add_development_dependency "rspec", "~> 3.1.0"
|
31
|
+
gem.add_development_dependency "factory_girl"
|
32
|
+
gem.add_development_dependency "pry"
|
33
|
+
end
|
data/spec/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nerve/configuration_manager'
|
3
|
+
|
4
|
+
describe Nerve::ConfigurationManager do
|
5
|
+
describe 'parsing config' do
|
6
|
+
let(:config_manager) { Nerve::ConfigurationManager.new() }
|
7
|
+
let(:nerve_config) { "#{File.dirname(__FILE__)}/../example/nerve.conf.json" }
|
8
|
+
let(:nerve_instance_id) { 'testid' }
|
9
|
+
|
10
|
+
it 'parses valid options' do
|
11
|
+
allow(config_manager).to receive(:parse_options_from_argv!) { {
|
12
|
+
:config => nerve_config,
|
13
|
+
:instance_id => nerve_instance_id,
|
14
|
+
:check_config => false
|
15
|
+
} }
|
16
|
+
|
17
|
+
expect{config_manager.reload!}.to raise_error(RuntimeError)
|
18
|
+
expect(config_manager.parse_options!).to eql({
|
19
|
+
:config => nerve_config,
|
20
|
+
:instance_id => nerve_instance_id,
|
21
|
+
:check_config => false
|
22
|
+
})
|
23
|
+
expect{config_manager.reload!}.not_to raise_error
|
24
|
+
expect(config_manager.config.keys()).to include('instance_id', 'services')
|
25
|
+
expect(config_manager.config['services'].keys()).to contain_exactly(
|
26
|
+
'your_http_service', 'your_tcp_service', 'rabbitmq_service',
|
27
|
+
'etcd_service1', 'zookeeper_service1'
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'nerve/reporter'
|
3
|
+
require 'nerve/reporter/base'
|
4
|
+
require 'nerve/service_watcher'
|
5
|
+
|
6
|
+
class Nerve::Reporter::Base
|
7
|
+
attr_reader :data
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "example services are valid" do
|
11
|
+
Dir.foreach("#{File.dirname(__FILE__)}/../example/nerve_services") do |item|
|
12
|
+
next if item == '.' or item == '..'
|
13
|
+
service_data = JSON.parse(IO.read("#{File.dirname(__FILE__)}/../example/nerve_services/#{item}"))
|
14
|
+
service_data['name'] = item.gsub(/\.json$/, '')
|
15
|
+
service_data['instance_id'] = '1'
|
16
|
+
|
17
|
+
context "when #{item} can be initialized as a valid reporter" do
|
18
|
+
it 'creates a valid reporter in new_from_service' do
|
19
|
+
reporter = nil
|
20
|
+
expect { reporter = Nerve::Reporter.new_from_service(service_data) }.to_not raise_error()
|
21
|
+
expect(reporter.is_a?(Nerve::Reporter::Base)).to eql(true)
|
22
|
+
end
|
23
|
+
it 'saves the weight data' do
|
24
|
+
expect(JSON.parse(Nerve::Reporter.new_from_service(service_data).data)['weight']).to eql(2)
|
25
|
+
end
|
26
|
+
it 'saves the labels data' do
|
27
|
+
labels = {'az' => 'us-west-1'}
|
28
|
+
service_data['labels'] = labels
|
29
|
+
expect(JSON.parse(Nerve::Reporter.new_from_service(service_data).data)['labels']).to eq(labels)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when #{item} can be initialized as a valid service watcher" do
|
34
|
+
it "creates a valid service watcher for #{item}" do
|
35
|
+
watcher = nil
|
36
|
+
expect { watcher = Nerve::ServiceWatcher.new(service_data) }.to_not raise_error()
|
37
|
+
expect(watcher.is_a?(Nerve::ServiceWatcher)).to eql(true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :check, :class => Hash do
|
3
|
+
type 'base'
|
4
|
+
timeout 0.2
|
5
|
+
rise 3
|
6
|
+
fall 2
|
7
|
+
|
8
|
+
trait :http do
|
9
|
+
type 'http'
|
10
|
+
uri '/health'
|
11
|
+
end
|
12
|
+
|
13
|
+
initialize_with { Hash[attributes.map{|k,v| [k.to_s,v]}] }
|
14
|
+
to_create {}
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :service, :class => Hash do
|
3
|
+
sequence(:name) {|n| "service_check_#{n}"}
|
4
|
+
instance_id 'public_hostname.example.com'
|
5
|
+
host 'localhost'
|
6
|
+
port 3000
|
7
|
+
reporter_type 'base'
|
8
|
+
checks { create_list(:check, checks_count) }
|
9
|
+
check_interval nil
|
10
|
+
|
11
|
+
trait :zookeeper do
|
12
|
+
reporter_type 'zookeeper'
|
13
|
+
zk_hosts ['localhost:2181']
|
14
|
+
zk_path { "/nerve/services/#{name}/services" }
|
15
|
+
end
|
16
|
+
|
17
|
+
# set up some service checks
|
18
|
+
transient do
|
19
|
+
checks_count 1
|
20
|
+
end
|
21
|
+
|
22
|
+
# thanks to https://stackoverflow.com/questions/10032760
|
23
|
+
initialize_with { Hash[attributes.map{|k,v| [k.to_s,v]}] }
|
24
|
+
to_create {}
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nerve/reporter/etcd'
|
3
|
+
|
4
|
+
describe Nerve::Reporter::Etcd do
|
5
|
+
let(:subject) { {
|
6
|
+
'etcd_host' => 'etcdhost1',
|
7
|
+
'etcd_port' => 4001,
|
8
|
+
'etcd_path' => '/path',
|
9
|
+
'instance_id' => 'instance_id',
|
10
|
+
'host' => 'host',
|
11
|
+
'port' => 'port'
|
12
|
+
}
|
13
|
+
}
|
14
|
+
it 'actually constructs an instance' do
|
15
|
+
expect(Nerve::Reporter::Etcd.new(subject).is_a?(Nerve::Reporter::Etcd)).to eql(true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nerve/reporter/zookeeper'
|
3
|
+
|
4
|
+
describe Nerve::Reporter do
|
5
|
+
let(:subject) { {
|
6
|
+
'zk_hosts' => ['zkhost1', 'zkhost2'],
|
7
|
+
'zk_path' => 'zk_path',
|
8
|
+
'instance_id' => 'instance_id',
|
9
|
+
'host' => 'host',
|
10
|
+
'port' => 'port'
|
11
|
+
}
|
12
|
+
}
|
13
|
+
it 'can new_from_service' do
|
14
|
+
expect(Nerve::Reporter::Zookeeper).to receive(:new).with(subject).and_return('kerplunk')
|
15
|
+
expect(Nerve::Reporter.new_from_service(subject)).to eq('kerplunk')
|
16
|
+
end
|
17
|
+
it 'actually constructs an instance of a specific backend' do
|
18
|
+
expect(Nerve::Reporter.new_from_service(subject).is_a?(Nerve::Reporter::Zookeeper)).to eql(true)
|
19
|
+
end
|
20
|
+
it 'the reporter backend inherits from the base class' do
|
21
|
+
expect(Nerve::Reporter.new_from_service(subject).is_a?(Nerve::Reporter::Base)).to eql(true)
|
22
|
+
end
|
23
|
+
it 'throws ArgumentError if you ask for a reporter type which does not exist' do
|
24
|
+
subject['reporter_type'] = 'does_not_exist'
|
25
|
+
expect { Nerve::Reporter.new_from_service(subject) }.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Nerve::Reporter::Test < Nerve::Reporter::Base
|
30
|
+
end
|
31
|
+
|
32
|
+
describe Nerve::Reporter::Test do
|
33
|
+
let(:subject) {Nerve::Reporter::Test.new({}) }
|
34
|
+
context 'parse_data method' do
|
35
|
+
it 'has parse data method that passes strings' do
|
36
|
+
expect(subject.send(:parse_data, 'foobar')).to eql('foobar')
|
37
|
+
end
|
38
|
+
it 'jsonifies anything that is not a string' do
|
39
|
+
thing_to_parse = double()
|
40
|
+
expect(thing_to_parse).to receive(:to_json).and_return('{"some":"json"}')
|
41
|
+
expect(subject.send(:parse_data, thing_to_parse)).to eql('{"some":"json"}')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'get_service_data method' do
|
46
|
+
it 'throws on missing arguments' do
|
47
|
+
expect { subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666}) }.to raise_error(ArgumentError)
|
48
|
+
expect { subject.get_service_data({'host' => '127.0.0.1', 'instance_id' => 'foobar'}) }.to raise_error(ArgumentError)
|
49
|
+
expect { subject.get_service_data({'port' => 6666, 'instance_id' => 'foobar'}) }.to raise_error(ArgumentError)
|
50
|
+
expect { subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar'}) }.not_to raise_error
|
51
|
+
end
|
52
|
+
it 'takes weight if present and +ve integer' do
|
53
|
+
expect(subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar', 'weight' => 3})['weight']).to eql(3)
|
54
|
+
end
|
55
|
+
it 'takes weight if present and 0' do
|
56
|
+
expect(subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar', 'weight' => 0})['weight']).to eql(0)
|
57
|
+
end
|
58
|
+
it 'skips weight if not present' do
|
59
|
+
expect(subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar'})['weight']).to eql(nil)
|
60
|
+
end
|
61
|
+
it 'skips weight if non integer' do
|
62
|
+
expect { subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar', 'weight' => 'hello'})['weight'] }.to raise_error(ArgumentError)
|
63
|
+
end
|
64
|
+
it 'skips weight if a negative integer' do
|
65
|
+
expect { subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar', 'weight' => -1})['weight'] }.to raise_error(ArgumentError)
|
66
|
+
end
|
67
|
+
it 'works if the weight is a string' do
|
68
|
+
expect(subject.get_service_data({'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar', 'weight' => '3'})['weight']).to eql(3)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'correctly passes haproxy_server_options' do
|
72
|
+
expect(subject.get_service_data({
|
73
|
+
'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar',
|
74
|
+
'haproxy_server_options' => 'backup'
|
75
|
+
})['haproxy_server_options']).to eql('backup')
|
76
|
+
end
|
77
|
+
it 'correctly passes labels' do
|
78
|
+
labels = {'az' => 'us-west-1', 'custom_key' => 'custom_value'}
|
79
|
+
expect(subject.get_service_data({
|
80
|
+
'host' => '127.0.0.1', 'port' => 6666, 'instance_id' => 'foobar',
|
81
|
+
'labels' => labels
|
82
|
+
})['labels']).to eql(labels)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nerve/reporter/zookeeper'
|
3
|
+
|
4
|
+
describe Nerve::Reporter::Zookeeper do
|
5
|
+
let(:subject) { {
|
6
|
+
'zk_hosts' => ['zkhost1', 'zkhost2'],
|
7
|
+
'zk_path' => 'zk_path',
|
8
|
+
'instance_id' => 'instance_id',
|
9
|
+
'host' => 'host',
|
10
|
+
'port' => 'port'
|
11
|
+
}
|
12
|
+
}
|
13
|
+
it 'actually constructs an instance' do
|
14
|
+
expect(Nerve::Reporter::Zookeeper.new(subject).is_a?(Nerve::Reporter::Zookeeper)).to eql(true)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'deregisters service on exit' do
|
18
|
+
zk = double("zk")
|
19
|
+
allow(zk).to receive(:close!)
|
20
|
+
expect(zk).to receive(:mkdir_p) { "zk_path" }
|
21
|
+
expect(zk).to receive(:create) { "full_path" }
|
22
|
+
expect(zk).to receive(:delete).with("full_path", anything())
|
23
|
+
|
24
|
+
allow(ZK).to receive(:new).and_return(zk)
|
25
|
+
|
26
|
+
reporter = Nerve::Reporter::Zookeeper.new(subject)
|
27
|
+
reporter.start
|
28
|
+
reporter.report_up
|
29
|
+
reporter.stop
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
describe Nerve::ServiceWatcher do
|
5
|
+
describe 'initialize' do
|
6
|
+
let(:service) { build(:service) }
|
7
|
+
|
8
|
+
it 'can successfully initialize' do
|
9
|
+
Nerve::ServiceWatcher.new(service)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'requires minimum parameters' do
|
13
|
+
%w[name instance_id host port].each do |req|
|
14
|
+
service_without = service.dup
|
15
|
+
service_without.delete(req)
|
16
|
+
|
17
|
+
expect { Nerve::ServiceWatcher.new(service_without) }.to raise_error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'check_and_report' do
|
23
|
+
let(:service_watcher) { Nerve::ServiceWatcher.new(build(:service)) }
|
24
|
+
let(:reporter) { service_watcher.instance_variable_get(:@reporter) }
|
25
|
+
|
26
|
+
it 'pings the reporter' do
|
27
|
+
expect(reporter).to receive(:ping?)
|
28
|
+
service_watcher.check_and_report
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'reports the service as down when the checks fail' do
|
32
|
+
expect(service_watcher).to receive(:check?).and_return(false)
|
33
|
+
expect(reporter).to receive(:report_down)
|
34
|
+
service_watcher.check_and_report
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'reports the service as up when the checks succeed' do
|
38
|
+
expect(service_watcher).to receive(:check?).and_return(true)
|
39
|
+
expect(reporter).to receive(:report_up)
|
40
|
+
service_watcher.check_and_report
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'doesn\'t report if the status hasn\'t changed' do
|
44
|
+
expect(service_watcher).to receive(:check?).and_return(true)
|
45
|
+
|
46
|
+
expect(reporter).to receive(:report_up).once
|
47
|
+
expect(reporter).not_to receive(:report_down)
|
48
|
+
service_watcher.check_and_report
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'run' do
|
53
|
+
let(:check_interval) { 0 }
|
54
|
+
let(:service_watcher) { Nerve::ServiceWatcher.new(build(:service, :check_interval => check_interval)) }
|
55
|
+
let(:reporter) { service_watcher.instance_variable_get(:@reporter) }
|
56
|
+
before { $EXIT = false }
|
57
|
+
|
58
|
+
it 'starts the reporter' do
|
59
|
+
$EXIT = true
|
60
|
+
expect(reporter).to receive(:start)
|
61
|
+
service_watcher.run()
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'calls check and report repeatedly' do
|
65
|
+
count = 0
|
66
|
+
|
67
|
+
# expect it to be called twice
|
68
|
+
expect(service_watcher).to receive(:check_and_report).twice do
|
69
|
+
# on the second call, set exit to true
|
70
|
+
$EXIT = true if count == 1
|
71
|
+
count += 1
|
72
|
+
end
|
73
|
+
|
74
|
+
service_watcher.run()
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'when the check interval is long' do
|
78
|
+
let(:check_interval) { 10 }
|
79
|
+
|
80
|
+
it 'still exits quickly during nap time' do
|
81
|
+
expect(service_watcher).to receive(:check_and_report) do
|
82
|
+
$EXIT = true
|
83
|
+
end
|
84
|
+
|
85
|
+
expect{ Timeout::timeout(1) { service_watcher.run() } }.not_to raise_error
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|