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.
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