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.
- checksums.yaml +7 -0
- data/README.md +160 -13
- data/lib/etcd/client/failover.rb +109 -0
- data/lib/etcd/client/observing.rb +60 -0
- data/lib/etcd/client/protocol.rb +186 -0
- data/lib/etcd/client.rb +15 -405
- data/lib/etcd/cluster.rb +98 -0
- data/lib/etcd/constants.rb +18 -0
- data/lib/etcd/heartbeat.rb +44 -0
- data/lib/etcd/loggable.rb +13 -0
- data/lib/etcd/node.rb +45 -0
- data/lib/etcd/observer.rb +76 -0
- data/lib/etcd/requestable.rb +24 -0
- data/lib/etcd/version.rb +1 -1
- data/lib/etcd.rb +16 -3
- data/spec/etcd/client_spec.rb +34 -257
- data/spec/etcd/cluster_spec.rb +176 -0
- data/spec/etcd/node_spec.rb +58 -0
- data/spec/etcd/observer_spec.rb +163 -0
- data/spec/integration/etcd_spec.rb +43 -10
- data/spec/resources/cluster_controller.rb +19 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/support/client_helper.rb +19 -0
- data/spec/support/cluster_helper.rb +75 -0
- data/spec/support/common_helper.rb +13 -0
- metadata +41 -22
@@ -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
data/lib/etcd.rb
CHANGED
@@ -1,10 +1,23 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Etcd
|
4
|
-
EtcdError
|
5
|
-
ConnectionError
|
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/
|
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'
|
data/spec/etcd/client_spec.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
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
|
-
|
33
|
-
|
9
|
+
def base_uri
|
10
|
+
"http://127.0.0.1:4001/v1"
|
34
11
|
end
|
35
12
|
|
36
|
-
|
37
|
-
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
300
|
-
|
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
|
-
|
347
|
-
|
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
|
-
|
361
|
-
|
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
|
376
|
-
|
377
|
-
|
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
|
-
|
386
|
-
|
387
|
-
|
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
|
-
|
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
|
-
|
415
|
-
stub_request(:
|
416
|
-
|
417
|
-
|
418
|
-
|
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
|