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 CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 09be8c301fe459dcab4e5aa469f863ebb27c9f51
4
- data.tar.gz: f458f7c7d2cbbd3e82f029d3a3488bcd5eff9088
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZDU0ZGNiMzk5OGRiZGU4ZjQwYTFmMmZiNzA5NGFiMWU2ODIzOGY2MQ==
5
+ data.tar.gz: !binary |-
6
+ NzJkZTAwMmIyOTQ0MDEyMGFiYTZlOTJjMGM3YTUwMmJmYTVmYjNmNg==
5
7
  SHA512:
6
- metadata.gz: bf15ca3c591c8bac19750d9c3e1ba96ef7af6096492dbd69f76c971563a22fb7948fa1322d11664b819e185f6d51b1e279ba54b08bba062433c6fb06b7b3be5b
7
- data.tar.gz: b4b4b89e7eb356611b11aea64f9df687af5c1dc657864d791d799f7b80b2c7e63c16b7fccdc4e502593e03b7f42b6f6f425f2c67dddacaaa99bb0b4778ffd7cf
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nerve (0.7.0)
4
+ nerve (0.8.0)
5
5
  bunny (= 1.1.0)
6
6
  etcd (~> 0.2.3)
7
7
  json
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
- # set command line options
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 talking to zookeeper
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 $EXIT
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
- nap_time = @check_interval
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: ending service watch #{@name}"
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
@@ -1,3 +1,3 @@
1
1
  module Nerve
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
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(opts={})
19
- log.info 'nerve: starting up!'
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
- # required options
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 run
39
- log.info 'nerve: starting run'
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
- @services.each do |name, config|
42
- launch_watcher(name, config)
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
- loop do
47
- # Check that watcher threads are still alive, auto-remediate if they
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, watcher_thread|
52
- unless watcher_thread.alive?
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, @services[name])
145
+ launch_watcher(name, @watchers_desired[name])
65
146
  end
66
147
 
67
- unless @heartbeat_path.nil?
68
- FileUtils.touch(@heartbeat_path)
69
- end
148
+ # Indicate we've made progress
149
+ heartbeat()
70
150
 
71
- sleep 10
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, watcher_thread|
80
- reap_watcher(name) rescue "nerve: watcher #{name} could not be immediately reaped; skippping"
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 launch_watcher(name, config)
90
- log.debug "nerve: launching service watcher #{name}"
91
- watcher = ServiceWatcher.new(config.merge({'instance_id' => @instance_id, 'name' => name}))
92
- @watchers[name] = Thread.new{watcher.run}
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
- watcher_thread = @watchers.delete(name)
97
- watcher_thread.join()
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
@@ -7,6 +7,7 @@
7
7
  require "#{File.dirname(__FILE__)}/../lib/nerve"
8
8
 
9
9
  require 'factory_girl'
10
+
10
11
  FactoryGirl.find_definitions
11
12
 
12
13
  RSpec.configure do |config|
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.7.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-02-17 00:00:00.000000000 Z
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
- - ".gitignore"
140
- - ".mailmap"
141
- - ".nerve.rc"
142
- - ".travis.yml"
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.4.5
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