Skip to content

Add ability to pass multiple endpoints, failover capability#106

Merged
kragniz merged 2 commits intokragniz:masterfrom
lavagetto:reconnections
Jul 6, 2021
Merged

Add ability to pass multiple endpoints, failover capability#106
kragniz merged 2 commits intokragniz:masterfrom
lavagetto:reconnections

Conversation

@lavagetto
Copy link
Contributor

@lavagetto lavagetto commented Apr 2, 2017

This two commit add two features:

  • Ability to define a list of endpoints, pass them to the client library, have it pick one at random
  • Upon a connection failure, one server is marked as temporarily down and subsequent requests are made to non-failed servers. This allows i.e. to let any service running without restarts during maintenance windows when one etcd server is either rebooted or just the service is restarted.

This is still a WiP (as you might have noticed, no docs and no additional tests, sorry but I really can only work on this on sundays :/), but I wanted to get some feedback on the direction I've taken with the code, given it is a non-negligible overhaul.

The way I built the reconnection logic might seem a bit of an overkill, but my experience with trying to build a simpler model for the etcd2 python library taught me otherwise. I tried to learn from that experience.

@codecov-io
Copy link

codecov-io commented Apr 2, 2017

Codecov Report

Merging #106 into master will increase coverage by 0.26%.
The diff coverage is 95.68%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #106      +/-   ##
==========================================
+ Coverage   92.86%   93.13%   +0.26%     
==========================================
  Files          10       10              
  Lines         617      714      +97     
==========================================
+ Hits          573      665      +92     
- Misses         44       49       +5
Impacted Files Coverage Δ
etcd3/__init__.py 100% <100%> (ø) ⬆️
etcd3/exceptions.py 100% <100%> (ø) ⬆️
etcd3/client.py 93.7% <95.58%> (+0.27%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 27f7335...df3bcac. Read the comment docs.

@kragniz
Copy link
Owner

kragniz commented Apr 3, 2017

Hey, I've only skimmed over your patch, but it looks great so far!

@lavagetto
Copy link
Contributor Author

@kragniz as expected, I had a little time to work on this on sunday, and I found a few small issues with my code, that I am still fixing. It might take another week or two (next sunday is Easter here).

@lavagetto
Copy link
Contributor Author

I'll look into the test coverage to make it better (I think the whole SRV DNS part is still not tested), but I consider the patch in a reviewable state now.

@jd
Copy link
Collaborator

jd commented Jun 16, 2017

Does this cover #150 also?
I'll try to review this patch ASAP.

@lavagetto
Copy link
Contributor Author

lavagetto commented Jun 16, 2017 via email

@lavagetto lavagetto changed the title [WiP] Add SRV record support, reconnection capability Add SRV record support, reconnection capability Jun 18, 2017
@lavagetto
Copy link
Contributor Author

I think this patch is ready to be reviewed/merged, but please let me know if you want me to improve something.

Copy link
Collaborator

@jd jd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a bunch of review. I feel bad that this is only one patch when there is actually 3 differents pieces:

  1. Handle jumping between multiple servers

  2. Doing a SRV requests to gather the initial server(s)

  3. Read the cluster topology to jump between servers

It'd have be much better to do 3 different incremental patches.

Thank you for doing that anyway! That looks promising.

self.host = host
self.netloc = "{host}:{port}".format(host=host, port=port)
self.secure = secure
self.protocol = 'https' if secure else 'http'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why http/https? there's no http to me here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's how etcd itself refers to its endpoints in the configuration. I checked and in fact there is still no official designation for the grpc protocol in terms of URI names. So I went with what coreos does.

etcd3/client.py Outdated
"""Represents an etcd cluster member."""

creds = None
time_retry = 300.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ought to be configurable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it can be configured simply by setting

etcd3.Node.time_retry = X

Do you have something else in mind?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well that's not obvious in the API since I don't expect users to go change every Node object that is stored in the Client object. And changing a global class is a bad idea.

etcd3/client.py Outdated
for node in available:
# A node might have failed in the meanwhile
if node.failed:
next
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this does what you think it does

>>> next
<built-in function next>

You meant continue I imagine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh, I fixed this already but didn't commit it.

etcd3/client.py Outdated
else:
available = [m for m in self._nodes_cache.values()
if not m.failed]
for node in available:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to build a list in available, just iterate on self._nodes_cache.values() and add a if on L221. This double iteration looks useless.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it's a leftover from a previous version of the code

etcd3/client.py Outdated
self._url = '{host}:{port}'.format(host=host, port=port)
def __init__(self, host='localhost', port=2379, srv_record=None,
ca_cert=None, cert_key=None, cert_cert=None,
timeout=None, reconnect=False):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I understand, reconnect is a misnomer. It's not reconnection that is handled but jumping from one node to another.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree but I couldn't come up with anything better: if you have a better name, I'm happy to use it!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allow_jump ?

etcd3/client.py Outdated
cert_cert_file.read()
)

def _discover(self, srv_name):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd make it clearer if you switch this method to be a @staticmethod. Just pass uses_secure_channel as an arg.

etcd3/client.py Outdated
self.uses_secure_channel
)
hosts[node.netloc] = node
if not len(hosts):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This'd be better be raised by the caller of _discover().

etcd3/client.py Outdated
def watchstub(self):
return etcdrpc.WatchStub(self.channel)

def _get_cluster_topology(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see 2 problems with that method:

  1. It's messing with _nodes_cache, and I don't like that. I'd prefer to it to return a new list of nodes that can be used by the caller

  2. It's only called at __init__ time. What if the topology changes later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 - I don't really see why it's a problem, the whole point of that method is to mess with _nodes_cache, but I'm ok with doing it within the caller.

2 - In python-etcd (the v2 client) we ran a similar function every time we had one connection failing: we would fail the node, try the call again to another node, and if successful, also upgrade the cluster topology to check if it changed. I thought it was too much to add that as well in a single patch, and there are some drawbacks from that approach too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then just remove that from that patch. You don't need. We can discuss this in another patch/PR.

@lavagetto
Copy link
Contributor Author

Thanks for the review @jd I'll try to fix these up ASAP.

@jordimarinvalle
Copy link

@lavagetto great work. I was going to develop it, I really need it and this library works with v3. I would like to know when it is going to be merged... I hope soon!

@Gollam
Copy link
Contributor

Gollam commented Aug 29, 2017

any updates?

@davissp14
Copy link

davissp14 commented Sep 15, 2017

Ability to fetch the cluster topology on initialization, and store it in a cache. Upon a connection failure, one server is marked as temporarily down and subsequent requests are made to non-failed servers. This allows i.e. to let any service running without restarts during maintenance windows when one etcd server is either rebooted or just the service is restarted.

Many people don't directly connect to their members, but instead front their cluster with gRPC proxy(s), HAProxy(s), etc. When this is the case, we wouldn't want to auto-discover the cluster endpoints. I think it may be worth considering simply offering users the ability to define multiple endpoints on initialization and rotate endpoints on failure. This would offer a generic failover / reconnection scheme that would could work across a variety of setups, which is something I think we want in a client library.

@martensson
Copy link

Its been a while since last update, what is the status right now to support SRV and/or multiple endpoints?

Since etcd is a distributed storage, it makes sense for a client library
to be able to connect to all the nodes in the cluster, and to manage
their current status without needing to be reinstantiated.

This patch adds the ability for the client to be aware of the status of
all nodes in the cluster, and - in case of failure - to mark each of
them as temporarily failed.

Individual requests will continue to raise an exception in case of a
failed connection at this point, but it would be easy to allow retrying
if it seems a good idea.

What has been done in practice:

* Added a 'Endpoint' class, a simple FSM to abstract a remote server.
* Refactored the initialization of the library, adding a switch to allow
  failing over to another server in case of need.
* Refactored self.channel to be a function returning a grpc channel to
  the first non-failed node, and all the Etcd3Client.*stub properties
  accordingly.
* Refactored the _handle_error wrapper, made it part of the class, and
  split it between the version for generators and the one for normal
  returners for code simplicity
@lavagetto
Copy link
Contributor Author

As requested in the past review, I factored everything out and I made the first patch completely about connection management:

  • It is possible to define multiple endpoints if we feel like it
  • If failover is allowed, endpoints can be temporarily marked as down and another one is selected.
  • This can be easily extended to use SRV records (it can even be done externally for the time being)

I would love to see this patch merged soon so that I don't get to rebase it again - I don't have much time to dedicate to this at the moment, to be honest.

@lavagetto lavagetto changed the title Add SRV record support, reconnection capability Add ability to pass multiple endpoints, failover capability Apr 7, 2018
@lavagetto
Copy link
Contributor Author

It should be noted that the travis tests fail randomly when trying to create keys via etcdctl, and they consistently succeed locally on my test infrastructure.

@kragniz
Copy link
Owner

kragniz commented Apr 13, 2018

@jd do you have any time to review this again?

Copy link
Collaborator

@jd jd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good but I'm sad that they are changes that seem to be completely unrelated to the purpose of the patch itself.

self.watcher = self.get_watcher()
self.transactions = Transactions()

@property
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not against this change but it really does not have anything to do with the intention of this patch, right?
It also makes new object each time which has a cost. I'd prefer to see this kind of change in a different PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does. You need to ensure the various stubs use the correct channel, which is the currently active one. If you don't do this, all these properties will contain a reference to the channel used at the time you created the object. The alternative approach would be to reinstantiate every single one of these objects on every reconnection, which is fine but it felt less elegant and error-prone to me.

try:
return self.endpoint_in_use.use()
except ValueError as e:
if not self.failover:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick:

if self.failover:
    pass
else:
    raise

avoids negation :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack, will fix :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack


class Etcd3Client(object):
def __init__(self, host='localhost', port=2379,
def __init__(self, host='localhost', port=2379, endpoints=None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of endpoints here is not documented by any docstring and looking at the code it seems to be a dict? I'd expect it to be a list. That needs clarification as to why it'd need to be a dict.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was undecided - probably a list of endpoints is the most user-friendly thing to do, yes.

@DanielSiebert
Copy link

Hi,

what is going on with this patch? Has equivalent functionality been added in the meantime?

Cheers,
Daniel

@chryseosTang
Copy link

Has the project been completed?

@InvalidInterrupt
Copy link
Contributor

I've done more work on this feature in my branch: https://github.com/InvalidInterrupt/python-etcd3/tree/reconnections.
It's still lacking documentation and probably could use more tests. Now that this project seems to be active again, what can I do to help get this feature merged?

@kragniz
Copy link
Owner

kragniz commented May 22, 2021

@InvalidInterrupt open a new PR and tag me in it - I'll set some time aside to review it

@AlexShemeshWix
Copy link

when release? pypi is still on 0.12.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.