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,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: