OLD | NEW |
(Empty) | |
| 1 """ |
| 2 Memcached Backends |
| 3 ------------------ |
| 4 |
| 5 Provides backends for talking to `memcached <http://memcached.org>`_. |
| 6 |
| 7 """ |
| 8 |
| 9 from ..api import CacheBackend, NO_VALUE |
| 10 from ...util import compat |
| 11 from ... import util |
| 12 import random |
| 13 import time |
| 14 |
| 15 __all__ = 'GenericMemcachedBackend', 'MemcachedBackend',\ |
| 16 'PylibmcBackend', 'BMemcachedBackend', 'MemcachedLock' |
| 17 |
| 18 |
| 19 class MemcachedLock(object): |
| 20 """Simple distributed lock using memcached. |
| 21 |
| 22 This is an adaptation of the lock featured at |
| 23 http://amix.dk/blog/post/19386 |
| 24 |
| 25 """ |
| 26 |
| 27 def __init__(self, client_fn, key, timeout=0): |
| 28 self.client_fn = client_fn |
| 29 self.key = "_lock" + key |
| 30 self.timeout = timeout |
| 31 |
| 32 def acquire(self, wait=True): |
| 33 client = self.client_fn() |
| 34 i = 0 |
| 35 while True: |
| 36 if client.add(self.key, 1, self.timeout): |
| 37 return True |
| 38 elif not wait: |
| 39 return False |
| 40 else: |
| 41 sleep_time = (((i + 1) * random.random()) + 2 ** i) / 2.5 |
| 42 time.sleep(sleep_time) |
| 43 if i < 15: |
| 44 i += 1 |
| 45 |
| 46 def release(self): |
| 47 client = self.client_fn() |
| 48 client.delete(self.key) |
| 49 |
| 50 |
| 51 class GenericMemcachedBackend(CacheBackend): |
| 52 """Base class for memcached backends. |
| 53 |
| 54 This base class accepts a number of paramters |
| 55 common to all backends. |
| 56 |
| 57 :param url: the string URL to connect to. Can be a single |
| 58 string or a list of strings. This is the only argument |
| 59 that's required. |
| 60 :param distributed_lock: boolean, when True, will use a |
| 61 memcached-lock as the dogpile lock (see :class:`.MemcachedLock`). |
| 62 Use this when multiple |
| 63 processes will be talking to the same memcached instance. |
| 64 When left at False, dogpile will coordinate on a regular |
| 65 threading mutex. |
| 66 :param lock_timeout: integer, number of seconds after acquiring a lock that |
| 67 memcached should expire it. This argument is only valid when |
| 68 ``distributed_lock`` is ``True``. |
| 69 |
| 70 .. versionadded:: 0.5.7 |
| 71 |
| 72 :param memcached_expire_time: integer, when present will |
| 73 be passed as the ``time`` parameter to ``pylibmc.Client.set``. |
| 74 This is used to set the memcached expiry time for a value. |
| 75 |
| 76 .. note:: |
| 77 |
| 78 This parameter is **different** from Dogpile's own |
| 79 ``expiration_time``, which is the number of seconds after |
| 80 which Dogpile will consider the value to be expired. |
| 81 When Dogpile considers a value to be expired, |
| 82 it **continues to use the value** until generation |
| 83 of a new value is complete, when using |
| 84 :meth:`.CacheRegion.get_or_create`. |
| 85 Therefore, if you are setting ``memcached_expire_time``, you'll |
| 86 want to make sure it is greater than ``expiration_time`` |
| 87 by at least enough seconds for new values to be generated, |
| 88 else the value won't be available during a regeneration, |
| 89 forcing all threads to wait for a regeneration each time |
| 90 a value expires. |
| 91 |
| 92 The :class:`.GenericMemachedBackend` uses a ``threading.local()`` |
| 93 object to store individual client objects per thread, |
| 94 as most modern memcached clients do not appear to be inherently |
| 95 threadsafe. |
| 96 |
| 97 In particular, ``threading.local()`` has the advantage over pylibmc's |
| 98 built-in thread pool in that it automatically discards objects |
| 99 associated with a particular thread when that thread ends. |
| 100 |
| 101 """ |
| 102 |
| 103 set_arguments = {} |
| 104 """Additional arguments which will be passed |
| 105 to the :meth:`set` method.""" |
| 106 |
| 107 def __init__(self, arguments): |
| 108 self._imports() |
| 109 # using a plain threading.local here. threading.local |
| 110 # automatically deletes the __dict__ when a thread ends, |
| 111 # so the idea is that this is superior to pylibmc's |
| 112 # own ThreadMappedPool which doesn't handle this |
| 113 # automatically. |
| 114 self.url = util.to_list(arguments['url']) |
| 115 self.distributed_lock = arguments.get('distributed_lock', False) |
| 116 self.lock_timeout = arguments.get('lock_timeout', 0) |
| 117 self.memcached_expire_time = arguments.get( |
| 118 'memcached_expire_time', 0) |
| 119 |
| 120 def has_lock_timeout(self): |
| 121 return self.lock_timeout != 0 |
| 122 |
| 123 def _imports(self): |
| 124 """client library imports go here.""" |
| 125 raise NotImplementedError() |
| 126 |
| 127 def _create_client(self): |
| 128 """Creation of a Client instance goes here.""" |
| 129 raise NotImplementedError() |
| 130 |
| 131 @util.memoized_property |
| 132 def _clients(self): |
| 133 backend = self |
| 134 |
| 135 class ClientPool(compat.threading.local): |
| 136 def __init__(self): |
| 137 self.memcached = backend._create_client() |
| 138 |
| 139 return ClientPool() |
| 140 |
| 141 @property |
| 142 def client(self): |
| 143 """Return the memcached client. |
| 144 |
| 145 This uses a threading.local by |
| 146 default as it appears most modern |
| 147 memcached libs aren't inherently |
| 148 threadsafe. |
| 149 |
| 150 """ |
| 151 return self._clients.memcached |
| 152 |
| 153 def get_mutex(self, key): |
| 154 if self.distributed_lock: |
| 155 return MemcachedLock(lambda: self.client, key, |
| 156 timeout=self.lock_timeout) |
| 157 else: |
| 158 return None |
| 159 |
| 160 def get(self, key): |
| 161 value = self.client.get(key) |
| 162 if value is None: |
| 163 return NO_VALUE |
| 164 else: |
| 165 return value |
| 166 |
| 167 def get_multi(self, keys): |
| 168 values = self.client.get_multi(keys) |
| 169 return [ |
| 170 NO_VALUE if key not in values |
| 171 else values[key] for key in keys |
| 172 ] |
| 173 |
| 174 def set(self, key, value): |
| 175 self.client.set( |
| 176 key, |
| 177 value, |
| 178 **self.set_arguments |
| 179 ) |
| 180 |
| 181 def set_multi(self, mapping): |
| 182 self.client.set_multi( |
| 183 mapping, |
| 184 **self.set_arguments |
| 185 ) |
| 186 |
| 187 def delete(self, key): |
| 188 self.client.delete(key) |
| 189 |
| 190 def delete_multi(self, keys): |
| 191 self.client.delete_multi(keys) |
| 192 |
| 193 |
| 194 class MemcacheArgs(object): |
| 195 """Mixin which provides support for the 'time' argument to set(), |
| 196 'min_compress_len' to other methods. |
| 197 |
| 198 """ |
| 199 def __init__(self, arguments): |
| 200 self.min_compress_len = arguments.get('min_compress_len', 0) |
| 201 |
| 202 self.set_arguments = {} |
| 203 if "memcached_expire_time" in arguments: |
| 204 self.set_arguments["time"] = arguments["memcached_expire_time"] |
| 205 if "min_compress_len" in arguments: |
| 206 self.set_arguments["min_compress_len"] = \ |
| 207 arguments["min_compress_len"] |
| 208 super(MemcacheArgs, self).__init__(arguments) |
| 209 |
| 210 pylibmc = None |
| 211 |
| 212 |
| 213 class PylibmcBackend(MemcacheArgs, GenericMemcachedBackend): |
| 214 """A backend for the |
| 215 `pylibmc <http://sendapatch.se/projects/pylibmc/index.html>`_ |
| 216 memcached client. |
| 217 |
| 218 A configuration illustrating several of the optional |
| 219 arguments described in the pylibmc documentation:: |
| 220 |
| 221 from dogpile.cache import make_region |
| 222 |
| 223 region = make_region().configure( |
| 224 'dogpile.cache.pylibmc', |
| 225 expiration_time = 3600, |
| 226 arguments = { |
| 227 'url':["127.0.0.1"], |
| 228 'binary':True, |
| 229 'behaviors':{"tcp_nodelay": True,"ketama":True} |
| 230 } |
| 231 ) |
| 232 |
| 233 Arguments accepted here include those of |
| 234 :class:`.GenericMemcachedBackend`, as well as |
| 235 those below. |
| 236 |
| 237 :param binary: sets the ``binary`` flag understood by |
| 238 ``pylibmc.Client``. |
| 239 :param behaviors: a dictionary which will be passed to |
| 240 ``pylibmc.Client`` as the ``behaviors`` parameter. |
| 241 :param min_compress_len: Integer, will be passed as the |
| 242 ``min_compress_len`` parameter to the ``pylibmc.Client.set`` |
| 243 method. |
| 244 |
| 245 """ |
| 246 |
| 247 def __init__(self, arguments): |
| 248 self.binary = arguments.get('binary', False) |
| 249 self.behaviors = arguments.get('behaviors', {}) |
| 250 super(PylibmcBackend, self).__init__(arguments) |
| 251 |
| 252 def _imports(self): |
| 253 global pylibmc |
| 254 import pylibmc # noqa |
| 255 |
| 256 def _create_client(self): |
| 257 return pylibmc.Client( |
| 258 self.url, |
| 259 binary=self.binary, |
| 260 behaviors=self.behaviors |
| 261 ) |
| 262 |
| 263 memcache = None |
| 264 |
| 265 |
| 266 class MemcachedBackend(MemcacheArgs, GenericMemcachedBackend): |
| 267 """A backend using the standard |
| 268 `Python-memcached <http://www.tummy.com/Community/software/\ |
| 269 python-memcached/>`_ |
| 270 library. |
| 271 |
| 272 Example:: |
| 273 |
| 274 from dogpile.cache import make_region |
| 275 |
| 276 region = make_region().configure( |
| 277 'dogpile.cache.memcached', |
| 278 expiration_time = 3600, |
| 279 arguments = { |
| 280 'url':"127.0.0.1:11211" |
| 281 } |
| 282 ) |
| 283 |
| 284 """ |
| 285 def _imports(self): |
| 286 global memcache |
| 287 import memcache # noqa |
| 288 |
| 289 def _create_client(self): |
| 290 return memcache.Client(self.url) |
| 291 |
| 292 |
| 293 bmemcached = None |
| 294 |
| 295 |
| 296 class BMemcachedBackend(GenericMemcachedBackend): |
| 297 """A backend for the |
| 298 `python-binary-memcached <https://github.com/jaysonsantos/\ |
| 299 python-binary-memcached>`_ |
| 300 memcached client. |
| 301 |
| 302 This is a pure Python memcached client which |
| 303 includes the ability to authenticate with a memcached |
| 304 server using SASL. |
| 305 |
| 306 A typical configuration using username/password:: |
| 307 |
| 308 from dogpile.cache import make_region |
| 309 |
| 310 region = make_region().configure( |
| 311 'dogpile.cache.bmemcached', |
| 312 expiration_time = 3600, |
| 313 arguments = { |
| 314 'url':["127.0.0.1"], |
| 315 'username':'scott', |
| 316 'password':'tiger' |
| 317 } |
| 318 ) |
| 319 |
| 320 Arguments which can be passed to the ``arguments`` |
| 321 dictionary include: |
| 322 |
| 323 :param username: optional username, will be used for |
| 324 SASL authentication. |
| 325 :param password: optional password, will be used for |
| 326 SASL authentication. |
| 327 |
| 328 """ |
| 329 def __init__(self, arguments): |
| 330 self.username = arguments.get('username', None) |
| 331 self.password = arguments.get('password', None) |
| 332 super(BMemcachedBackend, self).__init__(arguments) |
| 333 |
| 334 def _imports(self): |
| 335 global bmemcached |
| 336 import bmemcached |
| 337 |
| 338 class RepairBMemcachedAPI(bmemcached.Client): |
| 339 """Repairs BMemcached's non-standard method |
| 340 signatures, which was fixed in BMemcached |
| 341 ef206ed4473fec3b639e. |
| 342 |
| 343 """ |
| 344 |
| 345 def add(self, key, value, timeout=0): |
| 346 try: |
| 347 return super(RepairBMemcachedAPI, self).add( |
| 348 key, value, timeout) |
| 349 except ValueError: |
| 350 return False |
| 351 |
| 352 self.Client = RepairBMemcachedAPI |
| 353 |
| 354 def _create_client(self): |
| 355 return self.Client( |
| 356 self.url, |
| 357 username=self.username, |
| 358 password=self.password |
| 359 ) |
| 360 |
| 361 def delete_multi(self, keys): |
| 362 """python-binary-memcached api does not implements delete_multi""" |
| 363 for key in keys: |
| 364 self.delete(key) |
OLD | NEW |