summaryrefslogtreecommitdiff
path: root/keystone/common/cache/core.py
blob: fb9fc1ca85e854dda4d9d9ca1c8fbca2e42fdb1c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# 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 region
from dogpile.cache import util
from oslo_cache import core as cache

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


CONF = keystone.conf.CONF


class RegionInvalidationManager(object):

    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)

    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

    def is_region_key(self, key):
        return key == self._region_key


class DistributedInvalidationStrategy(region.RegionInvalidationStrategy):

    def __init__(self, region_manager):
        self._region_manager = region_manager

    def invalidate(self, hard=None):
        self._region_manager.invalidate_region()

    def is_invalidated(self, timestamp):
        return False

    def was_hard_invalidated(self):
        return False

    def is_hard_invalidated(self, timestamp):
        return False

    def was_soft_invalidated(self):
        return False

    def is_soft_invalidated(self, timestamp):
        return False


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 = '%s:%s' % (key, invalidation_manager.region_id)
        if orig_key_mangler:
            key = orig_key_mangler(key)
        return key
    return key_mangler


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


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)


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, '%s.' % 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


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)