Skip to content

Commit b6ba43c

Browse files
authored
Merge pull request #132 from graywolf-at-work/dont_use_recursion_for_reconnect
Remove automatic retries in case of errors
2 parents 3a542e8 + 27153fa commit b6ba43c

File tree

6 files changed

+83
-23
lines changed

6 files changed

+83
-23
lines changed

.gitignore

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
Gemfile.lock
2-
Guardfile
3-
*.sh
41
*.gem
5-
/tmp/
2+
*.sh
3+
*.swp
4+
/.bundle
65
/.idea
76
/coverage/*
87
/doc/
9-
*.swp
8+
/tmp/
9+
Gemfile.lock
10+
Guardfile

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,23 @@ conn = Etcdv3.new(endpoints: 'https://hostname:port', user: 'root', password: 'm
3232
```
3333
**High Availability**
3434

35-
In the event of a failure, the client will work to restore connectivity by cycling through the specified endpoints until a connection can be established. With that being said, it is encouraged to specify multiple endpoints when available.
35+
In the event of a failure, the client will work to restore connectivity by cycling through the specified endpoints until a connection can be established. With that being said, it is encouraged to specify multiple endpoints when available. That's the default
36+
behaviour.
3637

38+
However, sometimes this is not what you want. If you need more control over
39+
failures, you can suppress this mechanism by using
40+
41+
```ruby
42+
conn = Etcdv3.new(endpoints: 'https://hostname:port', allow_reconnect: false)
43+
```
44+
45+
This will still rotate the endpoints, but it will raise an exception so you can
46+
handle the failure yourself. On next call new endpoint (since they were
47+
rotated) is tried. One thing you need to keep in mind if you are using etcd with
48+
authorization is that you need to take care of `GRPC::Unauthenticated` exceptions
49+
and manually re-authenticate when token expires. To reiterate, you are
50+
responsible for handling the errors, so some understanding of how this gem and
51+
etcd works is recommended.
3752

3853
## Adding, Fetching and Deleting Keys
3954
```ruby

lib/etcdv3.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ class Etcdv3
2121
attr_reader :conn, :credentials, :options
2222
DEFAULT_TIMEOUT = 120
2323

24-
def initialize(options = {})
24+
def initialize(**options)
2525
@options = options
2626
@timeout = options[:command_timeout] || DEFAULT_TIMEOUT
27-
@conn = ConnectionWrapper.new(@timeout, *sanitized_endpoints)
27+
@conn = ConnectionWrapper.new(
28+
@timeout,
29+
*sanitized_endpoints,
30+
@options.fetch(:allow_reconnect, true),
31+
)
2832
warn "WARNING: `url` is deprecated. Please use `endpoints` instead." if @options.key?(:url)
2933
authenticate(@options[:user], @options[:password]) if @options.key?(:user)
3034
end

lib/etcdv3/connection_wrapper.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,40 @@ class ConnectionWrapper
33

44
attr_accessor :connection, :endpoints, :user, :password, :token, :timeout
55

6-
def initialize(timeout, *endpoints)
6+
def initialize(timeout, *endpoints, allow_reconnect)
77
@user, @password, @token = nil, nil, nil
88
@timeout = timeout
99
@endpoints = endpoints.map{|endpoint| Etcdv3::Connection.new(endpoint, @timeout) }
1010
@connection = @endpoints.first
11+
@allow_reconnect = allow_reconnect
12+
end
13+
14+
private def retry_or_raise(*args)
15+
if @allow_reconnect
16+
handle(*args)
17+
else
18+
raise
19+
end
1120
end
1221

1322
def handle(stub, method, method_args=[], retries: 1)
1423
@connection.call(stub, method, method_args)
1524

16-
rescue GRPC::Unavailable, GRPC::Core::CallError => exception
25+
rescue GRPC::Unavailable, GRPC::Core::CallError
1726
$stderr.puts("Failed to connect to endpoint '#{@connection.hostname}'")
1827
if @endpoints.size > 1
1928
rotate_connection_endpoint
2029
$stderr.puts("Failover event triggered. Failing over to '#{@connection.hostname}'")
21-
return handle(stub, method, method_args)
30+
return retry_or_raise(stub, method, method_args)
2231
else
23-
return handle(stub, method, method_args)
32+
return retry_or_raise(stub, method, method_args)
2433
end
2534
rescue GRPC::Unauthenticated => exception
2635
# Regenerate token in the event it expires.
2736
if exception.details == 'etcdserver: invalid auth token'
2837
if retries > 0
2938
authenticate(@user, @password)
30-
return handle(stub, method, method_args, retries: retries - 1)
39+
return retry_or_raise(stub, method, method_args, retries: retries - 1)
3140
end
3241
end
3342
raise exception

spec/etcdv3/connection_wrapper_spec.rb

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
describe Etcdv3::ConnectionWrapper do
44
let(:conn) { local_connection }
55
let(:endpoints) { ['http://localhost:2379', 'http://localhost:2389'] }
6+
subject { Etcdv3::ConnectionWrapper.new(10, *endpoints, true) }
67

78
describe '#initialize' do
8-
subject { Etcdv3::ConnectionWrapper.new(10, *endpoints) }
99
it { is_expected.to have_attributes(user: nil, password: nil, token: nil) }
1010
it 'sets hostnames in correct order' do
1111
expect(subject.endpoints.map(&:hostname)).to eq(['localhost:2379', 'localhost:2389'])
@@ -16,7 +16,6 @@
1616
end
1717

1818
describe "#rotate_connection_endpoint" do
19-
subject { Etcdv3::ConnectionWrapper.new(10, *endpoints) }
2019
before do
2120
subject.rotate_connection_endpoint
2221
end
@@ -29,11 +28,30 @@
2928
end
3029

3130
describe "Failover Simulation" do
32-
let(:modified_conn) { local_connection("http://localhost:2369, http://localhost:2379") }
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+
3340
context 'without auth' do
3441
# Set primary endpoint to a non-existing etcd endpoint
35-
subject { modified_conn.get('boom') }
36-
it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
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
3755
end
3856
context 'with auth' do
3957
before do
@@ -50,12 +68,19 @@
5068
modified_conn.auth_disable
5169
modified_conn.user_delete('root')
5270
end
53-
subject { modified_conn.get('boom') }
54-
it { is_expected.to be_an_instance_of(Etcdserverpb::RangeResponse) }
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
5578
end
5679
end
5780

5881
describe "GRPC::Unauthenticated recovery" do
82+
let(:allow_reconnect) { true }
83+
let(:conn) { local_connection(allow_reconnect: allow_reconnect) }
5984
let(:wrapper) { conn.send(:conn) }
6085
let(:connection) { wrapper.connection }
6186
before do
@@ -71,6 +96,12 @@
7196
conn.user_delete('root')
7297
end
7398
subject { conn.user_get('root') }
74-
it { is_expected.to be_an_instance_of(Etcdserverpb::AuthUserGetResponse) }
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
75106
end
76107
end

spec/helpers/connections.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ def local_connection_with_auth(user, password)
55
Etcdv3.new(endpoints: "http://#{local_url}", user: user, password: password)
66
end
77

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)
1010
end
1111

1212
def local_connection_with_timeout(timeout)

0 commit comments

Comments
 (0)