synapse 0.11.1 → 0.12.1

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.
@@ -1,13 +1,18 @@
1
1
  require "synapse/service_watcher/base"
2
2
 
3
+ require 'thread'
3
4
  require 'zk'
4
5
 
5
6
  module Synapse
6
7
  class ZookeeperWatcher < BaseWatcher
7
8
  NUMBERS_RE = /^\d+$/
8
9
 
10
+ @@zk_pool = {}
11
+ @@zk_pool_count = {}
12
+ @@zk_pool_lock = Mutex.new
13
+
9
14
  def start
10
- @zk_hosts = @discovery['hosts'].shuffle.join(',')
15
+ @zk_hosts = @discovery['hosts'].sort.join(',')
11
16
 
12
17
  @watcher = nil
13
18
  @zk = nil
@@ -22,6 +27,8 @@ module Synapse
22
27
  end
23
28
 
24
29
  def ping?
30
+ # @zk being nil implies no session *or* a lost session, do not remove
31
+ # the check on @zk being truthy
25
32
  @zk && @zk.connected?
26
33
  end
27
34
 
@@ -45,7 +52,7 @@ module Synapse
45
52
  @zk.create(path, ignore: :node_exists)
46
53
  end
47
54
 
48
- # find the current backends at the discovery path; sets @backends
55
+ # find the current backends at the discovery path
49
56
  def discover
50
57
  log.info "synapse: discovering backends for service #{@name}"
51
58
 
@@ -54,7 +61,7 @@ module Synapse
54
61
  node = @zk.get("#{@discovery['path']}/#{id}")
55
62
 
56
63
  begin
57
- host, port, name = deserialize_service_instance(node.first)
64
+ host, port, name, weight = deserialize_service_instance(node.first)
58
65
  rescue StandardError => e
59
66
  log.error "synapse: invalid data in ZK node #{id} at #{@discovery['path']}: #{e}"
60
67
  else
@@ -65,26 +72,17 @@ module Synapse
65
72
  numeric_id = NUMBERS_RE =~ numeric_id ? numeric_id.to_i : nil
66
73
 
67
74
  log.debug "synapse: discovered backend #{name} at #{host}:#{server_port} for service #{@name}"
68
- new_backends << { 'name' => name, 'host' => host, 'port' => server_port, 'id' => numeric_id}
75
+ new_backends << { 'name' => name, 'host' => host, 'port' => server_port, 'id' => numeric_id, 'weight' => weight }
69
76
  end
70
77
  end
71
78
 
72
- if new_backends.empty?
73
- if @default_servers.empty?
74
- log.warn "synapse: no backends and no default servers for service #{@name}; using previous backends: #{@backends.inspect}"
75
- else
76
- log.warn "synapse: no backends for service #{@name}; using default servers: #{@default_servers.inspect}"
77
- @backends = @default_servers
78
- end
79
- else
80
- log.info "synapse: discovered #{new_backends.length} backends for service #{@name}"
81
- set_backends(new_backends)
82
- end
79
+ set_backends(new_backends)
83
80
  end
84
81
 
85
82
  # sets up zookeeper callbacks if the data at the discovery path changes
86
83
  def watch
87
84
  return if @zk.nil?
85
+ log.debug "synapse: setting watch at #{@discovery['path']}"
88
86
 
89
87
  @watcher.unsubscribe unless @watcher.nil?
90
88
  @watcher = @zk.register(@discovery['path'], &watcher_callback)
@@ -94,6 +92,7 @@ module Synapse
94
92
  log.error "synapse: zookeeper watcher path #{@discovery['path']} does not exist!"
95
93
  raise RuntimeError.new('could not set a ZK watch on a node that should exist')
96
94
  end
95
+ log.debug "synapse: set watch at #{@discovery['path']}"
97
96
  end
98
97
 
99
98
  # handles the event that a watched path has changed in zookeeper
@@ -103,26 +102,55 @@ module Synapse
103
102
  watch
104
103
  # Rediscover
105
104
  discover
106
- # send a message to calling class to reconfigure
107
- reconfigure!
108
105
  end
109
106
  end
110
107
 
111
108
  def zk_cleanup
112
109
  log.info "synapse: zookeeper watcher cleaning up"
113
110
 
114
- @watcher.unsubscribe unless @watcher.nil?
115
- @watcher = nil
116
-
117
- @zk.close! unless @zk.nil?
118
- @zk = nil
111
+ begin
112
+ @watcher.unsubscribe unless @watcher.nil?
113
+ @watcher = nil
114
+ ensure
115
+ @@zk_pool_lock.synchronize {
116
+ if @@zk_pool.has_key?(@zk_hosts)
117
+ @@zk_pool_count[@zk_hosts] -= 1
118
+ # Last thread to use the connection closes it
119
+ if @@zk_pool_count[@zk_hosts] == 0
120
+ log.info "synapse: closing zk connection to #{@zk_hosts}"
121
+ begin
122
+ @zk.close! unless @zk.nil?
123
+ ensure
124
+ @@zk_pool.delete(@zk_hosts)
125
+ end
126
+ end
127
+ end
128
+ @zk = nil
129
+ }
130
+ end
119
131
 
120
132
  log.info "synapse: zookeeper watcher cleaned up successfully"
121
133
  end
122
134
 
123
135
  def zk_connect
124
136
  log.info "synapse: zookeeper watcher connecting to ZK at #{@zk_hosts}"
125
- @zk = ZK.new(@zk_hosts)
137
+
138
+ # Ensure that all Zookeeper watcher re-use a single zookeeper
139
+ # connection to any given set of zk hosts.
140
+ @@zk_pool_lock.synchronize {
141
+ unless @@zk_pool.has_key?(@zk_hosts)
142
+ log.info "synapse: creating pooled connection to #{@zk_hosts}"
143
+ @@zk_pool[@zk_hosts] = ZK.new(@zk_hosts, :timeout => 5, :thread => :per_callback)
144
+ @@zk_pool_count[@zk_hosts] = 1
145
+ log.info "synapse: successfully created zk connection to #{@zk_hosts}"
146
+ else
147
+ @@zk_pool_count[@zk_hosts] += 1
148
+ log.info "synapse: re-using existing zookeeper connection to #{@zk_hosts}"
149
+ end
150
+ }
151
+
152
+ @zk = @@zk_pool[@zk_hosts]
153
+ log.info "synapse: retrieved zk connection to #{@zk_hosts}"
126
154
 
127
155
  # handle session expiry -- by cleaning up zk, this will make `ping?`
128
156
  # fail and so synapse will exit
@@ -146,8 +174,9 @@ module Synapse
146
174
  host = decoded['host'] || (raise ValueError, 'instance json data does not have host key')
147
175
  port = decoded['port'] || (raise ValueError, 'instance json data does not have port key')
148
176
  name = decoded['name'] || nil
177
+ weight = decoded['weight'] || nil
149
178
 
150
- return host, port, name
179
+ return host, port, name, weight
151
180
  end
152
181
  end
153
182
  end
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = "0.11.1"
2
+ VERSION = "0.12.1"
3
3
  end
@@ -5,9 +5,28 @@ class MockWatcher; end;
5
5
  describe Synapse::Haproxy do
6
6
  subject { Synapse::Haproxy.new(config['haproxy']) }
7
7
 
8
- it 'updating the config' do
8
+ let(:mockwatcher) do
9
9
  mockWatcher = double(Synapse::ServiceWatcher)
10
- subject.should_receive(:generate_config)
11
- subject.update_config([mockWatcher])
10
+ allow(mockWatcher).to receive(:name).and_return('example_service')
11
+ backends = [{ 'host' => 'somehost', 'port' => '5555'}]
12
+ allow(mockWatcher).to receive(:backends).and_return(backends)
13
+ allow(mockWatcher).to receive(:haproxy).and_return({'server_options' => "check inter 2000 rise 3 fall 2"})
14
+ mockWatcher
12
15
  end
16
+
17
+ it 'updating the config' do
18
+ expect(subject).to receive(:generate_config)
19
+ subject.update_config([mockwatcher])
20
+ end
21
+
22
+ it 'generates backend stanza' do
23
+ mockConfig = []
24
+ expect(subject.generate_backend_stanza(mockwatcher, mockConfig)).to eql(["\nbackend example_service", [], ["\tserver somehost:5555 somehost:5555 cookie somehost:5555 check inter 2000 rise 3 fall 2"]])
25
+ end
26
+
27
+ it 'generates backend stanza without cookies for tcp mode' do
28
+ mockConfig = ['mode tcp']
29
+ expect(subject.generate_backend_stanza(mockwatcher, mockConfig)).to eql(["\nbackend example_service", ["\tmode tcp"], ["\tserver somehost:5555 somehost:5555 check inter 2000 rise 3 fall 2"]])
30
+ end
31
+
13
32
  end
@@ -6,7 +6,7 @@ end
6
6
 
7
7
  describe Synapse::BaseWatcher do
8
8
  let(:mocksynapse) { double() }
9
- subject { Synapse::BaseWatcher.new(args, mocksynapse) }
9
+ subject { Synapse::BaseWatcher.new(args, mocksynapse) }
10
10
  let(:testargs) { { 'name' => 'foo', 'discovery' => { 'method' => 'base' }, 'haproxy' => {} }}
11
11
 
12
12
  def remove_arg(name)
@@ -37,18 +37,76 @@ describe Synapse::BaseWatcher do
37
37
  end
38
38
  end
39
39
 
40
- context "with default_servers" do
41
- default_servers = ['server1', 'server2']
40
+ context 'set_backends test' do
41
+ default_servers = [
42
+ {'name' => 'default_server1', 'host' => 'default_server1', 'port' => 123},
43
+ {'name' => 'default_server2', 'host' => 'default_server2', 'port' => 123}
44
+ ]
45
+ backends = [
46
+ {'name' => 'server1', 'host' => 'server1', 'port' => 123},
47
+ {'name' => 'server2', 'host' => 'server2', 'port' => 123}
48
+ ]
42
49
  let(:args) { testargs.merge({'default_servers' => default_servers}) }
43
- it('sets default backends to default_servers') { expect(subject.backends).to equal(default_servers) }
44
50
 
45
- context "with keep_default_servers set" do
46
- let(:args) { testargs.merge({'default_servers' => default_servers, 'keep_default_servers' => true}) }
47
- let(:new_backends) { ['discovered1', 'discovered2'] }
51
+ it 'sets backends' do
52
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
53
+ expect(subject.send(:set_backends, backends)).to equal(true)
54
+ expect(subject.backends).to eq(backends)
55
+ end
56
+
57
+ it 'removes duplicate backends' do
58
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
59
+ duplicate_backends = backends + backends
60
+ expect(subject.send(:set_backends, duplicate_backends)).to equal(true)
61
+ expect(subject.backends).to eq(backends)
62
+ end
63
+
64
+ it 'sets backends to default_servers if no backends discovered' do
65
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
66
+ expect(subject.send(:set_backends, [])).to equal(true)
67
+ expect(subject.backends).to eq(default_servers)
68
+ end
69
+
70
+ context 'with no default_servers' do
71
+ let(:args) { remove_arg 'default_servers' }
72
+ it 'uses previous backends if no default_servers set' do
73
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
74
+ expect(subject.send(:set_backends, backends)).to equal(true)
75
+ expect(subject.send(:set_backends, [])).to equal(false)
76
+ expect(subject.backends).to eq(backends)
77
+ end
78
+ end
79
+
80
+ context 'with no default_servers set and use_previous_backends disabled' do
81
+ let(:args) {
82
+ remove_arg 'default_servers'
83
+ testargs.merge({'use_previous_backends' => false})
84
+ }
85
+ it 'removes all backends if no default_servers set and use_previous_backends disabled' do
86
+ expect(subject).to receive(:'reconfigure!').exactly(:twice)
87
+ expect(subject.send(:set_backends, backends)).to equal(true)
88
+ expect(subject.backends).to eq(backends)
89
+ expect(subject.send(:set_backends, [])).to equal(true)
90
+ expect(subject.backends).to eq([])
91
+ end
92
+ end
93
+
94
+ it 'calls reconfigure only once for duplicate backends' do
95
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
96
+ expect(subject.send(:set_backends, backends)).to equal(true)
97
+ expect(subject.backends).to eq(backends)
98
+ expect(subject.send(:set_backends, backends)).to equal(false)
99
+ expect(subject.backends).to eq(backends)
100
+ end
48
101
 
102
+ context 'with keep_default_servers set' do
103
+ let(:args) {
104
+ testargs.merge({'default_servers' => default_servers, 'keep_default_servers' => true})
105
+ }
49
106
  it('keeps default_servers when setting backends') do
50
- subject.send(:set_backends, new_backends)
51
- expect(subject.backends).to eq(default_servers + new_backends)
107
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
108
+ expect(subject.send(:set_backends, backends)).to equal(true)
109
+ expect(subject.backends).to eq(backends + default_servers)
52
110
  end
53
111
  end
54
112
  end
@@ -46,12 +46,7 @@ describe Synapse::DockerWatcher do
46
46
  end
47
47
  it('has a happy first run path, configuring backends') do
48
48
  expect(subject).to receive(:containers).and_return(['container1'])
49
- expect(subject).to receive(:configure_backends).with(['container1'])
50
- subject.send(:watch)
51
- end
52
- it('does not call configure_backends if there is no change') do
53
- expect(subject).to receive(:containers).and_return([])
54
- expect(subject).to_not receive(:configure_backends)
49
+ expect(subject).to receive(:set_backends).with(['container1'])
55
50
  subject.send(:watch)
56
51
  end
57
52
  end
@@ -65,33 +60,6 @@ describe Synapse::DockerWatcher do
65
60
  end
66
61
  end
67
62
 
68
- context "configure_backends tests" do
69
- before(:each) do
70
- expect(subject.synapse).to receive(:'reconfigure!').at_least(:once)
71
- end
72
- it 'runs' do
73
- expect { subject.send(:configure_backends, []) }.not_to raise_error
74
- end
75
- it 'sets backends right' do
76
- subject.send(:configure_backends, ['foo'])
77
- expect(subject.backends).to eq(['foo'])
78
- end
79
- it 'resets to default backends if no container found' do
80
- subject.default_servers = ['fallback1']
81
- subject.send(:configure_backends, ['foo'])
82
- expect(subject.backends).to eq(['foo'])
83
- subject.send(:configure_backends, [])
84
- expect(subject.backends).to eq(['fallback1'])
85
- end
86
- it 'does not reset to default backends if there are no default backends' do
87
- subject.default_servers = []
88
- subject.send(:configure_backends, ['foo'])
89
- expect(subject.backends).to eq(['foo'])
90
- subject.send(:configure_backends, [])
91
- expect(subject.backends).to eq(['foo'])
92
- end
93
- end
94
-
95
63
  context "rewrite_container_ports tests" do
96
64
  it 'doesnt break if Ports => nil' do
97
65
  subject.send(:rewrite_container_ports, nil)
@@ -0,0 +1,187 @@
1
+ require 'spec_helper'
2
+ require 'logging'
3
+
4
+ class Synapse::EC2Watcher
5
+ attr_reader :synapse
6
+ attr_accessor :default_servers, :ec2
7
+ end
8
+
9
+ class FakeAWSInstance
10
+ def ip_address
11
+ @ip_address ||= fake_address
12
+ end
13
+
14
+ def private_ip_address
15
+ @private_ip_address ||= fake_address
16
+ end
17
+
18
+ def dns_name
19
+ @dns_name ||= "ec2-#{ip_address.gsub('.', '-')}.eu-test-1.compute.amazonaws.com"
20
+ end
21
+
22
+ def private_dns_name
23
+ @private_dns_name ||= "ip-#{private_ip_address.gsub('.', '-')}.eu-test-1.compute.internal"
24
+ end
25
+
26
+ def fake_address
27
+ 4.times.map { (0...254).to_a.shuffle.pop.to_s }.join('.')
28
+ end
29
+ end
30
+
31
+ describe Synapse::EC2Watcher do
32
+ let(:mock_synapse) { double }
33
+ subject { Synapse::EC2Watcher.new(basic_config, mock_synapse) }
34
+
35
+ let(:basic_config) do
36
+ { 'name' => 'ec2tagtest',
37
+ 'haproxy' => {
38
+ 'port' => '8080',
39
+ 'server_port_override' => '8081'
40
+ },
41
+ "discovery" => {
42
+ "method" => "ec2tag",
43
+ "tag_name" => "fuNNy_tag_name",
44
+ "tag_value" => "funkyTagValue",
45
+ "aws_region" => 'eu-test-1',
46
+ "aws_access_key_id" => 'ABCDEFGHIJKLMNOPQRSTU',
47
+ "aws_secret_access_key" => 'verylongfakekeythatireallyneedtogenerate'
48
+ }
49
+ }
50
+ end
51
+
52
+ before(:all) do
53
+ # Clean up ENV so we don't inherit any actual AWS config.
54
+ %w[AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_REGION].each { |k| ENV.delete(k) }
55
+ end
56
+
57
+ before(:each) do
58
+ # https://ruby.awsblog.com/post/Tx2SU6TYJWQQLC3/Stubbing-AWS-Responses
59
+ # always returns empty results, so data may have to be faked.
60
+ AWS.stub!
61
+ end
62
+
63
+ def remove_discovery_arg(name)
64
+ args = basic_config.clone
65
+ args['discovery'].delete name
66
+ args
67
+ end
68
+
69
+ def remove_haproxy_arg(name)
70
+ args = basic_config.clone
71
+ args['haproxy'].delete name
72
+ args
73
+ end
74
+
75
+ def munge_haproxy_arg(name, new_value)
76
+ args = basic_config.clone
77
+ args['haproxy'][name] = new_value
78
+ args
79
+ end
80
+
81
+ describe '#new' do
82
+ let(:args) { basic_config }
83
+
84
+ it 'instantiates cleanly with basic config' do
85
+ expect { subject }.not_to raise_error
86
+ end
87
+
88
+ context 'when missing arguments' do
89
+ it 'complains if aws_region is missing' do
90
+ expect {
91
+ Synapse::EC2Watcher.new(remove_discovery_arg('aws_region'), mock_synapse)
92
+ }.to raise_error(ArgumentError, /Missing aws_region/)
93
+ end
94
+ it 'complains if aws_access_key_id is missing' do
95
+ expect {
96
+ Synapse::EC2Watcher.new(remove_discovery_arg('aws_access_key_id'), mock_synapse)
97
+ }.to raise_error(ArgumentError, /Missing aws_access_key_id/)
98
+ end
99
+ it 'complains if aws_secret_access_key is missing' do
100
+ expect {
101
+ Synapse::EC2Watcher.new(remove_discovery_arg('aws_secret_access_key'), mock_synapse)
102
+ }.to raise_error(ArgumentError, /Missing aws_secret_access_key/)
103
+ end
104
+ it 'complains if server_port_override is missing' do
105
+ expect {
106
+ Synapse::EC2Watcher.new(remove_haproxy_arg('server_port_override'), mock_synapse)
107
+ }.to raise_error(ArgumentError, /Missing server_port_override/)
108
+ end
109
+ end
110
+
111
+ context 'invalid data' do
112
+ it 'complains if the haproxy server_port_override is not a number' do
113
+ expect {
114
+ Synapse::EC2Watcher.new(munge_haproxy_arg('server_port_override', '80deadbeef'), mock_synapse)
115
+ }.to raise_error(ArgumentError, /Invalid server_port_override/)
116
+ end
117
+ end
118
+ end
119
+
120
+ context "instance discovery" do
121
+ let(:instance1) { FakeAWSInstance.new }
122
+ let(:instance2) { FakeAWSInstance.new }
123
+
124
+ context 'using the AWS API' do
125
+ let(:ec2_client) { double('AWS::EC2') }
126
+ let(:instance_collection) { double('AWS::EC2::InstanceCollection') }
127
+
128
+ before do
129
+ subject.ec2 = ec2_client
130
+ end
131
+
132
+ it 'fetches instances and filter instances' do
133
+ # Unfortunately there's quite a bit going on here, but this is
134
+ # a chained call to get then filter EC2 instances, which is
135
+ # done remotely; breaking into separate calls would result in
136
+ # unnecessary data being retrieved.
137
+
138
+ expect(subject.ec2).to receive(:instances).and_return(instance_collection)
139
+
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)
143
+
144
+ subject.send(:instances_with_tags, 'foo', 'bar')
145
+ end
146
+ end
147
+
148
+ context 'returned backend data structure' do
149
+ before do
150
+ allow(subject).to receive(:instances_with_tags).and_return([instance1, instance2])
151
+ end
152
+
153
+ let(:backends) { subject.send(:discover_instances) }
154
+
155
+ it 'returns an Array of backend name/host/port Hashes' do
156
+ required_keys = %w[name host port]
157
+ expect(
158
+ backends.all?{|b| required_keys.each{|k| b.has_key?(k)}}
159
+ ).to be_truthy
160
+ end
161
+
162
+ it 'sets the backend port to server_port_override for all backends' do
163
+ backends = subject.send(:discover_instances)
164
+ expect(
165
+ backends.all? { |b| b['port'] == basic_config['haproxy']['server_port_override'] }
166
+ ).to be_truthy
167
+ end
168
+ end
169
+
170
+ context 'returned instance fields' do
171
+ before do
172
+ allow(subject).to receive(:instances_with_tags).and_return([instance1])
173
+ end
174
+
175
+ let(:backend) { subject.send(:discover_instances).pop }
176
+
177
+ it "returns an instance's private IP as the hostname" do
178
+ expect( backend['host'] ).to eq instance1.private_ip_address
179
+ end
180
+
181
+ it "returns an instance's private hostname as the server name" do
182
+ expect( backend['name'] ).to eq instance1.private_dns_name
183
+ end
184
+ end
185
+ end
186
+ end
187
+