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