synapse 0.13.5 → 0.13.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -152,16 +152,17 @@ The base watcher is useful in situations where you only want to use the servers
152
152
  It has the following options:
153
153
 
154
154
  * `method`: base
155
- * `label_filter`: optional filter to be applied to discovered service nodes
155
+ * `label_filters`: optional list of filters to be applied to discovered service nodes
156
156
 
157
157
  ###### Filtering service nodes ######
158
- Synapse can be configured to only return service nodes that match a `label_filter` predicate. If provided, the `label_filter` hash should contain the following:
159
158
 
160
- * `label`: The label for which the filter is applied
159
+ Synapse can be configured to only return service nodes that match a `label_filters` predicate. If provided, `label_filters` should be an array of hashes which contain the following:
160
+
161
+ * `label`: The name of the label for which the filter is applied
161
162
  * `value`: The comparison value
162
- * `condition` (one of ['`equals`']): The type of filter condition to be applied. Only `equals` is supported at present
163
+ * `condition` (one of ['`equals`', '`not-equals`']): The type of filter condition to be applied.
163
164
 
164
- Given a `label_filter`: `{ "label": "cluster", "value": "dev", "condition": "equals" }`, this will return only service nodes that contain the label value `{ "cluster": "dev" }`.
165
+ Given a `label_filters`: `[{ "label": "cluster", "value": "dev", "condition": "equals" }]`, this will return only service nodes that contain the label value `{ "cluster": "dev" }`.
165
166
 
166
167
  ##### Zookeeper #####
167
168
 
@@ -284,6 +285,7 @@ The top level `haproxy` section of the config file has the following options:
284
285
  * `do_writes`: whether or not the config file will be written (default to `true`)
285
286
  * `do_reloads`: whether or not Synapse will reload HAProxy (default to `true`)
286
287
  * `do_socket`: whether or not Synapse will use the HAProxy socket commands to prevent reloads (default to `true`)
288
+ * `socket_file_path`: where to find the haproxy stats socket. can be a list (if using `nbproc`)
287
289
  * `global`: options listed here will be written into the `global` section of the HAProxy config
288
290
  * `defaults`: options listed here will be written into the `defaults` section of the HAProxy config
289
291
  * `extra_sections`: additional, manually-configured `frontend`, `backend`, or `listen` stanzas
@@ -332,7 +334,9 @@ For example:
332
334
  - "bind 127.0.0.1:8081"
333
335
  reload_command: "service haproxy reload"
334
336
  config_file_path: "/etc/haproxy/haproxy.cfg"
335
- socket_file_path: "/var/run/haproxy.sock"
337
+ socket_file_path:
338
+ - /var/run/haproxy.sock
339
+ - /var/run/haproxy2.sock
336
340
  global:
337
341
  - "daemon"
338
342
  - "user haproxy"
@@ -815,6 +815,10 @@ module Synapse
815
815
  @opts['do_socket'] = true unless @opts.key?('do_socket')
816
816
  @opts['do_reloads'] = true unless @opts.key?('do_reloads')
817
817
 
818
+ # socket_file_path can be a string or a list
819
+ # lets make a new option which is always a list (plural)
820
+ @opts['socket_file_paths'] = [@opts['socket_file_path']].flatten
821
+
818
822
  # how to restart haproxy
819
823
  @restart_interval = @opts.fetch('restart_interval', 2).to_i
820
824
  @restart_jitter = @opts.fetch('restart_jitter', 0).to_f
@@ -850,7 +854,9 @@ module Synapse
850
854
  def update_config(watchers)
851
855
  # if we support updating backends, try that whenever possible
852
856
  if @opts['do_socket']
853
- update_backends(watchers)
857
+ @opts['socket_file_paths'].each do |socket_path|
858
+ update_backends_at(socket_path, watchers)
859
+ end
854
860
  else
855
861
  @restart_required = true
856
862
  end
@@ -1030,8 +1036,8 @@ module Synapse
1030
1036
  ]
1031
1037
  end
1032
1038
 
1033
- def haproxy_exec(command)
1034
- s = UNIXSocket.new(@opts['socket_file_path'])
1039
+ def talk_to_socket(socket_file_path, command)
1040
+ s = UNIXSocket.new(socket_file_path)
1035
1041
  s.write(command)
1036
1042
  s.read
1037
1043
  ensure
@@ -1040,11 +1046,11 @@ module Synapse
1040
1046
 
1041
1047
  # tries to set active backends via haproxy's stats socket
1042
1048
  # because we can't add backends via the socket, we might still need to restart haproxy
1043
- def update_backends(watchers)
1049
+ def update_backends_at(socket_file_path, watchers)
1044
1050
  # first, get a list of existing servers for various backends
1045
1051
  begin
1046
1052
  stat_command = "show stat\n"
1047
- info = haproxy_exec(stat_command)
1053
+ info = talk_to_socket(socket_file_path, stat_command)
1048
1054
  rescue StandardError => e
1049
1055
  log.warn "synapse: restart required because socket command #{stat_command} failed "\
1050
1056
  "with error #{e.inspect}"
@@ -1098,7 +1104,7 @@ module Synapse
1098
1104
 
1099
1105
  # actually write the command to the socket
1100
1106
  begin
1101
- output = haproxy_exec(command)
1107
+ output = talk_to_socket(socket_file_path, command)
1102
1108
  rescue StandardError => e
1103
1109
  log.warn "synapse: restart required because socket command #{command} failed with "\
1104
1110
  "error #{e.inspect}"
@@ -1113,7 +1119,7 @@ module Synapse
1113
1119
  end
1114
1120
  end
1115
1121
 
1116
- log.info "synapse: reconfigured haproxy"
1122
+ log.info "synapse: reconfigured haproxy via #{socket_file_path}"
1117
1123
  end
1118
1124
 
1119
1125
  # writes the config
@@ -21,7 +21,14 @@ class Synapse::ServiceWatcher
21
21
 
22
22
  @name = opts['name']
23
23
  @discovery = opts['discovery']
24
- @label_filter = @discovery['label_filter'] || false
24
+
25
+ # deprecated singular filter
26
+ @singular_label_filter = @discovery['label_filter']
27
+ unless @singular_label_filter.nil?
28
+ log.warn "synapse: `label_filter` parameter is deprecated; use `label_filters` -- an array"
29
+ end
30
+
31
+ @label_filters = [@singular_label_filter, @discovery['label_filters']].flatten.compact
25
32
 
26
33
  @leader_election = opts['leader_election'] || false
27
34
  @leader_last_warn = Time.now - LEADER_WARN_INTERVAL
@@ -73,25 +80,36 @@ class Synapse::ServiceWatcher
73
80
  end
74
81
 
75
82
  def backends
83
+ filtered = backends_filtered_by_labels
84
+
76
85
  if @leader_election
77
- if @backends.all?{|b| b.key?('id') && b['id']}
78
- smallest = @backends.sort_by{ |b| b['id']}.first
79
- log.debug "synapse: leader election chose one of #{@backends.count} backends " \
86
+ failure_warning = nil
87
+ if filtered.empty?
88
+ failure_warning = "synapse: service #{@name}: leader election failed: no backends to choose from"
89
+ end
90
+
91
+ all_backends_have_ids = filtered.all?{|b| b.key?('id') && b['id']}
92
+ unless all_backends_have_ids
93
+ failure_warning = "synapse: service #{@name}: leader election failed; not all backends include an id"
94
+ end
95
+
96
+ # no problems encountered, lets do the leader election
97
+ if failure_warning.nil?
98
+ smallest = filtered.sort_by{ |b| b['id']}.first
99
+ log.debug "synapse: leader election chose one of #{filtered.count} backends " \
80
100
  "(#{smallest['host']}:#{smallest['port']} with id #{smallest['id']})"
81
101
 
82
102
  return [smallest]
103
+
104
+ # we had some sort of problem, lets log about it
83
105
  elsif (Time.now - @leader_last_warn) > LEADER_WARN_INTERVAL
84
- log.warn "synapse: service #{@name}: leader election failed; not all backends include an id"
85
106
  @leader_last_warn = Time.now
107
+ log.warn failure_warning
108
+ return []
86
109
  end
87
-
88
- # if leader election fails, return no backends
89
- return []
90
- elsif @label_filter
91
- return filter_backends_by_label(@backends, @label_filter)
92
110
  end
93
111
 
94
- return @backends
112
+ return filtered
95
113
  end
96
114
 
97
115
  private
@@ -102,15 +120,16 @@ class Synapse::ServiceWatcher
102
120
  log.warn "synapse: warning: a stub watcher with no default servers is pretty useless" if @default_servers.empty?
103
121
  end
104
122
 
105
- def filter_backends_by_label(backends, label_filter)
106
- filtered_backends = []
107
- backends.each do |backend|
123
+ def backends_filtered_by_labels
124
+ filtered_backends = @backends.select do |backend|
108
125
  backend_labels = backend['labels'] || {}
109
- if label_filter['condition'] == 'equals' and backend_labels[label_filter['label']] == label_filter['value']
110
- filtered_backends << backend
126
+ @label_filters.all? do |label_filter|
127
+ (label_filter['condition'] == 'equals' &&
128
+ backend_labels[label_filter['label']] == label_filter['value']) ||
129
+ (label_filter['condition'] == 'not-equals' &&
130
+ backend_labels[label_filter['label']] != label_filter['value'])
111
131
  end
112
132
  end
113
- return filtered_backends
114
133
  end
115
134
 
116
135
  def set_backends(new_backends)
@@ -1,3 +1,3 @@
1
1
  module Synapse
2
- VERSION = "0.13.5"
2
+ VERSION = "0.13.7"
3
3
  end
@@ -0,0 +1,20 @@
1
+ FactoryGirl.define do
2
+ factory :backend, :class => Hash do
3
+ sequence(:name) { |n| "server#{n}" }
4
+ sequence(:host) { |n| "hostname#{n}" }
5
+ sequence(:port)
6
+
7
+ labels {}
8
+
9
+ # needed to build hashes instead of classes
10
+ initialize_with { attributes }
11
+
12
+ # convert keys to strings, since backends always have string keys
13
+ after(:build) do |backend|
14
+ backend.keys.each do |k|
15
+ backend[k.to_s] = backend[k]
16
+ backend.delete(k)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -66,16 +66,27 @@ describe Synapse::Haproxy do
66
66
  end
67
67
 
68
68
  context 'when we support socket updates' do
69
- include_context 'generate_config is stubbed out'
69
+ let(:socket_file_path) { 'socket_file_path' }
70
70
  before do
71
71
  config['haproxy']['do_socket'] = true
72
- config['haproxy']['socket_file_path'] = 'socket_file_path'
72
+ config['haproxy']['socket_file_path'] = socket_file_path
73
73
  end
74
74
 
75
+ include_context 'generate_config is stubbed out'
76
+
75
77
  it 'updates backends via the socket' do
76
- expect(subject).to receive(:update_backends).with(watchers)
78
+ expect(subject).to receive(:update_backends_at).with(socket_file_path, watchers)
77
79
  subject.update_config(watchers)
78
80
  end
81
+
82
+ context 'when we specify multiple stats sockets' do
83
+ let(:socket_file_path) { ['socket_file_path1', 'socket_file_path2'] }
84
+
85
+ it 'updates all of them' do
86
+ expect(subject).to receive(:update_backends_at).exactly(socket_file_path.count).times
87
+ subject.update_config(watchers)
88
+ end
89
+ end
79
90
  end
80
91
 
81
92
  context 'when we do not support socket updates' do
@@ -83,7 +94,7 @@ describe Synapse::Haproxy do
83
94
  before { config['haproxy']['do_socket'] = false }
84
95
 
85
96
  it 'does not update the backends' do
86
- expect(subject).to_not receive(:update_backends)
97
+ expect(subject).to_not receive(:update_backends_at)
87
98
  subject.update_config(watchers)
88
99
  end
89
100
  end
@@ -111,28 +111,70 @@ describe Synapse::ServiceWatcher::BaseWatcher do
111
111
  end
112
112
 
113
113
  context 'with label_filter set' do
114
- let(:matching_labeled_backends) { [
115
- { 'name' => 'server1', 'host' => 'server1', 'port' => 1111, 'labels' => { 'az' => 'us-east-1a' } },
116
- { 'name' => 'server2', 'host' => 'server2', 'port' => 2222, 'labels' => { 'az' => 'us-east-1a' } },
117
- ] }
118
- let(:non_matching_labeled_backends) { [
119
- { 'name' => 'server3', 'host' => 'server3', 'port' => 3333, 'labels' => { 'az' => 'us-west-1c' } },
120
- { 'name' => 'server4', 'host' => 'server4', 'port' => 4444, 'labels' => { 'az' => 'us-west-2a' } },
121
- ] }
122
- let(:non_labeled_backends) { [
123
- { 'name' => 'server5', 'host' => 'server5', 'port' => 5555 },
124
- ] }
125
- let(:args) {
126
- testargs.merge({ 'discovery' => {
127
- 'method' => 'base',
128
- 'label_filter' => { 'condition' => 'equals', 'label' => 'az', 'value' => 'us-east-1a' } }
114
+ let(:matching_az) { 'us-east-1a' }
115
+ let(:matching_labels) { [{'az' => matching_az}] * 2 }
116
+ let(:non_matching_labels) { [{'az' => 'us-east-1b'}, {'az' => 'us-west-1a'}] }
117
+
118
+ let(:matching_labeled_backends) do
119
+ matching_labels.map{ |l| FactoryGirl.build(:backend, :labels => l) }
120
+ end
121
+ let(:non_matching_labeled_backends) do
122
+ non_matching_labels.map{ |l| FactoryGirl.build(:backend, :labels => l) }
123
+ end
124
+ let(:non_labeled_backends) do
125
+ [FactoryGirl.build(:backend, :labels => {})]
126
+ end
127
+
128
+ before do
129
+ expect(subject).to receive(:'reconfigure!').exactly(:once)
130
+ subject.send(:set_backends,
131
+ matching_labeled_backends + non_matching_labeled_backends + non_labeled_backends)
132
+ end
133
+
134
+ let(:condition) { 'equals' }
135
+ let(:label_filters) { [{ 'condition' => condition, 'label' => 'az', 'value' => 'us-east-1a' }] }
136
+ let(:args) do
137
+ testargs.merge({
138
+ 'discovery' => {
139
+ 'method' => 'base',
140
+ 'label_filters' => label_filters,
141
+ }
129
142
  })
130
- }
143
+ end
144
+
131
145
  it 'removes all backends that do not match the label_filter' do
132
- expect(subject).to receive(:'reconfigure!').exactly(:once)
133
- subject.send(:set_backends, matching_labeled_backends + non_matching_labeled_backends +
134
- non_labeled_backends)
135
- expect(subject.backends).to eq(matching_labeled_backends)
146
+ expect(subject.backends).to contain_exactly(*matching_labeled_backends)
147
+ end
148
+
149
+ context 'when the condition is not-equals' do
150
+ let(:condition) { 'not-equals' }
151
+
152
+ it 'removes all backends that DO match the label_filter' do
153
+ expect(subject.backends).to contain_exactly(*(non_labeled_backends + non_matching_labeled_backends))
154
+ end
155
+ end
156
+
157
+ context 'with multiple labels and conditions conditions' do
158
+ let(:matching_region) { 'region1' }
159
+ let(:matching_labels) { [{'az' => matching_az, 'region' => matching_region}] * 2 }
160
+ let(:non_matching_labels) do
161
+ [
162
+ {'az' => matching_az, 'region' => 'non-matching'},
163
+ {'az' => 'non-matching', 'region' => matching_region},
164
+ {'az' => 'non-matching', 'region' => 'non-matching'},
165
+ ]
166
+ end
167
+
168
+ let(:label_filters) do
169
+ [
170
+ { 'condition' => 'equals', 'label' => 'az', 'value' => matching_az },
171
+ { 'condition' => 'equals', 'label' => 'region', 'value' => matching_region },
172
+ ]
173
+ end
174
+
175
+ it 'returns only backends that match all labels' do
176
+ expect(subject.backends).to contain_exactly(*matching_labeled_backends)
177
+ end
136
178
  end
137
179
  end
138
180
  end
data/spec/spec_helper.rb CHANGED
@@ -13,6 +13,16 @@ require 'webmock/rspec'
13
13
  require 'timecop'
14
14
  Timecop.safe_mode = true
15
15
 
16
+ # configure factory girl
17
+ require 'factory_girl'
18
+ RSpec.configure do |config|
19
+ config.include FactoryGirl::Syntax::Methods
20
+ config.before(:suite) do
21
+ FactoryGirl.find_definitions
22
+ end
23
+ end
24
+
25
+ # general RSpec config
16
26
  RSpec.configure do |config|
17
27
  config.run_all_when_everything_filtered = true
18
28
  config.filter_run :focus
data/synapse.gemspec CHANGED
@@ -23,6 +23,7 @@ Gem::Specification.new do |gem|
23
23
 
24
24
  gem.add_development_dependency "rake"
25
25
  gem.add_development_dependency "rspec", "~> 3.1.0"
26
+ gem.add_development_dependency "factory_girl"
26
27
  gem.add_development_dependency "pry"
27
28
  gem.add_development_dependency "pry-nav"
28
29
  gem.add_development_dependency "webmock"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: synapse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.5
4
+ version: 0.13.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-06-08 00:00:00.000000000 Z
12
+ date: 2016-07-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aws-sdk
@@ -107,6 +107,22 @@ dependencies:
107
107
  - - ~>
108
108
  - !ruby/object:Gem::Version
109
109
  version: 3.1.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: factory_girl
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
110
126
  - !ruby/object:Gem::Dependency
111
127
  name: pry
112
128
  requirement: !ruby/object:Gem::Requirement
@@ -210,6 +226,7 @@ files:
210
226
  - lib/synapse/service_watcher/zookeeper.rb
211
227
  - lib/synapse/service_watcher/zookeeper_dns.rb
212
228
  - lib/synapse/version.rb
229
+ - spec/factories/backend.rb
213
230
  - spec/lib/synapse/file_output_spec.rb
214
231
  - spec/lib/synapse/haproxy_spec.rb
215
232
  - spec/lib/synapse/service_watcher_base_spec.rb
@@ -247,6 +264,7 @@ signing_key:
247
264
  specification_version: 3
248
265
  summary: ': Write a gem summary'
249
266
  test_files:
267
+ - spec/factories/backend.rb
250
268
  - spec/lib/synapse/file_output_spec.rb
251
269
  - spec/lib/synapse/haproxy_spec.rb
252
270
  - spec/lib/synapse/service_watcher_base_spec.rb