nerve 0.7.0 → 0.8.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.
- 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
|