nerve 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +13 -5
- data/Gemfile.lock +1 -1
- data/bin/nerve +6 -67
- data/lib/nerve/configuration_manager.rb +90 -0
- data/lib/nerve/service_watcher.rb +43 -10
- data/lib/nerve/utils.rb +9 -0
- data/lib/nerve/version.rb +1 -1
- data/lib/nerve.rb +146 -33
- data/nerve.gemspec +2 -2
- data/spec/configuration_manager_spec.rb +31 -0
- data/spec/lib/nerve_spec.rb +186 -0
- data/spec/spec_helper.rb +1 -0
- metadata +30 -23
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZDU0ZGNiMzk5OGRiZGU4ZjQwYTFmMmZiNzA5NGFiMWU2ODIzOGY2MQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NzJkZTAwMmIyOTQ0MDEyMGFiYTZlOTJjMGM3YTUwMmJmYTVmYjNmNg==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZjczNGM1MzEzOTFkNmJhOTdlYmI0MjE4YWE2ODQ4NWViNDY4MGI5Y2FkZDM3
|
10
|
+
NTA4MzQxNDUzZGE1MWIyODc3Y2MyOGQyYzgzZTE2ZGNjNGQ3MmExZDVmMTU0
|
11
|
+
ZmFjZTc1NzBjYjA2OWEyMjgwMDIxN2FiMTFjOWE0YThiYzFlYzc=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YTYyYzE3NDVmOTk4NGQzYTQ5MThlZWExOTdjNDY0OGMyMjU1MjhhYWRjNWE1
|
14
|
+
ZGZlOTgzNjcxZWUyYTIwNjVhMzNjNzY4NTg2ZTFjYzc0NzM1NDliOGIzNmEx
|
15
|
+
ZTY0MTkzZGE4NmNmMjdiYTRiN2RlMDQ4NDJhYWQ4OWNjYWNjZGI=
|
data/Gemfile.lock
CHANGED
data/bin/nerve
CHANGED
@@ -4,74 +4,13 @@ require 'yaml'
|
|
4
4
|
require 'optparse'
|
5
5
|
|
6
6
|
require 'nerve'
|
7
|
+
require 'nerve/configuration_manager'
|
7
8
|
|
8
|
-
options
|
9
|
-
|
10
|
-
|
11
|
-
optparse = OptionParser.new do |opts|
|
12
|
-
opts.banner =<<EOB
|
13
|
-
Welcome to nerve
|
14
|
-
|
15
|
-
Usage: nerve --config /path/to/nerve/config
|
16
|
-
EOB
|
17
|
-
|
18
|
-
options[:config] = ENV['NERVE_CONFIG']
|
19
|
-
opts.on('-c config','--config config', String, 'path to nerve config') do |key,value|
|
20
|
-
options[:config] = key
|
21
|
-
end
|
22
|
-
|
23
|
-
options[:instance_id] = ENV['NERVE_INSTANCE_ID']
|
24
|
-
opts.on('-i instance_id','--instance_id instance_id', String,
|
25
|
-
'reported as `name` to ZK; overrides instance id from config file') do |key,value|
|
26
|
-
options[:instance_id] = key
|
27
|
-
end
|
28
|
-
|
29
|
-
opts.on('-h', '--help', 'Display this screen') do
|
30
|
-
puts opts
|
31
|
-
exit
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
|
37
|
-
# parse command line arguments
|
38
|
-
optparse.parse!
|
39
|
-
|
40
|
-
def parseconfig(filename)
|
41
|
-
# parse synapse config file
|
42
|
-
begin
|
43
|
-
c = YAML::parse(File.read(filename))
|
44
|
-
rescue Errno::ENOENT => e
|
45
|
-
raise ArgumentError, "config file does not exist:\n#{e.inspect}"
|
46
|
-
rescue Errno::EACCES => e
|
47
|
-
raise ArgumentError, "could not open config file:\n#{e.inspect}"
|
48
|
-
rescue YAML::ParseError => e
|
49
|
-
raise "config file #{filename} is not yaml:\n#{e.inspect}"
|
50
|
-
end
|
51
|
-
return c.to_ruby
|
52
|
-
end
|
53
|
-
|
54
|
-
config = parseconfig(options[:config])
|
55
|
-
config['services'] ||= {}
|
56
|
-
|
57
|
-
if config.has_key?('service_conf_dir')
|
58
|
-
cdir = File.expand_path(config['service_conf_dir'])
|
59
|
-
if ! Dir.exists?(cdir) then
|
60
|
-
raise "service conf dir does not exist:#{cdir}"
|
61
|
-
end
|
62
|
-
cfiles = Dir.glob(File.join(cdir, '*.{yaml,json}'))
|
63
|
-
cfiles.each { |x| config['services'][File.basename(x[/(.*)\.(yaml|json)$/, 1])] = parseconfig(x) }
|
64
|
-
end
|
65
|
-
|
66
|
-
if options[:instance_id] && !options[:instance_id].empty?
|
67
|
-
config['instance_id'] = options[:instance_id]
|
68
|
-
end
|
69
|
-
|
70
|
-
# create nerve object
|
71
|
-
s = Nerve::Nerve.new config
|
72
|
-
|
73
|
-
# start nerve
|
74
|
-
s.run
|
9
|
+
# Parse command line options and parse configuration
|
10
|
+
config_manager = Nerve::ConfigurationManager.new()
|
11
|
+
config_manager.parse_options!
|
75
12
|
|
13
|
+
nerve_application = Nerve::Nerve.new(config_manager)
|
14
|
+
nerve_application.run
|
76
15
|
|
77
16
|
puts "exiting nerve"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Nerve
|
5
|
+
class ConfigurationManager
|
6
|
+
attr_reader :options
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
def parse_options_from_argv!
|
10
|
+
options = {}
|
11
|
+
# set command line options
|
12
|
+
optparse = OptionParser.new do |opts|
|
13
|
+
opts.banner =<<EOB
|
14
|
+
Welcome to nerve
|
15
|
+
|
16
|
+
Usage: nerve --config /path/to/nerve/config
|
17
|
+
EOB
|
18
|
+
|
19
|
+
options[:config] = ENV['NERVE_CONFIG']
|
20
|
+
opts.on('-c config','--config config', String, 'path to nerve config') do |key,value|
|
21
|
+
options[:config] = key
|
22
|
+
end
|
23
|
+
|
24
|
+
options[:instance_id] = ENV['NERVE_INSTANCE_ID']
|
25
|
+
opts.on('-i instance_id','--instance_id instance_id', String,
|
26
|
+
'reported as `name` to ZK; overrides instance id from config file') do |key,value|
|
27
|
+
options[:instance_id] = key
|
28
|
+
end
|
29
|
+
|
30
|
+
options[:check_config] = ENV['NERVE_CHECK_CONFIG']
|
31
|
+
opts.on('-k', '--check-config',
|
32
|
+
'Validate the nerve config ONLY and exit 0 if valid (non zero otherwise)') do |_|
|
33
|
+
options[:check_config] = true
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('-h', '--help', 'Display this screen') do
|
37
|
+
puts opts
|
38
|
+
exit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
optparse.parse!
|
42
|
+
options
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_options!
|
46
|
+
@options = parse_options_from_argv!
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_nerve_config(options)
|
50
|
+
config = parse_config_file(options[:config])
|
51
|
+
config['services'] ||= {}
|
52
|
+
|
53
|
+
if config.has_key?('service_conf_dir')
|
54
|
+
cdir = File.expand_path(config['service_conf_dir'])
|
55
|
+
if ! Dir.exists?(cdir) then
|
56
|
+
raise "service conf dir does not exist:#{cdir}"
|
57
|
+
end
|
58
|
+
cfiles = Dir.glob(File.join(cdir, '*.{yaml,json}'))
|
59
|
+
cfiles.each { |x| config['services'][File.basename(x[/(.*)\.(yaml|json)$/, 1])] = parse_config_file(x) }
|
60
|
+
end
|
61
|
+
|
62
|
+
if options[:instance_id] && !options[:instance_id].empty?
|
63
|
+
config['instance_id'] = options[:instance_id]
|
64
|
+
end
|
65
|
+
|
66
|
+
config
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_config_file(filename)
|
70
|
+
# parse nerve config file
|
71
|
+
begin
|
72
|
+
c = YAML::parse(File.read(filename))
|
73
|
+
rescue Errno::ENOENT => e
|
74
|
+
raise ArgumentError, "config file does not exist:\n#{e.inspect}"
|
75
|
+
rescue Errno::EACCES => e
|
76
|
+
raise ArgumentError, "could not open config file:\n#{e.inspect}"
|
77
|
+
rescue YAML::SyntaxError => e
|
78
|
+
raise "config file #{filename} is not proper yaml:\n#{e.inspect}"
|
79
|
+
end
|
80
|
+
return c.to_ruby
|
81
|
+
end
|
82
|
+
|
83
|
+
def reload!
|
84
|
+
raise "You must parse command line options before reloading config" if @options.nil?
|
85
|
+
@config = generate_nerve_config(@options)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
@@ -7,6 +7,8 @@ module Nerve
|
|
7
7
|
include Utils
|
8
8
|
include Logging
|
9
9
|
|
10
|
+
attr_reader :was_up
|
11
|
+
|
10
12
|
def initialize(service={})
|
11
13
|
log.debug "nerve: creating service watcher object"
|
12
14
|
|
@@ -17,7 +19,7 @@ module Nerve
|
|
17
19
|
|
18
20
|
@name = service['name']
|
19
21
|
|
20
|
-
# configure the reporter, which we use for
|
22
|
+
# configure the reporter, which we use for reporting status to the registry
|
21
23
|
@reporter = Reporter.new_from_service(service)
|
22
24
|
|
23
25
|
# instantiate the checks for this service
|
@@ -57,32 +59,57 @@ module Nerve
|
|
57
59
|
# force an initial report on startup
|
58
60
|
@was_up = nil
|
59
61
|
|
62
|
+
# when this watcher is started it will store the
|
63
|
+
# thread here
|
64
|
+
@run_thread = nil
|
65
|
+
@should_finish = false
|
66
|
+
|
60
67
|
log.debug "nerve: created service watcher for #{@name} with #{@service_checks.size} checks"
|
61
68
|
end
|
62
69
|
|
70
|
+
def start()
|
71
|
+
unless @run_thread
|
72
|
+
@run_thread = Thread.new { self.run() }
|
73
|
+
else
|
74
|
+
log.error "nerve: tried to double start a watcher"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stop()
|
79
|
+
log.info "nerve: stopping service watch #{@name}"
|
80
|
+
@should_finish = true
|
81
|
+
return true if @run_thread.nil?
|
82
|
+
|
83
|
+
unclean_shutdown = @run_thread.join(10).nil?
|
84
|
+
if unclean_shutdown
|
85
|
+
log.error "nerve: unclean shutdown of #{@name}, killing thread"
|
86
|
+
Thread.kill(@run_thread)
|
87
|
+
end
|
88
|
+
@run_thread = nil
|
89
|
+
!unclean_shutdown
|
90
|
+
end
|
91
|
+
|
92
|
+
def alive?()
|
93
|
+
!@run_thread.nil? && @run_thread.alive?
|
94
|
+
end
|
95
|
+
|
63
96
|
def run()
|
64
97
|
log.info "nerve: starting service watch #{@name}"
|
65
|
-
|
66
98
|
@reporter.start()
|
67
99
|
|
68
|
-
until
|
100
|
+
until watcher_should_exit?
|
69
101
|
check_and_report
|
70
102
|
|
71
103
|
# wait to run more checks but make sure to exit if $EXIT
|
72
104
|
# we avoid sleeping for the entire check interval at once
|
73
105
|
# so that nerve can exit promptly if required
|
74
|
-
|
75
|
-
while nap_time > 0
|
76
|
-
break if $EXIT
|
77
|
-
sleep [nap_time, 1].min
|
78
|
-
nap_time -= 1
|
79
|
-
end
|
106
|
+
responsive_sleep (@check_interval) { watcher_should_exit? }
|
80
107
|
end
|
81
108
|
rescue StandardError => e
|
82
109
|
log.error "nerve: error in service watcher #{@name}: #{e.inspect}"
|
83
110
|
raise e
|
84
111
|
ensure
|
85
|
-
log.info "nerve:
|
112
|
+
log.info "nerve: stopping reporter for #{@name}"
|
86
113
|
@reporter.stop
|
87
114
|
end
|
88
115
|
|
@@ -115,5 +142,11 @@ module Nerve
|
|
115
142
|
end
|
116
143
|
return true
|
117
144
|
end
|
145
|
+
|
146
|
+
private
|
147
|
+
def watcher_should_exit?
|
148
|
+
$EXIT || @should_finish
|
149
|
+
end
|
150
|
+
|
118
151
|
end
|
119
152
|
end
|
data/lib/nerve/utils.rb
CHANGED
@@ -4,5 +4,14 @@ module Nerve
|
|
4
4
|
res = `#{command}`.chomp
|
5
5
|
raise "command '#{command}' failed to run:\n#{res}" unless $?.success?
|
6
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
|
7
16
|
end
|
8
17
|
end
|
data/lib/nerve/version.rb
CHANGED
data/lib/nerve.rb
CHANGED
@@ -12,44 +12,125 @@ require 'nerve/service_watcher'
|
|
12
12
|
|
13
13
|
module Nerve
|
14
14
|
class Nerve
|
15
|
-
|
16
15
|
include Logging
|
16
|
+
include Utils
|
17
|
+
|
18
|
+
MAIN_LOOP_SLEEP_S = 10.freeze
|
19
|
+
LAUNCH_WAIT_FOR_REPORT_S = 30.freeze
|
17
20
|
|
18
|
-
def initialize(
|
19
|
-
log.info 'nerve:
|
21
|
+
def initialize(config_manager)
|
22
|
+
log.info 'nerve: setting up!'
|
23
|
+
@config_manager = config_manager
|
20
24
|
|
21
25
|
# set global variable for exit signal
|
22
26
|
$EXIT = false
|
23
27
|
|
24
|
-
#
|
25
|
-
log.debug 'nerve: checking for required inputs'
|
26
|
-
%w{instance_id services}.each do |required|
|
27
|
-
raise ArgumentError, "you need to specify required argument #{required}" unless opts[required]
|
28
|
-
end
|
29
|
-
|
30
|
-
@instance_id = opts['instance_id']
|
31
|
-
@services = opts['services']
|
32
|
-
@heartbeat_path = opts['heartbeat_path']
|
28
|
+
# State of currently running watchers according to Nerve
|
33
29
|
@watchers = {}
|
30
|
+
@watcher_versions = {}
|
31
|
+
|
32
|
+
# instance_id, heartbeat_path, and watchers_desired are populated by
|
33
|
+
# load_config! in the main loop from the configuration source
|
34
|
+
@instance_id = nil
|
35
|
+
@heartbeat_path = nil
|
36
|
+
@watchers_desired = {}
|
37
|
+
|
38
|
+
# Flag to indicate a config reload is required by the main loop
|
39
|
+
# This decoupling is required for gracefully reloading config on SIGHUP
|
40
|
+
# as one should do as little as possible in a signal handler
|
41
|
+
@config_to_load = true
|
42
|
+
|
43
|
+
Signal.trap("HUP") do
|
44
|
+
@config_to_load = true
|
45
|
+
end
|
34
46
|
|
35
47
|
log.debug 'nerve: completed init'
|
36
48
|
end
|
37
49
|
|
38
|
-
def
|
39
|
-
log.info 'nerve:
|
50
|
+
def load_config!
|
51
|
+
log.info 'nerve: loading config'
|
52
|
+
@config_to_load = false
|
53
|
+
@config_manager.reload!
|
54
|
+
config = @config_manager.config
|
40
55
|
|
41
|
-
|
42
|
-
|
56
|
+
# required options
|
57
|
+
log.debug 'nerve: checking for required inputs'
|
58
|
+
%w{instance_id services}.each do |required|
|
59
|
+
raise ArgumentError, "you need to specify required argument #{required}" unless config[required]
|
43
60
|
end
|
61
|
+
@instance_id = config['instance_id']
|
62
|
+
@watchers_desired = config['services']
|
63
|
+
@heartbeat_path = config['heartbeat_path']
|
64
|
+
end
|
44
65
|
|
66
|
+
def run
|
67
|
+
log.info 'nerve: starting main run loop'
|
45
68
|
begin
|
46
|
-
|
47
|
-
# Check
|
69
|
+
until $EXIT
|
70
|
+
# Check if configuration needs to be reloaded and reconcile any new
|
71
|
+
# configuration of watchers with old configuration
|
72
|
+
if @config_to_load
|
73
|
+
load_config!
|
74
|
+
|
75
|
+
# Reap undesired service watchers
|
76
|
+
services_to_reap = @watchers.select{ |name, _|
|
77
|
+
!@watchers_desired.has_key?(name)
|
78
|
+
}.keys()
|
79
|
+
|
80
|
+
unless services_to_reap.empty?
|
81
|
+
log.info "nerve: reaping old watchers: #{services_to_reap}"
|
82
|
+
services_to_reap.each do |name|
|
83
|
+
reap_watcher(name)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Start new desired service watchers
|
88
|
+
services_to_launch = @watchers_desired.select{ |name, _|
|
89
|
+
!@watchers.has_key?(name)
|
90
|
+
}.keys()
|
91
|
+
|
92
|
+
unless services_to_launch.empty?
|
93
|
+
log.info "nerve: launching new watchers: #{services_to_launch}"
|
94
|
+
services_to_launch.each do |name|
|
95
|
+
launch_watcher(name, @watchers_desired[name])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Detect and update existing service watchers which are in both
|
100
|
+
# the currently running state and the desired (config) watcher
|
101
|
+
# state but have different configurations
|
102
|
+
services_to_update = @watchers.select { |name, _|
|
103
|
+
@watchers_desired.has_key?(name) &&
|
104
|
+
merged_config(@watchers_desired[name], name).hash != @watcher_versions[name]
|
105
|
+
}.keys()
|
106
|
+
|
107
|
+
services_to_update.each do |name|
|
108
|
+
log.info "nerve: detected new config for #{name}"
|
109
|
+
# Keep the old watcher running until the replacement is launched
|
110
|
+
# This keeps the service registered while we change it over
|
111
|
+
# This also keeps connection pools active across diffs
|
112
|
+
temp_name = "#{name}_#{@watcher_versions[name]}"
|
113
|
+
@watchers[temp_name] = @watchers.delete(name)
|
114
|
+
@watcher_versions[temp_name] = @watcher_versions.delete(name)
|
115
|
+
log.info "nerve: launching new watcher for #{name}"
|
116
|
+
launch_watcher(name, @watchers_desired[name], :wait => true)
|
117
|
+
log.info "nerve: reaping old watcher #{temp_name}"
|
118
|
+
reap_watcher(temp_name)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# If this was a configuration check, bail out now
|
123
|
+
if @config_manager.options[:check_config]
|
124
|
+
log.info 'nerve: configuration check succeeded, exiting immediately'
|
125
|
+
break
|
126
|
+
end
|
127
|
+
|
128
|
+
# Check that watchers are still alive, auto-remediate if they
|
48
129
|
# are not. Sometimes zookeeper flakes out or connections are lost to
|
49
130
|
# remote datacenter zookeeper clusters, failing is not an option
|
50
131
|
relaunch = []
|
51
|
-
@watchers.each do |name,
|
52
|
-
unless
|
132
|
+
@watchers.each do |name, watcher|
|
133
|
+
unless watcher.alive?
|
53
134
|
relaunch << name
|
54
135
|
end
|
55
136
|
end
|
@@ -61,14 +142,13 @@ module Nerve
|
|
61
142
|
rescue => e
|
62
143
|
log.warn "nerve: could not reap #{name}, got #{e.inspect}"
|
63
144
|
end
|
64
|
-
launch_watcher(name, @
|
145
|
+
launch_watcher(name, @watchers_desired[name])
|
65
146
|
end
|
66
147
|
|
67
|
-
|
68
|
-
|
69
|
-
end
|
148
|
+
# Indicate we've made progress
|
149
|
+
heartbeat()
|
70
150
|
|
71
|
-
|
151
|
+
responsive_sleep(MAIN_LOOP_SLEEP_S) { @config_to_load || $EXIT }
|
72
152
|
end
|
73
153
|
rescue => e
|
74
154
|
log.error "nerve: encountered unexpected exception #{e.inspect} in main thread"
|
@@ -76,8 +156,8 @@ module Nerve
|
|
76
156
|
ensure
|
77
157
|
$EXIT = true
|
78
158
|
log.warn 'nerve: reaping all watchers'
|
79
|
-
@watchers.each do |name,
|
80
|
-
reap_watcher(name)
|
159
|
+
@watchers.each do |name, _|
|
160
|
+
reap_watcher(name)
|
81
161
|
end
|
82
162
|
end
|
83
163
|
|
@@ -86,15 +166,48 @@ module Nerve
|
|
86
166
|
$EXIT = true
|
87
167
|
end
|
88
168
|
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
|
169
|
+
def heartbeat
|
170
|
+
unless @heartbeat_path.nil?
|
171
|
+
FileUtils.touch(@heartbeat_path)
|
172
|
+
end
|
173
|
+
log.debug 'nerve: heartbeat'
|
174
|
+
end
|
175
|
+
|
176
|
+
def merged_config(config, name)
|
177
|
+
# Get a deep copy so sub-hashes are properly handled
|
178
|
+
deep_copy = Marshal.load(Marshal.dump(config))
|
179
|
+
return deep_copy.merge({'instance_id' => @instance_id, 'name' => name})
|
180
|
+
end
|
181
|
+
|
182
|
+
def launch_watcher(name, config, opts = {})
|
183
|
+
wait = opts[:wait] || false
|
184
|
+
|
185
|
+
watcher_config = merged_config(config, name)
|
186
|
+
# The ServiceWatcher may mutate the configs, so record the version before
|
187
|
+
# passing the config to the ServiceWatcher
|
188
|
+
@watcher_versions[name] = watcher_config.hash
|
189
|
+
|
190
|
+
watcher = ServiceWatcher.new(watcher_config)
|
191
|
+
unless @config_manager.options[:check_config]
|
192
|
+
log.debug "nerve: launching service watcher #{name}"
|
193
|
+
watcher.start()
|
194
|
+
@watchers[name] = watcher
|
195
|
+
if wait
|
196
|
+
log.info "nerve: waiting for watcher thread #{name} to report"
|
197
|
+
responsive_sleep(LAUNCH_WAIT_FOR_REPORT_S) { !watcher.was_up.nil? || $EXIT }
|
198
|
+
log.info "nerve: watcher thread #{name} has reported!"
|
199
|
+
end
|
200
|
+
else
|
201
|
+
log.info "nerve: not launching #{name} due to --check-config option"
|
202
|
+
end
|
93
203
|
end
|
94
204
|
|
95
205
|
def reap_watcher(name)
|
96
|
-
|
97
|
-
|
206
|
+
watcher = @watchers.delete(name)
|
207
|
+
@watcher_versions.delete(name)
|
208
|
+
shutdown_status = watcher.stop()
|
209
|
+
log.info "nerve: stopped #{name}, clean shutdown? #{shutdown_status}"
|
210
|
+
shutdown_status
|
98
211
|
end
|
99
212
|
end
|
100
213
|
end
|
data/nerve.gemspec
CHANGED
@@ -6,8 +6,8 @@ require 'nerve/version'
|
|
6
6
|
Gem::Specification.new do |gem|
|
7
7
|
gem.name = "nerve"
|
8
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"]
|
9
|
+
gem.authors = ["Martin Rhoads", "Igor Serebryany", "Pierre Carrier", "Joseph Lynch"]
|
10
|
+
gem.email = ["martin.rhoads@airbnb.com", "igor.serebryany@airbnb.com", "jlynch@yelp.com"]
|
11
11
|
gem.description = "Nerve is a service registration daemon. It performs health "\
|
12
12
|
"checks on your service and then publishes success or failure "\
|
13
13
|
"into one of several registries (currently, zookeeper or etcd). "\
|
@@ -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,186 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'nerve/configuration_manager'
|
3
|
+
require 'nerve/service_watcher'
|
4
|
+
require 'nerve/reporter'
|
5
|
+
require 'nerve/reporter/base'
|
6
|
+
require 'nerve'
|
7
|
+
|
8
|
+
def make_mock_service_watcher
|
9
|
+
mock_service_watcher = instance_double(Nerve::ServiceWatcher)
|
10
|
+
allow(mock_service_watcher).to receive(:start)
|
11
|
+
allow(mock_service_watcher).to receive(:stop)
|
12
|
+
allow(mock_service_watcher).to receive(:alive?).and_return(true)
|
13
|
+
allow(mock_service_watcher).to receive(:was_up).and_return(true)
|
14
|
+
mock_service_watcher
|
15
|
+
end
|
16
|
+
|
17
|
+
describe Nerve::Nerve do
|
18
|
+
let(:config_manager) { Nerve::ConfigurationManager.new() }
|
19
|
+
let(:mock_config_manager) { instance_double(Nerve::ConfigurationManager) }
|
20
|
+
let(:nerve_config) { "#{File.dirname(__FILE__)}/../../example/nerve.conf.json" }
|
21
|
+
let(:nerve_instance_id) { 'testid' }
|
22
|
+
let(:mock_service_watcher_one) { make_mock_service_watcher() }
|
23
|
+
let(:mock_service_watcher_two) { make_mock_service_watcher() }
|
24
|
+
let(:mock_reporter) { Nerve::Reporter::Base.new({}) }
|
25
|
+
|
26
|
+
describe 'check run' do
|
27
|
+
subject {
|
28
|
+
expect(config_manager).to receive(:parse_options_from_argv!).and_return({
|
29
|
+
:config => nerve_config,
|
30
|
+
:instance_id => nerve_instance_id,
|
31
|
+
:check_config => true
|
32
|
+
})
|
33
|
+
config_manager.parse_options!
|
34
|
+
Nerve::Nerve.new(config_manager)
|
35
|
+
}
|
36
|
+
|
37
|
+
it 'starts up and checks config' do
|
38
|
+
expect{subject.run}.not_to raise_error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'full application run' do
|
43
|
+
before(:each) {
|
44
|
+
$EXIT = false
|
45
|
+
|
46
|
+
allow(Nerve::Reporter).to receive(:new_from_service) {
|
47
|
+
mock_reporter
|
48
|
+
}
|
49
|
+
allow(Nerve::ServiceWatcher).to receive(:new) { |config|
|
50
|
+
if config['name'] == 'service1'
|
51
|
+
mock_service_watcher_one
|
52
|
+
else
|
53
|
+
mock_service_watcher_two
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
allow(mock_config_manager).to receive(:reload!) { }
|
58
|
+
allow(mock_config_manager).to receive(:config) { {
|
59
|
+
'instance_id' => nerve_instance_id,
|
60
|
+
'services' => {
|
61
|
+
'service1' => {
|
62
|
+
'host' => 'localhost',
|
63
|
+
'port' => 1234
|
64
|
+
},
|
65
|
+
'service2' => {
|
66
|
+
'host' => 'localhost',
|
67
|
+
'port' => 1235
|
68
|
+
},
|
69
|
+
}
|
70
|
+
} }
|
71
|
+
allow(mock_config_manager).to receive(:options) { {
|
72
|
+
:config => 'noop',
|
73
|
+
:instance_id => nerve_instance_id,
|
74
|
+
:check_config => false
|
75
|
+
} }
|
76
|
+
|
77
|
+
}
|
78
|
+
|
79
|
+
it 'does a regular run and finishes' do
|
80
|
+
nerve = Nerve::Nerve.new(mock_config_manager)
|
81
|
+
|
82
|
+
expect(nerve).to receive(:heartbeat) {
|
83
|
+
$EXIT = true
|
84
|
+
}
|
85
|
+
|
86
|
+
expect{ nerve.run }.not_to raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'relaunches dead watchers' do
|
90
|
+
nerve = Nerve::Nerve.new(mock_config_manager)
|
91
|
+
|
92
|
+
iterations = 2
|
93
|
+
|
94
|
+
# One service will fail an alive? call and need to be respawned
|
95
|
+
expect(nerve).to receive(:launch_watcher).twice.with('service1', anything).and_call_original
|
96
|
+
expect(nerve).to receive(:reap_watcher).twice.with('service1').and_call_original
|
97
|
+
expect(nerve).to receive(:launch_watcher).once.with('service2', anything).and_call_original
|
98
|
+
expect(nerve).to receive(:reap_watcher).once.with('service2').and_call_original
|
99
|
+
|
100
|
+
expect(nerve).to receive(:heartbeat).exactly(iterations + 1).times do
|
101
|
+
if iterations == 2
|
102
|
+
expect(mock_service_watcher_one).to receive(:alive?).and_return(false)
|
103
|
+
nerve.instance_variable_set(:@config_to_load, true)
|
104
|
+
elsif iterations == 1
|
105
|
+
expect(mock_service_watcher_one).to receive(:alive?).and_return(true)
|
106
|
+
nerve.instance_variable_set(:@config_to_load, true)
|
107
|
+
else
|
108
|
+
$EXIT = true
|
109
|
+
end
|
110
|
+
iterations -= 1
|
111
|
+
end
|
112
|
+
|
113
|
+
expect{ nerve.run }.not_to raise_error
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'responds to changes in configuration' do
|
117
|
+
nerve = Nerve::Nerve.new(mock_config_manager)
|
118
|
+
|
119
|
+
iterations = 4
|
120
|
+
expect(nerve).to receive(:heartbeat).exactly(iterations + 1).times do
|
121
|
+
if iterations == 4
|
122
|
+
expect(nerve.instance_variable_get(:@watchers).keys).to contain_exactly('service1', 'service2')
|
123
|
+
|
124
|
+
# Remove service2 from the config
|
125
|
+
expect(mock_config_manager).to receive(:config).and_return({
|
126
|
+
'instance_id' => nerve_instance_id,
|
127
|
+
'services' => {
|
128
|
+
'service1' => {
|
129
|
+
'host' => 'localhost',
|
130
|
+
'port' => 1234
|
131
|
+
},
|
132
|
+
}
|
133
|
+
})
|
134
|
+
nerve.instance_variable_set(:@config_to_load, true)
|
135
|
+
elsif iterations == 3
|
136
|
+
expect(nerve.instance_variable_get(:@watchers).keys).to contain_exactly('service1')
|
137
|
+
expect(nerve.instance_variable_get(:@config_to_load)).to eq(false)
|
138
|
+
|
139
|
+
# Change the configuration of service1
|
140
|
+
expect(mock_config_manager).to receive(:config).and_return({
|
141
|
+
'instance_id' => nerve_instance_id,
|
142
|
+
'services' => {
|
143
|
+
'service1' => {
|
144
|
+
'host' => 'localhost',
|
145
|
+
'port' => 1236
|
146
|
+
},
|
147
|
+
}
|
148
|
+
})
|
149
|
+
nerve.instance_variable_set(:@config_to_load, true)
|
150
|
+
elsif iterations == 2
|
151
|
+
expect(nerve.instance_variable_get(:@watchers).keys).to contain_exactly('service1')
|
152
|
+
expect(nerve.instance_variable_get(:@watchers_desired).keys).to contain_exactly('service1')
|
153
|
+
expect(nerve.instance_variable_get(:@watchers_desired)['service1']['port']).to eq(1236)
|
154
|
+
expect(nerve.instance_variable_get(:@config_to_load)).to eq(false)
|
155
|
+
|
156
|
+
# Add another service
|
157
|
+
expect(mock_config_manager).to receive(:config) { {
|
158
|
+
'instance_id' => nerve_instance_id,
|
159
|
+
'services' => {
|
160
|
+
'service1' => {
|
161
|
+
'host' => 'localhost',
|
162
|
+
'port' => 1236
|
163
|
+
},
|
164
|
+
'service4' => {
|
165
|
+
'host' => 'localhost',
|
166
|
+
'port' => 1235
|
167
|
+
},
|
168
|
+
}
|
169
|
+
} }
|
170
|
+
|
171
|
+
nerve.instance_variable_set(:@config_to_load, true)
|
172
|
+
elsif iterations == 1
|
173
|
+
expect(nerve.instance_variable_get(:@watchers).keys).to contain_exactly('service1', 'service4')
|
174
|
+
nerve.instance_variable_set(:@config_to_load, true)
|
175
|
+
else
|
176
|
+
$EXIT = true
|
177
|
+
end
|
178
|
+
iterations -= 1
|
179
|
+
end
|
180
|
+
|
181
|
+
expect{ nerve.run }.not_to raise_error
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,43 +1,44 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nerve
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Rhoads
|
8
8
|
- Igor Serebryany
|
9
9
|
- Pierre Carrier
|
10
|
+
- Joseph Lynch
|
10
11
|
autorequire:
|
11
12
|
bindir: bin
|
12
13
|
cert_chain: []
|
13
|
-
date: 2016-
|
14
|
+
date: 2016-09-01 00:00:00.000000000 Z
|
14
15
|
dependencies:
|
15
16
|
- !ruby/object:Gem::Dependency
|
16
17
|
name: json
|
17
18
|
requirement: !ruby/object:Gem::Requirement
|
18
19
|
requirements:
|
19
|
-
- -
|
20
|
+
- - ! '>='
|
20
21
|
- !ruby/object:Gem::Version
|
21
22
|
version: '0'
|
22
23
|
type: :runtime
|
23
24
|
prerelease: false
|
24
25
|
version_requirements: !ruby/object:Gem::Requirement
|
25
26
|
requirements:
|
26
|
-
- -
|
27
|
+
- - ! '>='
|
27
28
|
- !ruby/object:Gem::Version
|
28
29
|
version: '0'
|
29
30
|
- !ruby/object:Gem::Dependency
|
30
31
|
name: zk
|
31
32
|
requirement: !ruby/object:Gem::Requirement
|
32
33
|
requirements:
|
33
|
-
- -
|
34
|
+
- - ~>
|
34
35
|
- !ruby/object:Gem::Version
|
35
36
|
version: 1.9.2
|
36
37
|
type: :runtime
|
37
38
|
prerelease: false
|
38
39
|
version_requirements: !ruby/object:Gem::Requirement
|
39
40
|
requirements:
|
40
|
-
- -
|
41
|
+
- - ~>
|
41
42
|
- !ruby/object:Gem::Version
|
42
43
|
version: 1.9.2
|
43
44
|
- !ruby/object:Gem::Dependency
|
@@ -58,70 +59,70 @@ dependencies:
|
|
58
59
|
name: etcd
|
59
60
|
requirement: !ruby/object:Gem::Requirement
|
60
61
|
requirements:
|
61
|
-
- -
|
62
|
+
- - ~>
|
62
63
|
- !ruby/object:Gem::Version
|
63
64
|
version: 0.2.3
|
64
65
|
type: :runtime
|
65
66
|
prerelease: false
|
66
67
|
version_requirements: !ruby/object:Gem::Requirement
|
67
68
|
requirements:
|
68
|
-
- -
|
69
|
+
- - ~>
|
69
70
|
- !ruby/object:Gem::Version
|
70
71
|
version: 0.2.3
|
71
72
|
- !ruby/object:Gem::Dependency
|
72
73
|
name: rake
|
73
74
|
requirement: !ruby/object:Gem::Requirement
|
74
75
|
requirements:
|
75
|
-
- -
|
76
|
+
- - ! '>='
|
76
77
|
- !ruby/object:Gem::Version
|
77
78
|
version: '0'
|
78
79
|
type: :development
|
79
80
|
prerelease: false
|
80
81
|
version_requirements: !ruby/object:Gem::Requirement
|
81
82
|
requirements:
|
82
|
-
- -
|
83
|
+
- - ! '>='
|
83
84
|
- !ruby/object:Gem::Version
|
84
85
|
version: '0'
|
85
86
|
- !ruby/object:Gem::Dependency
|
86
87
|
name: rspec
|
87
88
|
requirement: !ruby/object:Gem::Requirement
|
88
89
|
requirements:
|
89
|
-
- -
|
90
|
+
- - ~>
|
90
91
|
- !ruby/object:Gem::Version
|
91
92
|
version: 3.1.0
|
92
93
|
type: :development
|
93
94
|
prerelease: false
|
94
95
|
version_requirements: !ruby/object:Gem::Requirement
|
95
96
|
requirements:
|
96
|
-
- -
|
97
|
+
- - ~>
|
97
98
|
- !ruby/object:Gem::Version
|
98
99
|
version: 3.1.0
|
99
100
|
- !ruby/object:Gem::Dependency
|
100
101
|
name: factory_girl
|
101
102
|
requirement: !ruby/object:Gem::Requirement
|
102
103
|
requirements:
|
103
|
-
- -
|
104
|
+
- - ! '>='
|
104
105
|
- !ruby/object:Gem::Version
|
105
106
|
version: '0'
|
106
107
|
type: :development
|
107
108
|
prerelease: false
|
108
109
|
version_requirements: !ruby/object:Gem::Requirement
|
109
110
|
requirements:
|
110
|
-
- -
|
111
|
+
- - ! '>='
|
111
112
|
- !ruby/object:Gem::Version
|
112
113
|
version: '0'
|
113
114
|
- !ruby/object:Gem::Dependency
|
114
115
|
name: pry
|
115
116
|
requirement: !ruby/object:Gem::Requirement
|
116
117
|
requirements:
|
117
|
-
- -
|
118
|
+
- - ! '>='
|
118
119
|
- !ruby/object:Gem::Version
|
119
120
|
version: '0'
|
120
121
|
type: :development
|
121
122
|
prerelease: false
|
122
123
|
version_requirements: !ruby/object:Gem::Requirement
|
123
124
|
requirements:
|
124
|
-
- -
|
125
|
+
- - ! '>='
|
125
126
|
- !ruby/object:Gem::Version
|
126
127
|
version: '0'
|
127
128
|
description: Nerve is a service registration daemon. It performs health checks on
|
@@ -131,15 +132,16 @@ description: Nerve is a service registration daemon. It performs health checks o
|
|
131
132
|
email:
|
132
133
|
- martin.rhoads@airbnb.com
|
133
134
|
- igor.serebryany@airbnb.com
|
135
|
+
- jlynch@yelp.com
|
134
136
|
executables:
|
135
137
|
- nerve
|
136
138
|
extensions: []
|
137
139
|
extra_rdoc_files: []
|
138
140
|
files:
|
139
|
-
-
|
140
|
-
-
|
141
|
-
-
|
142
|
-
-
|
141
|
+
- .gitignore
|
142
|
+
- .mailmap
|
143
|
+
- .nerve.rc
|
144
|
+
- .travis.yml
|
143
145
|
- CONTRIBUTING.md
|
144
146
|
- Gemfile
|
145
147
|
- Gemfile.lock
|
@@ -152,6 +154,7 @@ files:
|
|
152
154
|
- example/nerve_services/etcd_service1.json
|
153
155
|
- example/nerve_services/zookeeper_service1.json
|
154
156
|
- lib/nerve.rb
|
157
|
+
- lib/nerve/configuration_manager.rb
|
155
158
|
- lib/nerve/log.rb
|
156
159
|
- lib/nerve/reporter.rb
|
157
160
|
- lib/nerve/reporter/base.rb
|
@@ -167,6 +170,7 @@ files:
|
|
167
170
|
- lib/nerve/version.rb
|
168
171
|
- nerve.gemspec
|
169
172
|
- spec/.gitkeep
|
173
|
+
- spec/configuration_manager_spec.rb
|
170
174
|
- spec/example_services_spec.rb
|
171
175
|
- spec/factories/check.rb
|
172
176
|
- spec/factories/service.rb
|
@@ -174,6 +178,7 @@ files:
|
|
174
178
|
- spec/lib/nerve/reporter_spec.rb
|
175
179
|
- spec/lib/nerve/reporter_zookeeper_spec.rb
|
176
180
|
- spec/lib/nerve/service_watcher_spec.rb
|
181
|
+
- spec/lib/nerve_spec.rb
|
177
182
|
- spec/spec_helper.rb
|
178
183
|
homepage: https://github.com/airbnb/nerve
|
179
184
|
licenses: []
|
@@ -184,22 +189,23 @@ require_paths:
|
|
184
189
|
- lib
|
185
190
|
required_ruby_version: !ruby/object:Gem::Requirement
|
186
191
|
requirements:
|
187
|
-
- -
|
192
|
+
- - ! '>='
|
188
193
|
- !ruby/object:Gem::Version
|
189
194
|
version: '0'
|
190
195
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
191
196
|
requirements:
|
192
|
-
- -
|
197
|
+
- - ! '>='
|
193
198
|
- !ruby/object:Gem::Version
|
194
199
|
version: '0'
|
195
200
|
requirements: []
|
196
201
|
rubyforge_project:
|
197
|
-
rubygems_version: 2.
|
202
|
+
rubygems_version: 2.5.1
|
198
203
|
signing_key:
|
199
204
|
specification_version: 4
|
200
205
|
summary: A service registration daemon
|
201
206
|
test_files:
|
202
207
|
- spec/.gitkeep
|
208
|
+
- spec/configuration_manager_spec.rb
|
203
209
|
- spec/example_services_spec.rb
|
204
210
|
- spec/factories/check.rb
|
205
211
|
- spec/factories/service.rb
|
@@ -207,4 +213,5 @@ test_files:
|
|
207
213
|
- spec/lib/nerve/reporter_spec.rb
|
208
214
|
- spec/lib/nerve/reporter_zookeeper_spec.rb
|
209
215
|
- spec/lib/nerve/service_watcher_spec.rb
|
216
|
+
- spec/lib/nerve_spec.rb
|
210
217
|
- spec/spec_helper.rb
|