Skip to content

Commit 15bfb29

Browse files
committed
synapse etcd service watcher
1 parent e012cd3 commit 15bfb29

File tree

6 files changed

+194
-47
lines changed

6 files changed

+194
-47
lines changed

Gemfile.lock

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,70 +4,62 @@ PATH
44
synapse (0.11.1)
55
aws-sdk (~> 1.39)
66
docker-api (~> 1.7.2)
7+
etcd (~> 0.2.4)
78
zk (~> 1.9.4)
89

910
GEM
1011
remote: https://rubygems.org/
1112
specs:
1213
archive-tar-minitar (0.5.2)
13-
aws-sdk (1.47.0)
14+
aws-sdk (1.51.0)
1415
json (~> 1.4)
1516
nokogiri (>= 1.4.4)
16-
coderay (1.0.9)
17-
diff-lcs (1.2.4)
17+
coderay (1.1.0)
18+
diff-lcs (1.2.5)
1819
docker-api (1.7.6)
1920
archive-tar-minitar
2021
excon (>= 0.28)
2122
json
22-
excon (0.38.0)
23-
ffi (1.9.3-java)
23+
etcd (0.2.4)
24+
mixlib-log
25+
excon (0.39.5)
2426
json (1.8.1)
25-
json (1.8.1-java)
2627
little-plugger (1.1.3)
2728
logging (1.8.2)
2829
little-plugger (>= 1.1.3)
2930
multi_json (>= 1.8.4)
3031
method_source (0.8.2)
3132
mini_portile (0.6.0)
33+
mixlib-log (1.6.0)
3234
multi_json (1.10.1)
33-
nokogiri (1.6.2.1)
35+
nokogiri (1.6.3.1)
3436
mini_portile (= 0.6.0)
35-
nokogiri (1.6.2.1-java)
36-
pry (0.9.12.2)
37-
coderay (~> 1.0.5)
38-
method_source (~> 0.8)
37+
pry (0.10.1)
38+
coderay (~> 1.1.0)
39+
method_source (~> 0.8.1)
3940
slop (~> 3.4)
40-
pry (0.9.12.2-java)
41-
coderay (~> 1.0.5)
42-
method_source (~> 0.8)
43-
slop (~> 3.4)
44-
spoon (~> 0.0)
45-
pry-nav (0.2.3)
46-
pry (~> 0.9.10)
47-
rake (10.1.1)
48-
rspec (2.14.1)
49-
rspec-core (~> 2.14.0)
50-
rspec-expectations (~> 2.14.0)
51-
rspec-mocks (~> 2.14.0)
52-
rspec-core (2.14.5)
53-
rspec-expectations (2.14.2)
54-
diff-lcs (>= 1.1.3, < 2.0)
55-
rspec-mocks (2.14.3)
56-
slop (3.4.6)
57-
slyphon-log4j (1.2.15)
58-
slyphon-zookeeper_jar (3.3.5-java)
59-
spoon (0.0.4)
60-
ffi
41+
pry-nav (0.2.4)
42+
pry (>= 0.9.10, < 0.11.0)
43+
rake (10.3.2)
44+
rspec (3.0.0)
45+
rspec-core (~> 3.0.0)
46+
rspec-expectations (~> 3.0.0)
47+
rspec-mocks (~> 3.0.0)
48+
rspec-core (3.0.4)
49+
rspec-support (~> 3.0.0)
50+
rspec-expectations (3.0.4)
51+
diff-lcs (>= 1.2.0, < 2.0)
52+
rspec-support (~> 3.0.0)
53+
rspec-mocks (3.0.4)
54+
rspec-support (~> 3.0.0)
55+
rspec-support (3.0.4)
56+
slop (3.6.0)
6157
zk (1.9.4)
6258
logging (~> 1.8.2)
6359
zookeeper (~> 1.4.0)
6460
zookeeper (1.4.8)
65-
zookeeper (1.4.8-java)
66-
slyphon-log4j (= 1.2.15)
67-
slyphon-zookeeper_jar (= 3.3.5)
6861

6962
PLATFORMS
70-
java
7163
ruby
7264

7365
DEPENDENCIES

lib/synapse/service_watcher.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require "synapse/service_watcher/dns"
55
require "synapse/service_watcher/docker"
66
require "synapse/service_watcher/zookeeper_dns"
7+
require "synapse/service_watcher/etcd"
78

89
module Synapse
910
class ServiceWatcher
@@ -15,6 +16,7 @@ class ServiceWatcher
1516
'dns' => DnsWatcher,
1617
'docker' => DockerWatcher,
1718
'zookeeper_dns' => ZookeeperDnsWatcher,
19+
'etcd' => EtcdWatcher
1820
}
1921

2022
# the method which actually dispatches watcher creation requests
@@ -32,3 +34,4 @@ def self.create(name, opts, synapse)
3234
end
3335
end
3436
end
37+
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
require "synapse/service_watcher/base"
2+
3+
require 'etcd'
4+
5+
module Synapse
6+
class EtcdWatcher < BaseWatcher
7+
NUMBERS_RE = /^\d+$/
8+
9+
def start
10+
etcd_hosts = @discovery['host']
11+
12+
log.info "synapse: starting etcd watcher #{@name} @ host: #{@discovery['host']}, path: #{@discovery['path']}"
13+
@should_exit = false
14+
@etcd = ::Etcd.client(:host => @discovery['host'], :port => @discovery['port'])
15+
16+
# call the callback to bootstrap the process
17+
discover
18+
@synapse.reconfigure!
19+
@watcher = Thread.new do
20+
watch
21+
end
22+
end
23+
24+
def stop
25+
log.warn "synapse: etcd watcher exiting"
26+
27+
@should_exit = true
28+
@etcd = nil
29+
30+
log.info "synapse: etcd watcher cleaned up successfully"
31+
end
32+
33+
def ping?
34+
@etcd.leader
35+
end
36+
37+
private
38+
def validate_discovery_opts
39+
raise ArgumentError, "invalid discovery method #{@discovery['method']}" \
40+
unless @discovery['method'] == 'etcd'
41+
raise ArgumentError, "missing or invalid etcd host for service #{@name}" \
42+
unless @discovery['host']
43+
raise ArgumentError, "missing or invalid etcd port for service #{@name}" \
44+
unless @discovery['port']
45+
raise ArgumentError, "invalid etcd path for service #{@name}" \
46+
unless @discovery['path']
47+
end
48+
49+
# helper method that ensures that the discovery path exists
50+
def create(path)
51+
log.debug "synapse: creating etcd path: #{path}"
52+
@etcd.create(path, dir: true)
53+
end
54+
55+
def each_node(node)
56+
begin
57+
host, port, name = deserialize_service_instance(node.value)
58+
rescue StandardError => e
59+
log.error "synapse: invalid data in etcd node #{node.inspect} at #{@discovery['path']}: #{e} DATA #{node.value}"
60+
nil
61+
else
62+
server_port = @server_port_override ? @server_port_override : port
63+
64+
# find the numberic id in the node name; used for leader elections if enabled
65+
numeric_id = node.key.split('/').last
66+
numeric_id = NUMBERS_RE =~ numeric_id ? numeric_id.to_i : nil
67+
68+
log.warn "synapse: discovered backend #{name} at #{host}:#{server_port} for service #{@name}"
69+
{ 'name' => name, 'host' => host, 'port' => server_port, 'id' => numeric_id}
70+
end
71+
end
72+
73+
def each_dir(d)
74+
new_backends = []
75+
d.children.each do |node|
76+
if node.directory?
77+
new_backends << each_dir(@etcd.get(node.key))
78+
else
79+
backend = each_node(node)
80+
if backend
81+
new_backends << backend
82+
end
83+
end
84+
end
85+
new_backends.flatten
86+
end
87+
88+
# find the current backends at the discovery path; sets @backends
89+
def discover
90+
log.info "synapse: discovering backends for service #{@name}"
91+
92+
d = nil
93+
begin
94+
d = @etcd.get(@discovery['path'])
95+
rescue Etcd::KeyNotFound
96+
create(@discovery['path'])
97+
d = @etcd.get(@discovery['path'])
98+
end
99+
100+
new_backends = []
101+
if d.directory?
102+
new_backends = each_dir(d)
103+
else
104+
log.warn "synapse: path #{@discovery['path']} is not a directory"
105+
end
106+
107+
if new_backends.empty?
108+
if @default_servers.empty?
109+
log.warn "synapse: no backends and no default servers for service #{@name}; using previous backends: #{@backends.inspect}"
110+
false
111+
else
112+
log.warn "synapse: no backends for service #{@name}; using default servers: #{@default_servers.inspect}"
113+
@backends = @default_servers
114+
true
115+
end
116+
else
117+
if @backends != new_backends
118+
log.info "synapse: discovered #{new_backends.length} backends (including new) for service #{@name}"
119+
@backends = new_backends
120+
true
121+
else
122+
log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
123+
false
124+
end
125+
end
126+
end
127+
128+
def watch
129+
while !@should_exit
130+
begin
131+
@etcd.watch(@discovery['path'], :timeout => 60, :recursive => true)
132+
rescue Timeout::Error
133+
else
134+
if discover
135+
@synapse.reconfigure!
136+
end
137+
end
138+
end
139+
end
140+
141+
# decode the data at a zookeeper endpoint
142+
def deserialize_service_instance(data)
143+
log.debug "synapse: deserializing process data"
144+
decoded = JSON.parse(data)
145+
146+
host = decoded['host'] || (raise ValueError, 'instance json data does not have host key')
147+
port = decoded['port'] || (raise ValueError, 'instance json data does not have port key')
148+
name = decoded['name'] || nil
149+
150+
return host, port, name
151+
end
152+
end
153+
end
154+

spec/lib/synapse/service_watcher_ec2tags_spec.rb

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,39 +135,37 @@ def munge_haproxy_arg(name, new_value)
135135
# done remotely; breaking into separate calls would result in
136136
# unnecessary data being retrieved.
137137

138-
subject.ec2.should_receive(:instances).and_return(instance_collection)
138+
expect(subject.ec2).to receive(:instances).and_return(instance_collection)
139139

140-
instance_collection.should_receive(:tagged).with('foo').and_return(instance_collection)
141-
instance_collection.should_receive(:tagged_values).with('bar').and_return(instance_collection)
142-
instance_collection.should_receive(:select).and_return(instance_collection)
140+
expect(instance_collection).to receive(:tagged).with('foo').and_return(instance_collection)
141+
expect(instance_collection).to receive(:tagged_values).with('bar').and_return(instance_collection)
142+
expect(instance_collection).to receive(:select).and_return(instance_collection)
143143

144144
subject.send(:instances_with_tags, 'foo', 'bar')
145145
end
146146
end
147147

148148
context 'returned backend data structure' do
149149
before do
150-
subject.stub(:instances_with_tags).and_return([instance1, instance2])
150+
expect(subject).to receive(:instances_with_tags).and_return([instance1, instance2])
151151
end
152152

153153
let(:backends) { subject.send(:discover_instances) }
154154

155155
it 'returns an Array of backend name/host/port Hashes' do
156156

157-
expect { backends.all? {|b| %w[name host port].each {|k| b.has_key?(k) }} }.to be_true
157+
expect(backends.all? {|b| %w[name host port].each {|k| b.has_key?(k) }}).to eql(true)
158158
end
159159

160160
it 'sets the backend port to server_port_override for all backends' do
161161
backends = subject.send(:discover_instances)
162-
expect {
163-
backends.all? { |b| b['port'] == basic_config['haproxy']['server_port_override'] }
164-
}.to be_true
162+
expect(backends.all? { |b| b['port'] == basic_config['haproxy']['server_port_override'] }).to eql(true)
165163
end
166164
end
167165

168166
context 'returned instance fields' do
169167
before do
170-
subject.stub(:instances_with_tags).and_return([instance1])
168+
expect(subject).to receive(:instances_with_tags).and_return([instance1])
171169
end
172170

173171
let(:backend) { subject.send(:discover_instances).pop }

spec/spec_helper.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
require 'support/configuration'
1010

1111
RSpec.configure do |config|
12-
config.treat_symbols_as_metadata_keys_with_true_values = true
1312
config.run_all_when_everything_filtered = true
1413
config.filter_run :focus
1514
config.include Configuration

synapse.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Gem::Specification.new do |gem|
1919
gem.add_runtime_dependency "aws-sdk", "~> 1.39"
2020
gem.add_runtime_dependency "docker-api", "~> 1.7.2"
2121
gem.add_runtime_dependency "zk", "~> 1.9.4"
22+
gem.add_runtime_dependency "etcd", "~> 0.2.4"
2223

2324
gem.add_development_dependency "rake"
2425
gem.add_development_dependency "rspec"

0 commit comments

Comments
 (0)