nerve 0.5.4 → 0.6.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.
- 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
|