Skip to content

Commit

Permalink
Merge pull request #51 from airbnb/next_release
Browse files Browse the repository at this point in the history
Next release
  • Loading branch information
igor47 committed Apr 23, 2014
2 parents c283b05 + 3e0e0f9 commit b9a0201
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 11 deletions.
11 changes: 8 additions & 3 deletions Gemfile.lock
Expand Up @@ -3,6 +3,8 @@ PATH
specs:
nerve (0.5.2)
bunny (= 1.1.0)
etcd (~> 0.2.3)
json
zk (~> 1.9.2)

GEM
Expand All @@ -12,25 +14,28 @@ GEM
bunny (1.1.0)
amq-protocol (>= 1.9.2)
diff-lcs (1.2.5)
etcd (0.2.3)
mixlib-log
json (1.8.1)
little-plugger (1.1.3)
logging (1.7.2)
little-plugger (>= 1.1.3)
mixlib-log (1.6.0)
rake (10.1.1)
rspec (2.14.1)
rspec-core (~> 2.14.0)
rspec-expectations (~> 2.14.0)
rspec-mocks (~> 2.14.0)
rspec-core (2.14.7)
rspec-expectations (2.14.4)
rspec-expectations (2.14.5)
diff-lcs (>= 1.1.3, < 2.0)
rspec-mocks (2.14.4)
rspec-mocks (2.14.5)
zk (1.9.3)
logging (~> 1.7.2)
zookeeper (~> 1.4.0)
zookeeper (1.4.8)

PLATFORMS
java
ruby

DEPENDENCIES
Expand Down
10 changes: 10 additions & 0 deletions README.md
Expand Up @@ -61,6 +61,16 @@ If you set your `reporter_type` to `"zookeeper"` you should also set these param
* `zk_hosts`: a list of the zookeeper hosts comprising the [ensemble](https://zookeeper.apache.org/doc/r3.1.2/zookeeperAdmin.html#sc_zkMulitServerSetup) that nerve will submit registration to
* `zk_path`: the path (or [znode](https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html#sc_zkDataModel_znodes)) where the registration will be created; nerve will create the [ephemeral node](https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html#Ephemeral+Nodes) that is the registration as a child of this path

#### Etcd Reporter ####

Note: Etcd support is currently experimental!

If you set your `reporter_type` to `"etcd"` you should also set these parameters:

* `etcd_host`: etcd host that nerve will submit registration to
* `etcd_port`: port to connect to etcd.
* `etcd_path`: the path where the registration will be created; nerve will create a node with a 30s ttl that is the registration as a child of this path, and then update it every few seconds

### Checks ###

The core of nerve is a set of service checks.
Expand Down
18 changes: 18 additions & 0 deletions example/nerve_services/etcd_service1.json
@@ -0,0 +1,18 @@
{
"host": "1.2.3.4",
"port": 3000,
"reporter_type": "etcd",
"etcd_host": "localhost",
"etcd_port": 4001,
"etcd_path": "/nerve/services/your_http_service/services",
"check_interval": 2,
"checks": [
{
"type": "http",
"uri": "/health",
"timeout": 0.2,
"rise": 3,
"fall": 2
}
]
}
File renamed without changes.
2 changes: 2 additions & 0 deletions lib/nerve/reporter.rb
@@ -1,3 +1,5 @@
require 'nerve/utils'
require 'nerve/log'

module Nerve
class Reporter
Expand Down
6 changes: 6 additions & 0 deletions lib/nerve/reporter/base.rb
Expand Up @@ -24,6 +24,12 @@ def update_data(new_data='')

def ping?
end

protected
def parse_data(data)
return data if data.class == String
return data.to_json
end
end
end

75 changes: 75 additions & 0 deletions lib/nerve/reporter/etcd.rb
@@ -0,0 +1,75 @@
require 'nerve/reporter/base'
require 'etcd'

class Nerve::Reporter
class Etcd < Base
def initialize(service)
%w{etcd_host instance_id host port}.each do |required|
raise ArgumentError, "missing required argument #{required} for new service watcher" unless service[required]
end
@host = service['etcd_host']
@port = service['etcd_port'] || 4003
path = service['etcd_path'] || '/'
@key = path.split('/').push(service['instance_id']).join('/')
@data = parse_data({'host' => service['host'], 'port' => service['port'], 'name' => service['instance_id']})
end

def start()
log.info "nerve: waiting to connect to etcd at #{@path}"
@etcd = ::Etcd.client(:host => @host, :port => @port)
log.info "nerve: successfully created etcd connection to #{@key}"
end

def stop()
report_down
@etcd = nil
end

def report_up()
etcd_save
end

def report_down
etcd_delete
end

def update_data(new_data='')
@data = parse_data(new_data)
etcd_save
end

def ping?
if @full_key
etcd_save
else
@etcd.leader
end
end

private

def etcd_delete
return unless @etcd and @full_key
begin
@etcd.delete(@full_key)
rescue ::Etcd::KeyNotFound
rescue Errno::ECONNREFUSED
end
end

def etcd_create
@full_key = @etcd.create_in_order(@key, :value => @data).key
log.info "Created etcd node path #{@full_key}"
end

def etcd_save
return etcd_create unless @full_key
begin
@etcd.set(@key, :value => @data, ttl => 30)
rescue ::Etcd::KeyNotFound
etcd_create
end
end
end
end

5 changes: 0 additions & 5 deletions lib/nerve/reporter/zookeeper.rb
Expand Up @@ -65,11 +65,6 @@ def zk_save
zk_create
end
end

def parse_data(data)
return data if data.class == String
return data.to_json
end
end
end

2 changes: 2 additions & 0 deletions lib/nerve/service_watcher/base.rb
@@ -1,3 +1,5 @@
require 'nerve/ring_buffer'

module Nerve
module ServiceCheck
class BaseServiceCheck
Expand Down
9 changes: 6 additions & 3 deletions lib/nerve/service_watcher/http.rb
Expand Up @@ -21,6 +21,8 @@ def initialize(opts={})
@open_timeout = opts['open_timeout'] || 0.2
@ssl_timeout = opts['ssl_timeout'] || 0.2

@expect = opts['expect']

@name = "http-#{@host}:#{@port}#{@uri}"
end

Expand All @@ -30,12 +32,13 @@ def check
connection = get_connection
response = connection.get(@uri)
code = response.code.to_i
body = response.body

if code >= 200 and code < 300
log.debug "nerve: check #{@name} got response code #{code} with body \"#{response.body}\""
if code >= 200 and code < 300 and (@expect == nil || body.include?(@expect))
log.debug "nerve: check #{@name} got response code #{code} with body \"#{body}\""
return true
else
log.error "nerve: check #{@name} got response code #{code} with body \"#{response.body}\""
log.error "nerve: check #{@name} got response code #{code} with body \"#{body}\""
return false
end
end
Expand Down
2 changes: 2 additions & 0 deletions nerve.gemspec
Expand Up @@ -17,8 +17,10 @@ Gem::Specification.new do |gem|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ["lib"]

gem.add_runtime_dependency "json"
gem.add_runtime_dependency "zk", "~> 1.9.2"
gem.add_runtime_dependency "bunny", "= 1.1.0"
gem.add_runtime_dependency "etcd", "~> 0.2.3"

gem.add_development_dependency "rake"
gem.add_development_dependency "rspec"
Expand Down
29 changes: 29 additions & 0 deletions spec/example_services_spec.rb
@@ -0,0 +1,29 @@
require 'json'
require 'nerve/reporter'
require 'nerve/service_watcher'

describe "example services are valid" do
Dir.foreach("#{File.dirname(__FILE__)}/../example/nerve_services") do |item|
next if item == '.' or item == '..'
service_data = JSON.parse(IO.read("#{File.dirname(__FILE__)}/../example/nerve_services/#{item}"))
service_data['name'] = item.gsub(/\.json$/, '')
service_data['instance_id'] = '1'
context "when #{item} can be initialized as a valid reporter" do
reporter = nil
it 'Can new_from_service' do
expect { reporter = Nerve::Reporter.new_from_service(service_data) }.to_not raise_error()
end
it 'Created a reporter object' do
expect(reporter.is_a?(Nerve::Reporter::Base)).to eql(true)
end
end
context "when #{item} can be initialized as a valid service watcher" do
it do
watcher = nil
expect { watcher = Nerve::ServiceWatcher.new(service_data) }.to_not raise_error()
expect(watcher.is_a?(Nerve::ServiceWatcher)).to eql(true)
end
end
end
end

18 changes: 18 additions & 0 deletions spec/lib/nerve/reporter_etcd_spec.rb
@@ -0,0 +1,18 @@
require 'spec_helper'
require 'nerve/reporter/etcd'

describe Nerve::Reporter::Etcd do
let(:subject) { {
'etcd_host' => 'etcdhost1',
'etcd_port' => 4001,
'etcd_path' => '/path',
'instance_id' => 'instance_id',
'host' => 'host',
'port' => 'port'
}
}
it 'actually constructs an instance' do
expect(Nerve::Reporter::Etcd.new(subject).is_a?(Nerve::Reporter::Etcd)).to eql(true)
end
end

15 changes: 15 additions & 0 deletions spec/lib/nerve/reporter_spec.rb
Expand Up @@ -25,3 +25,18 @@
end
end

class Nerve::Reporter::Test < Nerve::Reporter::Base
end

describe Nerve::Reporter::Test do
let(:subject) {Nerve::Reporter::Test.new({}) }
it 'has parse data method that passes strings' do
expect(subject.send(:parse_data, 'foobar')).to eql('foobar')
end
it 'jsonifies anything that is not a string' do
thing_to_parse = double()
expect(thing_to_parse).to receive(:to_json).and_return('{"some":"json"}')
expect(subject.send(:parse_data, thing_to_parse)).to eql('{"some":"json"}')
end
end

0 comments on commit b9a0201

Please sign in to comment.