From dd90d16a7278c76aa69f8901d2fca8df257e3bd2 Mon Sep 17 00:00:00 2001 From: Charles Gordon Date: Mon, 22 Oct 2012 08:38:13 -0700 Subject: [PATCH] Fixing documentation, adding initial FallbackClient impl --- README.md | 61 ++++++++++++++++++++++- pymemcache/client.py | 82 +++++++++++++++---------------- pymemcache/fallback.py | 108 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 45 deletions(-) create mode 100644 pymemcache/fallback.py diff --git a/README.md b/README.md index 6506046..c936fa2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,63 @@ pymemcache ========== -A comprehensive, fast, pure-Python memcached client. BETA! \ No newline at end of file +NOTE: this is still BETA, use with caution! + +A comprehensive, fast, pure-Python memcached client. + +pymemcache supports the following features: + +* Complete implementation of the memcached text protocol. +* Configurable timeouts for socket connect and send/recv calls. +* Access to the "noreply" flag, which can significantly increase speed. +* Flexible, simple approach to serialization and deserialization. +* The (optional) ability to treat network and memcached errors as cache misses. + +Installing pymemcache +===================== + +You can install pymemcache manually, with Nose tests, by doing the following: + + git clone https://github.com/pinterest/pymemcache.git + cd pymemcache + python setup.py nosetests + sudo python setup.py install + +You can also use pip: + + sudo pip install https://github.com/pinterest/pymemcache.git + +Comparison with Other Libraries +=============================== + +pylibmc +------- + +The pylibmc library is a wrapper around libmemcached, implemented in C. It is +fast, implements consistent hashing, the full memcached protocol and timeouts. +It does not provide access to the "noreply" flag, and it doesn't provide a +built-in API for serialization and deserialization. It also isn't pure Python, +so using it with libraries like gevent is out of the question. + +Python-memcache +--------------- + +The python-memcache library implements the entire memcached text protocol, has +a single timeout for all socket calls and has a flexible approach to +serialization and deserialization. It is also written entirely in Python, so +it works well with libraries like gevent. However, it is tied to using thread +locals, doesn't implement "noreply", can't treat errors as cache misses and is +slower than both pylibmc and pymemcache. It is also tied to a specific method +for handling clusters of memcached servers. + +memcache_client +--------------- + +The team at mixpanel put together a pure Python memcached client as well. It +has more fine grained support for socket timeouts, only connects to a single +host. However, it doesn't support most of the memcached API (just get, set, +delete and stats), doesn't support "noreply", has no serialization or +deserialization support and can't treat errors as cache misses. + +External Links +============== diff --git a/pymemcache/client.py b/pymemcache/client.py index f6271ec..34df9e1 100644 --- a/pymemcache/client.py +++ b/pymemcache/client.py @@ -41,13 +41,16 @@ Best Practices: - Always set the connect_timeout and timeout arguments in the constructor to avoid blocking your process when memcached is slow. Consider setting them to small values like 0.05 (50ms) or less. - - Use the "noreply" flag whenever possible for a significant performance - boost. The flag is on by default, so you are using it unless you specify - otherwise. + - Use the "noreply" flag for a significant performance boot. The "noreply" + flag is enabled by default for "set", "add", "replace", "append", "prepend", + and "delete". It is disabled by default for "cas", "incr" and "decr". - Use get_many and gets_many whenever possible, as they result in less round trip times for fetching multiple keys. - Use the "ignore_exc" flag to treat memcache/network errors as cache misses - on calls to the get* methods. + on calls to the get* methods. This prevents failures in memcache, or network + errors, from killing your web requests. Do not use this flag if you need to + know about errors from memcache, and make sure you have some other way to + detect memcache failures. Not Implemented: @@ -252,11 +255,10 @@ class Client(object): value: str, see class docs for details. expire: optional int, number of seconds until the item is expired from the cache, or zero for no expiry (the default). - noreply: optional bool, False to wait for the reply (the default). + noreply: optional bool, True to not wait for the reply (the default). Returns: - The string 'STORED' on success, or raises an Exception on error (see - class documentation). + If noreply is True, always returns None, otherwise returns 'STORED'. """ return self._store_cmd('set', key, expire, noreply, value) @@ -269,11 +271,11 @@ class Client(object): value: str, see class docs for details. expire: optional int, number of seconds until the item is expired from the cache, or zero for no expiry (the default). - noreply: optional bool, False to wait for the reply (the default). + noreply: optional bool, True to not wait for the reply (the default). Returns: - The string 'STORED' if the value was stored, 'NOT_STORED' if the key - already existed, or an Exception on error (see class docs). + If noreply is True, always returns None, otherwise returns 'STORED' + if the key didn't exist already, and 'NOT_STORED' otherwise. """ return self._store_cmd('add', key, expire, noreply, value) @@ -286,11 +288,12 @@ class Client(object): value: str, see class docs for details. expire: optional int, number of seconds until the item is expired from the cache, or zero for no expiry (the default). - noreply: optional bool, False to wait for the reply (the default). + noreply: optional bool, True to not wait for the reply (the default). Returns: - The string 'STORED' if the value was stored, 'NOT_STORED' if the key - didn't already exist or an Exception on error (see class docs). + If noreply is True, always returns None, otherwise returns 'STORED' + if the value was stored, and 'NOT_STORED' if the key did not already + exist. """ return self._store_cmd('replace', key, expire, noreply, value) @@ -303,11 +306,10 @@ class Client(object): value: str, see class docs for details. expire: optional int, number of seconds until the item is expired from the cache, or zero for no expiry (the default). - noreply: optional bool, False to wait for the reply (the default). + noreply: optional bool, True to not wait for the reply (the default). Returns: - The string 'STORED' on success, or raises an Exception on error (see - the class docs). + If noreply is True, always returns None, otherwise returns 'STORED'. """ return self._store_cmd('append', key, expire, noreply, value) @@ -323,12 +325,11 @@ class Client(object): noreply: optional bool, False to wait for the reply (the default). Returns: - The string 'STORED' on success, or raises an Exception on error (see - the class docs). + If noreply is True, always returns None, otherwise returns 'STORED'. """ return self._store_cmd('prepend', key, expire, noreply, value) - def cas(self, key, value, cas, expire=0, noreply=True): + def cas(self, key, value, cas, expire=0, noreply=False): """ The memcached "cas" command. @@ -341,9 +342,9 @@ class Client(object): noreply: optional bool, False to wait for the reply (the default). Returns: - The string 'STORED' if the value was stored, 'EXISTS' if the key - already existed with a different cas, 'NOT_FOUND' if the key didn't - exist or raises an Exception on error (see the class docs). + If noreply is True, always returns None, otherwise returns 'STORED' + if the value was stored, 'EXISTS' if the key already existed with a + different cas value or 'NOT_FOUND' if the key didn't exist. """ return self._store_cmd('cas', key, expire, noreply, value, cas) @@ -355,8 +356,7 @@ class Client(object): key: str, see class docs for details. Returns: - The value for the key, or None if the key wasn't found, or raises - an Exception on error (see class docs). + The value for the key, or None if the key wasn't found. """ return self._fetch_cmd('get', [key], False).get(key, None) @@ -370,8 +370,7 @@ class Client(object): Returns: A dict in which the keys are elements of the "keys" argument list and the values are values from the cache. The dict may contain all, - some or none of the given keys. An exception is raised on errors (see - the class docs for details). + some or none of the given keys. """ return self._fetch_cmd('get', keys, False) @@ -384,7 +383,6 @@ class Client(object): Returns: A tuple of (key, cas), or (None, None) if the key was not found. - Raises an Exception on errors (see class docs for details). """ return self._fetch_cmd('gets', [key], True).get(key, (None, None)) @@ -398,8 +396,7 @@ class Client(object): Returns: A dict in which the keys are elements of the "keys" argument list and the values are tuples of (value, cas) from the cache. The dict may - contain all, some or none of the given keys. An exception is raised - on errors (see the class docs for details). + contain all, some or none of the given keys. """ return self._fetch_cmd('gets', keys, True) @@ -411,14 +408,13 @@ class Client(object): key: str, see class docs for details. Returns: - The string 'DELTED' if the key existed, and was deleted, 'NOT_FOUND' - if the string did not exist, or raises an Exception on error (see the - class docs for details). + If noreply is True, always returns None, otherwise returns 'DELETED' + if the key existed, or 'NOT_FOUND' if it did not. """ cmd = 'delete {}{}\r\n'.format(key, ' noreply' if noreply else '') return self._misc_cmd(cmd, 'delete', noreply) - def incr(self, key, value, noreply=True): + def incr(self, key, value, noreply=False): """ The memcached "incr" command. @@ -428,9 +424,9 @@ class Client(object): noreply: optional bool, False to wait for the reply (the default). Returns: - The string 'NOT_FOUND', or an integer which is the value of the key - after incrementing. Raises an Exception on errors (see the class docs - for details). + If noreply is True, always returns None, otherwise returns 'NOT_FOUND' + if the key wasn't found, or an integer which is the value of the key + after incrementing by value. """ cmd = "incr {} {}{}\r\n".format( key, @@ -443,7 +439,7 @@ class Client(object): return result return int(result) - def decr(self, key, value, noreply=True): + def decr(self, key, value, noreply=False): """ The memcached "decr" command. @@ -453,9 +449,9 @@ class Client(object): noreply: optional bool, False to wait for the reply (the default). Returns: - The string 'NOT_FOUND', or an integer which is the value of the key - after decrementing. Raises an Exception on errors (see the class - docs for details). + If noreply is True, always returns None, otherwise returns 'NOT_FOUND' + if the key wasn't found, or an integer which is the value of the key + after decrementing by value. """ cmd = "decr {} {}{}\r\n".format( key, @@ -479,8 +475,7 @@ class Client(object): noreply: optional bool, False to wait for the reply (the default). Returns: - The string 'OK' if the value was stored or raises an Exception on - error (see the class docs). + If noreply is True, always returns None, otherwise returns 'OK'. """ cmd = "touch {} {}{}\r\n".format( key, @@ -502,8 +497,7 @@ class Client(object): noreply: optional bool, False to wait for the response (the default). Returns: - The string 'OK' on success, or raises an Exception on error (see the - class docs). + If noreply is True, always returns None, otherwise returns 'OK'. """ cmd = "flush_all {}{}\r\n".format(delay, ' noreply' if noreply else '') return self._misc_cmd(cmd, 'flush_all', noreply) diff --git a/pymemcache/fallback.py b/pymemcache/fallback.py new file mode 100644 index 0000000..1818186 --- /dev/null +++ b/pymemcache/fallback.py @@ -0,0 +1,108 @@ +""" +A client for falling back to older memcached servers when performing reads. + +It is sometimes necessary to deploy memcached on new servers, or with a +different configuration. In theses cases, it is undesirable to start up an +empty memcached server and point traffic to it, since the cache will be cold, +and the backing store will have a large increase in traffic. + +This class attempts to solve that problem by providing an interface identical +to the Client interface, but which can fall back to older memcached servers +when reads to the primary server fail. The approach for upgrading memcached +servers or configuration then becomes: + + 1. Deploy a new host (or fleet) with memcached, possibly with a new + configuration. + 2. From your application servers, use FallbackClient to write and read from + the new cluster, and to read from the old cluster when there is a miss in + the new cluster. + 3. Wait until the new cache is warm enough to support the load. + 4. Switch from FallbackClient to a regular Client library for doing all + reads and writes to the new cluster. + 5. Take down the old cluster. + +Best Practices: +--------------- + - Make sure that the old client has "ignore_exc" set to True, so that it + treats failures like cache misses. That will allow you to take down the + old cluster before you switch away from FallbackClient. +""" + +class FallbackClient(object): + def __init__(self, caches): + assert len(caches) > 0 + self.caches = caches + + def close(self): + "Close each of the memcached clients" + for cache in self.caches: + cache.close() + + def set(self, key, value, expire=0, noreply=True): + self.caches[0].set(key, value, expire, noreply) + + def add(self, key, value, expire=0, noreply=True): + self.caches[0].add(key, value, expire, noreply) + + def replace(self, key, value, expire=0, noreply=True): + self.caches[0].replace(key, value, expire, noreply) + + def append(self, key, value, expire=0, noreply=True): + self.caches[0].append(key, value, expire, noreply) + + def prepend(self, key, value, expire=0, noreply=True): + self.caches[0].prepend(key, value, expire, noreply) + + def cas(self, key, value, cas, expire=0, noreply=True): + self.caches[0].cas(key, value, cas, expire, noreply) + + def get(self, key): + for cache in self.caches: + result = cache.get(key) + if result is not None: + return result + return None + + def get_many(self, keys): + for cache in self.caches: + result = cache.get_many(keys) + if result: + return result + return [] + + def gets(self, key): + for cache in self.caches: + result = cache.gets(key) + if result is not None: + return result + return None + + def gets_many(self, keys): + for cache in self.caches: + result = cache.gets_many(keys) + if result: + return result + return [] + + def delete(self, key, noreply=True): + self.caches[0].delete(key, noreply) + + def incr(self, key, value, noreply=True): + self.caches[0].incr(key, value, noreply) + + def decr(self, key, value, noreply=True): + self.caches[0].decr(key, value, noreply) + + def touch(self, key, expire=0, noreply=True): + self.caches[0].touch(key, expire, noreply) + + def stats(self): + # TODO: ?? + pass + + def flush_all(self, delay=0, noreply=True): + self.caches[0].flush_all(delay, noreply) + + def quit(self): + # TODO: ?? + pass