Source code for keystone.common.cache.core

# Copyright 2013 Metacloud
#
# 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 Caching Layer Implementation."""

import secrets

from dogpile.cache import api as cache_api
from dogpile.cache import region
from dogpile.cache import util
from oslo_cache import core as cache
from oslo_log import log

from keystone.common.cache import _context_cache
import keystone.conf

CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)


[docs] class RegionInvalidationManager: REGION_KEY_PREFIX = '<<<region>>>:' def __init__(self, invalidation_region, region_name): self._invalidation_region = invalidation_region self._region_key = self.REGION_KEY_PREFIX + region_name def _generate_new_id(self): return secrets.token_bytes(10) @property def region_id(self): return self._invalidation_region.get_or_create( self._region_key, self._generate_new_id, expiration_time=-1 )
[docs] def invalidate_region(self): new_region_id = self._generate_new_id() self._invalidation_region.set(self._region_key, new_region_id) return new_region_id
[docs] def is_region_key(self, key): return key == self._region_key
[docs] class DistributedInvalidationStrategy(region.RegionInvalidationStrategy): def __init__(self, region_manager): self._region_manager = region_manager
[docs] def invalidate(self, hard=None): self._region_manager.invalidate_region()
[docs] def is_invalidated(self, timestamp): return False
[docs] def was_hard_invalidated(self): return False
[docs] def is_hard_invalidated(self, timestamp): return False
[docs] def was_soft_invalidated(self): return False
[docs] def is_soft_invalidated(self, timestamp): return False
[docs] def key_mangler_factory(invalidation_manager, orig_key_mangler): def key_mangler(key): # NOTE(dstanek): Since *all* keys go through the key mangler we # need to make sure the region keys don't get the region_id added. # If it were there would be no way to get to it, making the cache # effectively useless. if not invalidation_manager.is_region_key(key): key = f'{key}:{invalidation_manager.region_id}' if orig_key_mangler: key = orig_key_mangler(key) return key return key_mangler
[docs] def create_region(name): """Create a dopile region. Wraps oslo_cache.core.create_region. This is used to ensure that the Region is properly patched and allows us to more easily specify a region name. :param str name: The region name :returns: The new region. :rtype: :class:`dogpile.cache.region.CacheRegion` """ region = cache.create_region() region.name = name # oslo.cache doesn't allow this yet return region
CACHE_REGION = create_region(name='shared default') CACHE_INVALIDATION_REGION = create_region(name='invalidation region') register_model_handler = _context_cache._register_model_handler
[docs] def configure_cache(region=None): if region is None: region = CACHE_REGION # NOTE(morganfainberg): running cache.configure_cache_region() # sets region.is_configured, this must be captured before # cache.configure_cache_region is called. configured = region.is_configured cache.configure_cache_region(CONF, region) # Only wrap the region if it was not configured. This should be pushed # to oslo_cache lib somehow. if not configured: region.wrap(_context_cache._ResponseCacheProxy) region_manager = RegionInvalidationManager( CACHE_INVALIDATION_REGION, region.name ) region.key_mangler = key_mangler_factory( region_manager, region.key_mangler ) region.region_invalidator = DistributedInvalidationStrategy( region_manager )
def _sha1_mangle_key(key): """Wrapper for dogpile's sha1_mangle_key. dogpile's sha1_mangle_key function expects an encoded string, so we should take steps to properly handle multiple inputs before passing the key through. NOTE(dstanek): this was copied directly from olso_cache """ try: key = key.encode('utf-8', errors='xmlcharrefreplace') except (UnicodeError, AttributeError): # NOTE(stevemar): if encoding fails just continue anyway. pass return util.sha1_mangle_key(key)
[docs] def configure_invalidation_region(): if CACHE_INVALIDATION_REGION.is_configured: return # NOTE(dstanek): Configuring this region manually so that we control the # expiration and can ensure that the keys don't expire. config_dict = cache._build_cache_config(CONF) config_dict['expiration_time'] = None # we don't want an expiration CACHE_INVALIDATION_REGION.configure_from_config( config_dict, f'{CONF.cache.config_prefix}.' ) # NOTE(breton): Wrap the cache invalidation region to avoid excessive # calls to memcached, which would result in poor performance. CACHE_INVALIDATION_REGION.wrap(_context_cache._ResponseCacheProxy) # NOTE(morganfainberg): if the backend requests the use of a # key_mangler, we should respect that key_mangler function. If a # key_mangler is not defined by the backend, use the sha1_mangle_key # mangler provided by dogpile.cache. This ensures we always use a fixed # size cache-key. if CACHE_INVALIDATION_REGION.key_mangler is None: CACHE_INVALIDATION_REGION.key_mangler = _sha1_mangle_key
[docs] def get_memoization_decorator(group, expiration_group=None, region=None): if region is None: region = CACHE_REGION return cache.get_memoization_decorator( CONF, region, group, expiration_group=expiration_group )
[docs] def is_replay(region, key, expiration_time=None): """Return whether ``key`` has already been recorded in ``region``. Check this before honoring a one-time-use value (nonce, passcode, receipt, ...), then call :func:`mark_used` once it is consumed. :param region: a configured dogpile CacheRegion. :param key: the identifying string for the one-time-use value. :param expiration_time: seconds to honor a recorded value for, overriding the region's own expiration_time. Pass the value's real validity window plus a margin, so protection isn't at the mercy of an operator's general-purpose ``[cache] expiration_time``. :returns: True if ``key`` was already recorded (a replay). """ if not CONF.cache.enabled or CONF.cache.backend == 'dogpile.cache.null': LOG.warning( 'Caching is disabled or using the null backend; one-time-use ' 'replay protection for the "%s" cache region is degraded.', region.name, ) # backend_expiration_time (if set) purges entries natively in # memcached/redis, independent of the expiration_time passed to # region.get() below. Clamp to it rather than claim a longer window # than the backend can actually honor. backend_ttl = CONF.cache.backend_expiration_time if ( expiration_time is not None and backend_ttl is not None and backend_ttl < expiration_time ): LOG.warning( '[cache] backend_expiration_time (%(backend_ttl)ss) is ' 'shorter than the %(wanted)ss window requested for ' '"%(region)s"; raise it to avoid an early replay gap.', { 'backend_ttl': backend_ttl, 'wanted': expiration_time, 'region': region.name, }, ) expiration_time = backend_ttl value = region.get(key, expiration_time=expiration_time) return value is not cache_api.NO_VALUE
[docs] def mark_used(region, key): """Record ``key`` in ``region`` for a later :func:`is_replay` check. Expiration is enforced on the read side by :func:`is_replay`. """ region.set(key, True)