etcdv3 0.10.1 → 0.11.5
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 +5 -5
- data/.gitignore +6 -5
- data/.travis.yml +1 -0
- data/README.md +40 -0
- data/lib/etcdv3/connection.rb +26 -5
- data/lib/etcdv3/connection_wrapper.rb +22 -7
- data/lib/etcdv3/lock.rb +2 -2
- data/lib/etcdv3/namespace/kv/requests.rb +54 -0
- data/lib/etcdv3/namespace/kv/transaction.rb +91 -0
- data/lib/etcdv3/namespace/kv.rb +62 -0
- data/lib/etcdv3/namespace/lock.rb +32 -0
- data/lib/etcdv3/namespace/utilities.rb +44 -0
- data/lib/etcdv3/namespace/watch.rb +38 -0
- data/lib/etcdv3/version.rb +1 -1
- data/lib/etcdv3.rb +16 -2
- data/spec/etcdv3/connection_spec.rb +3 -3
- data/spec/etcdv3/connection_wrapper_spec.rb +39 -8
- data/spec/etcdv3/lock_spec.rb +19 -5
- data/spec/etcdv3/namespace/kv_spec.rb +82 -0
- data/spec/etcdv3/namespace/lock_spec.rb +42 -0
- data/spec/etcdv3/namespace/watch_spec.rb +36 -0
- data/spec/etcdv3/watch_spec.rb +12 -5
- data/spec/etcdv3_spec.rb +257 -1
- data/spec/helpers/connections.rb +18 -2
- data/spec/helpers/metadata_passthrough.rb +21 -0
- data/spec/helpers/shared_examples_for_timeout.rb +4 -4
- data/spec/helpers/test_instance.rb +2 -0
- data/spec/spec_helper.rb +4 -0
- metadata +17 -4
@@ -3,9 +3,9 @@ require 'spec_helper'
|
|
3
3
|
describe Etcdv3::ConnectionWrapper do
|
4
4
|
let(:conn) { local_connection }
|
5
5
|
let(:endpoints) { ['http://localhost:2379', 'http://localhost:2389'] }
|
6
|
+
subject { Etcdv3::ConnectionWrapper.new(10, *endpoints, nil, true) }
|
6
7
|
|
7
8
|
describe '#initialize' do
|
8
|
-
subject { Etcdv3::ConnectionWrapper.new(10, *endpoints) }
|
9
9
|
it { is_expected.to have_attributes(user: nil, password: nil, token: nil) }
|
10
10
|
it 'sets hostnames in correct order' do
|
11
11
|
expect(subject.endpoints.map(&:hostname)).to eq(['localhost:2379', 'localhost:2389'])
|
@@ -16,7 +16,6 @@ describe Etcdv3::ConnectionWrapper do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
describe "#rotate_connection_endpoint" do
|
19
|
-
subject { Etcdv3::ConnectionWrapper.new(10, *endpoints) }
|
20
19
|
before do
|
21
20
|
subject.rotate_connection_endpoint
|
22
21
|
end
|
@@ -29,11 +28,30 @@ describe Etcdv3::ConnectionWrapper do
|
|
29
28
|
end
|
30
29
|
|
31
30
|
describe "Failover Simulation" do
|
32
|
-
let(:
|
31
|
+
let(:allow_reconnect) { true }
|
32
|
+
let(:modified_conn) {
|
33
|
+
local_connection(
|
34
|
+
"http://localhost:2369, http://localhost:2379",
|
35
|
+
allow_reconnect: allow_reconnect
|
36
|
+
)
|
37
|
+
}
|
38
|
+
subject { modified_conn.get('boom') }
|
39
|
+
|
33
40
|
context 'without auth' do
|
34
41
|
# Set primary endpoint to a non-existing etcd endpoint
|
35
|
-
|
36
|
-
|
42
|
+
context 'with reconnect' do
|
43
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
|
44
|
+
end
|
45
|
+
context 'without reconnect' do
|
46
|
+
let(:allow_reconnect) { false }
|
47
|
+
it { expect { subject }.to raise_error(GRPC::Unavailable) }
|
48
|
+
end
|
49
|
+
context 'without reconnect with single endpoint' do
|
50
|
+
let(:modified_conn) {
|
51
|
+
local_connection("http://localhost:2369",allow_reconnect: false)
|
52
|
+
}
|
53
|
+
it { expect { subject }.to raise_error(GRPC::Unavailable) }
|
54
|
+
end
|
37
55
|
end
|
38
56
|
context 'with auth' do
|
39
57
|
before do
|
@@ -50,12 +68,19 @@ describe Etcdv3::ConnectionWrapper do
|
|
50
68
|
modified_conn.auth_disable
|
51
69
|
modified_conn.user_delete('root')
|
52
70
|
end
|
53
|
-
|
54
|
-
|
71
|
+
context 'with reconnect' do
|
72
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
|
73
|
+
end
|
74
|
+
context 'without reconnect' do
|
75
|
+
let(:allow_reconnect) { false }
|
76
|
+
it { expect { subject }.to raise_error(GRPC::Unavailable) }
|
77
|
+
end
|
55
78
|
end
|
56
79
|
end
|
57
80
|
|
58
81
|
describe "GRPC::Unauthenticated recovery" do
|
82
|
+
let(:allow_reconnect) { true }
|
83
|
+
let(:conn) { local_connection(allow_reconnect: allow_reconnect) }
|
59
84
|
let(:wrapper) { conn.send(:conn) }
|
60
85
|
let(:connection) { wrapper.connection }
|
61
86
|
before do
|
@@ -71,6 +96,12 @@ describe Etcdv3::ConnectionWrapper do
|
|
71
96
|
conn.user_delete('root')
|
72
97
|
end
|
73
98
|
subject { conn.user_get('root') }
|
74
|
-
|
99
|
+
context 'with reconnect' do
|
100
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::AuthUserGetResponse) }
|
101
|
+
end
|
102
|
+
context 'without reconnect' do
|
103
|
+
let(:allow_reconnect) { false }
|
104
|
+
it { expect { subject }.to raise_error(GRPC::Unauthenticated) }
|
105
|
+
end
|
75
106
|
end
|
76
107
|
end
|
data/spec/etcdv3/lock_spec.rb
CHANGED
@@ -10,14 +10,28 @@ unless $instance.version < Gem::Version.new("3.2.0")
|
|
10
10
|
#it_should_behave_like "a method with a GRPC timeout", described_class, :lock, :lock, 'foo'
|
11
11
|
|
12
12
|
describe '#lock' do
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
it 'returns a response' do
|
14
|
+
lease_id = lease_stub.lease_grant(10)['ID']
|
15
|
+
|
16
|
+
expect(stub.lock('example1', lease_id)).to be_an_instance_of(V3lockpb::LockResponse)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'passes metadata correctly' do
|
20
|
+
lease_id = lease_stub.lease_grant(10)['ID']
|
21
|
+
stub = expect_metadata_passthrough(described_class, :lock, :lock)
|
22
|
+
stub.lock('example2', lease_id)
|
23
|
+
end
|
16
24
|
end
|
17
25
|
|
18
26
|
describe '#unlock' do
|
19
|
-
|
20
|
-
|
27
|
+
it 'returns a response' do
|
28
|
+
expect(stub.unlock('example3')).to be_an_instance_of(V3lockpb::UnlockResponse)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'passes metadata correctly' do
|
32
|
+
stub = expect_metadata_passthrough(described_class, :unlock, :unlock)
|
33
|
+
stub.unlock('example4')
|
34
|
+
end
|
21
35
|
end
|
22
36
|
end
|
23
37
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Etcdv3::KV do
|
4
|
+
let(:stub) { local_namespace_stub(Etcdv3::Namespace::KV, 1, '/namespace/') }
|
5
|
+
let(:lease_stub) { local_stub(Etcdv3::Lease, 1) }
|
6
|
+
|
7
|
+
it_should_behave_like "a method with a GRPC timeout", described_class, :get, :range, "key"
|
8
|
+
it_should_behave_like "a method with a GRPC timeout", described_class, :del, :delete_range, "key"
|
9
|
+
it_should_behave_like "a method with a GRPC timeout", described_class, :put, :put, "key", "val"
|
10
|
+
|
11
|
+
it "should timeout transactions" do
|
12
|
+
stub = local_namespace_stub(Etcdv3::Namespace::KV, 0, '/namespace/')
|
13
|
+
expect { stub.transaction(Proc.new { nil }) }.to raise_error(GRPC::DeadlineExceeded)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#put' do
|
17
|
+
context 'without lease' do
|
18
|
+
subject { stub.put('test', 'test') }
|
19
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::PutResponse) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'with lease' do
|
23
|
+
let(:lease_id) { lease_stub.lease_grant(1)['ID'] }
|
24
|
+
subject { stub.put('lease', 'test', lease: lease_id) }
|
25
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::PutResponse) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#get' do
|
30
|
+
subject { stub.get('test') }
|
31
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#del' do
|
35
|
+
context 'del without range' do
|
36
|
+
subject { stub.del('test') }
|
37
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::DeleteRangeResponse) }
|
38
|
+
end
|
39
|
+
context 'del with range' do
|
40
|
+
subject { stub.del('test', range_end: 'testtt') }
|
41
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::DeleteRangeResponse) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#transaction' do
|
46
|
+
context 'put' do
|
47
|
+
let!(:block) do
|
48
|
+
Proc.new do |txn|
|
49
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
50
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
51
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
subject { stub.transaction(block) }
|
55
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'del' do
|
59
|
+
let!(:block) do
|
60
|
+
Proc.new do |txn|
|
61
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
62
|
+
txn.success = [ txn.del('txn-one') ]
|
63
|
+
txn.failure = [ txn.del('txn-two') ]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
subject { stub.transaction(block) }
|
67
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'get' do
|
72
|
+
let!(:block) do
|
73
|
+
Proc.new do |txn|
|
74
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
75
|
+
txn.success = [ txn.get('txn-success') ]
|
76
|
+
txn.failure = [ txn.get('txn-failure') ]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
subject { stub.transaction(block) }
|
80
|
+
it { is_expected.to be_an_instance_of(Etcdserverpb::TxnResponse) }
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Locking is not implemented in etcd v3.1.X
|
4
|
+
unless $instance.version < Gem::Version.new("3.2.0")
|
5
|
+
describe Etcdv3::Namespace::Lock do
|
6
|
+
let(:stub) { local_namespace_stub(Etcdv3::Namespace::Lock, 1, '/namespace/') }
|
7
|
+
let(:lease_stub) { local_stub(Etcdv3::Lease, 1) }
|
8
|
+
|
9
|
+
# NOTE: this was running duplicate tests against Etcdv3::Lock before, but it
|
10
|
+
# doesn't work with Etcdv3::Namespace::Lock
|
11
|
+
#
|
12
|
+
# it_should_behave_like "a method with a GRPC timeout", described_class, :unlock, :unlock, 'foo'
|
13
|
+
|
14
|
+
# it_should_behave_like "a method with a GRPC timeout", described_class, :lock, :lock, 'foo'
|
15
|
+
|
16
|
+
describe '#lock' do
|
17
|
+
it 'returns a response' do
|
18
|
+
lease_id = lease_stub.lease_grant(10)['ID']
|
19
|
+
expect(stub.lock('example1', lease_id)).to(
|
20
|
+
be_an_instance_of(V3lockpb::LockResponse)
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'passes metadata correctly' do
|
25
|
+
lease_id = lease_stub.lease_grant(10)['ID']
|
26
|
+
stub = expect_metadata_passthrough_namespace(described_class, :lock, :lock, '/namespace/')
|
27
|
+
stub.lock('example2', lease_id)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#unlock' do
|
32
|
+
it 'returns a response' do
|
33
|
+
expect(stub.unlock('example3')).to be_an_instance_of(V3lockpb::UnlockResponse)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'passes metadata correctly' do
|
37
|
+
stub = expect_metadata_passthrough_namespace(described_class, :unlock, :unlock, '/namespace/')
|
38
|
+
stub.unlock('example4')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
|
5
|
+
describe Etcdv3::Namespace::Watch do
|
6
|
+
let(:stub) { local_namespace_stub(Etcdv3::Namespace::Watch, 5, '/namespace/') }
|
7
|
+
let(:kv_stub_no_ns) { local_stub(Etcdv3::KV, 1) }
|
8
|
+
let(:kv_stub) { local_namespace_stub(Etcdv3::Namespace::KV, 1, '/namespace/') }
|
9
|
+
|
10
|
+
context 'watch' do
|
11
|
+
it 'should return an event' do
|
12
|
+
resp = nil
|
13
|
+
thr = Thread.new do |thr|
|
14
|
+
resp = stub.watch("foo", nil, 1, nil)
|
15
|
+
end
|
16
|
+
sleep 2
|
17
|
+
kv_stub.put("foo", "works")
|
18
|
+
thr.join
|
19
|
+
expect(resp).to be_an_instance_of(Google::Protobuf::RepeatedField)
|
20
|
+
expect(resp.last.kv.key).to eq('foo')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should return event when non-namespace client writes to key' do
|
24
|
+
resp = nil
|
25
|
+
thr = Thread.new do |thr|
|
26
|
+
resp = stub.watch("foobar", nil, 1, nil)
|
27
|
+
end
|
28
|
+
sleep 2
|
29
|
+
kv_stub_no_ns.put("/namespace/foobar", "works")
|
30
|
+
thr.join
|
31
|
+
expect(resp).to be_an_instance_of(Google::Protobuf::RepeatedField)
|
32
|
+
expect(resp.last.kv.key).to eq('foobar')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/spec/etcdv3/watch_spec.rb
CHANGED
@@ -4,14 +4,21 @@ require 'securerandom'
|
|
4
4
|
# Locking is not implemented in etcd v3.1.X
|
5
5
|
unless $instance.version < Gem::Version.new("3.2.0")
|
6
6
|
describe Etcdv3::Watch do
|
7
|
-
let(:stub) { local_stub(Etcdv3::Watch,
|
7
|
+
let(:stub) { local_stub(Etcdv3::Watch, 5) }
|
8
8
|
let(:kv_stub) { local_stub(Etcdv3::KV, 1) }
|
9
9
|
|
10
|
-
context '
|
11
|
-
|
12
|
-
|
10
|
+
context 'watch' do
|
11
|
+
it 'should return an event' do
|
12
|
+
resp = nil
|
13
|
+
thr = Thread.new do |thr|
|
14
|
+
resp = stub.watch("foo", nil, 1, nil)
|
15
|
+
end
|
16
|
+
sleep 2
|
17
|
+
kv_stub.put("foo", "works")
|
18
|
+
thr.join
|
19
|
+
expect(resp).to be_an_instance_of(Google::Protobuf::RepeatedField)
|
20
|
+
expect(resp.last.kv.key).to eq('foo')
|
13
21
|
end
|
14
|
-
it_should_behave_like "a method with a GRPC timeout", described_class, :watch, :watch, 'foo', "\0", 1, nil
|
15
22
|
end
|
16
23
|
end
|
17
24
|
end
|
data/spec/etcdv3_spec.rb
CHANGED
@@ -293,7 +293,7 @@ describe Etcdv3 do
|
|
293
293
|
describe '#role_grant_permission' do
|
294
294
|
before { conn.role_add('grant') }
|
295
295
|
after { conn.role_delete('grant') }
|
296
|
-
subject { conn.role_grant_permission('grant', :readwrite, 'a', {range_end: 'Z'}) }
|
296
|
+
subject { conn.role_grant_permission('grant', :readwrite, 'a', **{range_end: 'Z'}) }
|
297
297
|
it { is_expected.to_not be_nil }
|
298
298
|
it_should_behave_like "Etcdv3 instance using a timeout", :role_grant_permission, 'grant', :readwrite, 'a'
|
299
299
|
end
|
@@ -521,5 +521,261 @@ describe Etcdv3 do
|
|
521
521
|
end
|
522
522
|
end
|
523
523
|
end
|
524
|
+
|
525
|
+
describe "namespace" do
|
526
|
+
|
527
|
+
describe '#get' do
|
528
|
+
let(:get_conn) { local_connection_with_namespace("/namespace-get/") }
|
529
|
+
|
530
|
+
before do
|
531
|
+
conn.put('/apples/', 'app')
|
532
|
+
conn.put('/namespace-get/apple', 'apple')
|
533
|
+
conn.put('/namespace-get/apples', 'apples')
|
534
|
+
conn.put('/namespace-get/appless', 'appless')
|
535
|
+
end
|
536
|
+
|
537
|
+
it 'returns key w/o namespace' do
|
538
|
+
expect(get_conn.get("apple").kvs.last.value).to eq('apple')
|
539
|
+
end
|
540
|
+
|
541
|
+
it 'returns keys w/o namespace' do
|
542
|
+
expect(get_conn.get("apple", range_end: 'applf').kvs.size).to eq(3)
|
543
|
+
end
|
544
|
+
|
545
|
+
it 'returns all keys under namespace' do
|
546
|
+
expect(get_conn.get("", range_end: "\0").kvs.size).to eq(3)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
describe '#put' do
|
551
|
+
let(:put_conn) { local_connection_with_namespace("/namespace-put/") }
|
552
|
+
|
553
|
+
before do
|
554
|
+
put_conn.put('apple_put', 'test')
|
555
|
+
end
|
556
|
+
it 'returns key with namespace' do
|
557
|
+
expect(conn.get("/namespace-put/apple_put").kvs.last.value).to eq('test')
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
describe '#del' do
|
562
|
+
let(:del_conn) { local_connection_with_namespace("/del-test/") }
|
563
|
+
|
564
|
+
context 'zero-byte' do
|
565
|
+
before do
|
566
|
+
del_conn.put('test', "key")
|
567
|
+
del_conn.put('test2', "key2")
|
568
|
+
conn.put('wall', 'zzzz')
|
569
|
+
conn.put('walzz', 'adsfas')
|
570
|
+
end
|
571
|
+
|
572
|
+
it 'deleting all keys should be scoped to namespace' do
|
573
|
+
resp = del_conn.del("", range_end: "\0")
|
574
|
+
expect(resp.deleted).to eq(2)
|
575
|
+
expect(conn.get("wall").kvs.last.value).to eq('zzzz')
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
context 'no range' do
|
580
|
+
before { del_conn.put('test', 'value') }
|
581
|
+
subject { del_conn.del('test') }
|
582
|
+
it { is_expected.to_not be_nil }
|
583
|
+
end
|
584
|
+
|
585
|
+
context 'ranged del' do
|
586
|
+
before do
|
587
|
+
del_conn.put('test', 'value')
|
588
|
+
del_conn.put('testt', 'value')
|
589
|
+
end
|
590
|
+
subject { del_conn.del('test', range_end: 'testtt') }
|
591
|
+
it { is_expected.to_not be_nil }
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
describe '#transaction' do
|
596
|
+
let(:trans_conn) { local_connection_with_namespace("/namespace/") }
|
597
|
+
|
598
|
+
describe 'txn.value' do
|
599
|
+
before { trans_conn.put('txn', 'value') }
|
600
|
+
after { trans_conn.del('txn') }
|
601
|
+
context 'success' do
|
602
|
+
subject! do
|
603
|
+
trans_conn.transaction do |txn|
|
604
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
605
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
606
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
607
|
+
end
|
608
|
+
end
|
609
|
+
it 'sets correct key' do
|
610
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
|
611
|
+
expect(conn.get("/namespace/txn-test").kvs.first.value).to eq('success')
|
612
|
+
end
|
613
|
+
it "raises a GRPC::DeadlineExceeded exception when it takes too long" do
|
614
|
+
expect do
|
615
|
+
trans_conn.transaction(timeout: 0) do |txn|
|
616
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
617
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
618
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
619
|
+
end
|
620
|
+
end.to raise_exception(GRPC::DeadlineExceeded)
|
621
|
+
end
|
622
|
+
it "accepts a timeout" do
|
623
|
+
expect do
|
624
|
+
trans_conn.transaction(timeout: 1) do |txn|
|
625
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
626
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
627
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
628
|
+
end
|
629
|
+
end.to_not raise_exception
|
630
|
+
end
|
631
|
+
end
|
632
|
+
context "success, value with lease" do
|
633
|
+
let!(:lease_id) { trans_conn.lease_grant(2)['ID'] }
|
634
|
+
subject! do
|
635
|
+
trans_conn.transaction do |txn|
|
636
|
+
txn.compare = [ txn.value('txn', :equal, 'value') ]
|
637
|
+
txn.success = [ txn.put('txn-test', 'success', lease_id) ]
|
638
|
+
txn.failure = [ txn.put('txn-test', 'failed', lease_id) ]
|
639
|
+
end
|
640
|
+
end
|
641
|
+
it 'sets correct key, with a lease' do
|
642
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
|
643
|
+
expect(trans_conn.get('txn-test').kvs.first.lease).to eq(lease_id)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
context 'failure' do
|
647
|
+
subject! do
|
648
|
+
trans_conn.transaction do |txn|
|
649
|
+
txn.compare = [ txn.value('txn', :equal, 'notright') ]
|
650
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
651
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
652
|
+
end
|
653
|
+
end
|
654
|
+
it 'sets correct key' do
|
655
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
describe 'txn.create_revision' do
|
661
|
+
before { trans_conn.put('txn', 'value') }
|
662
|
+
after { trans_conn.del('txn') }
|
663
|
+
context 'success' do
|
664
|
+
subject! do
|
665
|
+
trans_conn.transaction do |txn|
|
666
|
+
txn.compare = [ txn.create_revision('txn', :greater, 1) ]
|
667
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
668
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
669
|
+
end
|
670
|
+
end
|
671
|
+
it 'sets correct key' do
|
672
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
|
673
|
+
end
|
674
|
+
end
|
675
|
+
context 'failure' do
|
676
|
+
subject! do
|
677
|
+
trans_conn.transaction do |txn|
|
678
|
+
txn.compare = [ txn.create_revision('txn', :equal, 1) ]
|
679
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
680
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
681
|
+
end
|
682
|
+
end
|
683
|
+
it 'sets correct key' do
|
684
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
|
685
|
+
expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed')
|
686
|
+
end
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
describe 'txn.mod_revision' do
|
691
|
+
before { trans_conn.put('txn', 'value') }
|
692
|
+
after { trans_conn.del('txn') }
|
693
|
+
context 'success' do
|
694
|
+
subject! do
|
695
|
+
trans_conn.transaction do |txn|
|
696
|
+
txn.compare = [ txn.mod_revision('txn', :less, 1000) ]
|
697
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
698
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
699
|
+
end
|
700
|
+
end
|
701
|
+
it 'sets correct key' do
|
702
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
|
703
|
+
expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('success')
|
704
|
+
end
|
705
|
+
end
|
706
|
+
context 'failure' do
|
707
|
+
subject! do
|
708
|
+
trans_conn.transaction do |txn|
|
709
|
+
txn.compare = [ txn.mod_revision('txn', :greater, 1000) ]
|
710
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
711
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
712
|
+
end
|
713
|
+
end
|
714
|
+
it 'sets correct key' do
|
715
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
|
716
|
+
expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed')
|
717
|
+
|
718
|
+
end
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
describe 'txn.version' do
|
723
|
+
before { trans_conn.put('txn-version', 'value') }
|
724
|
+
after { trans_conn.del('txn-version') }
|
725
|
+
context 'success' do
|
726
|
+
subject! do
|
727
|
+
trans_conn.transaction do |txn|
|
728
|
+
txn.compare = [ txn.version('txn-version', :equal, 1) ]
|
729
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
730
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
731
|
+
end
|
732
|
+
end
|
733
|
+
it 'sets correct key' do
|
734
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('success')
|
735
|
+
expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('success')
|
736
|
+
end
|
737
|
+
end
|
738
|
+
context 'failure' do
|
739
|
+
subject! do
|
740
|
+
trans_conn.transaction do |txn|
|
741
|
+
txn.compare = [ txn.version('txn', :equal, 100)]
|
742
|
+
txn.success = [ txn.put('txn-test', 'success') ]
|
743
|
+
txn.failure = [ txn.put('txn-test', 'failed') ]
|
744
|
+
end
|
745
|
+
end
|
746
|
+
it 'sets correct key' do
|
747
|
+
expect(trans_conn.get('txn-test').kvs.first.value).to eq('failed')
|
748
|
+
expect(conn.get('/namespace/txn-test').kvs.first.value).to eq('failed')
|
749
|
+
end
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
# Locking is not implemented in etcd v3.1.X
|
755
|
+
unless $instance.version < Gem::Version.new("3.2.0")
|
756
|
+
describe "locking" do
|
757
|
+
let(:ns_conn) { local_connection_with_namespace("/namespace/") }
|
758
|
+
|
759
|
+
describe '#lock' do
|
760
|
+
let(:lease_id) { lease_stub.lease_grant(10)['ID'] }
|
761
|
+
subject { ns_conn.lock('mylocklock', lease_id) }
|
762
|
+
it 'should lock key under specified namespace' do
|
763
|
+
expect(conn.get("/namespace/#{subject.key}").kvs).to_not be_empty
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
describe '#with_lock' do
|
768
|
+
let(:lease_id) { lease_stub.lease_grant(10)['ID'] }
|
769
|
+
let(:lease_id_2) { lease_stub.lease_grant(15)['ID'] }
|
770
|
+
it 'enforces lock' do
|
771
|
+
ns_conn.with_lock('mylock', lease_id) do
|
772
|
+
expect { ns_conn.lock('mylock', lease_id_2, timeout: 0.1) }
|
773
|
+
.to raise_error(GRPC::DeadlineExceeded)
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|
779
|
+
end
|
524
780
|
end
|
525
781
|
end
|
data/spec/helpers/connections.rb
CHANGED
@@ -5,18 +5,34 @@ module Helpers
|
|
5
5
|
Etcdv3.new(endpoints: "http://#{local_url}", user: user, password: password)
|
6
6
|
end
|
7
7
|
|
8
|
-
def local_connection(endpoints="http://#{local_url}")
|
9
|
-
Etcdv3.new(endpoints: endpoints)
|
8
|
+
def local_connection(endpoints="http://#{local_url}", allow_reconnect: true)
|
9
|
+
Etcdv3.new(endpoints: endpoints, allow_reconnect: allow_reconnect)
|
10
10
|
end
|
11
11
|
|
12
12
|
def local_connection_with_timeout(timeout)
|
13
13
|
Etcdv3.new(endpoints: "http://#{local_url}", command_timeout: timeout)
|
14
14
|
end
|
15
15
|
|
16
|
+
def local_connection_with_namespace(namespace)
|
17
|
+
Etcdv3.new(endpoints: "http://#{local_url}", namespace: namespace)
|
18
|
+
end
|
19
|
+
|
16
20
|
def local_stub(interface, timeout=nil)
|
17
21
|
interface.new(local_url, :this_channel_is_insecure, timeout, {})
|
18
22
|
end
|
19
23
|
|
24
|
+
def local_stub_with_metadata(interface, timeout: nil, metadata: {})
|
25
|
+
interface.new(local_url, :this_channel_is_insecure, timeout, metadata)
|
26
|
+
end
|
27
|
+
|
28
|
+
def local_namespace_stub(interface, timeout=nil, namespace)
|
29
|
+
interface.new(local_url, :this_channel_is_insecure, timeout, namespace, {})
|
30
|
+
end
|
31
|
+
|
32
|
+
def local_namespace_stub_with_metadata(interface, timeout: nil, namespace:, metadata: {})
|
33
|
+
interface.new(local_url, :this_channel_is_insecure, timeout, namespace, metadata)
|
34
|
+
end
|
35
|
+
|
20
36
|
def local_url
|
21
37
|
"127.0.0.1:#{port}"
|
22
38
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Helpers
|
2
|
+
module MetadataPassthrough
|
3
|
+
include Connections
|
4
|
+
|
5
|
+
def expect_metadata_passthrough(stub_class, method_name, expectation_target)
|
6
|
+
metadata = { user: "foo", password: "bar" }
|
7
|
+
handler = local_stub_with_metadata(stub_class, metadata: metadata, timeout: 1)
|
8
|
+
inner_stub = handler.instance_variable_get("@stub")
|
9
|
+
expect(inner_stub).to receive(expectation_target).with(anything, hash_including(metadata: metadata)).and_call_original
|
10
|
+
return handler
|
11
|
+
end
|
12
|
+
|
13
|
+
def expect_metadata_passthrough_namespace(stub_class, method_name, expectation_target, namespace)
|
14
|
+
metadata = { user: "foo", password: "bar" }
|
15
|
+
handler = local_namespace_stub_with_metadata(stub_class, metadata: metadata, timeout: 1, namespace: namespace)
|
16
|
+
inner_stub = handler.instance_variable_get("@stub")
|
17
|
+
expect(inner_stub).to receive(expectation_target).with(anything, hash_including(metadata: metadata)).and_call_original
|
18
|
+
return handler
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -32,13 +32,13 @@ shared_examples_for "Etcdv3 instance using a timeout" do |command, *args|
|
|
32
32
|
it "raises a GRPC::DeadlineExceeded exception when it takes too long" do
|
33
33
|
expect do
|
34
34
|
test_args = args.dup
|
35
|
-
|
36
|
-
conn.public_send(command, *test_args)
|
35
|
+
test_kwargs = {timeout: 0}
|
36
|
+
conn.public_send(command, *test_args, **test_kwargs)
|
37
37
|
end.to raise_exception(GRPC::DeadlineExceeded)
|
38
38
|
end
|
39
39
|
it "accepts a timeout" do
|
40
40
|
test_args = args.dup
|
41
|
-
|
42
|
-
expect{ conn.public_send(command, *test_args) }.to_not raise_exception
|
41
|
+
test_kwargs = {timeout: 10}
|
42
|
+
expect{ conn.public_send(command, *test_args, **test_kwargs) }.to_not raise_exception
|
43
43
|
end
|
44
44
|
end
|
@@ -3,10 +3,12 @@ require 'tmpdir'
|
|
3
3
|
require 'socket'
|
4
4
|
require 'timeout'
|
5
5
|
require 'helpers/connections'
|
6
|
+
require 'helpers/metadata_passthrough'
|
6
7
|
|
7
8
|
module Helpers
|
8
9
|
class TestInstance
|
9
10
|
include Helpers::Connections
|
11
|
+
include Helpers::MetadataPassthrough
|
10
12
|
|
11
13
|
class InvalidVersionException < StandardError; end
|
12
14
|
class PortInUseException < StandardError; end
|