diff --git a/doc/source/api.rst b/doc/source/api.rst index 37a832ac..ab0750ee 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -16,3 +16,6 @@ .. automodule:: oslo_cache.backends.noop :members: + +.. automodule:: oslo_cache.backends.dictionary + :members: diff --git a/oslo_cache/backends/dictionary.py b/oslo_cache/backends/dictionary.py new file mode 100644 index 00000000..2c50e47b --- /dev/null +++ b/oslo_cache/backends/dictionary.py @@ -0,0 +1,84 @@ +# Copyright 2015 Mirantis 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. + +"""dogpile.cache backend that uses dictionary as a storage""" + +from dogpile.cache import api +from oslo_utils import timeutils + +__all__ = [ + 'DictCacheBackend' +] + + +# Value for nonexistent and expired keys +NO_VALUE = api.NO_VALUE + + +class DictCacheBackend(api.CacheBackend): + """A DictCacheBackend based on dictionary. + + Arguments accepted in the arguments dictionary: + + :param expiration_time: interval in seconds to indicate maximum + time-to-live value. This parameter is common for a single backend + instance. Default expiration_time value is 0, that means that all keys + have infinite time-to-live value. + :type expiration_time: real + """ + + def __init__(self, arguments): + self.expiration_time = arguments.get('expiration_time', 0) + self.cache = {} + + def get(self, key): + """Retrieves the value for a key or NO_VALUE for nonexistent and + expired keys. + + :param key: dictionary key + """ + (value, timeout) = self.cache.get(key, (NO_VALUE, 0)) + if self.expiration_time > 0 and timeutils.utcnow_ts() >= timeout: + self.cache.pop(key, None) + return NO_VALUE + + return value + + def set(self, key, value): + """Sets the value for a key. + Expunges expired keys during each set. + + :param key: dictionary key + :param value: value associated with the key + """ + self._clear() + timeout = 0 + if self.expiration_time > 0: + timeout = timeutils.utcnow_ts() + self.expiration_time + self.cache[key] = (value, timeout) + + def delete(self, key): + """Deletes the value associated with the key if it exists. + + :param key: dictionary key + """ + self.cache.pop(key, None) + + def _clear(self): + """Expunges expired keys.""" + now = timeutils.utcnow_ts() + for k in list(self.cache): + (_value, timeout) = self.cache[k] + if timeout > 0 and now >= timeout: + del self.cache[k] diff --git a/oslo_cache/core.py b/oslo_cache/core.py index 924a71f3..a8ac1748 100644 --- a/oslo_cache/core.py +++ b/oslo_cache/core.py @@ -21,6 +21,8 @@ When this library is imported, it registers the following backends in * ``oslo_cache.mongo`` - :class:`oslo_cache.backends.mongo.MongoCacheBackend` * ``oslo_cache.memcache_pool`` - :class:`oslo_cache.backends.memcache_pool.PooledMemcachedBackend` +* ``oslo_cache.dict`` - + :class:`oslo_cache.backends.dictionary.DictCacheBackend` To use this library: @@ -62,7 +64,8 @@ _BACKENDS = [ ('oslo_cache.noop', 'oslo_cache.backends.noop', 'NoopCacheBackend'), ('oslo_cache.mongo', 'oslo_cache.backends.mongo', 'MongoCacheBackend'), ('oslo_cache.memcache_pool', 'oslo_cache.backends.memcache_pool', - 'PooledMemcachedBackend') + 'PooledMemcachedBackend'), + ('oslo_cache.dict', 'oslo_cache.backends.dictionary', 'DictCacheBackend'), ] for backend in _BACKENDS: diff --git a/oslo_cache/tests/test_dict_backend.py b/oslo_cache/tests/test_dict_backend.py new file mode 100644 index 00000000..44561f90 --- /dev/null +++ b/oslo_cache/tests/test_dict_backend.py @@ -0,0 +1,94 @@ +# Copyright 2015 Mirantis 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. + +from dogpile.cache import api +from dogpile.cache import region as dp_region + +from oslo_cache.tests import test_cache +from oslo_config import fixture as config_fixture +from oslo_utils import fixture as time_fixture + + +NO_VALUE = api.NO_VALUE +KEY = 'test_key' +VALUE = 'test_value' + + +class CacheDictBackendTest(test_cache.BaseTestCase): + + def setUp(self): + super(CacheDictBackendTest, self).setUp() + self.config_fixture = self.useFixture(config_fixture.Config()) + self.config_fixture.config(group='cache', backend='oslo_cache.dict') + self.time_fixture = self.useFixture(time_fixture.TimeFixture()) + self.region = dp_region.make_region() + self.region.configure( + 'oslo_cache.dict', arguments={'expiration_time': 0.5}) + + def test_dict_backend(self): + self.assertIs(NO_VALUE, self.region.get(KEY)) + + self.region.set(KEY, VALUE) + self.assertEqual(VALUE, self.region.get(KEY)) + + self.region.delete(KEY) + self.assertIs(NO_VALUE, self.region.get(KEY)) + + def test_dict_backend_expiration_time(self): + self.region.set(KEY, VALUE) + self.assertEqual(VALUE, self.region.get(KEY)) + + self.time_fixture.advance_time_seconds(1) + self.assertIs(NO_VALUE, self.region.get(KEY)) + + def test_dict_backend_clear_cache(self): + self.region.set(KEY, VALUE) + + self.time_fixture.advance_time_seconds(1) + + self.assertEqual(1, len(self.region.backend.cache)) + self.region.backend._clear() + self.assertEqual(0, len(self.region.backend.cache)) + + def test_dict_backend_zero_expiration_time(self): + self.region = dp_region.make_region() + self.region.configure( + 'oslo_cache.dict', arguments={'expiration_time': 0}) + + self.region.set(KEY, VALUE) + self.time_fixture.advance_time_seconds(1) + + self.assertEqual(VALUE, self.region.get(KEY)) + self.assertEqual(1, len(self.region.backend.cache)) + + self.region.backend._clear() + + self.assertEqual(VALUE, self.region.get(KEY)) + self.assertEqual(1, len(self.region.backend.cache)) + + def test_dict_backend_multi_keys(self): + self.region.set('key1', 'value1') + self.region.set('key2', 'value2') + self.time_fixture.advance_time_seconds(1) + self.region.set('key3', 'value3') + + self.assertEqual(1, len(self.region.backend.cache)) + self.assertIs(NO_VALUE, self.region.get('key1')) + self.assertIs(NO_VALUE, self.region.get('key2')) + self.assertEqual('value3', self.region.get('key3')) + + def test_dict_backend_rewrite_value(self): + self.region.set(KEY, 'value1') + self.region.set(KEY, 'value2') + self.assertEqual('value2', self.region.get(KEY))