nerve 0.5.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.travis.yml +4 -1
- data/Gemfile.lock +5 -2
- data/README.md +2 -0
- data/example/nerve_services/etcd_service1.json +1 -0
- data/example/nerve_services/zookeeper_service1.json +1 -0
- data/lib/nerve/reporter/base.rb +15 -2
- data/lib/nerve/reporter/etcd.rb +2 -11
- data/lib/nerve/reporter/zookeeper.rb +48 -18
- data/lib/nerve/service_watcher/http.rb +1 -1
- data/lib/nerve/service_watcher.rb +5 -2
- data/lib/nerve/version.rb +1 -1
- data/lib/nerve.rb +31 -7
- data/nerve.gemspec +7 -3
- data/spec/example_services_spec.rb +8 -0
- data/spec/lib/nerve/reporter_spec.rb +36 -6
- data/spec/lib/nerve/reporter_zookeeper_spec.rb +1 -0
- data/spec/spec_helper.rb +2 -1
- metadata +8 -5
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
nerve (0.
|
4
|
+
nerve (0.6.0)
|
5
5
|
bunny (= 1.1.0)
|
6
6
|
etcd (~> 0.2.3)
|
7
7
|
json
|
@@ -34,7 +34,7 @@ GEM
|
|
34
34
|
method_source (0.8.2)
|
35
35
|
minitest (5.5.1)
|
36
36
|
mixlib-log (1.6.0)
|
37
|
-
multi_json (1.
|
37
|
+
multi_json (1.11.2)
|
38
38
|
pry (0.10.1)
|
39
39
|
coderay (~> 1.1.0)
|
40
40
|
method_source (~> 0.8.1)
|
@@ -70,3 +70,6 @@ DEPENDENCIES
|
|
70
70
|
pry
|
71
71
|
rake
|
72
72
|
rspec (~> 3.1.0)
|
73
|
+
|
74
|
+
BUNDLED WITH
|
75
|
+
1.10.5
|
data/README.md
CHANGED
@@ -39,6 +39,7 @@ An example config file is available in `example/nerve.conf.json`.
|
|
39
39
|
The config file is composed of two main sections:
|
40
40
|
|
41
41
|
* `instance_id`: the name nerve will submit when registering services; makes debugging easier
|
42
|
+
* `heartbeat_path`: a path to a file on disk to touch as nerve makes progress. This allows you to work around https://github.com/zk-ruby/zk/issues/50 by restarting a stuck nerve.
|
42
43
|
* `services`: the hash (from service name to config) of the services nerve will be monitoring
|
43
44
|
* `service_conf_dir`: path to a directory in which each json file will be interpreted as a service with the basename of the file minus the .json extension
|
44
45
|
|
@@ -53,6 +54,7 @@ The configuration contains the following options:
|
|
53
54
|
* `reporter_type`: the mechanism used to report up/down information; depending on the reporter you choose, additional parameters may be required. Defaults to `zookeeper`
|
54
55
|
* `check_interval`: the frequency with which service checks will be initiated; defaults to `500ms`
|
55
56
|
* `checks`: a list of checks that nerve will perform; if all of the pass, the service will be registered; otherwise, it will be un-registered
|
57
|
+
* `weight`: a positive integer weight value which can be used to affect the haproxy backend weighting in synapse.
|
56
58
|
|
57
59
|
#### Zookeeper Reporter ####
|
58
60
|
|
data/lib/nerve/reporter/base.rb
CHANGED
@@ -19,10 +19,23 @@ class Nerve::Reporter
|
|
19
19
|
def report_down
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
22
|
+
def ping?
|
23
23
|
end
|
24
24
|
|
25
|
-
def
|
25
|
+
def get_service_data(service)
|
26
|
+
%w{instance_id host port}.each do |required|
|
27
|
+
raise ArgumentError, "missing required argument #{required} for new service watcher" unless service[required]
|
28
|
+
end
|
29
|
+
d = {'host' => service['host'], 'port' => service['port'], 'name' => service['instance_id']}
|
30
|
+
return d unless service.has_key?('weight')
|
31
|
+
|
32
|
+
# Weight is optional, but it should be well formed if supplied
|
33
|
+
if service['weight'].to_i >= 0 and "#{service['weight']}".match /^\d+$/
|
34
|
+
d['weight'] = service['weight'].to_i
|
35
|
+
else
|
36
|
+
raise ArgumentError, "invalid 'weight' argument in service data: #{service.inspect}"
|
37
|
+
end
|
38
|
+
d
|
26
39
|
end
|
27
40
|
|
28
41
|
protected
|
data/lib/nerve/reporter/etcd.rb
CHANGED
@@ -4,14 +4,12 @@ require 'etcd'
|
|
4
4
|
class Nerve::Reporter
|
5
5
|
class Etcd < Base
|
6
6
|
def initialize(service)
|
7
|
-
|
8
|
-
raise ArgumentError, "missing required argument #{required} for new service watcher" unless service[required]
|
9
|
-
end
|
7
|
+
raise ArgumentError, "missing required argument etcd_host for new service watcher" unless service['etcd_host']
|
10
8
|
@host = service['etcd_host']
|
11
9
|
@port = service['etcd_port'] || 4003
|
12
10
|
path = service['etcd_path'] || '/'
|
13
11
|
@path = path.split('/').push(service['instance_id']).join('/')
|
14
|
-
@data = parse_data(
|
12
|
+
@data = parse_data(get_service_data(service))
|
15
13
|
@key = nil
|
16
14
|
@ttl = (service['check_interval'] || 0.5) * 5
|
17
15
|
@ttl = @ttl.ceil
|
@@ -36,13 +34,6 @@ class Nerve::Reporter
|
|
36
34
|
etcd_delete
|
37
35
|
end
|
38
36
|
|
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
37
|
def ping?
|
47
38
|
# we get a ping every check_interval.
|
48
39
|
if @key
|
@@ -1,30 +1,64 @@
|
|
1
1
|
require 'nerve/reporter/base'
|
2
|
+
require 'thread'
|
2
3
|
require 'zk'
|
3
4
|
|
5
|
+
|
4
6
|
class Nerve::Reporter
|
5
7
|
class Zookeeper < Base
|
8
|
+
@@zk_pool = {}
|
9
|
+
@@zk_pool_count = {}
|
10
|
+
@@zk_pool_lock = Mutex.new
|
11
|
+
|
6
12
|
def initialize(service)
|
7
|
-
%w{zk_hosts zk_path
|
13
|
+
%w{zk_hosts zk_path}.each do |required|
|
8
14
|
raise ArgumentError, "missing required argument #{required} for new service watcher" unless service[required]
|
9
15
|
end
|
10
|
-
|
11
|
-
@
|
16
|
+
# Since we pool we get one connection per zookeeper cluster
|
17
|
+
@zk_connection_string = service['zk_hosts'].sort.join(',')
|
18
|
+
@data = parse_data(get_service_data(service))
|
12
19
|
|
13
|
-
@
|
20
|
+
@zk_path = service['zk_path']
|
21
|
+
@key_prefix = @zk_path + "/#{service['instance_id']}_"
|
14
22
|
@full_key = nil
|
15
23
|
end
|
16
24
|
|
17
25
|
def start()
|
18
|
-
log.info "nerve: waiting to connect to zookeeper
|
19
|
-
|
20
|
-
|
21
|
-
|
26
|
+
log.info "nerve: waiting to connect to zookeeper to #{@zk_connection_string}"
|
27
|
+
# Ensure that all Zookeeper reporters re-use a single zookeeper
|
28
|
+
# connection to any given set of zk hosts.
|
29
|
+
@@zk_pool_lock.synchronize {
|
30
|
+
unless @@zk_pool.has_key?(@zk_connection_string)
|
31
|
+
log.info "nerve: creating pooled connection to #{@zk_connection_string}"
|
32
|
+
@@zk_pool[@zk_connection_string] = ZK.new(@zk_connection_string, :timeout => 5)
|
33
|
+
@@zk_pool_count[@zk_connection_string] = 1
|
34
|
+
log.info "nerve: successfully created zk connection to #{@zk_connection_string}"
|
35
|
+
else
|
36
|
+
@@zk_pool_count[@zk_connection_string] += 1
|
37
|
+
log.info "nerve: re-using existing zookeeper connection to #{@zk_connection_string}"
|
38
|
+
end
|
39
|
+
@zk = @@zk_pool[@zk_connection_string]
|
40
|
+
log.info "nerve: retrieved zk connection to #{@zk_connection_string}"
|
41
|
+
}
|
22
42
|
end
|
23
43
|
|
24
44
|
def stop()
|
25
|
-
log.info "nerve:
|
26
|
-
|
27
|
-
|
45
|
+
log.info "nerve: removing zk node at #{@full_key}" if @full_key
|
46
|
+
begin
|
47
|
+
report_down
|
48
|
+
ensure
|
49
|
+
@@zk_pool_lock.synchronize {
|
50
|
+
@@zk_pool_count[@zk_connection_string] -= 1
|
51
|
+
# Last thread to use the connection closes it
|
52
|
+
if @@zk_pool_count[@zk_connection_string] == 0
|
53
|
+
log.info "nerve: closing zk connection to #{@zk_connection_string}"
|
54
|
+
begin
|
55
|
+
@zk.close!
|
56
|
+
ensure
|
57
|
+
@@zk_pool.delete(@zk_connection_string)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
}
|
61
|
+
end
|
28
62
|
end
|
29
63
|
|
30
64
|
def report_up()
|
@@ -35,13 +69,8 @@ class Nerve::Reporter
|
|
35
69
|
zk_delete
|
36
70
|
end
|
37
71
|
|
38
|
-
def update_data(new_data='')
|
39
|
-
@data = parse_data(new_data)
|
40
|
-
zk_save
|
41
|
-
end
|
42
|
-
|
43
72
|
def ping?
|
44
|
-
return @zk.
|
73
|
+
return @zk.connected? && @zk.exists?(@full_key || '/')
|
45
74
|
end
|
46
75
|
|
47
76
|
private
|
@@ -54,7 +83,8 @@ class Nerve::Reporter
|
|
54
83
|
end
|
55
84
|
|
56
85
|
def zk_create
|
57
|
-
@
|
86
|
+
@zk.mkdir_p(@zk_path)
|
87
|
+
@full_key = @zk.create(@key_prefix, :data => @data, :mode => :ephemeral_sequential)
|
58
88
|
end
|
59
89
|
|
60
90
|
def zk_save
|
@@ -40,7 +40,7 @@ module Nerve
|
|
40
40
|
log.debug "nerve: check #{@name} got response code #{code} with body \"#{body}\""
|
41
41
|
return true
|
42
42
|
else
|
43
|
-
log.
|
43
|
+
log.warn "nerve: check #{@name} got response code #{code} with body \"#{body}\""
|
44
44
|
return false
|
45
45
|
end
|
46
46
|
end
|
@@ -83,12 +83,15 @@ module Nerve
|
|
83
83
|
raise e
|
84
84
|
ensure
|
85
85
|
log.info "nerve: ending service watch #{@name}"
|
86
|
-
$EXIT = true
|
87
86
|
@reporter.stop
|
88
87
|
end
|
89
88
|
|
90
89
|
def check_and_report
|
91
|
-
|
90
|
+
if !@reporter.ping?
|
91
|
+
# If the reporter can't ping, then we do not know the status
|
92
|
+
# and must force a new report.
|
93
|
+
@was_up = nil
|
94
|
+
end
|
92
95
|
|
93
96
|
# what is the status of the service?
|
94
97
|
is_up = check?
|
data/lib/nerve/version.rb
CHANGED
data/lib/nerve.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'fileutils'
|
1
2
|
require 'logger'
|
2
3
|
require 'json'
|
3
4
|
require 'timeout'
|
@@ -28,12 +29,9 @@ module Nerve
|
|
28
29
|
|
29
30
|
@instance_id = opts['instance_id']
|
30
31
|
@services = opts['services']
|
32
|
+
@heartbeat_path = opts['heartbeat_path']
|
31
33
|
@watchers = {}
|
32
34
|
|
33
|
-
# Any exceptions in the watcher threads should wake the main thread so
|
34
|
-
# that we can fail fast.
|
35
|
-
Thread.abort_on_exception = true
|
36
|
-
|
37
35
|
log.debug 'nerve: completed init'
|
38
36
|
end
|
39
37
|
|
@@ -45,15 +43,41 @@ module Nerve
|
|
45
43
|
end
|
46
44
|
|
47
45
|
begin
|
48
|
-
|
49
|
-
|
46
|
+
loop do
|
47
|
+
# Check that watcher threads are still alive, auto-remediate if they
|
48
|
+
# are not. Sometimes zookeeper flakes out or connections are lost to
|
49
|
+
# remote datacenter zookeeper clusters, failing is not an option
|
50
|
+
relaunch = []
|
51
|
+
@watchers.each do |name, watcher_thread|
|
52
|
+
unless watcher_thread.alive?
|
53
|
+
relaunch << name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
relaunch.each do |name|
|
58
|
+
begin
|
59
|
+
log.warn "nerve: watcher #{name} not alive; reaping and relaunching"
|
60
|
+
reap_watcher(name)
|
61
|
+
rescue => e
|
62
|
+
log.warn "nerve: could not reap #{name}, got #{e.inspect}"
|
63
|
+
end
|
64
|
+
launch_watcher(name, @services[name])
|
65
|
+
end
|
66
|
+
|
67
|
+
unless @heartbeat_path.nil?
|
68
|
+
FileUtils.touch(@heartbeat_path)
|
69
|
+
end
|
70
|
+
|
71
|
+
sleep 10
|
72
|
+
end
|
73
|
+
rescue => e
|
50
74
|
log.error "nerve: encountered unexpected exception #{e.inspect} in main thread"
|
51
75
|
raise e
|
52
76
|
ensure
|
53
77
|
$EXIT = true
|
54
78
|
log.warn 'nerve: reaping all watchers'
|
55
79
|
@watchers.each do |name, watcher_thread|
|
56
|
-
reap_watcher(name)
|
80
|
+
reap_watcher(name) rescue "nerve: watcher #{name} could not be immediately reaped; skippping"
|
57
81
|
end
|
58
82
|
end
|
59
83
|
|
data/nerve.gemspec
CHANGED
@@ -8,9 +8,13 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = Nerve::VERSION
|
9
9
|
gem.authors = ["Martin Rhoads", "Igor Serebryany", "Pierre Carrier"]
|
10
10
|
gem.email = ["martin.rhoads@airbnb.com", "igor.serebryany@airbnb.com"]
|
11
|
-
gem.description =
|
12
|
-
|
13
|
-
|
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"
|
14
18
|
|
15
19
|
gem.files = `git ls-files`.split($/)
|
16
20
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
@@ -1,7 +1,12 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'nerve/reporter'
|
3
|
+
require 'nerve/reporter/base'
|
3
4
|
require 'nerve/service_watcher'
|
4
5
|
|
6
|
+
class Nerve::Reporter::Base
|
7
|
+
attr_reader :data
|
8
|
+
end
|
9
|
+
|
5
10
|
describe "example services are valid" do
|
6
11
|
Dir.foreach("#{File.dirname(__FILE__)}/../example/nerve_services") do |item|
|
7
12
|
next if item == '.' or item == '..'
|
@@ -15,6 +20,9 @@ describe "example services are valid" do
|
|
15
20
|
expect { reporter = Nerve::Reporter.new_from_service(service_data) }.to_not raise_error()
|
16
21
|
expect(reporter.is_a?(Nerve::Reporter::Base)).to eql(true)
|
17
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
|
18
26
|
end
|
19
27
|
|
20
28
|
context "when #{item} can be initialized as a valid service watcher" do
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'nerve/reporter/zookeeper'
|
2
3
|
|
3
4
|
describe Nerve::Reporter do
|
4
5
|
let(:subject) { {
|
@@ -30,13 +31,42 @@ end
|
|
30
31
|
|
31
32
|
describe Nerve::Reporter::Test do
|
32
33
|
let(:subject) {Nerve::Reporter::Test.new({}) }
|
33
|
-
|
34
|
-
|
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
|
35
43
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
40
70
|
end
|
41
71
|
end
|
42
72
|
|
@@ -17,6 +17,7 @@ describe Nerve::Reporter::Zookeeper do
|
|
17
17
|
it 'deregisters service on exit' do
|
18
18
|
zk = double("zk")
|
19
19
|
allow(zk).to receive(:close!)
|
20
|
+
expect(zk).to receive(:mkdir_p) { "zk_path" }
|
20
21
|
expect(zk).to receive(:create) { "full_path" }
|
21
22
|
expect(zk).to receive(:delete).with("full_path", anything())
|
22
23
|
|
data/spec/spec_helper.rb
CHANGED
@@ -12,7 +12,8 @@ FactoryGirl.find_definitions
|
|
12
12
|
RSpec.configure do |config|
|
13
13
|
config.run_all_when_everything_filtered = true
|
14
14
|
config.filter_run :focus
|
15
|
-
config.include
|
15
|
+
config.include RbConfig
|
16
|
+
config.color = true
|
16
17
|
|
17
18
|
# verify every double we can think of
|
18
19
|
config.mock_with :rspec do |mocks|
|
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.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2015-
|
14
|
+
date: 2015-08-04 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: json
|
@@ -141,7 +141,10 @@ dependencies:
|
|
141
141
|
- - ! '>='
|
142
142
|
- !ruby/object:Gem::Version
|
143
143
|
version: '0'
|
144
|
-
description:
|
144
|
+
description: Nerve is a service registration daemon. It performs health checks on
|
145
|
+
your service and then publishes success or failure into one of several registries
|
146
|
+
(currently, zookeeper or etcd). Nerve is half or SmartStack, and is designed to
|
147
|
+
be operated along with Synapse to provide a full service discovery framework
|
145
148
|
email:
|
146
149
|
- martin.rhoads@airbnb.com
|
147
150
|
- igor.serebryany@airbnb.com
|
@@ -189,7 +192,7 @@ files:
|
|
189
192
|
- spec/lib/nerve/reporter_zookeeper_spec.rb
|
190
193
|
- spec/lib/nerve/service_watcher_spec.rb
|
191
194
|
- spec/spec_helper.rb
|
192
|
-
homepage:
|
195
|
+
homepage: https://github.com/airbnb/nerve
|
193
196
|
licenses: []
|
194
197
|
post_install_message:
|
195
198
|
rdoc_options: []
|
@@ -212,7 +215,7 @@ rubyforge_project:
|
|
212
215
|
rubygems_version: 1.8.23.2
|
213
216
|
signing_key:
|
214
217
|
specification_version: 3
|
215
|
-
summary:
|
218
|
+
summary: A service registration daemon
|
216
219
|
test_files:
|
217
220
|
- spec/.gitkeep
|
218
221
|
- spec/example_services_spec.rb
|