synapse 0.13.5 → 0.13.7

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.
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