summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNizamudeen A <nia@redhat.com>2023-06-07 10:29:50 +0200
committerGitHub <noreply@github.com>2023-06-07 10:29:50 +0200
commit6baf9c28240969857adba1ccd79b0f738a12b2ef (patch)
tree2e49864dd7abcee5d2c28f3fc3bd31ae72e908b1
parentMerge pull request #51927 from ljflores/wip-rook-tests (diff)
parentmgr/dashboard: invalidate rbd image cache on CRUD ops (diff)
downloadceph-6baf9c28240969857adba1ccd79b0f738a12b2ef.tar.xz
ceph-6baf9c28240969857adba1ccd79b0f738a12b2ef.zip
Merge pull request #50054 from rhcs-dashboard/cache-invalidation
mgr/dashboard: RBD cache invalidation Reviewed-by: Ernesto Puerta <epuertat@redhat.com> Reviewed-by: Nizamudeen A <nia@redhat.com>
-rw-r--r--src/pybind/mgr/dashboard/controllers/rbd.py172
-rw-r--r--src/pybind/mgr/dashboard/plugins/ttl_cache.py112
-rw-r--r--src/pybind/mgr/dashboard/services/rbd.py200
-rw-r--r--src/pybind/mgr/dashboard/tests/test_cache.py48
4 files changed, 343 insertions, 189 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py
index 7d58380e245..1c3d101e701 100644
--- a/src/pybind/mgr/dashboard/controllers/rbd.py
+++ b/src/pybind/mgr/dashboard/controllers/rbd.py
@@ -49,36 +49,10 @@ def RbdTask(name, metadata, wait_for): # noqa: N802
return composed_decorator
-def _sort_features(features, enable=True):
- """
- Sorts image features according to feature dependencies:
-
- object-map depends on exclusive-lock
- journaling depends on exclusive-lock
- fast-diff depends on object-map
- """
- ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
-
- def key_func(feat):
- try:
- return ORDER.index(feat)
- except ValueError:
- return id(feat)
-
- features.sort(key=key_func, reverse=not enable)
-
-
@APIRouter('/block/image', Scope.RBD_IMAGE)
@APIDoc("RBD Management API", "Rbd")
class Rbd(RESTController):
- # set of image features that can be enable on existing images
- ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
-
- # set of image features that can be disabled on existing images
- ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
- "journaling"}
-
DEFAULT_LIMIT = 5
def _rbd_list(self, pool_name=None, offset=0, limit=DEFAULT_LIMIT, search='', sort=''):
@@ -142,29 +116,9 @@ class Rbd(RESTController):
data_pool=None, configuration=None, metadata=None,
mirror_mode=None):
- size = int(size)
-
- def _create(ioctx):
- rbd_inst = rbd.RBD()
-
- # Set order
- l_order = None
- if obj_size and obj_size > 0:
- l_order = int(round(math.log(float(obj_size), 2)))
-
- # Set features
- feature_bitmask = format_features(features)
-
- rbd_inst.create(ioctx, name, size, order=l_order, old_format=False,
- features=feature_bitmask, stripe_unit=stripe_unit,
- stripe_count=stripe_count, data_pool=data_pool)
- RbdConfiguration(pool_ioctx=ioctx, namespace=namespace,
- image_name=name).set_configuration(configuration)
- if metadata:
- with rbd.Image(ioctx, name) as image:
- RbdImageMetadataService(image).set_metadata(metadata)
-
- rbd_call(pool_name, namespace, _create)
+ RbdService.create(name, pool_name, size, namespace,
+ obj_size, features, stripe_unit, stripe_count,
+ data_pool, configuration, metadata)
if mirror_mode:
RbdMirroringService.enable_image(name, pool_name, namespace,
@@ -176,86 +130,17 @@ class Rbd(RESTController):
@RbdTask('delete', ['{image_spec}'], 2.0)
def delete(self, image_spec):
- pool_name, namespace, image_name = parse_image_spec(image_spec)
-
- image = RbdService.get_image(image_spec)
- snapshots = image['snapshots']
- for snap in snapshots:
- RbdSnapshotService.remove_snapshot(image_spec, snap['name'], snap['is_protected'])
-
- rbd_inst = rbd.RBD()
- return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
+ return RbdService.delete(image_spec)
@RbdTask('edit', ['{image_spec}', '{name}'], 4.0)
def set(self, image_spec, name=None, size=None, features=None,
configuration=None, metadata=None, enable_mirror=None, primary=None,
force=False, resync=False, mirror_mode=None, schedule_interval='',
remove_scheduling=False):
-
- pool_name, namespace, image_name = parse_image_spec(image_spec)
-
- def _edit(ioctx, image):
- rbd_inst = rbd.RBD()
- # check rename image
- if name and name != image_name:
- rbd_inst.rename(ioctx, image_name, name)
-
- # check resize
- if size and size != image.size():
- image.resize(size)
-
- mirror_image_info = image.mirror_image_get_info()
- if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED:
- RbdMirroringService.enable_image(
- image_name, pool_name, namespace,
- MIRROR_IMAGE_MODE[mirror_mode])
- elif (enable_mirror is False
- and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED):
- RbdMirroringService.disable_image(
- image_name, pool_name, namespace)
-
- # check enable/disable features
- if features is not None:
- curr_features = format_bitmask(image.features())
- # check disabled features
- _sort_features(curr_features, enable=False)
- for feature in curr_features:
- if (feature not in features
- and feature in self.ALLOW_DISABLE_FEATURES
- and feature in format_bitmask(image.features())):
- f_bitmask = format_features([feature])
- image.update_features(f_bitmask, False)
- # check enabled features
- _sort_features(features)
- for feature in features:
- if (feature not in curr_features
- and feature in self.ALLOW_ENABLE_FEATURES
- and feature not in format_bitmask(image.features())):
- f_bitmask = format_features([feature])
- image.update_features(f_bitmask, True)
-
- RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
- configuration)
- if metadata:
- RbdImageMetadataService(image).set_metadata(metadata)
-
- if primary and not mirror_image_info['primary']:
- RbdMirroringService.promote_image(
- image_name, pool_name, namespace, force)
- elif primary is False and mirror_image_info['primary']:
- RbdMirroringService.demote_image(
- image_name, pool_name, namespace)
-
- if resync:
- RbdMirroringService.resync_image(image_name, pool_name, namespace)
-
- if schedule_interval:
- RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
-
- if remove_scheduling:
- RbdMirroringService.snapshot_schedule_remove(image_spec)
-
- return rbd_image_call(pool_name, namespace, image_name, _edit)
+ return RbdService.set(image_spec, name, size, features,
+ configuration, metadata, enable_mirror, primary,
+ force, resync, mirror_mode, schedule_interval,
+ remove_scheduling)
@RbdTask('copy',
{'src_image_spec': '{image_spec}',
@@ -268,44 +153,17 @@ class Rbd(RESTController):
snapshot_name=None, obj_size=None, features=None,
stripe_unit=None, stripe_count=None, data_pool=None,
configuration=None, metadata=None):
- pool_name, namespace, image_name = parse_image_spec(image_spec)
-
- def _src_copy(s_ioctx, s_img):
- def _copy(d_ioctx):
- # Set order
- l_order = None
- if obj_size and obj_size > 0:
- l_order = int(round(math.log(float(obj_size), 2)))
-
- # Set features
- feature_bitmask = format_features(features)
-
- if snapshot_name:
- s_img.set_snap(snapshot_name)
-
- s_img.copy(d_ioctx, dest_image_name, feature_bitmask, l_order,
- stripe_unit, stripe_count, data_pool)
- RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration(
- configuration)
- if metadata:
- with rbd.Image(d_ioctx, dest_image_name) as image:
- RbdImageMetadataService(image).set_metadata(metadata)
-
- return rbd_call(dest_pool_name, dest_namespace, _copy)
-
- return rbd_image_call(pool_name, namespace, image_name, _src_copy)
+ return RbdService.copy(image_spec, dest_pool_name, dest_namespace, dest_image_name,
+ snapshot_name, obj_size, features,
+ stripe_unit, stripe_count, data_pool,
+ configuration, metadata)
@RbdTask('flatten', ['{image_spec}'], 2.0)
@RESTController.Resource('POST')
@UpdatePermission
@allow_empty_body
def flatten(self, image_spec):
-
- def _flatten(ioctx, image):
- image.flatten()
-
- pool_name, namespace, image_name = parse_image_spec(image_spec)
- return rbd_image_call(pool_name, namespace, image_name, _flatten)
+ return RbdService.flatten(image_spec)
@RESTController.Collection('GET')
def default_features(self):
@@ -335,9 +193,7 @@ class Rbd(RESTController):
Images, even ones actively in-use by clones,
can be moved to the trash and deleted at a later time.
"""
- pool_name, namespace, image_name = parse_image_spec(image_spec)
- rbd_inst = rbd.RBD()
- return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
+ return RbdService.move_image_to_trash(image_spec, delay)
@UIRouter('/block/rbd')
diff --git a/src/pybind/mgr/dashboard/plugins/ttl_cache.py b/src/pybind/mgr/dashboard/plugins/ttl_cache.py
index ad3c36d1198..a509f1228e4 100644
--- a/src/pybind/mgr/dashboard/plugins/ttl_cache.py
+++ b/src/pybind/mgr/dashboard/plugins/ttl_cache.py
@@ -8,6 +8,7 @@ from collections import OrderedDict
from functools import wraps
from threading import RLock
from time import time
+from typing import Any, Dict
try:
from typing import Tuple
@@ -15,42 +16,99 @@ except ImportError:
pass # For typing only
-def ttl_cache(ttl, maxsize=128, typed=False):
+class TTLCache:
+ class CachedValue:
+ def __init__(self, value, timestamp):
+ self.value = value
+ self.timestamp = timestamp
+
+ def __init__(self, reference, ttl, maxsize=128):
+ self.reference = reference
+ self.ttl: int = ttl
+ self.maxsize = maxsize
+ self.cache: OrderedDict[Tuple[Any], TTLCache.CachedValue] = OrderedDict()
+ self.hits = 0
+ self.misses = 0
+ self.expired = 0
+ self.rlock = RLock()
+
+ def __getitem__(self, key):
+ with self.rlock:
+ if key not in self.cache:
+ self.misses += 1
+ raise KeyError(f'"{key}" is not set')
+
+ cached_value = self.cache[key]
+ if time() - cached_value.timestamp >= self.ttl:
+ del self.cache[key]
+ self.expired += 1
+ self.misses += 1
+ raise KeyError(f'"{key}" is not set')
+
+ self.hits += 1
+ return cached_value.value
+
+ def __setitem__(self, key, value):
+ with self.rlock:
+ if key in self.cache:
+ cached_value = self.cache[key]
+ if time() - cached_value.timestamp >= self.ttl:
+ self.expired += 1
+ if len(self.cache) == self.maxsize:
+ self.cache.popitem(last=False)
+
+ self.cache[key] = TTLCache.CachedValue(value, time())
+
+ def clear(self):
+ with self.rlock:
+ self.cache.clear()
+
+ def info(self) -> str:
+ return (f'cache={self.reference} hits={self.hits}, misses={self.misses},'
+ f'expired={self.expired}, maxsize={self.maxsize}, currsize={len(self.cache)}')
+
+
+class CacheManager:
+ caches: Dict[str, TTLCache] = {}
+
+ @classmethod
+ def get(cls, reference: str, ttl=30, maxsize=128):
+ if reference in cls.caches:
+ return cls.caches[reference]
+ cls.caches[reference] = TTLCache(reference, ttl, maxsize)
+ return cls.caches[reference]
+
+
+def ttl_cache(ttl, maxsize=128, typed=False, label: str = ''):
if typed is not False:
raise NotImplementedError("typed caching not supported")
def decorating_function(function):
- cache = OrderedDict() # type: OrderedDict[object, Tuple[bool, float]]
- stats = [0, 0, 0]
- rlock = RLock()
- setattr(function, 'cache_info', lambda:
- "hits={}, misses={}, expired={}, maxsize={}, currsize={}".format(
- stats[0], stats[1], stats[2], maxsize, len(cache)))
+ cache_name = label
+ if not cache_name:
+ cache_name = function.__name__
+ cache = CacheManager.get(cache_name, ttl, maxsize)
@wraps(function)
def wrapper(*args, **kwargs):
key = args + tuple(kwargs.items())
- with rlock:
- refresh = True
- if key in cache:
- (ret, ts) = cache[key]
- del cache[key]
- if time() - ts < ttl:
- refresh = False
- stats[0] += 1
- else:
- stats[2] += 1
-
- if refresh:
- ret = function(*args, **kwargs)
- ts = time()
- if len(cache) == maxsize:
- cache.popitem(last=False)
- stats[1] += 1
-
- cache[key] = (ret, ts)
+ try:
+ return cache[key]
+ except KeyError:
+ ret = function(*args, **kwargs)
+ cache[key] = ret
+ return ret
- return ret
+ return wrapper
+ return decorating_function
+
+def ttl_cache_invalidator(label: str):
+ def decorating_function(function):
+ @wraps(function)
+ def wrapper(*args, **kwargs):
+ ret = function(*args, **kwargs)
+ CacheManager.get(label).clear()
+ return ret
return wrapper
return decorating_function
diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py
index 10c16ce56ff..c6137930317 100644
--- a/src/pybind/mgr/dashboard/services/rbd.py
+++ b/src/pybind/mgr/dashboard/services/rbd.py
@@ -2,6 +2,7 @@
# pylint: disable=unused-argument
import errno
import json
+import math
from enum import IntEnum
import cherrypy
@@ -10,7 +11,7 @@ import rbd
from .. import mgr
from ..exceptions import DashboardException
-from ..plugins.ttl_cache import ttl_cache
+from ..plugins.ttl_cache import ttl_cache, ttl_cache_invalidator
from ._paginate import ListPaginator
from .ceph_service import CephService
@@ -32,6 +33,10 @@ RBD_FEATURES_NAME_MAPPING = {
rbd.RBD_FEATURE_OPERATIONS: "operations",
}
+RBD_IMAGE_REFS_CACHE_REFERENCE = 'rbd_image_refs'
+GET_IOCTX_CACHE = 'get_ioctx'
+POOL_NAMESPACES_CACHE = 'pool_namespaces'
+
class MIRROR_IMAGE_MODE(IntEnum):
journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL
@@ -86,6 +91,25 @@ def format_features(features):
return res
+def _sort_features(features, enable=True):
+ """
+ Sorts image features according to feature dependencies:
+
+ object-map depends on exclusive-lock
+ journaling depends on exclusive-lock
+ fast-diff depends on object-map
+ """
+ ORDER = ['exclusive-lock', 'journaling', 'object-map', 'fast-diff'] # noqa: N806
+
+ def key_func(feat):
+ try:
+ return ORDER.index(feat)
+ except ValueError:
+ return id(feat)
+
+ features.sort(key=key_func, reverse=not enable)
+
+
def get_image_spec(pool_name, namespace, rbd_name):
namespace = '{}/'.format(namespace) if namespace else ''
return '{}/{}{}'.format(pool_name, namespace, rbd_name)
@@ -244,6 +268,13 @@ class RbdConfiguration(object):
class RbdService(object):
_rbd_inst = rbd.RBD()
+ # set of image features that can be enable on existing images
+ ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"}
+
+ # set of image features that can be disabled on existing images
+ ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
+ "journaling"}
+
@classmethod
def _rbd_disk_usage(cls, image, snaps, whole_object=True):
class DUCallback(object):
@@ -394,14 +425,14 @@ class RbdService(object):
return stat_parent
@classmethod
- @ttl_cache(10)
+ @ttl_cache(10, label=GET_IOCTX_CACHE)
def get_ioctx(cls, pool_name, namespace=''):
ioctx = mgr.rados.open_ioctx(pool_name)
ioctx.set_namespace(namespace)
return ioctx
@classmethod
- @ttl_cache(30)
+ @ttl_cache(30, label=RBD_IMAGE_REFS_CACHE_REFERENCE)
def _rbd_image_refs(cls, pool_name, namespace=''):
# We add and set the namespace here so that we cache by ioctx and namespace.
images = []
@@ -410,7 +441,7 @@ class RbdService(object):
return images
@classmethod
- @ttl_cache(30)
+ @ttl_cache(30, label=POOL_NAMESPACES_CACHE)
def _pool_namespaces(cls, pool_name, namespace=None):
namespaces = []
if namespace:
@@ -492,6 +523,167 @@ class RbdService(object):
except rbd.ImageNotFound:
raise cherrypy.HTTPError(404, 'Image not found')
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def create(cls, name, pool_name, size, namespace=None,
+ obj_size=None, features=None, stripe_unit=None, stripe_count=None,
+ data_pool=None, configuration=None, metadata=None):
+ size = int(size)
+
+ def _create(ioctx):
+ rbd_inst = cls._rbd_inst
+
+ # Set order
+ l_order = None
+ if obj_size and obj_size > 0:
+ l_order = int(round(math.log(float(obj_size), 2)))
+
+ # Set features
+ feature_bitmask = format_features(features)
+
+ rbd_inst.create(ioctx, name, size, order=l_order, old_format=False,
+ features=feature_bitmask, stripe_unit=stripe_unit,
+ stripe_count=stripe_count, data_pool=data_pool)
+ RbdConfiguration(pool_ioctx=ioctx, namespace=namespace,
+ image_name=name).set_configuration(configuration)
+ if metadata:
+ with rbd.Image(ioctx, name) as image:
+ RbdImageMetadataService(image).set_metadata(metadata)
+ rbd_call(pool_name, namespace, _create)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def set(cls, image_spec, name=None, size=None, features=None,
+ configuration=None, metadata=None, enable_mirror=None, primary=None,
+ force=False, resync=False, mirror_mode=None, schedule_interval='',
+ remove_scheduling=False):
+ # pylint: disable=too-many-branches
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ def _edit(ioctx, image):
+ rbd_inst = cls._rbd_inst
+ # check rename image
+ if name and name != image_name:
+ rbd_inst.rename(ioctx, image_name, name)
+
+ # check resize
+ if size and size != image.size():
+ image.resize(size)
+
+ mirror_image_info = image.mirror_image_get_info()
+ if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED:
+ RbdMirroringService.enable_image(
+ image_name, pool_name, namespace,
+ MIRROR_IMAGE_MODE[mirror_mode])
+ elif (enable_mirror is False
+ and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED):
+ RbdMirroringService.disable_image(
+ image_name, pool_name, namespace)
+
+ # check enable/disable features
+ if features is not None:
+ curr_features = format_bitmask(image.features())
+ # check disabled features
+ _sort_features(curr_features, enable=False)
+ for feature in curr_features:
+ if (feature not in features
+ and feature in cls.ALLOW_DISABLE_FEATURES
+ and feature in format_bitmask(image.features())):
+ f_bitmask = format_features([feature])
+ image.update_features(f_bitmask, False)
+ # check enabled features
+ _sort_features(features)
+ for feature in features:
+ if (feature not in curr_features
+ and feature in cls.ALLOW_ENABLE_FEATURES
+ and feature not in format_bitmask(image.features())):
+ f_bitmask = format_features([feature])
+ image.update_features(f_bitmask, True)
+
+ RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration(
+ configuration)
+ if metadata:
+ RbdImageMetadataService(image).set_metadata(metadata)
+
+ if primary and not mirror_image_info['primary']:
+ RbdMirroringService.promote_image(
+ image_name, pool_name, namespace, force)
+ elif primary is False and mirror_image_info['primary']:
+ RbdMirroringService.demote_image(
+ image_name, pool_name, namespace)
+
+ if resync:
+ RbdMirroringService.resync_image(image_name, pool_name, namespace)
+
+ if schedule_interval:
+ RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval)
+
+ if remove_scheduling:
+ RbdMirroringService.snapshot_schedule_remove(image_spec)
+
+ return rbd_image_call(pool_name, namespace, image_name, _edit)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def delete(cls, image_spec):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ image = RbdService.get_image(image_spec)
+ snapshots = image['snapshots']
+ for snap in snapshots:
+ RbdSnapshotService.remove_snapshot(image_spec, snap['name'], snap['is_protected'])
+
+ rbd_inst = rbd.RBD()
+ return rbd_call(pool_name, namespace, rbd_inst.remove, image_name)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def copy(cls, image_spec, dest_pool_name, dest_namespace, dest_image_name,
+ snapshot_name=None, obj_size=None, features=None,
+ stripe_unit=None, stripe_count=None, data_pool=None,
+ configuration=None, metadata=None):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+
+ def _src_copy(s_ioctx, s_img):
+ def _copy(d_ioctx):
+ # Set order
+ l_order = None
+ if obj_size and obj_size > 0:
+ l_order = int(round(math.log(float(obj_size), 2)))
+
+ # Set features
+ feature_bitmask = format_features(features)
+
+ if snapshot_name:
+ s_img.set_snap(snapshot_name)
+
+ s_img.copy(d_ioctx, dest_image_name, feature_bitmask, l_order,
+ stripe_unit, stripe_count, data_pool)
+ RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration(
+ configuration)
+ if metadata:
+ with rbd.Image(d_ioctx, dest_image_name) as image:
+ RbdImageMetadataService(image).set_metadata(metadata)
+
+ return rbd_call(dest_pool_name, dest_namespace, _copy)
+
+ return rbd_image_call(pool_name, namespace, image_name, _src_copy)
+
+ @classmethod
+ @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE)
+ def flatten(cls, image_spec):
+ def _flatten(ioctx, image):
+ image.flatten()
+
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ return rbd_image_call(pool_name, namespace, image_name, _flatten)
+
+ @classmethod
+ def move_image_to_trash(cls, image_spec, delay):
+ pool_name, namespace, image_name = parse_image_spec(image_spec)
+ rbd_inst = cls._rbd_inst
+ return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
+
class RbdSnapshotService(object):
diff --git a/src/pybind/mgr/dashboard/tests/test_cache.py b/src/pybind/mgr/dashboard/tests/test_cache.py
new file mode 100644
index 00000000000..f767676c475
--- /dev/null
+++ b/src/pybind/mgr/dashboard/tests/test_cache.py
@@ -0,0 +1,48 @@
+
+import unittest
+
+from ..plugins.ttl_cache import CacheManager, TTLCache
+
+
+class TTLCacheTest(unittest.TestCase):
+ def test_get(self):
+ ref = 'testcache'
+ cache = TTLCache(ref, 30)
+ with self.assertRaises(KeyError):
+ val = cache['foo']
+ cache['foo'] = 'var'
+ val = cache['foo']
+ self.assertEqual(val, 'var')
+ self.assertEqual(cache.hits, 1)
+ self.assertEqual(cache.misses, 1)
+
+ def test_ttl(self):
+ ref = 'testcache'
+ cache = TTLCache(ref, 0.0000001)
+ cache['foo'] = 'var'
+ # pylint: disable=pointless-statement
+ with self.assertRaises(KeyError):
+ cache['foo']
+ self.assertEqual(cache.hits, 0)
+ self.assertEqual(cache.misses, 1)
+ self.assertEqual(cache.expired, 1)
+
+ def test_maxsize_fifo(self):
+ ref = 'testcache'
+ cache = TTLCache(ref, 30, 2)
+ cache['foo0'] = 'var0'
+ cache['foo1'] = 'var1'
+ cache['foo2'] = 'var2'
+ # pylint: disable=pointless-statement
+ with self.assertRaises(KeyError):
+ cache['foo0']
+ self.assertEqual(cache.hits, 0)
+ self.assertEqual(cache.misses, 1)
+
+
+class TTLCacheManagerTest(unittest.TestCase):
+ def test_get(self):
+ ref = 'testcache'
+ cache0 = CacheManager.get(ref)
+ cache1 = CacheManager.get(ref)
+ self.assertEqual(id(cache0), id(cache1))