diff options
author | Nizamudeen A <nia@redhat.com> | 2023-06-07 10:29:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-07 10:29:50 +0200 |
commit | 6baf9c28240969857adba1ccd79b0f738a12b2ef (patch) | |
tree | 2e49864dd7abcee5d2c28f3fc3bd31ae72e908b1 | |
parent | Merge pull request #51927 from ljflores/wip-rook-tests (diff) | |
parent | mgr/dashboard: invalidate rbd image cache on CRUD ops (diff) | |
download | ceph-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.py | 172 | ||||
-rw-r--r-- | src/pybind/mgr/dashboard/plugins/ttl_cache.py | 112 | ||||
-rw-r--r-- | src/pybind/mgr/dashboard/services/rbd.py | 200 | ||||
-rw-r--r-- | src/pybind/mgr/dashboard/tests/test_cache.py | 48 |
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)) |