nerve_pharmeasy 0.7.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/.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
|