nerve 0.5.2 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +48 -15
- data/README.md +19 -1
- data/example/nerve_services/etcd_service1.json +18 -0
- data/example/nerve_services/{service1.json → zookeeper_service1.json} +0 -0
- data/lib/nerve/reporter/base.rb +6 -0
- data/lib/nerve/reporter/etcd.rb +82 -0
- data/lib/nerve/reporter/zookeeper.rb +2 -6
- data/lib/nerve/reporter.rb +3 -0
- data/lib/nerve/service_watcher/base.rb +5 -0
- data/lib/nerve/service_watcher/http.rb +9 -4
- data/lib/nerve/service_watcher.rb +51 -25
- data/lib/nerve/version.rb +1 -1
- data/nerve.gemspec +5 -1
- data/spec/example_services_spec.rb +29 -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 +15 -0
- data/spec/lib/nerve/reporter_zookeeper_spec.rb +14 -0
- data/spec/lib/nerve/service_watcher_spec.rb +89 -0
- data/spec/spec_helper.rb +12 -1
- metadata +80 -4
data/Gemfile.lock
CHANGED
@@ -1,39 +1,72 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
nerve (0.5.
|
4
|
+
nerve (0.5.4)
|
5
5
|
bunny (= 1.1.0)
|
6
|
+
etcd (~> 0.2.3)
|
7
|
+
json
|
6
8
|
zk (~> 1.9.2)
|
7
9
|
|
8
10
|
GEM
|
9
11
|
remote: https://rubygems.org/
|
10
12
|
specs:
|
13
|
+
activesupport (4.2.0)
|
14
|
+
i18n (~> 0.7)
|
15
|
+
json (~> 1.7, >= 1.7.7)
|
16
|
+
minitest (~> 5.1)
|
17
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
18
|
+
tzinfo (~> 1.1)
|
11
19
|
amq-protocol (1.9.2)
|
12
20
|
bunny (1.1.0)
|
13
21
|
amq-protocol (>= 1.9.2)
|
22
|
+
coderay (1.1.0)
|
14
23
|
diff-lcs (1.2.5)
|
24
|
+
etcd (0.2.4)
|
25
|
+
mixlib-log
|
26
|
+
factory_girl (4.5.0)
|
27
|
+
activesupport (>= 3.0.0)
|
28
|
+
i18n (0.7.0)
|
29
|
+
json (1.8.2)
|
15
30
|
little-plugger (1.1.3)
|
16
|
-
logging (1.
|
31
|
+
logging (1.8.2)
|
17
32
|
little-plugger (>= 1.1.3)
|
33
|
+
multi_json (>= 1.8.4)
|
34
|
+
method_source (0.8.2)
|
35
|
+
minitest (5.5.1)
|
36
|
+
mixlib-log (1.6.0)
|
37
|
+
multi_json (1.10.1)
|
38
|
+
pry (0.10.1)
|
39
|
+
coderay (~> 1.1.0)
|
40
|
+
method_source (~> 0.8.1)
|
41
|
+
slop (~> 3.4)
|
18
42
|
rake (10.1.1)
|
19
|
-
rspec (
|
20
|
-
rspec-core (~>
|
21
|
-
rspec-expectations (~>
|
22
|
-
rspec-mocks (~>
|
23
|
-
rspec-core (
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
43
|
+
rspec (3.1.0)
|
44
|
+
rspec-core (~> 3.1.0)
|
45
|
+
rspec-expectations (~> 3.1.0)
|
46
|
+
rspec-mocks (~> 3.1.0)
|
47
|
+
rspec-core (3.1.7)
|
48
|
+
rspec-support (~> 3.1.0)
|
49
|
+
rspec-expectations (3.1.2)
|
50
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
51
|
+
rspec-support (~> 3.1.0)
|
52
|
+
rspec-mocks (3.1.3)
|
53
|
+
rspec-support (~> 3.1.0)
|
54
|
+
rspec-support (3.1.2)
|
55
|
+
slop (3.6.0)
|
56
|
+
thread_safe (0.3.4)
|
57
|
+
tzinfo (1.2.2)
|
58
|
+
thread_safe (~> 0.1)
|
59
|
+
zk (1.9.5)
|
60
|
+
logging (~> 1.8.2)
|
29
61
|
zookeeper (~> 1.4.0)
|
30
|
-
zookeeper (1.4.
|
62
|
+
zookeeper (1.4.10)
|
31
63
|
|
32
64
|
PLATFORMS
|
33
|
-
java
|
34
65
|
ruby
|
35
66
|
|
36
67
|
DEPENDENCIES
|
68
|
+
factory_girl
|
37
69
|
nerve!
|
70
|
+
pry
|
38
71
|
rake
|
39
|
-
rspec
|
72
|
+
rspec (~> 3.1.0)
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ Nerve simplifies underlying services, enables code reuse, and allows us to creat
|
|
15
15
|
It does so by factoring out the boilerplate into it's own application, which independenly handles monitoring and reporting.
|
16
16
|
|
17
17
|
Beyond those benefits, nerve also acts as a general watchdog on systems.
|
18
|
-
The information it reports can be used to take action from a
|
18
|
+
The information it reports can be used to take action from a centralized automation center: action like scaling distributed systems up or down or alerting ops or engineering about downtime.
|
19
19
|
|
20
20
|
## Installation ##
|
21
21
|
|
@@ -61,6 +61,16 @@ If you set your `reporter_type` to `"zookeeper"` you should also set these param
|
|
61
61
|
* `zk_hosts`: a list of the zookeeper hosts comprising the [ensemble](https://zookeeper.apache.org/doc/r3.1.2/zookeeperAdmin.html#sc_zkMulitServerSetup) that nerve will submit registration to
|
62
62
|
* `zk_path`: the path (or [znode](https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html#sc_zkDataModel_znodes)) where the registration will be created; nerve will create the [ephemeral node](https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html#Ephemeral+Nodes) that is the registration as a child of this path
|
63
63
|
|
64
|
+
#### Etcd Reporter ####
|
65
|
+
|
66
|
+
Note: Etcd support is currently experimental!
|
67
|
+
|
68
|
+
If you set your `reporter_type` to `"etcd"` you should also set these parameters:
|
69
|
+
|
70
|
+
* `etcd_host`: etcd host that nerve will submit registration to
|
71
|
+
* `etcd_port`: port to connect to etcd.
|
72
|
+
* `etcd_path`: the path where the registration will be created; nerve will create a node with a 30s ttl that is the registration as a child of this path, and then update it every few seconds
|
73
|
+
|
64
74
|
### Checks ###
|
65
75
|
|
66
76
|
The core of nerve is a set of service checks.
|
@@ -75,6 +85,14 @@ Although the exact parameters passed to each check are different, all take a num
|
|
75
85
|
* `rise`: (optional) how many consecutive checks must pass before the check is considered passing; defaults to 1
|
76
86
|
* `fall`: (optional) how many consecutive checks must fail before the check is considered failing; defaults to 1
|
77
87
|
|
88
|
+
#### Custom External Checks ####
|
89
|
+
|
90
|
+
If you would like to run a custom check but don't feel like trying to get it merged into this project, there is a mechanism for including external checks thanks to @bakins (airbnb/nerve#36).
|
91
|
+
Build your custom check as a separate gem and make sure to `bundle install` it on your system.
|
92
|
+
|
93
|
+
Ideally, you should name your gem `"nerve-watcher-#{type}"`, as that is what nerve will `require` on boot.
|
94
|
+
However, if you have a custom name for your gem, you can specify that in the `module` argument to the check.
|
95
|
+
|
78
96
|
## Contributing
|
79
97
|
|
80
98
|
1. Fork it
|
@@ -0,0 +1,18 @@
|
|
1
|
+
{
|
2
|
+
"host": "1.2.3.4",
|
3
|
+
"port": 3000,
|
4
|
+
"reporter_type": "etcd",
|
5
|
+
"etcd_host": "localhost",
|
6
|
+
"etcd_port": 4001,
|
7
|
+
"etcd_path": "/nerve/services/your_http_service/services",
|
8
|
+
"check_interval": 2,
|
9
|
+
"checks": [
|
10
|
+
{
|
11
|
+
"type": "http",
|
12
|
+
"uri": "/health",
|
13
|
+
"timeout": 0.2,
|
14
|
+
"rise": 3,
|
15
|
+
"fall": 2
|
16
|
+
}
|
17
|
+
]
|
18
|
+
}
|
File without changes
|
data/lib/nerve/reporter/base.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'nerve/reporter/base'
|
2
|
+
require 'etcd'
|
3
|
+
|
4
|
+
class Nerve::Reporter
|
5
|
+
class Etcd < Base
|
6
|
+
def initialize(service)
|
7
|
+
%w{etcd_host instance_id host port}.each do |required|
|
8
|
+
raise ArgumentError, "missing required argument #{required} for new service watcher" unless service[required]
|
9
|
+
end
|
10
|
+
@host = service['etcd_host']
|
11
|
+
@port = service['etcd_port'] || 4003
|
12
|
+
path = service['etcd_path'] || '/'
|
13
|
+
@path = path.split('/').push(service['instance_id']).join('/')
|
14
|
+
@data = parse_data({'host' => service['host'], 'port' => service['port'], 'name' => service['instance_id']})
|
15
|
+
@key = nil
|
16
|
+
@ttl = (service['check_interval'] || 0.5) * 5
|
17
|
+
@ttl = @ttl.ceil
|
18
|
+
end
|
19
|
+
|
20
|
+
def start()
|
21
|
+
log.info "nerve: connecting to etcd at #{@host}:#{@port}"
|
22
|
+
@etcd = ::Etcd.client(:host => @host, :port => @port)
|
23
|
+
log.info "nerve: successfully created etcd connection to #{@host}:#{@port}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop()
|
27
|
+
report_down
|
28
|
+
@etcd = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def report_up()
|
32
|
+
etcd_save
|
33
|
+
end
|
34
|
+
|
35
|
+
def report_down
|
36
|
+
etcd_delete
|
37
|
+
end
|
38
|
+
|
39
|
+
def update_data(new_data='')
|
40
|
+
# nothing in nerve calls this, but implement it like the zookeeper
|
41
|
+
# reporter just for fun.
|
42
|
+
@data = parse_data(new_data)
|
43
|
+
etcd_save
|
44
|
+
end
|
45
|
+
|
46
|
+
def ping?
|
47
|
+
# we get a ping every check_interval.
|
48
|
+
if @key
|
49
|
+
# we have made a key: save it to prevent the TTL from expiring.
|
50
|
+
etcd_save
|
51
|
+
else
|
52
|
+
# we haven't created a key, so just frob the etcd API to assure that
|
53
|
+
# it's alive.
|
54
|
+
@etcd.leader
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def etcd_delete
|
61
|
+
return unless @etcd and @key
|
62
|
+
begin
|
63
|
+
@etcd.delete(@key)
|
64
|
+
rescue ::Etcd::NotFile
|
65
|
+
rescue Errno::ECONNREFUSED
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def etcd_create
|
70
|
+
# we use create_in_order to create a unique key under our path,
|
71
|
+
# permitting multiple registrations from the same instance_id.
|
72
|
+
@key = @etcd.create_in_order(@path, :value => @data, :ttl => @ttl).key
|
73
|
+
log.info "registered etcd key #{@key} with value #{@data}, TTL #{@ttl}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def etcd_save
|
77
|
+
return etcd_create unless @key
|
78
|
+
@etcd.set(@key, :value => @data, :ttl => @ttl)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
@@ -23,7 +23,8 @@ class Nerve::Reporter
|
|
23
23
|
|
24
24
|
def stop()
|
25
25
|
log.info "nerve: closing zk connection at #{@path}"
|
26
|
-
|
26
|
+
report_down
|
27
|
+
@zk.close!
|
27
28
|
end
|
28
29
|
|
29
30
|
def report_up()
|
@@ -65,11 +66,6 @@ class Nerve::Reporter
|
|
65
66
|
zk_create
|
66
67
|
end
|
67
68
|
end
|
68
|
-
|
69
|
-
def parse_data(data)
|
70
|
-
return data if data.class == String
|
71
|
-
return data.to_json
|
72
|
-
end
|
73
69
|
end
|
74
70
|
end
|
75
71
|
|
data/lib/nerve/reporter.rb
CHANGED
@@ -21,6 +21,10 @@ module Nerve
|
|
21
21
|
@open_timeout = opts['open_timeout'] || 0.2
|
22
22
|
@ssl_timeout = opts['ssl_timeout'] || 0.2
|
23
23
|
|
24
|
+
@headers = opts['headers'] || {}
|
25
|
+
|
26
|
+
@expect = opts['expect']
|
27
|
+
|
24
28
|
@name = "http-#{@host}:#{@port}#{@uri}"
|
25
29
|
end
|
26
30
|
|
@@ -28,14 +32,15 @@ module Nerve
|
|
28
32
|
log.debug "running health check #{@name}"
|
29
33
|
|
30
34
|
connection = get_connection
|
31
|
-
response = connection.get(@uri)
|
35
|
+
response = connection.get(@uri, @headers)
|
32
36
|
code = response.code.to_i
|
37
|
+
body = response.body
|
33
38
|
|
34
|
-
if code >= 200 and code < 300
|
35
|
-
log.debug "nerve: check #{@name} got response code #{code} with body \"#{
|
39
|
+
if code >= 200 and code < 300 and (@expect == nil || body.include?(@expect))
|
40
|
+
log.debug "nerve: check #{@name} got response code #{code} with body \"#{body}\""
|
36
41
|
return true
|
37
42
|
else
|
38
|
-
log.error "nerve: check #{@name} got response code #{code} with body \"#{
|
43
|
+
log.error "nerve: check #{@name} got response code #{code} with body \"#{body}\""
|
39
44
|
return false
|
40
45
|
end
|
41
46
|
end
|
@@ -24,23 +24,39 @@ module Nerve
|
|
24
24
|
@service_checks = []
|
25
25
|
service['checks'] ||= []
|
26
26
|
service['checks'].each do |check|
|
27
|
+
# checks inherit attributes from the service overall
|
28
|
+
check['host'] ||= service['host']
|
29
|
+
check['port'] ||= service['port']
|
30
|
+
|
31
|
+
# generate a nice readable name for each check
|
32
|
+
check['name'] ||= "#{@name} #{check['type']}-#{check['host']}:#{check['port']}"
|
33
|
+
|
34
|
+
# make sure a type is set
|
27
35
|
check['type'] ||= "undefined"
|
28
|
-
|
29
|
-
|
30
|
-
|
36
|
+
|
37
|
+
# require a 3rd-party module if necessary for external checkers
|
38
|
+
unless ServiceCheck::CHECKS[check['type']]
|
39
|
+
m = check['module'] ? check['module'] : "nerve-watcher-#{check['type']}"
|
40
|
+
require m
|
41
|
+
end
|
42
|
+
|
43
|
+
# instantiate the check object
|
44
|
+
service_check_class = ServiceCheck::CHECKS[check['type']]
|
45
|
+
if service_check_class.nil?
|
31
46
|
raise ArgumentError,
|
32
47
|
"invalid service check type #{check['type']}; valid types: #{ServiceCheck::CHECKS.keys.join(',')}"
|
33
48
|
end
|
34
49
|
|
35
|
-
check
|
36
|
-
check['port'] ||= service['port']
|
37
|
-
check['name'] ||= "#{@name} #{check['type']}-#{check['host']}:#{check['port']}"
|
50
|
+
# save the check object
|
38
51
|
@service_checks << service_check_class.new(check)
|
39
52
|
end
|
40
53
|
|
41
54
|
# how often do we initiate service checks?
|
42
55
|
@check_interval = service['check_interval'] || 0.5
|
43
56
|
|
57
|
+
# force an initial report on startup
|
58
|
+
@was_up = nil
|
59
|
+
|
44
60
|
log.debug "nerve: created service watcher for #{@name} with #{@service_checks.size} checks"
|
45
61
|
end
|
46
62
|
|
@@ -48,28 +64,19 @@ module Nerve
|
|
48
64
|
log.info "nerve: starting service watch #{@name}"
|
49
65
|
|
50
66
|
@reporter.start()
|
51
|
-
was_up = false
|
52
67
|
|
53
68
|
until $EXIT
|
54
|
-
|
55
|
-
|
56
|
-
#
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
if
|
62
|
-
|
63
|
-
|
64
|
-
else
|
65
|
-
@reporter.report_down
|
66
|
-
log.warn "nerve: service #{@name} is now down"
|
67
|
-
end
|
68
|
-
was_up = is_up
|
69
|
+
check_and_report
|
70
|
+
|
71
|
+
# wait to run more checks but make sure to exit if $EXIT
|
72
|
+
# we avoid sleeping for the entire check interval at once
|
73
|
+
# so that nerve can exit promptly if required
|
74
|
+
nap_time = @check_interval
|
75
|
+
while nap_time > 0
|
76
|
+
break if $EXIT
|
77
|
+
sleep [nap_time, 1].min
|
78
|
+
nap_time -= 1
|
69
79
|
end
|
70
|
-
|
71
|
-
# wait to run more checks
|
72
|
-
sleep @check_interval
|
73
80
|
end
|
74
81
|
rescue StandardError => e
|
75
82
|
log.error "nerve: error in service watcher #{@name}: #{e.inspect}"
|
@@ -80,6 +87,25 @@ module Nerve
|
|
80
87
|
@reporter.stop
|
81
88
|
end
|
82
89
|
|
90
|
+
def check_and_report
|
91
|
+
@reporter.ping?
|
92
|
+
|
93
|
+
# what is the status of the service?
|
94
|
+
is_up = check?
|
95
|
+
log.debug "nerve: current service status for #{@name} is #{is_up.inspect}"
|
96
|
+
|
97
|
+
if is_up != @was_up
|
98
|
+
if is_up
|
99
|
+
@reporter.report_up
|
100
|
+
log.info "nerve: service #{@name} is now up"
|
101
|
+
else
|
102
|
+
@reporter.report_down
|
103
|
+
log.warn "nerve: service #{@name} is now down"
|
104
|
+
end
|
105
|
+
@was_up = is_up
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
83
109
|
def check?
|
84
110
|
@service_checks.each do |check|
|
85
111
|
return false unless check.up?
|
data/lib/nerve/version.rb
CHANGED
data/nerve.gemspec
CHANGED
@@ -17,9 +17,13 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
|
+
gem.add_runtime_dependency "json"
|
20
21
|
gem.add_runtime_dependency "zk", "~> 1.9.2"
|
21
22
|
gem.add_runtime_dependency "bunny", "= 1.1.0"
|
23
|
+
gem.add_runtime_dependency "etcd", "~> 0.2.3"
|
22
24
|
|
23
25
|
gem.add_development_dependency "rake"
|
24
|
-
gem.add_development_dependency "rspec"
|
26
|
+
gem.add_development_dependency "rspec", "~> 3.1.0"
|
27
|
+
gem.add_development_dependency "factory_girl"
|
28
|
+
gem.add_development_dependency "pry"
|
25
29
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'nerve/reporter'
|
3
|
+
require 'nerve/service_watcher'
|
4
|
+
|
5
|
+
describe "example services are valid" do
|
6
|
+
Dir.foreach("#{File.dirname(__FILE__)}/../example/nerve_services") do |item|
|
7
|
+
next if item == '.' or item == '..'
|
8
|
+
service_data = JSON.parse(IO.read("#{File.dirname(__FILE__)}/../example/nerve_services/#{item}"))
|
9
|
+
service_data['name'] = item.gsub(/\.json$/, '')
|
10
|
+
service_data['instance_id'] = '1'
|
11
|
+
|
12
|
+
context "when #{item} can be initialized as a valid reporter" do
|
13
|
+
it 'creates a valid reporter in new_from_service' do
|
14
|
+
reporter = nil
|
15
|
+
expect { reporter = Nerve::Reporter.new_from_service(service_data) }.to_not raise_error()
|
16
|
+
expect(reporter.is_a?(Nerve::Reporter::Base)).to eql(true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when #{item} can be initialized as a valid service watcher" do
|
21
|
+
it "creates a valid service watcher for #{item}" do
|
22
|
+
watcher = nil
|
23
|
+
expect { watcher = Nerve::ServiceWatcher.new(service_data) }.to_not raise_error()
|
24
|
+
expect(watcher.is_a?(Nerve::ServiceWatcher)).to eql(true)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -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
|
+
|
@@ -25,3 +25,18 @@ describe Nerve::Reporter do
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
class Nerve::Reporter::Test < Nerve::Reporter::Base
|
29
|
+
end
|
30
|
+
|
31
|
+
describe Nerve::Reporter::Test do
|
32
|
+
let(:subject) {Nerve::Reporter::Test.new({}) }
|
33
|
+
it 'has parse data method that passes strings' do
|
34
|
+
expect(subject.send(:parse_data, 'foobar')).to eql('foobar')
|
35
|
+
end
|
36
|
+
it 'jsonifies anything that is not a string' do
|
37
|
+
thing_to_parse = double()
|
38
|
+
expect(thing_to_parse).to receive(:to_json).and_return('{"some":"json"}')
|
39
|
+
expect(subject.send(:parse_data, thing_to_parse)).to eql('{"some":"json"}')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -13,5 +13,19 @@ describe Nerve::Reporter::Zookeeper do
|
|
13
13
|
it 'actually constructs an instance' do
|
14
14
|
expect(Nerve::Reporter::Zookeeper.new(subject).is_a?(Nerve::Reporter::Zookeeper)).to eql(true)
|
15
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(:create) { "full_path" }
|
21
|
+
expect(zk).to receive(:delete).with("full_path", anything())
|
22
|
+
|
23
|
+
allow(ZK).to receive(:new).and_return(zk)
|
24
|
+
|
25
|
+
reporter = Nerve::Reporter::Zookeeper.new(subject)
|
26
|
+
reporter.start
|
27
|
+
reporter.report_up
|
28
|
+
reporter.stop
|
29
|
+
end
|
16
30
|
end
|
17
31
|
|
@@ -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
|
data/spec/spec_helper.rb
CHANGED
@@ -6,12 +6,23 @@
|
|
6
6
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
7
|
require "#{File.dirname(__FILE__)}/../lib/nerve"
|
8
8
|
|
9
|
+
require 'factory_girl'
|
10
|
+
FactoryGirl.find_definitions
|
11
|
+
|
9
12
|
RSpec.configure do |config|
|
10
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
13
|
config.run_all_when_everything_filtered = true
|
12
14
|
config.filter_run :focus
|
13
15
|
config.include Config
|
14
16
|
|
17
|
+
# verify every double we can think of
|
18
|
+
config.mock_with :rspec do |mocks|
|
19
|
+
mocks.verify_doubled_constant_names = true
|
20
|
+
mocks.verify_partial_doubles = true
|
21
|
+
end
|
22
|
+
|
23
|
+
# include factory-girl when running tests
|
24
|
+
config.include FactoryGirl::Syntax::Methods
|
25
|
+
|
15
26
|
# Run specs in random order to surface order dependencies. If you find an
|
16
27
|
# order dependency and want to debug it, you can fix the order by providing
|
17
28
|
# the seed, which is printed after each run.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nerve
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,8 +11,24 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2015-02-19 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: json
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ! '>='
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '0'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: '0'
|
16
32
|
- !ruby/object:Gem::Dependency
|
17
33
|
name: zk
|
18
34
|
requirement: !ruby/object:Gem::Requirement
|
@@ -45,6 +61,22 @@ dependencies:
|
|
45
61
|
- - '='
|
46
62
|
- !ruby/object:Gem::Version
|
47
63
|
version: 1.1.0
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: etcd
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 0.2.3
|
72
|
+
type: :runtime
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 0.2.3
|
48
80
|
- !ruby/object:Gem::Dependency
|
49
81
|
name: rake
|
50
82
|
requirement: !ruby/object:Gem::Requirement
|
@@ -63,6 +95,38 @@ dependencies:
|
|
63
95
|
version: '0'
|
64
96
|
- !ruby/object:Gem::Dependency
|
65
97
|
name: rspec
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.1.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ~>
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 3.1.0
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: factory_girl
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
type: :development
|
121
|
+
prerelease: false
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ! '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
- !ruby/object:Gem::Dependency
|
129
|
+
name: pry
|
66
130
|
requirement: !ruby/object:Gem::Requirement
|
67
131
|
none: false
|
68
132
|
requirements:
|
@@ -99,11 +163,13 @@ files:
|
|
99
163
|
- Vagrantfile
|
100
164
|
- bin/nerve
|
101
165
|
- example/nerve.conf.json
|
102
|
-
- example/nerve_services/
|
166
|
+
- example/nerve_services/etcd_service1.json
|
167
|
+
- example/nerve_services/zookeeper_service1.json
|
103
168
|
- lib/nerve.rb
|
104
169
|
- lib/nerve/log.rb
|
105
170
|
- lib/nerve/reporter.rb
|
106
171
|
- lib/nerve/reporter/base.rb
|
172
|
+
- lib/nerve/reporter/etcd.rb
|
107
173
|
- lib/nerve/reporter/zookeeper.rb
|
108
174
|
- lib/nerve/ring_buffer.rb
|
109
175
|
- lib/nerve/service_watcher.rb
|
@@ -115,8 +181,13 @@ files:
|
|
115
181
|
- lib/nerve/version.rb
|
116
182
|
- nerve.gemspec
|
117
183
|
- spec/.gitkeep
|
184
|
+
- spec/example_services_spec.rb
|
185
|
+
- spec/factories/check.rb
|
186
|
+
- spec/factories/service.rb
|
187
|
+
- spec/lib/nerve/reporter_etcd_spec.rb
|
118
188
|
- spec/lib/nerve/reporter_spec.rb
|
119
189
|
- spec/lib/nerve/reporter_zookeeper_spec.rb
|
190
|
+
- spec/lib/nerve/service_watcher_spec.rb
|
120
191
|
- spec/spec_helper.rb
|
121
192
|
homepage: ''
|
122
193
|
licenses: []
|
@@ -138,12 +209,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
209
|
version: '0'
|
139
210
|
requirements: []
|
140
211
|
rubyforge_project:
|
141
|
-
rubygems_version: 1.8.23
|
212
|
+
rubygems_version: 1.8.23.2
|
142
213
|
signing_key:
|
143
214
|
specification_version: 3
|
144
215
|
summary: summary
|
145
216
|
test_files:
|
146
217
|
- spec/.gitkeep
|
218
|
+
- spec/example_services_spec.rb
|
219
|
+
- spec/factories/check.rb
|
220
|
+
- spec/factories/service.rb
|
221
|
+
- spec/lib/nerve/reporter_etcd_spec.rb
|
147
222
|
- spec/lib/nerve/reporter_spec.rb
|
148
223
|
- spec/lib/nerve/reporter_zookeeper_spec.rb
|
224
|
+
- spec/lib/nerve/service_watcher_spec.rb
|
149
225
|
- spec/spec_helper.rb
|