aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/common/kvs/backends/memcached.py
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/common/kvs/backends/memcached.py')
-rw-r--r--keystone-moon/keystone/common/kvs/backends/memcached.py188
1 files changed, 188 insertions, 0 deletions
diff --git a/keystone-moon/keystone/common/kvs/backends/memcached.py b/keystone-moon/keystone/common/kvs/backends/memcached.py
new file mode 100644
index 00000000..db453143
--- /dev/null
+++ b/keystone-moon/keystone/common/kvs/backends/memcached.py
@@ -0,0 +1,188 @@
+# Copyright 2013 Metacloud, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Keystone Memcached dogpile.cache backend implementation.
+"""
+
+import random as _random
+import time
+
+from dogpile.cache import api
+from dogpile.cache.backends import memcached
+from oslo_config import cfg
+from oslo_log import log
+
+from keystone.common.cache.backends import memcache_pool
+from keystone.common import manager
+from keystone import exception
+from keystone.i18n import _
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+NO_VALUE = api.NO_VALUE
+random = _random.SystemRandom()
+
+VALID_DOGPILE_BACKENDS = dict(
+ pylibmc=memcached.PylibmcBackend,
+ bmemcached=memcached.BMemcachedBackend,
+ memcached=memcached.MemcachedBackend,
+ pooled_memcached=memcache_pool.PooledMemcachedBackend)
+
+
+class MemcachedLock(object):
+ """Simple distributed lock using memcached.
+
+ This is an adaptation of the lock featured at
+ http://amix.dk/blog/post/19386
+
+ """
+ def __init__(self, client_fn, key, lock_timeout, max_lock_attempts):
+ self.client_fn = client_fn
+ self.key = "_lock" + key
+ self.lock_timeout = lock_timeout
+ self.max_lock_attempts = max_lock_attempts
+
+ def acquire(self, wait=True):
+ client = self.client_fn()
+ for i in range(self.max_lock_attempts):
+ if client.add(self.key, 1, self.lock_timeout):
+ return True
+ elif not wait:
+ return False
+ else:
+ sleep_time = random.random()
+ time.sleep(sleep_time)
+ raise exception.UnexpectedError(
+ _('Maximum lock attempts on %s occurred.') % self.key)
+
+ def release(self):
+ client = self.client_fn()
+ client.delete(self.key)
+
+
+class MemcachedBackend(manager.Manager):
+ """Pivot point to leverage the various dogpile.cache memcached backends.
+
+ To specify a specific dogpile.cache memcached driver, pass the argument
+ `memcached_driver` set to one of the provided memcached drivers (at this
+ time `memcached`, `bmemcached`, `pylibmc` are valid).
+ """
+ def __init__(self, arguments):
+ self._key_mangler = None
+ self.raw_no_expiry_keys = set(arguments.pop('no_expiry_keys', set()))
+ self.no_expiry_hashed_keys = set()
+
+ self.lock_timeout = arguments.pop('lock_timeout', None)
+ self.max_lock_attempts = arguments.pop('max_lock_attempts', 15)
+ # NOTE(morganfainberg): Remove distributed locking from the arguments
+ # passed to the "real" backend if it exists.
+ arguments.pop('distributed_lock', None)
+ backend = arguments.pop('memcached_backend', None)
+ if 'url' not in arguments:
+ # FIXME(morganfainberg): Log deprecation warning for old-style
+ # configuration once full dict_config style configuration for
+ # KVS backends is supported. For now use the current memcache
+ # section of the configuration.
+ arguments['url'] = CONF.memcache.servers
+
+ if backend is None:
+ # NOTE(morganfainberg): Use the basic memcached backend if nothing
+ # else is supplied.
+ self.driver = VALID_DOGPILE_BACKENDS['memcached'](arguments)
+ else:
+ if backend not in VALID_DOGPILE_BACKENDS:
+ raise ValueError(
+ _('Backend `%(driver)s` is not a valid memcached '
+ 'backend. Valid drivers: %(driver_list)s') %
+ {'driver': backend,
+ 'driver_list': ','.join(VALID_DOGPILE_BACKENDS.keys())})
+ else:
+ self.driver = VALID_DOGPILE_BACKENDS[backend](arguments)
+
+ def _get_set_arguments_driver_attr(self, exclude_expiry=False):
+
+ # NOTE(morganfainberg): Shallow copy the .set_arguments dict to
+ # ensure no changes cause the values to change in the instance
+ # variable.
+ set_arguments = getattr(self.driver, 'set_arguments', {}).copy()
+
+ if exclude_expiry:
+ # NOTE(morganfainberg): Explicitly strip out the 'time' key/value
+ # from the set_arguments in the case that this key isn't meant
+ # to expire
+ set_arguments.pop('time', None)
+ return set_arguments
+
+ def set(self, key, value):
+ mapping = {key: value}
+ self.set_multi(mapping)
+
+ def set_multi(self, mapping):
+ mapping_keys = set(mapping.keys())
+ no_expiry_keys = mapping_keys.intersection(self.no_expiry_hashed_keys)
+ has_expiry_keys = mapping_keys.difference(self.no_expiry_hashed_keys)
+
+ if no_expiry_keys:
+ # NOTE(morganfainberg): For keys that have expiry excluded,
+ # bypass the backend and directly call the client. Bypass directly
+ # to the client is required as the 'set_arguments' are applied to
+ # all ``set`` and ``set_multi`` calls by the driver, by calling
+ # the client directly it is possible to exclude the ``time``
+ # argument to the memcached server.
+ new_mapping = {k: mapping[k] for k in no_expiry_keys}
+ set_arguments = self._get_set_arguments_driver_attr(
+ exclude_expiry=True)
+ self.driver.client.set_multi(new_mapping, **set_arguments)
+
+ if has_expiry_keys:
+ new_mapping = {k: mapping[k] for k in has_expiry_keys}
+ self.driver.set_multi(new_mapping)
+
+ @classmethod
+ def from_config_dict(cls, config_dict, prefix):
+ prefix_len = len(prefix)
+ return cls(
+ {key[prefix_len:]: config_dict[key] for key in config_dict
+ if key.startswith(prefix)})
+
+ @property
+ def key_mangler(self):
+ if self._key_mangler is None:
+ self._key_mangler = self.driver.key_mangler
+ return self._key_mangler
+
+ @key_mangler.setter
+ def key_mangler(self, key_mangler):
+ if callable(key_mangler):
+ self._key_mangler = key_mangler
+ self._rehash_keys()
+ elif key_mangler is None:
+ # NOTE(morganfainberg): Set the hashed key map to the unhashed
+ # list since we no longer have a key_mangler.
+ self._key_mangler = None
+ self.no_expiry_hashed_keys = self.raw_no_expiry_keys
+ else:
+ raise TypeError(_('`key_mangler` functions must be callable.'))
+
+ def _rehash_keys(self):
+ no_expire = set()
+ for key in self.raw_no_expiry_keys:
+ no_expire.add(self._key_mangler(key))
+ self.no_expiry_hashed_keys = no_expire
+
+ def get_mutex(self, key):
+ return MemcachedLock(lambda: self.driver.client, key,
+ self.lock_timeout, self.max_lock_attempts)