nerve_pharmeasy 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.mailmap +2 -0
  4. data/.nerve.rc +2 -0
  5. data/.travis.yml +8 -0
  6. data/CONTRIBUTING.md +28 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +75 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +116 -0
  11. data/Rakefile +7 -0
  12. data/Vagrantfile +121 -0
  13. data/bin/nerve +16 -0
  14. data/example/nerve.conf.json +54 -0
  15. data/example/nerve_services/etcd_service1.json +19 -0
  16. data/example/nerve_services/zookeeper_service1.json +18 -0
  17. data/lib/nerve/configuration_manager.rb +106 -0
  18. data/lib/nerve/log.rb +24 -0
  19. data/lib/nerve/reporter/base.rb +61 -0
  20. data/lib/nerve/reporter/etcd.rb +73 -0
  21. data/lib/nerve/reporter/zookeeper.rb +101 -0
  22. data/lib/nerve/reporter.rb +18 -0
  23. data/lib/nerve/ring_buffer.rb +30 -0
  24. data/lib/nerve/service_watcher/base.rb +65 -0
  25. data/lib/nerve/service_watcher/http.rb +70 -0
  26. data/lib/nerve/service_watcher/rabbitmq.rb +68 -0
  27. data/lib/nerve/service_watcher/tcp.rb +56 -0
  28. data/lib/nerve/service_watcher.rb +152 -0
  29. data/lib/nerve/utils.rb +17 -0
  30. data/lib/nerve/version.rb +3 -0
  31. data/lib/nerve.rb +249 -0
  32. data/nerve.conf.json +23 -0
  33. data/nerve.gemspec +33 -0
  34. data/spec/.gitkeep +0 -0
  35. data/spec/configuration_manager_spec.rb +31 -0
  36. data/spec/example_services_spec.rb +42 -0
  37. data/spec/factories/check.rb +16 -0
  38. data/spec/factories/service.rb +26 -0
  39. data/spec/lib/nerve/reporter_etcd_spec.rb +18 -0
  40. data/spec/lib/nerve/reporter_spec.rb +86 -0
  41. data/spec/lib/nerve/reporter_zookeeper_spec.rb +32 -0
  42. data/spec/lib/nerve/service_watcher_spec.rb +89 -0
  43. data/spec/lib/nerve_spec.rb +186 -0
  44. data/spec/spec_helper.rb +33 -0
  45. metadata +216 -0
@@ -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
@@ -0,0 +1,3 @@
1
+ module Nerve
2
+ VERSION = "0.8.0"
3
+ 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