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,58 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+
5
+ module Etcd
6
+ describe Node do
7
+
8
+ def default_node(opts = {})
9
+ args = {:name => "node1", :etcd => "http://example.com:4001", :raft => "http://example.com:7001"}
10
+ Etcd::Node.new(args.merge(opts))
11
+ end
12
+
13
+ describe '#initialize' do
14
+ it "works with all required parameters" do
15
+ default_node({}).should_not == nil
16
+ end
17
+
18
+ it "raises if :etcd url is missing" do
19
+ expect {
20
+ default_node({:etcd => nil})
21
+ }.to raise_error(ArgumentError)
22
+ end
23
+ end
24
+
25
+ describe '#update_status' do
26
+ it "sets status :running if alive" do
27
+ node = default_node
28
+ stub_request(:get, node.leader_uri).to_return(body: node.raft)
29
+ node.update_status
30
+ node.status.should == :running
31
+ end
32
+
33
+ it "sets status :down if down" do
34
+ node = default_node
35
+ stub_request(:get, node.leader_uri).to_timeout
36
+ node.update_status
37
+ node.status.should == :down
38
+ end
39
+
40
+ it "marks leader-flag if leader" do
41
+ node = default_node
42
+ stub_request(:get, node.leader_uri).to_return(body: node.raft)
43
+ node.update_status
44
+ node.is_leader.should eq(true)
45
+ end
46
+
47
+ it "marks leader-flag as :false if leader looses leadership" do
48
+ node = default_node
49
+ stub_request(:get, node.leader_uri).to_return(body: node.raft)
50
+ node.update_status
51
+ node.is_leader.should eq(true)
52
+ stub_request(:get, node.leader_uri).to_return(body: "bla")
53
+ node.update_status
54
+ node.is_leader.should eq(false)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,163 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ module Etcd
5
+ describe Client do
6
+ include ClusterHelper
7
+ include ClientHelper
8
+
9
+ def base_uri
10
+ "http://127.0.0.1:4001/v1"
11
+ end
12
+
13
+ let :client do
14
+ default_client
15
+ end
16
+
17
+ describe '#watch' do
18
+ it 'sends a GET request for a watch of a key prefix' do
19
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({}))
20
+ client.watch('/foo') { }
21
+ WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {})
22
+ end
23
+
24
+ it 'sends a GET request for a watch of a key prefix from a specified index' do
25
+ stub_request(:post, "#{base_uri}/watch/foo").with(query: {'index' => 3}).to_return(body: MultiJson.dump({}))
26
+ client.watch('/foo', index: 3) { }
27
+ WebMock.should have_requested(:post, "#{base_uri}/watch/foo").with(query: {'index' => 3})
28
+ end
29
+
30
+ it 'yields the value' do
31
+ body = MultiJson.dump({'value' => 'bar'})
32
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
33
+ value = nil
34
+ client.watch('/foo') do |v|
35
+ value = v
36
+ end
37
+ value.should == 'bar'
38
+ end
39
+
40
+ it 'yields the changed key' do
41
+ body = MultiJson.dump({'key' => '/foo/bar', 'value' => 'bar'})
42
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
43
+ key = nil
44
+ client.watch('/foo') do |_, k|
45
+ key = k
46
+ end
47
+ key.should == '/foo/bar'
48
+ end
49
+
50
+ it 'yields info about the key, when it is a new key' do
51
+ body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'newKey' => true})
52
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
53
+ info = nil
54
+ client.watch('/foo') do |_, _, i|
55
+ info = i
56
+ end
57
+ info[:action].should == :set
58
+ info[:key].should == '/foo/bar'
59
+ info[:value].should == 'bar'
60
+ info[:index].should == 3
61
+ info[:new_key].should eq(true)
62
+ end
63
+
64
+ it 'yields info about the key, when the key was changed' do
65
+ body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'prevValue' => 'baz', 'index' => 3})
66
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
67
+ info = nil
68
+ client.watch('/foo') do |_, _, i|
69
+ info = i
70
+ end
71
+ info[:action].should == :set
72
+ info[:key].should == '/foo/bar'
73
+ info[:value].should == 'bar'
74
+ info[:index].should == 3
75
+ info[:previous_value].should == 'baz'
76
+ end
77
+
78
+ it 'yields info about the key, when the key has a TTL' do
79
+ body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'expiration' => '2013-12-11T12:09:08.123+02:00', 'ttl' => 7})
80
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
81
+ info = nil
82
+ client.watch('/foo') do |_, _, i|
83
+ info = i
84
+ end
85
+ info[:action].should == :set
86
+ info[:key].should == '/foo/bar'
87
+ info[:value].should == 'bar'
88
+ info[:index].should == 3
89
+ # rounding because of ruby 2.0 time parsing bug @see https://gist.github.com/mindreframer/6746829
90
+ info[:expiration].to_f.round.should == (Time.utc(2013, 12, 11, 10, 9, 8) + 0.123).to_f.round
91
+ info[:ttl].should == 7
92
+ end
93
+
94
+ it 'returns the return value of the block' do
95
+ body = MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'bar', 'index' => 3, 'expiration' => '2013-12-11T12:09:08.123+02:00', 'ttl' => 7})
96
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: body)
97
+ return_value = client.watch('/foo') do |_, k, _|
98
+ k
99
+ end
100
+ return_value.should == '/foo/bar'
101
+ end
102
+ end
103
+
104
+
105
+ describe '#observe' do
106
+ it 'watches the specified key prefix' do
107
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({}))
108
+ barrier = Queue.new
109
+ observer = client.observe('/foo') do
110
+ barrier << :ping
111
+ observer.cancel
112
+ observer.join
113
+ end
114
+ barrier.pop
115
+ WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {})
116
+ end
117
+
118
+ it 're-watches the prefix with the (last seen index + 1) immediately' do
119
+ stub_request(:get, "#{base_uri}/watch/foo").with(query: {}).to_return(body: MultiJson.dump({'index' => 3}))
120
+ stub_request(:post, "#{base_uri}/watch/foo").with(query: {'index' => 4}).to_return(body: MultiJson.dump({'index' => 4}))
121
+ barrier = Queue.new
122
+ observer = client.observe('/foo') do |_, _, info|
123
+ if info[:index] == 4
124
+ barrier << :ping
125
+ observer.cancel
126
+ observer.join
127
+ end
128
+ end
129
+ barrier.pop
130
+ WebMock.should have_requested(:get, "#{base_uri}/watch/foo").with(query: {})
131
+ WebMock.should have_requested(:post, "#{base_uri}/watch/foo").with(query: {'index' => 4})
132
+ end
133
+
134
+ it 'yields the value, key and info to the block given' do
135
+ 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}))
136
+ stub_request(:post, "#{base_uri}/watch/foo").with(query: {'index' => 4}).to_return(body: MultiJson.dump({'action' => 'DELETE', 'key' => '/foo/baz', 'value' => 'foo', 'index' => 4}))
137
+ stub_request(:post, "#{base_uri}/watch/foo").with(query: {'index' => 5}).to_return(body: MultiJson.dump({'action' => 'SET', 'key' => '/foo/bar', 'value' => 'hello', 'index' => 5}))
138
+ barrier = Queue.new
139
+ values = []
140
+ keys = []
141
+ actions = []
142
+ new_keys = []
143
+ observer = client.observe('/foo') do |value, key, info|
144
+ values << value
145
+ keys << key
146
+ actions << info[:action]
147
+ new_keys << info[:new_key]
148
+ if info[:index] == 5
149
+ barrier << :ping
150
+ observer.cancel
151
+ observer.join
152
+ end
153
+ end
154
+ barrier.pop
155
+ values.should == %w[bar foo hello]
156
+ keys.should == %w[/foo/bar /foo/baz /foo/bar]
157
+ actions.should == [:set, :delete, :set]
158
+ new_keys.should == [true, nil, nil]
159
+ end
160
+ end
161
+
162
+ end
163
+ end
@@ -4,9 +4,10 @@ require 'spec_helper'
4
4
  require 'open-uri'
5
5
 
6
6
 
7
- describe 'A etcd client' do
7
+ describe 'With real server an etcd client' do
8
+
8
9
  let :client do
9
- Etcd::Client.new(uri: ENV['ETCD_URI']).connect
10
+ Etcd::Client.test_client
10
11
  end
11
12
 
12
13
  let :prefix do
@@ -19,14 +20,15 @@ describe 'A etcd client' do
19
20
 
20
21
  before do
21
22
  WebMock.disable!
23
+ WebMock.allow_net_connect!
22
24
  end
23
25
 
24
26
  before do
25
- begin
26
- open("#{ENV['ETCD_URI']}/v1/leader").read
27
- rescue Errno::ECONNREFUSED, Errno::ENOENT
28
- pending('etcd not running, start it with `./spec/resources/etcd-cluster start`')
29
- end
27
+ ClusterController.start_cluster
28
+ end
29
+
30
+ before do
31
+ pending('etcd could not be started, check it with `sh/cluster start`') unless client
30
32
  end
31
33
 
32
34
  before do
@@ -46,13 +48,44 @@ describe 'A etcd client' do
46
48
 
47
49
  it 'watches for changes to a key' do
48
50
  Thread.start { sleep(0.1); client.set(key, 'baz') }
49
- new_value = client.watch(key) { |value| value }
51
+ new_value, info = *client.watch(key) { |v, k, info| [v, info] }
50
52
  new_value.should == 'baz'
53
+ info[:new_key].should == true
51
54
  end
52
55
 
53
56
  it 'conditionally sets the value for a key' do
54
57
  client.set(key, 'bar')
55
- client.update(key, 'qux', 'baz').should be_false
56
- client.update(key, 'qux', 'bar').should be_true
58
+ client.update(key, 'qux', 'baz').should eq(false)
59
+ client.update(key, 'qux', 'bar').should eq(true)
60
+ end
61
+
62
+
63
+ it "has heartbeat, that resets observed watches" do
64
+ ClusterController.start_cluster
65
+ client = Etcd::Client.test_client(:heartbeat_freq => 0.2)
66
+ client.cluster.nodes.map(&:status).uniq.should == [:running]
67
+ changes = Queue.new
68
+
69
+ client.observe('/foo') do |v,k,info|
70
+ puts "triggered #{info.inspect}"
71
+ changes << info
72
+ end
73
+
74
+ changes.size.should == 0
75
+
76
+ ### simulate second console
77
+ a = Thread.new do
78
+ client = Etcd::Client.test_client
79
+ ClusterController.kill_node(client.cluster.leader.name)
80
+ sleep 0.4
81
+ puts "1.st try"
82
+ client.set("/foo", "bar")
83
+ sleep 0.4
84
+ puts "2.nd try"
85
+ client.set("/foo", "barss")
86
+ end
87
+
88
+ sleep 1.5
89
+ changes.size.should == 2
57
90
  end
58
91
  end
@@ -0,0 +1,19 @@
1
+ class ClusterController
2
+
3
+ def self.sh_path
4
+ File.expand_path(File.join(File.dirname(__FILE__), '..', '..', "sh"))
5
+ end
6
+
7
+ def self.kill_node(node_name)
8
+ node_pid = `ps -ef|grep #{node_name}|grep -v grep`.split[1]
9
+ `kill -9 #{node_pid}`
10
+ end
11
+
12
+ def self.start_cluster
13
+ `#{sh_path}/cluster start`
14
+ end
15
+
16
+ def self.stop_cluster
17
+ `#{sh_path}/cluster stop`
18
+ end
19
+ end
data/spec/spec_helper.rb CHANGED
@@ -18,7 +18,8 @@ unless ENV['COVERAGE'] == 'no'
18
18
  end
19
19
  end
20
20
 
21
- ENV['ETCD_URI'] ||= 'http://127.0.0.1:4001'
22
-
23
21
  require 'webmock/rspec'
24
22
  require 'etcd'
23
+ require 'json'
24
+
25
+ Dir["./spec/support/*.rb"].each { |f| require f}
@@ -0,0 +1,19 @@
1
+ module ClientHelper
2
+ def default_client(uri = "http://127.0.0.1:4001")
3
+ client = Etcd::Client.new(:uris => uri)
4
+ client.cluster = healthy_cluster(uri)
5
+ client
6
+ end
7
+
8
+ # manually construct a valid cluster object
9
+ # clumsy, but works atm
10
+ def healthy_cluster(uri = "http://127.0.0.1:4001")
11
+ data = Etcd::Cluster.parse_cluster_status(status_data)
12
+ nodes = Etcd::Cluster.nodes_from_attributes(data)
13
+ cluster = Etcd::Cluster.new(uri)
14
+ cluster.nodes = nodes
15
+ nodes.map{|x| x.status = :running}
16
+ nodes.first.is_leader = true
17
+ cluster
18
+ end
19
+ end
@@ -0,0 +1,75 @@
1
+ module ClusterHelper
2
+ def status_data
3
+ [
4
+ {"action"=>"GET",
5
+ "key"=>"/_etcd/machines/node1",
6
+ "value"=>
7
+ "raft=http://127.0.0.1:7001&etcd=http://127.0.0.1:4001&raftVersion=v0.1.1",
8
+ "index"=>360},
9
+ {"action"=>"GET",
10
+ "key"=>"/_etcd/machines/node2",
11
+ "value"=>
12
+ "raft=http://127.0.0.1:7002&etcd=http://127.0.0.1:4002&raftVersion=v0.1.1",
13
+ "index"=>360},
14
+ {"action"=>"GET",
15
+ "key"=>"/_etcd/machines/node3",
16
+ "value"=>
17
+ "raft=http://127.0.0.1:7003&etcd=http://127.0.0.1:4003&raftVersion=v0.1.1",
18
+ "index"=>360}
19
+ ]
20
+ end
21
+
22
+ def healthy_cluster_config
23
+ {
24
+ 'http://127.0.0.1:4001' => 'http://127.0.0.1:7001',
25
+ 'http://127.0.0.1:4002' => 'http://127.0.0.1:7001',
26
+ 'http://127.0.0.1:4003' => 'http://127.0.0.1:7001'
27
+ }
28
+ end
29
+
30
+ def one_down_cluster_config
31
+ {
32
+ 'http://127.0.0.1:4001' => 'http://127.0.0.1:7001',
33
+ 'http://127.0.0.1:4002' => 'http://127.0.0.1:7001',
34
+ 'http://127.0.0.1:4003' => :down
35
+ }
36
+ end
37
+
38
+ def healthy_cluster_changed_leader_config
39
+ {
40
+ 'http://127.0.0.1:4001' => 'http://127.0.0.1:7002',
41
+ 'http://127.0.0.1:4002' => 'http://127.0.0.1:7002',
42
+ 'http://127.0.0.1:4003' => 'http://127.0.0.1:7002'
43
+ }
44
+ end
45
+
46
+ def with_stubbed_status(uri)
47
+ status_uri = Etcd::Cluster.status_uri(uri)
48
+ stub_request(:get, status_uri).to_return(body: MultiJson.dump(status_data))
49
+ yield if block_given?
50
+ #WebMock.should have_requested(:get, status_uri)
51
+ end
52
+
53
+
54
+ def leader_uri(uri)
55
+ "#{uri}/v1/leader"
56
+ end
57
+
58
+ def stub_leader_uri(uri, opts = {})
59
+ leader = (opts[:leader] ||"http://127.0.0.1:7001")
60
+ if leader == :down
61
+ stub_request(:get, leader_uri(uri)).to_timeout
62
+ else
63
+ stub_request(:get, leader_uri(uri)).to_return(body: leader)
64
+ end
65
+ end
66
+
67
+ # [{etcd_url => leader_string},{etcd_url => leader_string}]
68
+ def with_stubbed_leaders(cluster_config)
69
+ cluster_config.each do |url, leader_uri|
70
+ stub_leader_uri(url, :leader => leader_uri)
71
+ end
72
+ yield if block_given?
73
+ #urls.each { |url| WebMock.should have_requested(:get, leader_uri(url))}
74
+ end
75
+ end
@@ -0,0 +1,13 @@
1
+ require './spec/resources/cluster_controller'
2
+
3
+ class Etcd::Client
4
+ def self.test_client(opts = {})
5
+ seed_uris = ["http://127.0.0.1:4001", "http://127.0.0.1:4002", "http://127.0.0.1:4003"]
6
+ opts.merge!(:uris => seed_uris)
7
+ begin
8
+ client = Etcd::Client.connect(opts)
9
+ rescue Etcd::AllNodesDownError
10
+ return nil
11
+ end
12
+ end
13
+ end
metadata CHANGED
@@ -1,48 +1,44 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etcd-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre1
5
- prerelease: 6
4
+ version: 1.0.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Theo Hultberg
8
+ - Roman Heinrich
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-27 00:00:00.000000000 Z
12
+ date: 2014-10-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httpclient
16
16
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
17
  requirements:
19
- - - ~>
18
+ - - "~>"
20
19
  - !ruby/object:Gem::Version
21
20
  version: 2.3.0
22
21
  type: :runtime
23
22
  prerelease: false
24
23
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
24
  requirements:
27
- - - ~>
25
+ - - "~>"
28
26
  - !ruby/object:Gem::Version
29
27
  version: 2.3.0
30
28
  - !ruby/object:Gem::Dependency
31
29
  name: multi_json
32
30
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
31
  requirements:
35
- - - ~>
32
+ - - "~>"
36
33
  - !ruby/object:Gem::Version
37
- version: 1.7.0
34
+ version: '1.7'
38
35
  type: :runtime
39
36
  prerelease: false
40
37
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
38
  requirements:
43
- - - ~>
39
+ - - "~>"
44
40
  - !ruby/object:Gem::Version
45
- version: 1.7.0
41
+ version: '1.7'
46
42
  description: ''
47
43
  email:
48
44
  - theo@iconara.net
@@ -50,40 +46,63 @@ executables: []
50
46
  extensions: []
51
47
  extra_rdoc_files: []
52
48
  files:
49
+ - README.md
50
+ - lib/etcd.rb
53
51
  - lib/etcd/client.rb
52
+ - lib/etcd/client/failover.rb
53
+ - lib/etcd/client/observing.rb
54
+ - lib/etcd/client/protocol.rb
55
+ - lib/etcd/cluster.rb
56
+ - lib/etcd/constants.rb
57
+ - lib/etcd/heartbeat.rb
58
+ - lib/etcd/loggable.rb
59
+ - lib/etcd/node.rb
60
+ - lib/etcd/observer.rb
61
+ - lib/etcd/requestable.rb
54
62
  - lib/etcd/version.rb
55
- - lib/etcd.rb
56
- - README.md
57
63
  - spec/etcd/client_spec.rb
64
+ - spec/etcd/cluster_spec.rb
65
+ - spec/etcd/node_spec.rb
66
+ - spec/etcd/observer_spec.rb
58
67
  - spec/integration/etcd_spec.rb
68
+ - spec/resources/cluster_controller.rb
59
69
  - spec/spec_helper.rb
70
+ - spec/support/client_helper.rb
71
+ - spec/support/cluster_helper.rb
72
+ - spec/support/common_helper.rb
60
73
  homepage: http://github.com/iconara/etcd-rb
61
74
  licenses:
62
75
  - Apache License 2.0
76
+ metadata: {}
63
77
  post_install_message:
64
78
  rdoc_options: []
65
79
  require_paths:
66
80
  - lib
67
81
  required_ruby_version: !ruby/object:Gem::Requirement
68
- none: false
69
82
  requirements:
70
- - - ! '>='
83
+ - - ">="
71
84
  - !ruby/object:Gem::Version
72
85
  version: 1.9.3
73
86
  required_rubygems_version: !ruby/object:Gem::Requirement
74
- none: false
75
87
  requirements:
76
- - - ! '>'
88
+ - - ">="
77
89
  - !ruby/object:Gem::Version
78
- version: 1.3.1
90
+ version: '0'
79
91
  requirements: []
80
92
  rubyforge_project:
81
- rubygems_version: 1.8.25
93
+ rubygems_version: 2.2.2
82
94
  signing_key:
83
- specification_version: 3
95
+ specification_version: 4
84
96
  summary: ''
85
97
  test_files:
86
98
  - spec/etcd/client_spec.rb
99
+ - spec/etcd/cluster_spec.rb
100
+ - spec/etcd/node_spec.rb
101
+ - spec/etcd/observer_spec.rb
87
102
  - spec/integration/etcd_spec.rb
103
+ - spec/resources/cluster_controller.rb
88
104
  - spec/spec_helper.rb
105
+ - spec/support/client_helper.rb
106
+ - spec/support/cluster_helper.rb
107
+ - spec/support/common_helper.rb
89
108
  has_rdoc: