Skip to content

Commit 0fe0600

Browse files
Vedanta JhaVedanta Jha
authored andcommitted
Memcache auto discovery
1 parent c98a9ba commit 0fe0600

6 files changed

Lines changed: 94 additions & 5 deletions

File tree

ChangeLog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
Changelog
22
=========
3+
New in version 4.0.1
4+
--------------------
5+
* Added auto discovery feature to HashClient
6+
37
New in version 4.0.0
48
--------------------
59
* Dropped Python 2 and 3.6 support

docs/getting_started.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ follows:
118118
``node3`` is added back into the hasher and will be retried for any future
119119
operations.
120120

121+
122+
Using a configuration node endpoint and auto discovery
123+
------------------------------------------------------
124+
This will use AWS elasticache auto discovery method to discover nodes by just
125+
using Configuration node's endpoint
126+
127+
.. code-block:: python
128+
129+
from pymemcache.client.hash import HashClient
130+
131+
client = HashClient('127.0.0.1:11211', enable_autodiscovery=True)
132+
client.set('some_key', 'some value')
133+
result = client.get('some_key')
134+
135+
The client internally fetches all the nodes from the configuration nodes and sets up a connection with them,
136+
Refer AWS `doc`<https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/AutoDiscovery.html> for more information.
137+
138+
121139
Using the built-in retrying mechanism
122140
-------------------------------------
123141
The library comes with retry mechanisms that can be used to wrap all kinds of

pymemcache/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "4.0.0"
1+
__version__ = "4.0.1"
22

33
from pymemcache.client.base import Client # noqa
44
from pymemcache.client.base import PooledClient # noqa

pymemcache/client/base.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,28 @@ def flush_all(self, delay: int = 0, noreply: Optional[bool] = None) -> bool:
10301030
return True
10311031
return results[0] == b"OK"
10321032

1033+
def auto_discover(self):
1034+
"""
1035+
This is specific to AWS Elasticache
1036+
1037+
Returns list of hostname and ip address of the nodes
1038+
1039+
The response received is as follows:
1040+
0: CONFIG cluster 0 134
1041+
1: configversion\r\n
1042+
2: hostname|ip-address|port hostname|ip-address|port ...\r\n
1043+
3:
1044+
4: END
1045+
5: blank
1046+
"""
1047+
cmd = b"config get cluster"
1048+
data = self._misc_cmd([cmd], b"config get cluster", noreply=False)
1049+
lines = data.split(b'\n')
1050+
configs = [conf.split(b'|') for conf in lines[2].split(b' ')]
1051+
self.quit()
1052+
nodes = [(ip, int(port)) for host, ip, port in configs]
1053+
return nodes
1054+
10331055
def quit(self) -> None:
10341056
"""
10351057
The memcached "quit" command.
@@ -1356,7 +1378,19 @@ def __delitem__(self, key):
13561378
self.delete(key, noreply=True)
13571379

13581380

1359-
class PooledClient:
1381+
class PooledClient: # 0: CONFIG cluster 0 134 # 0: CONFIG cluster 0 134
1382+
# 1: configversion\r\n
1383+
# 2: hostname|ip-address|port hostname|ip-address|port ...\r\n
1384+
# 3:
1385+
# 4: END
1386+
# 5: blank
1387+
1388+
# 1: configversion\r\n
1389+
# 2: hostname|ip-address|port hostname|ip-address|port ...\r\n
1390+
# 3:
1391+
# 4: END
1392+
# 5: blank
1393+
13601394
"""A thread-safe pool of clients (with the same client api).
13611395
13621396
Args:

pymemcache/client/hash.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ def __init__(
4848
default_noreply=True,
4949
encoding="ascii",
5050
tls_context=None,
51+
enable_auto_discovery=False
5152
):
5253
"""
5354
Constructor.
5455
5556
Args:
5657
servers: list() of tuple(hostname, port) or string containing a UNIX
5758
socket path.
59+
Or If enable_auto_discovery is set, just tuple(hostname, port) or UNIX socket path string
60+
of configuration node would suffice.
5861
hasher: optional class three functions ``get_node``, ``add_node``,
5962
and ``remove_node``
6063
defaults to Rendezvous (HRW) hash.
@@ -70,12 +73,14 @@ def __init__(
7073
dead_timeout (float): Time in seconds before attempting to add a node
7174
back in the pool.
7275
encoding: optional str, controls data encoding (defaults to 'ascii').
76+
enable_auto_discovery (bool): If enabled, nodes would be discovered from the configuration endpoint.
7377
7478
Further arguments are interpreted as for :py:class:`.Client`
7579
constructor.
7680
"""
7781
self.clients = {}
7882
self.retry_attempts = retry_attempts
83+
self.connect_timeout = connect_timeout
7984
self.retry_timeout = retry_timeout
8085
self.dead_timeout = dead_timeout
8186
self.use_pooling = use_pooling
@@ -112,11 +117,18 @@ def __init__(
112117
"lock_generator": lock_generator,
113118
}
114119
)
115-
116-
for server in servers:
117-
self.add_server(normalize_server_spec(server))
118120
self.encoding = encoding
119121
self.tls_context = tls_context
122+
if not isinstance(servers, list):
123+
if not enable_auto_discovery:
124+
raise ValueError(f"Auto Discovery should be enabled if configuration endpoint is used: {servers!r}")
125+
126+
if enable_auto_discovery and servers is not None:
127+
# AutoDiscovery is enabled and a address of configuration node is provided
128+
servers = self._auto_discover(normalize_server_spec(servers))
129+
130+
for server in servers:
131+
self.add_server(normalize_server_spec(server))
120132

121133
def _make_client_key(self, server):
122134
if isinstance(server, (list, tuple)) and len(server) == 2:
@@ -340,6 +352,12 @@ def _set_many(self, client, values, *args, **kwargs):
340352
succeeded = [key for key in values if key not in failed]
341353
return succeeded, failed, None
342354

355+
def _auto_discover(self, server):
356+
_class = PooledClient if self.use_pooling else self.client_class
357+
client = _class(server)
358+
nodes = client.auto_discover()
359+
return nodes
360+
343361
def close(self):
344362
for client in self.clients.values():
345363
self._safely_run_func(client, client.close, False)

pymemcache/test/test_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,20 @@ def test_send_end_token_types(self):
12191219
assert client.raw_command("key", "\r\n") == b"REPLY"
12201220
assert client.raw_command(b"key", b"\r\n") == b"REPLY"
12211221

1222+
def test_auto_discover(self):
1223+
mock_response = (
1224+
b"CONFIG cluster 0 134\r\n"
1225+
b"configversion\r\n"
1226+
b"hostname1|10.0.0.1|11211 hostname2|10.0.0.2|11211\r\n"
1227+
b"END\r\n"
1228+
)
1229+
1230+
client = self.make_client([mock_response])
1231+
nodes = client.auto_discover()
1232+
1233+
expected_nodes = [("10.0.0.1", 11211), ("10.0.0.2", 11211)]
1234+
assert nodes == expected_nodes
1235+
12221236

12231237
@pytest.mark.unit()
12241238
class TestClientSocketConnect(unittest.TestCase):
@@ -1431,6 +1445,7 @@ class MyClient(Client):
14311445
client = PooledClient(("host", 11211))
14321446
client.client_class = MyClient
14331447
assert isinstance(client.client_pool.get(), MyClient)
1448+
14341449

14351450

14361451
class TestPooledClientIdleTimeout(ClientTestMixin, unittest.TestCase):

0 commit comments

Comments
 (0)