etcd-rb 1.0.0.pre1 → 1.0.0

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