etcd-rb 1.0.0.pre1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ module Etcd
2
+ class Observer
3
+ include Etcd::Loggable
4
+
5
+ def initialize(client, prefix, handler)
6
+ @client = client
7
+ @prefix = prefix
8
+ @handler = handler
9
+ @index = nil
10
+ reset_logger!(Logger::DEBUG)
11
+ end
12
+
13
+ def run
14
+ @running = true
15
+ @thread = Thread.start do
16
+ while @running
17
+ logger.debug "********* watching #{@prefix} with index #{@index}"
18
+ @client.watch(@prefix, index: @index) do |value, key, info|
19
+ if @running
20
+ logger.debug "watch fired for #{@prefix} with #{info.inspect} "
21
+ call_handler_in_needed(value, key, info)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ self
27
+ end
28
+
29
+ ## etcd has a bug: after restart watches with index fire __sometimes__ with all the previous values
30
+ ## workaround:
31
+ ## - execute @handler only if info[:index] had higher value than the last index
32
+ def call_handler_in_needed(value, key, info)
33
+ if info[:index] && @index.to_i <= info[:index]
34
+ # next time start watching from next index
35
+ @index = info[:index] + 1
36
+ logger.debug "index for #{@prefix} ---- #{@index} "
37
+ @handler.call(value, key, info)
38
+ # first-time-fire
39
+ elsif @index == nil
40
+ @handler.call(value, key, info)
41
+ end
42
+ end
43
+
44
+ def cancel
45
+ @running = false
46
+ self
47
+ end
48
+
49
+ def rerun
50
+ logger.debug "rerun for #{@prefix}"
51
+ @thread.terminate if @thread.alive?
52
+ logger.debug "after termination for #{@prefix}"
53
+ run
54
+ end
55
+
56
+ def join
57
+ @thread.join
58
+ self
59
+ end
60
+
61
+ def status
62
+ @thread.status
63
+ end
64
+
65
+ def pp_status
66
+ "#{@prefix}: #{pp_thread_status}"
67
+ end
68
+
69
+ def pp_thread_status
70
+ st = @thread.status
71
+ st = 'dead by exception' if st == nil
72
+ st = 'dead by termination' if st == false
73
+ st
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,24 @@
1
+ module Etcd
2
+ module Requestable
3
+ include Etcd::Loggable
4
+ def http_client
5
+ @http_client ||= reset_http_client!
6
+ end
7
+
8
+ def reset_http_client!
9
+ @http_client = HTTPClient.new(agent_name: "etcd-rb/#{VERSION}")
10
+ end
11
+
12
+ def request(method, uri, args={})
13
+ logger.debug("request - #{method} #{uri} #{args.inspect}")
14
+ http_client.request(method, uri, args.merge(follow_redirect: true))
15
+ end
16
+
17
+ def request_data(method, uri, args={})
18
+ response = request(method, uri, args)
19
+ if response.status_code == 200
20
+ MultiJson.load(response.body)
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/etcd/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Etcd
4
- VERSION = '1.0.0.pre1'.freeze
4
+ VERSION = '1.0.0'.freeze
5
5
  end
data/lib/etcd.rb CHANGED
@@ -1,10 +1,23 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Etcd
4
- EtcdError = Class.new(StandardError)
5
- ConnectionError = Class.new(EtcdError)
4
+ EtcdError = Class.new(StandardError)
5
+ ConnectionError = Class.new(EtcdError)
6
6
  AllNodesDownError = Class.new(EtcdError)
7
7
  end
8
8
 
9
+ require 'time'
10
+ require 'thread'
11
+ require 'httpclient'
12
+ require 'multi_json'
13
+ require 'logger'
14
+
9
15
  require 'etcd/version'
10
- require 'etcd/client'
16
+ require 'etcd/constants'
17
+ require 'etcd/loggable'
18
+ require 'etcd/requestable'
19
+ require 'etcd/heartbeat'
20
+ require 'etcd/node'
21
+ require 'etcd/cluster'
22
+ require 'etcd/observer'
23
+ require 'etcd/client'
@@ -1,61 +1,17 @@
1
1
  # encoding: utf-8
2
-
3
2
  require 'spec_helper'
4
3
 
5
-
6
4
  module Etcd
7
5
  describe Client do
8
- let :client do
9
- described_class.new(uri: "http://#{host}:#{port}").connect
10
- end
11
-
12
- let :host do
13
- 'example.com'
14
- end
15
-
16
- let :port do
17
- 50_000
18
- end
19
-
20
- let :base_uri do
21
- "http://#{host}:#{port}/v1"
22
- end
23
-
24
- let :machines_uri do
25
- "http://#{host}:#{port}/v1/machines"
26
- end
27
-
28
- let :leader_uri do
29
- "http://#{host}:#{port}/v1/leader"
30
- end
6
+ include ClusterHelper
7
+ include ClientHelper
31
8
 
32
- let :machines do
33
- %W[http://#{host}:#{port} http://#{host}:#{port + 2} http://#{host}:#{port + 1}]
9
+ def base_uri
10
+ "http://127.0.0.1:4001/v1"
34
11
  end
35
12
 
36
- before do
37
- stub_request(:get, leader_uri).to_return(body: "http://#{host}:#{port}")
38
- stub_request(:get, machines_uri).to_return(body: machines.join(', '))
39
- end
40
-
41
- describe '#connect' do
42
- it 'gets the list of machines' do
43
- client
44
- WebMock.should have_requested(:get, machines_uri)
45
- end
46
-
47
- it 'raises an error when the seed node cannot be contacted' do
48
- stub_request(:get, machines_uri).to_timeout
49
- expect { client }.to raise_error(ConnectionError)
50
- end
51
-
52
- it 'discards the seed node and instead talks to the leader (the first in the list of machines)' do
53
- machines.push(machines.shift)
54
- stub_request(:get, machines_uri).to_return(body: machines.join(','))
55
- stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'value' => 'wrong value'}))
56
- stub_request(:get, "#{machines[0]}/v1/keys/foo").to_return(body: MultiJson.dump({'value' => 'right value'}))
57
- client.get('/foo').should == 'right value'
58
- end
13
+ let :client do
14
+ default_client
59
15
  end
60
16
 
61
17
  describe '#get' do
@@ -92,68 +48,9 @@ module Etcd
92
48
  client.get('/foo').should eql({'/foo/bar' => 'bar', '/foo/baz' => 'baz'})
93
49
  end
94
50
  end
95
-
96
- context 'when not talking to the master' do
97
- before do
98
- stub_request(:get, "#{base_uri}/keys/foo").to_return(status: 307, headers: {'Location' => 'http://example.com:7654/v1/keys/foo'})
99
- stub_request(:get, 'http://example.com:7654/v1/machines').to_return(body: %w[http://example.com:7654 http://example.com:7655].join(', '))
100
- stub_request(:get, 'http://example.com:7654/v1/keys/foo').to_return(body: MultiJson.dump({'value' => 'bar'}))
101
- end
102
-
103
- it 'follows redirects' do
104
- client.get('/foo').should == 'bar'
105
- end
106
-
107
- it 'sets the new host as the new leader when it is redirected' do
108
- client.get('/foo')
109
- client.get('/foo').should == 'bar'
110
- WebMock.should have_requested(:get, "#{base_uri}/keys/foo").once
111
- end
112
-
113
- it 'asks for a new list of cluster machines' do
114
- client.get('/foo')
115
- WebMock.should have_requested(:get, 'http://example.com:7654/v1/machines').once
116
- end
117
- end
118
-
119
- context 'when nodes go down' do
120
- let :uris do
121
- machines.map { |m| "#{m}/v1/keys/thing" }
122
- end
123
-
124
- it 'tries with another node when the leader appears to be down' do
125
- stub_request(:get, uris[0]).to_timeout
126
- stub_request(:get, uris[1]).to_return(body: MultiJson.dump({'value' => 'bar'}))
127
- stub_request(:get, uris[2]).to_return(body: MultiJson.dump({'value' => 'baz'}))
128
- client.get('/thing').should == 'bar'
129
- end
130
-
131
- it 'tries other nodes until it finds one that responds' do
132
- stub_request(:get, uris[0]).to_timeout
133
- stub_request(:get, uris[1]).to_timeout
134
- stub_request(:get, uris[2]).to_return(body: MultiJson.dump({'value' => 'bar'}))
135
- client.get('/thing').should == 'bar'
136
- end
137
-
138
- it 'follows redirects when trying a node after a leader failure, and saves the new leader' do
139
- stub_request(:get, uris[0]).to_timeout
140
- stub_request(:get, uris[1]).to_return(status: 307, headers: {'Location' => uris[2]})
141
- stub_request(:get, uris[2]).to_return(body: MultiJson.dump({'value' => 'bar'}))
142
- stub_request(:get, uris[2].sub('/keys/thing', '/machines')).to_return(body: [uris[1], uris[2]].join(', '))
143
- stub_request(:get, uris[2].sub('/thing', '/bar')).to_return(body: MultiJson.dump({'value' => 'baz'}))
144
- client.get('/thing').should == 'bar'
145
- client.get('/bar').should == 'baz'
146
- end
147
-
148
- it 'raises an error when all leader candidates have been exhausted' do
149
- stub_request(:get, uris[0]).to_timeout
150
- stub_request(:get, uris[1]).to_timeout
151
- stub_request(:get, uris[2]).to_timeout
152
- expect { client.get('/thing') }.to raise_error(AllNodesDownError)
153
- end
154
- end
155
51
  end
156
52
 
53
+
157
54
  describe '#set' do
158
55
  before do
159
56
  stub_request(:post, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
@@ -197,12 +94,12 @@ module Etcd
197
94
 
198
95
  it 'returns true when the key is successfully changed' do
199
96
  stub_request(:post, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
200
- client.update('/foo', 'bar', 'baz').should be_true
97
+ client.update('/foo', 'bar', 'baz').should eq(true)
201
98
  end
202
99
 
203
100
  it 'returns false when an error is returned' do
204
101
  stub_request(:post, "#{base_uri}/keys/foo").to_return(status: 400, body: MultiJson.dump({}))
205
- client.update('/foo', 'bar', 'baz').should be_false
102
+ client.update('/foo', 'bar', 'baz').should eq(false)
206
103
  end
207
104
 
208
105
  it 'sets a TTL when the :ttl option is given' do
@@ -211,6 +108,7 @@ module Etcd
211
108
  end
212
109
  end
213
110
 
111
+
214
112
  describe '#delete' do
215
113
  before do
216
114
  stub_request(:delete, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({}))
@@ -235,12 +133,12 @@ module Etcd
235
133
  describe '#exists?' do
236
134
  it 'returns true if the key has a value' do
237
135
  stub_request(:get, "#{base_uri}/keys/foo").to_return(body: MultiJson.dump({'value' => 'bar'}))
238
- client.exists?('/foo').should be_true
136
+ client.exists?('/foo').should eq(true)
239
137
  end
240
138
 
241
139
  it 'returns false if the key does not exist' do
242
140
  stub_request(:get, "#{base_uri}/keys/foo").to_return(status: 404, body: 'Not found')
243
- client.exists?('/foo').should be_false
141
+ client.exists?('/foo').should eq(false)
244
142
  end
245
143
  end
246
144
 
@@ -252,7 +150,8 @@ module Etcd
252
150
  info[:key].should == '/foo'
253
151
  info[:value].should == 'bar'
254
152
  info[:index].should == 31
255
- info[:expiration].to_f.should == (Time.utc(2013, 12, 11, 10, 9, 8) + 0.123).to_f
153
+ # rounding because of ruby 2.0 time parsing bug @see https://gist.github.com/mindreframer/6746829
154
+ info[:expiration].to_f.round.should == (Time.utc(2013, 12, 11, 10, 9, 8) + 0.123).to_f.round
256
155
  info[:ttl].should == 7
257
156
  end
258
157
 
@@ -261,7 +160,7 @@ module Etcd
261
160
  stub_request(:get, "#{base_uri}/keys/foo").to_return(body: body)
262
161
  info = client.info('/foo')
263
162
  info[:key].should == '/foo'
264
- info[:dir].should be_true
163
+ info[:dir].should eq(true)
265
164
  end
266
165
 
267
166
  it 'returns only the pieces of information that are returned' do
@@ -296,156 +195,34 @@ module Etcd
296
195
  end
297
196
  end
298
197
 
299
- describe '#watch' do
300
- it 'sends a GET request for a watch of a key prefix' do
301
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({}))
302
- client.watch('/foo') { }
303
- WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {})
304
- end
305
-
306
- it 'sends a GET request for a watch of a key prefix from a specified index' do
307
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {'index' => 3}).to_return(body: MultiJson.dump({}))
308
- client.watch('/foo', index: 3) { }
309
- WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {'index' => 3})
310
- end
311
-
312
- it 'yields the value' do
313
- body = MultiJson.dump({'value' => 'bar'})
314
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
315
- value = nil
316
- client.watch('/foo') do |v|
317
- value = v
318
- end
319
- value.should == 'bar'
320
- end
321
-
322
- it 'yields the changed key' do
323
- body = MultiJson.dump({'key' => '/foo/bar', 'value' => 'bar'})
324
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
325
- key = nil
326
- client.watch('/foo') do |_, k|
327
- key = k
328
- end
329
- key.should == '/foo/bar'
330
- end
331
-
332
- it 'yields info about the key, when it is a new key' do
333
- body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'newKey' => true})
334
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
335
- info = nil
336
- client.watch('/foo') do |_, _, i|
337
- info = i
338
- end
339
- info[:action].should == :set
340
- info[:key].should == '/foo/bar'
341
- info[:value].should == 'bar'
342
- info[:index].should == 3
343
- info[:new_key].should be_true
344
- end
198
+ describe "when cluster leader changes" do
199
+ include ClusterHelper
345
200
 
346
- it 'yields info about the key, when the key was changed' do
347
- body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'prevValue' => 'baz', 'index' => 3})
348
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
349
- info = nil
350
- client.watch('/foo') do |_, _, i|
351
- info = i
352
- end
353
- info[:action].should == :set
354
- info[:key].should == '/foo/bar'
355
- info[:value].should == 'bar'
356
- info[:index].should == 3
357
- info[:previous_value].should == 'baz'
201
+ let :etcd1_uri do
202
+ "http://127.0.0.1:4001"
358
203
  end
359
204
 
360
- it 'yields info about the key, when the key has a TTL' do
361
- body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'expiration' => '2013-12-11T12:09:08.123+02:00', 'ttl' => 7})
362
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
363
- info = nil
364
- client.watch('/foo') do |_, _, i|
365
- info = i
366
- end
367
- info[:action].should == :set
368
- info[:key].should == '/foo/bar'
369
- info[:value].should == 'bar'
370
- info[:index].should == 3
371
- info[:expiration].to_f.should == (Time.utc(2013, 12, 11, 10, 9, 8) + 0.123).to_f
372
- info[:ttl].should == 7
205
+ let :etcd2_uri do
206
+ "http://127.0.0.1:4002"
373
207
  end
374
208
 
375
- it 'returns the return value of the block' do
376
- body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'expiration' => '2013-12-11T12:09:08.123+02:00', 'ttl' => 7})
377
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
378
- return_value = client.watch('/foo') do |_, k, _|
379
- k
380
- end
381
- return_value.should == '/foo/bar'
382
- end
383
- end
209
+ it "#post - follows redirects and updates the cluster status transparently" do
210
+ with_stubbed_status(etcd1_uri)
211
+ with_stubbed_leaders(healthy_cluster_config)
384
212
 
385
- describe '#observe' do
386
- it 'watches the specified key prefix' do
387
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({}))
388
- barrier = Queue.new
389
- observer = client.observe('/foo') do
390
- barrier << :ping
391
- observer.cancel
392
- observer.join
393
- end
394
- barrier.pop
395
- WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {})
396
- end
213
+ client = Etcd::Client.connect(:uris => etcd1_uri)
214
+ client.leader.etcd.should == etcd1_uri
215
+ client.leader.name.should == "node1"
397
216
 
398
- it 're-watches the prefix with the last seen index immediately' do
399
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({'index' => 3}))
400
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {'index' => 3}).to_return(body: MultiJson.dump({'index' => 4}))
401
- barrier = Queue.new
402
- observer = client.observe('/foo') do |_, _, info|
403
- if info[:index] == 4
404
- barrier << :ping
405
- observer.cancel
406
- observer.join
407
- end
408
- end
409
- barrier.pop
410
- WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {})
411
- WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {'index' => 3})
412
- end
217
+ with_stubbed_leaders(healthy_cluster_changed_leader_config)
413
218
 
414
- it 'yields the value, key and info to the block given' do
415
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'newKey' => true}))
416
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {'index' => 3}).to_return(body: MultiJson.dump({'action' => 'DELETE', 'key' => '/foo/baz', 'value' => 'foo', 'index' => 4}))
417
- stub_request(:get, "#{base_uri}/watch/foo").with(query: {'index' => 4}).to_return(body: MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'hello', 'index' => 5}))
418
- barrier = Queue.new
419
- values = []
420
- keys = []
421
- actions = []
422
- new_keys = []
423
- observer = client.observe('/foo') do |value, key, info|
424
- values << value
425
- keys << key
426
- actions << info[:action]
427
- new_keys << info[:new_key]
428
- if info[:index] == 5
429
- barrier << :ping
430
- observer.cancel
431
- observer.join
432
- end
433
- end
434
- barrier.pop
435
- values.should == %w[bar foo hello]
436
- keys.should == %w[/foo/bar /foo/baz /foo/bar]
437
- actions.should == [:set, :delete, :set]
438
- new_keys.should == [true, nil, nil]
219
+ stub_request(:post, "#{etcd1_uri}/v1/keys/foo").to_return(status: 307, headers: {'Location' => "#{etcd2_uri}/v1/keys/foo"})
220
+ stub_request(:post, "#{etcd2_uri}/v1/keys/foo").to_return(body: MultiJson.dump({'value' => 'bar'}))
221
+ client.set("foo", "bar")
222
+ client.leader.etcd.should == etcd2_uri
223
+ client.leader.name.should == "node2"
439
224
  end
440
225
  end
441
226
 
442
- describe '#machines' do
443
- it 'returns a list of host and ports of the machines in the etcd cluster' do
444
- body = 'http://host01:4001, http://host02:4001, http://host03:4001'
445
- stub_request(:get, machines_uri).to_return(body: body)
446
- stub_request(:get, machines_uri.sub(base_uri, 'http://host01:4001/v1')).to_return(body: body)
447
- client.machines.should == %w[http://host01:4001 http://host02:4001 http://host03:4001]
448
- end
449
- end
450
227
  end
451
- end
228
+ end
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ module Etcd
5
+ describe Cluster do
6
+
7
+ include ClusterHelper
8
+
9
+ let :cluster_uri do
10
+ "http://127.0.0.1:4001"
11
+ end
12
+
13
+ describe :class_methods do
14
+ describe '#cluster_status' do
15
+ it "returns parsed data" do
16
+ with_stubbed_status(cluster_uri) do
17
+ res = Etcd::Cluster.cluster_status(cluster_uri)
18
+ res[0][:name].should == "node1"
19
+ end
20
+ end
21
+ end
22
+
23
+ describe '#parse_cluster_status' do
24
+ it "works with correct parsed json" do
25
+ res = Etcd::Cluster.parse_cluster_status(status_data)
26
+ res[0].should == {
27
+ :name => "node1",
28
+ :raft => "http://127.0.0.1:7001",
29
+ :etcd => "http://127.0.0.1:4001",
30
+ }
31
+ end
32
+ end
33
+
34
+ describe '#nodes_from_uri' do
35
+ it "returns node instances created from uri" do
36
+ with_stubbed_status(cluster_uri) do
37
+ nodes = Etcd::Cluster.nodes_from_uri(cluster_uri)
38
+ nodes.size.should == 3
39
+ nodes.first.class.should == Etcd::Node
40
+ end
41
+ end
42
+
43
+ it "but those instances have no real status yet" do
44
+ with_stubbed_status(cluster_uri) do
45
+ nodes = Etcd::Cluster.nodes_from_uri(cluster_uri)
46
+ nodes.size.should == 3
47
+ nodes.first.status == :unknown
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#init_from_uris', "- preferred way to initialize cluster" do
53
+ describe "in healthy cluster" do
54
+ it "has all nodes at status :running" do
55
+ with_stubbed_status(cluster_uri) do
56
+ with_stubbed_leaders(healthy_cluster_config) do
57
+ cluster = Etcd::Cluster.init_from_uris(cluster_uri)
58
+ nodes = cluster.nodes
59
+ nodes.size.should == 3
60
+ nodes.map{|x| x.status}.uniq.should == [:running]
61
+ end
62
+ end
63
+ end
64
+
65
+ it "has one leader node" do
66
+ with_stubbed_status(cluster_uri) do
67
+ with_stubbed_leaders(healthy_cluster_config) do
68
+ cluster = Etcd::Cluster.init_from_uris(cluster_uri)
69
+ leader = cluster.leader
70
+ leader.etcd.should == cluster_uri
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+
77
+ describe "in un-healthy cluster" do
78
+ it "has some nodes at status :down" do
79
+ with_stubbed_status(cluster_uri) do
80
+ with_stubbed_leaders(one_down_cluster_config) do
81
+ cluster = Etcd::Cluster.init_from_uris(cluster_uri)
82
+ nodes = cluster.nodes
83
+ nodes.size.should == 3
84
+ nodes.map{|x| x.status}.uniq.should == [:running, :down]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ describe :instance_methods do
93
+ describe '#new' do
94
+ it "will not request any info on initialization" do
95
+ cluster = Etcd::Cluster.new(cluster_uri)
96
+ WebMock.should_not have_requested(:get, "http://127.0.0.1:4001/v1/keys/_etcd/machines/")
97
+ end
98
+ end
99
+
100
+ describe '#nodes' do
101
+ it "will update nodes info on first nodes access" do
102
+ with_stubbed_status(cluster_uri) do
103
+ with_stubbed_leaders(healthy_cluster_config) do
104
+ cluster = Etcd::Cluster.new(cluster_uri)
105
+ nodes = cluster.nodes
106
+ nodes.size.should == 3
107
+ end
108
+ end
109
+ end
110
+
111
+ it "caches result on further queries" do
112
+ with_stubbed_status(cluster_uri) do
113
+ with_stubbed_leaders(healthy_cluster_config) do
114
+ cluster = Etcd::Cluster.new(cluster_uri)
115
+ nodes = cluster.nodes
116
+ nodes.map{|x| x.status}.uniq.should == [:running]
117
+ with_stubbed_leaders(one_down_cluster_config) do
118
+ nodes = cluster.nodes
119
+ nodes.map{|x| x.status}.uniq.should_not == [:running, :down]
120
+ # now update for real
121
+ nodes = cluster.update_status
122
+ nodes.map{|x| x.status}.uniq.should == [:running, :down]
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ end
129
+
130
+ describe '#update_status' do
131
+ it "will re-update node stati from etcd" do
132
+ with_stubbed_status(cluster_uri) do
133
+ with_stubbed_leaders(healthy_cluster_config) do
134
+ cluster = Etcd::Cluster.new(cluster_uri)
135
+ nodes = cluster.update_status
136
+ nodes.size.should == 3
137
+ nodes.map{|x| x.status}.uniq.should == [:running]
138
+ with_stubbed_leaders(one_down_cluster_config) do
139
+ nodes = cluster.update_status
140
+ nodes.map{|x| x.status}.uniq.should == [:running, :down]
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '#leader' do
148
+
149
+ it "returns the leader node from nodes" do
150
+ with_stubbed_status(cluster_uri) do
151
+ with_stubbed_leaders(healthy_cluster_config) do
152
+ cluster = Etcd::Cluster.new(cluster_uri)
153
+ leader = cluster.leader
154
+ leader.etcd.should == cluster_uri
155
+ leader.is_leader.should eq(true)
156
+ end
157
+ end
158
+ end
159
+
160
+ it "re-sets leader after every update_status" do
161
+ with_stubbed_status(cluster_uri) do
162
+ with_stubbed_leaders(healthy_cluster_config) do
163
+ cluster = Etcd::Cluster.new(cluster_uri)
164
+ cluster.leader.etcd.should == cluster_uri
165
+ with_stubbed_leaders(healthy_cluster_changed_leader_config) do
166
+ nodes = cluster.update_status
167
+ cluster.leader.etcd.should == "http://127.0.0.1:4002"
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ end
174
+ end
175
+ end
176
+ end