diff --git a/hotsos/core/plugintools.py b/hotsos/core/plugintools.py index dec845e9e..dcde98468 100644 --- a/hotsos/core/plugintools.py +++ b/hotsos/core/plugintools.py @@ -7,6 +7,7 @@ from hotsos.core.issues import IssuesManager from hotsos.core.log import log from hotsos.core.ycheck.scenarios import YScenarioChecker +from hotsos.core.ycheck.common import GlobalSearchContext from hotsos.core.ycheck.events import EventsPreloader PLUGINS = {} @@ -390,6 +391,10 @@ def __init__(self, plugin): self.parts = PLUGINS[plugin] def run(self): + with GlobalSearchContext(): + return self._run() + + def _run(self): part_mgr = PartManager() failed_parts = [] # The following are executed as part of each plugin run (but not last). diff --git a/hotsos/core/ycheck/common.py b/hotsos/core/ycheck/common.py new file mode 100644 index 000000000..7f7fc4a37 --- /dev/null +++ b/hotsos/core/ycheck/common.py @@ -0,0 +1,217 @@ +from collections import UserDict + +from hotsos.core.config import HotSOSConfig +from hotsos.core.ycheck.engine import ( + YDefsLoader, + YDefsSection, +) +from hotsos.core.log import log +from hotsos.core.search import ( + FileSearcher, + SearchConstraintSearchSince, +) +from hotsos.core.ycheck.engine.properties import search +from hotsos.core.ycheck.engine.properties.search import CommonTimestampMatcher + + +class SearchRegistryKeyConflict(Exception): + def __init__(self, key, all_keys): + self.key = key + self.all_keys = all_keys + + def __str__(self): + return (f"'{self.key}' key already exists in search registry. " + "Available keys are:\n - {}". + format('\n - '.join(self.all_keys))) + + +class SearchRegistryKeyNotFound(Exception): + def __init__(self, key, all_keys): + self.key = key + self.all_keys = all_keys + + def __str__(self): + return ("'{}' not found in search registry. Available keys are:" + "\n - {}". + format(self.key, '\n - '.join(self.all_keys))) + + +class GlobalSearcher(FileSearcher): + """ Searcher with deferred execution and cached results. """ + + def __init__(self): + constraint = SearchConstraintSearchSince( + ts_matcher_cls=CommonTimestampMatcher) + self._results = None + log.debug("creating new global searcher (%s)", self) + super().__init__(constraint=constraint) + + @property + def results(self): + """ + Execute searches of first time called and cached results for future + callers. + """ + if self._results is not None: + log.debug("using cached global searcher results") + return self._results + + log.debug("fetching global searcher results") + self._results = self.run() + return self._results + + +class GlobalSearchRegistry(UserDict): + """ + Maintains a set of properties e.g. dot paths to events or scenarios in yaml + tree - that have been registered as having a search property, a global + FileSearcher object and the results from running searches. This information + is used to load searches from a set of events, run them and save their + results for later retrieval. Search results are tagged with the names + stored here. + """ + + def __init__(self): + self._global_searcher = None + super().__init__() + + def __setitem__(self, key, item): + if key in self: + raise SearchRegistryKeyConflict(key, list(self.data)) + + log.debug("adding key=%s to search registry", key) + super().__setitem__(key, item) + + def __getitem__(self, key): + try: + return super().__getitem__(key) + except KeyError: + raise SearchRegistryKeyNotFound(key, list(self.data)) from KeyError + + @property + def searcher(self): + if self._global_searcher is None: + raise Exception("global searcher is not set but is expected to " + "be.") + + log.debug("using existing global searcher (%s)", + self._global_searcher) + return self._global_searcher + + def reset(self): + log.info("resetting global searcher registry") + self.data = {} + self._global_searcher = GlobalSearcher() + + @staticmethod + def skip_filtered_item(event_path): + e_filter = HotSOSConfig.event_filter + if e_filter and event_path != e_filter: + log.info("skipping event %s (filter=%s)", event_path, e_filter) + return True + + return False + + @staticmethod + def _find_search_prop_parent(items, path): + """ + Walk down path until we hit the item containing the + search property. We skip root/plugin name at start and + ".search" at the end. + + @param item: YDefsSection object representing the entire tree of + items. + @param path: item search property resolve path. + """ + item = None + for branch in path.split('.')[1:-1]: + item = getattr(items if item is None else item, branch) + + return item + + @classmethod + def _load_item_search(cls, item, searcher): + """ Load search information from item into searcher. + + @param item: YDefsSection item object + @param searcher: FileSearcher object + """ + if len(item.input.paths) == 0: + return + + allow_constraints = True + if item.input.command: + # don't apply constraints to command outputs + allow_constraints = False + + # Add to registry in case it is needed by handlers e.g. for + # sequence lookups. + GLOBAL_SEARCH_REGISTRY[item.resolve_path] = {'search': item.search} + + for path in item.input.paths: + log.debug("loading search for item %s (path=%s, tag=%s)", + item.resolve_path, + path, item.search.unique_search_tag) + item.search.load_searcher( + searcher, path, + allow_constraints=allow_constraints) + + @classmethod + def preload_event_searches(cls, group=None): + """ + Find all items that have a search property and load their search into + the global searcher. + + @param group: a group path can be provided to filter a subset of + items. + """ + searcher = GLOBAL_SEARCH_REGISTRY.searcher + if len(searcher.catalog) > 0: + raise Exception("global searcher catalog is not empty " + "and must be reset before loading so as not " + "to include searches from a previous run.") + + log.debug("started loading (group=%s) searches into searcher " + "(%s)", group, searcher) + + search_props = set() + plugin_defs = YDefsLoader('events', filter_path=group).plugin_defs + items = YDefsSection(HotSOSConfig.plugin_name, plugin_defs or {}) + for prop in items.manager.properties.values(): + for item in prop: + if not issubclass(item['cls'], search.YPropertySearch): + break + + search_props.add(item['path']) + + if len(search_props) == 0: + log.debug("finished loading searches but no search " + "properties found") + return + + log.debug("loading searches for %s items", len(search_props)) + for item_search_prop_path in search_props: + item = cls._find_search_prop_parent(items, item_search_prop_path) + if cls.skip_filtered_item(item.resolve_path): + log.debug("skipping item %s", item.resolve_path) + continue + + cls._load_item_search(item, searcher) + + log.debug("finished loading item searches into searcher " + "(registry has %s items)", len(GLOBAL_SEARCH_REGISTRY)) + + +# Maintain a global searcher in module scope so that it is available to +# everyone. Upon the start of each plugin this should be cleared and populated +# then executed as early as possible so that results are ready to be used. +GLOBAL_SEARCH_REGISTRY = GlobalSearchRegistry() + + +class GlobalSearchContext(object): + + def __enter__(self): + GLOBAL_SEARCH_REGISTRY.reset() + + def __exit__(self, *args, **kwargs): + GLOBAL_SEARCH_REGISTRY.reset() diff --git a/hotsos/core/ycheck/engine/common.py b/hotsos/core/ycheck/engine/common.py index 6d2e3bdfd..fbc141945 100644 --- a/hotsos/core/ycheck/engine/common.py +++ b/hotsos/core/ycheck/engine/common.py @@ -9,13 +9,14 @@ class YDefsLoader(object): """ Load yaml definitions. """ - def __init__(self, ytype): + def __init__(self, ytype, filter_path=None): """ @param ytype: the type of defs we are loading i.e. defs/ """ self.ytype = ytype self._loaded_defs = None self.stats_num_files_loaded = 0 + self.filter_path = filter_path def _is_def(self, abs_path): return abs_path.endswith('.yaml') @@ -54,6 +55,24 @@ def _get_defs_recursive(self, path): return defs + def _apply_filter(self, loaded): + """ + If a path filter has been provided, exclude any/all properties that are + not descendants of that path. + """ + if not self.filter_path: + return loaded + + groups = self.filter_path.split('.') + for i, subgroup in enumerate(groups): + if i == 0: + loaded = {subgroup: loaded[subgroup]} + else: + prev = groups[i - 1] + loaded[prev] = {subgroup: loaded[prev][subgroup]} + + return loaded + @property def plugin_defs(self): """ Load yaml defs for the current plugin and type. """ @@ -73,19 +92,13 @@ def plugin_defs(self): HotSOSConfig.plugin_name, self.stats_num_files_loaded) # only return if we loaded actual definitions (not just globals) if self.stats_num_files_loaded: + loaded = self._apply_filter(loaded) self._loaded_defs = loaded return loaded class YHandlerBase(object): - @property - @abc.abstractmethod - def searcher(self): - """ - @return: FileSearcher object to be used by this handler. - """ - @abc.abstractmethod def run(self): """ Process operations. """ diff --git a/hotsos/core/ycheck/events.py b/hotsos/core/ycheck/events.py index 95c19a3d4..42bb775c0 100644 --- a/hotsos/core/ycheck/events.py +++ b/hotsos/core/ycheck/events.py @@ -1,22 +1,15 @@ import abc from functools import cached_property -from collections import UserDict from hotsos.core.config import HotSOSConfig from hotsos.core.log import log -from hotsos.core.search import ( - FileSearcher, - SearchConstraintSearchSince, -) from hotsos.core.utils import sorted_dict from hotsos.core.ycheck.engine import ( YDefsLoader, YHandlerBase, YDefsSection, ) -from hotsos.core.ycheck.engine.properties import search -from hotsos.core.ycheck.engine.properties.search import CommonTimestampMatcher - +from hotsos.core.ycheck.common import GLOBAL_SEARCH_REGISTRY CALLBACKS = {} @@ -29,17 +22,6 @@ class EventCallbackNotFound(Exception): pass -class EventsSearchRegistryKeyNotFound(Exception): - def __init__(self, key, all_keys): - self.key = key - self.all_keys = all_keys - - def __str__(self): - return ("'{}' not found in event registry. Available keys are:" - "\n - {}". - format(self.key, '\n - '.join(self.all_keys))) - - class EventCallbackMeta(type): def __init__(cls, _name, _mro, members): @@ -265,111 +247,65 @@ def __call__(self): """ Callback method. """ -class EventsSearchRegistry(UserDict): +class EventsPreloader(object): """ - Maintains a set of event names - dot paths to events in yaml tree - that - have been registered as having a search property, a global FileSearcher - object and the results from running searches. This information is used - to load searches from a set of events, run them and save their results for - later retrieval. Search results are tagged with the names stored here. - - It might be the case that an event handler wants to use its own - FileSearcher in which case this supports setting a _custom_searcher that - is cleared when the global searcher is accessed. + Pre-load all searches used in event definitions into a global FileSearcher + object and execute the search before running any event callbacks. """ - def __init__(self): - self._global_searcher = None - self._custom_searcher = None - self._global_searcher_results = None - super().__init__() - - def __getitem__(self, key): - try: - return super().__getitem__(key) - except KeyError: - raise EventsSearchRegistryKeyNotFound( - key, - list(self.data)) from KeyError - - def get_global_searcher_results(self): - if self._global_searcher is None: - raise Exception("registry global searcher is None") - - if self._global_searcher_results is not None: - log.debug("using cached global event search results") - return self._global_searcher_results - - log.debug("fetching global event search results") - self._global_searcher_results = self._global_searcher.run() - return self._global_searcher_results - - def get_global_searcher(self, allow_create=False): - if self._global_searcher: - log.debug("using existing global searcher (%s)", - self._global_searcher) - return self._global_searcher - - if not allow_create: - raise Exception("global events searcher is not set but is " - "expected to be.") - - self._custom_searcher = None - constraint = SearchConstraintSearchSince( - ts_matcher_cls=CommonTimestampMatcher) - searcher = FileSearcher(constraint=constraint) - self._global_searcher = searcher - self._global_searcher_results = None - log.debug("creating new global searcher (%s)", searcher) - return searcher - - def set_custom_searcher(self, searcher): - self._custom_searcher = searcher - - @property - def current_searcher(self): - return self._custom_searcher or self.get_global_searcher() + @classmethod + def run(cls): + # Pre-load all event searches into a global event searcher + GLOBAL_SEARCH_REGISTRY.preload_event_searches() + # Run the searches so that results are ready when event handlers are + # run. + GLOBAL_SEARCH_REGISTRY.searcher.results - def _reset_searchers(self): - self._global_searcher = None - self._custom_searcher = None - self._global_searcher_results = None - def reset(self, create_new_global_searcher=False): - log.info("resetting events global registry") - self._reset_searchers() - self.data = {} - if create_new_global_searcher: - self.get_global_searcher(allow_create=True) +class EventHandlerBase(YHandlerBase, EventProcessingUtils): + """ + Root name used to identify a group of event definitions. Once all the + yaml definitions are loaded this defines the level below which events + for this checker are expected to be found. + """ + event_group = None + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # It is assumed that the global searcher already exists, is loaded with + # searches and they have been executed. Unit tests however, should be + # resetting the registry prior to each run and we will therefore need + # to load searches each time which is why we do this here. This is + # therefore not intended to be used outside of a test scenario. + if len(GLOBAL_SEARCH_REGISTRY) == 0: + log.info("global searcher catalog is empty so launching " + "pre-load of event searches for group '%s'", + self.event_group) + # NOTE: this is not re-entrant safe and is only ever expected + # to be done from a unit test. + GLOBAL_SEARCH_REGISTRY.preload_event_searches( + group=self.event_group) -class EventsBase(object): - # IMPORTANT: this state is maintained at class level so that all - # implementations can share it. It is therefore crucial that state is reset - # before loading a new set of event searches. - search_registry = EventsSearchRegistry() + self._event_results = None @staticmethod - def meets_requirements(event): + def meets_requirements(item): """ - If an event or group has a requirements property it must return True - for the events to be executed. + If an item or group has a requirements property it must return True + in order to be executed. """ if HotSOSConfig.force_mode: return True - if event.requires and not event.requires.result: - log.debug("event '%s' pre-requisites not met - " - "skipping", event.name) + if item.requires and not item.requires.result: + log.debug("item '%s' pre-requisites not met - " + "skipping", item.name) return False return True @staticmethod - def skip_filtered_event(event_path): - """ - Apply event filter if provided. - """ + def skip_filtered_item(event_path): e_filter = HotSOSConfig.event_filter if e_filter and event_path != e_filter: log.info("skipping event %s (filter=%s)", event_path, e_filter) @@ -377,218 +313,23 @@ def skip_filtered_event(event_path): return False - @staticmethod - def get_defs(group=None): - """ - Load the event definitions for the current plugin. By default all are - loaded and if a group path is provided, only events that are part of - that group are included. - - @param group: a group path can be provided to include events part of a - group. - """ - log.debug("loading event defs (group=%s)", group) - plugin_defs = YDefsLoader('events').plugin_defs - if not plugin_defs: - return {} - - if not group: - return plugin_defs - - # Exclude events that are not part of the group. - groups = group.split('.') - for i, subgroup in enumerate(groups): - if i == 0: - plugin_defs = {subgroup: plugin_defs[subgroup]} - else: - prev = groups[i - 1] - plugin_defs[prev] = {subgroup: plugin_defs[prev][subgroup]} - - return plugin_defs - - @staticmethod - def _get_event_from_path(events, path): - """ - Walk down path until we hit the event containing the - search property. We skip root/plugin name at start and - ".search" at the end. - - @param event: YDefsSection object representing the entire tree of - events. - @param path: event search property resolve path. - """ - event = None - for branch in path.split('.')[1:-1]: - if event is None: - event = getattr(events, branch) - else: - event = getattr(event, branch) - - return event - - @classmethod - def _load_event_search(cls, event, searcher): - """ Load search information from event into searcher. - - @param event: YDefsSection event object - @param searcher: FileSearcher object - """ - allow_constraints = True - if event.input.command: - # don't apply constraints to command outputs - allow_constraints = False - - for path in event.input.paths: - log.debug("loading search for event %s (path=%s, tag=%s)", - event.resolve_path, - path, event.search.unique_search_tag) - # Add to registry in case it is needed by handlers e.g. for - # sequence lookups. - cls.search_registry[event.resolve_path] = {'search': - event.search} - event.search.load_searcher( - searcher, path, - allow_constraints=allow_constraints) - - @classmethod - def load_searches(cls, group=None, searcher=None): - """ - Find all events that have a search property and load their search into - the global searcher. A custom searcher will be used instead if - provided. - - @param group: a group path can be provided to filter a subset of - events. - @param searcher: customer FileSearcher object to be used instead of the - global searcher. - """ - if searcher is None: - searcher = cls.search_registry.get_global_searcher() - if len(searcher.catalog) > 0: - raise Exception("global event searcher catalog is not empty " - "and must be reset before loading so as not " - "to include searches from a previous run.") - - log.debug("started loading event (group=%s) searches into searcher " - "(%s)", group, searcher) - - search_props = set() - events = YDefsSection(HotSOSConfig.plugin_name, - cls.get_defs(group) or {}) - for prop in events.manager.properties.values(): - for item in prop: - if not issubclass(item['cls'], search.YPropertySearch): - break - - search_props.add(item['path']) - - if len(search_props) == 0: - log.debug("finished loading event searches but no search " - "properties found") - return - - log.debug("loading searches for %s events", len(search_props)) - for event_search_prop_path in search_props: - event = cls._get_event_from_path(events, event_search_prop_path) - if cls.skip_filtered_event(event.resolve_path): - log.debug("skipping event %s", event.resolve_path) - continue - - cls._load_event_search(event, searcher) - - log.debug("finished loading event searches into searcher " - "(registry has %s items)", len(cls.search_registry)) - - -class EventsPreloader(EventsBase): - """ - Pre-load all searches used in event definitions into a global FileSearcher - object and execute the search before running any event callbacks. - """ - - @classmethod - def execute(cls): - # Pre-load all event searches into a global event searcher - cls.load_searches() - # Run the searches so that results are ready when event handlers are - # run. - cls.search_registry.get_global_searcher_results() - - @classmethod - def reset(cls): - # Make sure we start with a clean registry - cls.search_registry.reset(create_new_global_searcher=True) - - @classmethod - def run(cls): - cls.reset() - cls.execute() - - -class EventHandlerBase(EventsBase, YHandlerBase, EventProcessingUtils): - """ - Root name used to identify a group of event definitions. Once all the - yaml definitions are loaded this defines the level below which events - for this checker are expected to be found. - """ - event_group = None - - def __init__(self, *args, searcher=None, **kwargs): - """ - @param searcher: optional FileSearcher object. If not provided then the - global searcher will be used which is the recommended - approach so that all searches are aggregated into one - operation and therefore files only need to be searched - once. - """ - super().__init__(*args, **kwargs) - if searcher is None: - log.debug("no searcher provided - using global searcher") - searcher = self.search_registry.get_global_searcher() - # If no searcher is provided it is assumed that the global searcher - # already exists, is loaded with searches and they have been - # executed. Unit tests however, should be resetting the registry - # prior to each run and we will therefore need to load searches - # each time which is why we do this here. This is therefore not - # intended to be used outside of a test scenario. - if len(searcher.catalog) == 0: - log.info("global searcher catalog is empty so launching " - "pre-load of event searches for group '%s'", - self.event_group) - # NOTE: this is not re-entrant safe and is only ever expected - # to be done from a unit test. - self.load_searches(group=self.event_group) - else: - # If a searcher is provided we switch over but do not clear global - # searcher. - if self.search_registry._custom_searcher != searcher: - self.search_registry.set_custom_searcher(searcher) - - self.load_searches(group=self.event_group, searcher=searcher) - - self._event_results = None - @property def searcher(self): - """ - Return the current searcher we are using. If custom searcher is no - longer needed it is expected that it will have been cleared in the - __init__ method. - """ - return self.search_registry.current_searcher + return GLOBAL_SEARCH_REGISTRY.searcher @cached_property def events(self): """ Load event definitions from yaml. """ - group = YDefsSection(HotSOSConfig.plugin_name, - self.get_defs(self.event_group) or {}) + group_defs = YDefsLoader('events', + filter_path=self.event_group).plugin_defs + group = YDefsSection(HotSOSConfig.plugin_name, group_defs or {}) log.debug("sections=%s, events=%s", len(list(group.branch_sections)), len(list(group.leaf_sections))) _events = {} for event in group.leaf_sections: - if self.skip_filtered_event(event.resolve_path): + if self.skip_filtered_item(event.resolve_path): continue if not self.meets_requirements(event): @@ -625,18 +366,13 @@ def _get_event_search_results(self, event_search, global_results): else: return global_results.find_by_tag(event_search.unique_search_tag) - def run(self, results=None): + def run(self): """ Process each event and call respective callback functions when results where found. - - @param results: If no results are provides we get them from the global - searcher. This is provided for the case where a custom - searcher is in use. """ - if results is None: - results = self.search_registry.get_global_searcher_results() + results = GLOBAL_SEARCH_REGISTRY.searcher.results if self.final_event_results is not None: return self.final_event_results @@ -649,7 +385,7 @@ def run(self, results=None): info = {} for section_name, section in self.events.items(): for event, fullname in section.items(): - event_search = self.search_registry[fullname]['search'] + event_search = GLOBAL_SEARCH_REGISTRY[fullname]['search'] search_results = self._get_event_search_results(event_search, results) if not search_results: @@ -667,11 +403,13 @@ def run(self, results=None): callback = CALLBACKS[callback_name] seq_def = event_search.sequence_search - event_result = EventCheckResult(section_name, event, - search_results, - event_search.unique_search_tag, - self.searcher, - sequence_def=seq_def) + event_result = EventCheckResult( + section_name, + event, + search_results, + event_search.unique_search_tag, + GLOBAL_SEARCH_REGISTRY.searcher, + sequence_def=seq_def) log.debug("executing event %s.%s callback '%s'", event_result.section, event, callback_name) ret = callback()(event_result) diff --git a/hotsos/plugin_extensions/openstack/agent/events.py b/hotsos/plugin_extensions/openstack/agent/events.py index 53eccb51a..6c7bd9114 100644 --- a/hotsos/plugin_extensions/openstack/agent/events.py +++ b/hotsos/plugin_extensions/openstack/agent/events.py @@ -17,13 +17,8 @@ OpenstackEventCallbackBase, ) from hotsos.core.plugins.openstack.neutron import NeutronHAInfo -from hotsos.core.search import ( - FileSearcher, - SearchConstraintSearchSince, -) from hotsos.core import utils from hotsos.core.utils import sorted_dict -from hotsos.core.ycheck.engine.properties.search import CommonTimestampMatcher VRRP_TRANSITION_WARN_THRESHOLD = 8 @@ -374,13 +369,10 @@ def _run_checks(self, checks): if not self.openstack_installed: return - searcher = FileSearcher(constraint=SearchConstraintSearchSince( - ts_matcher_cls=CommonTimestampMatcher)) - check_objs = [c(searcher=searcher) for c in checks] - results = searcher.run() + check_objs = [c() for c in checks] _final_results = {} for check in check_objs: - check.run(results) + check.run() check_results = check.raw_output if check_results: _final_results.update(check_results) diff --git a/tests/unit/storage/test_ceph_osd.py b/tests/unit/storage/test_ceph_osd.py index 7bf4dc804..211c41f59 100644 --- a/tests/unit/storage/test_ceph_osd.py +++ b/tests/unit/storage/test_ceph_osd.py @@ -5,7 +5,7 @@ from hotsos.core.plugins.storage import ( ceph as ceph_core, ) -from hotsos.core.ycheck.events import EventsPreloader +from hotsos.core.ycheck.common import GLOBAL_SEARCH_REGISTRY from hotsos.plugin_extensions.storage import ( ceph_summary, ceph_event_checks, @@ -216,7 +216,7 @@ def test_ceph_daemon_log_checker(self, mock_cli): # This is done in setUp but we have to repeat here otherwise the # date() mock will not be used in the search constraints. - EventsPreloader.reset() + GLOBAL_SEARCH_REGISTRY.reset() inst = ceph_event_checks.CephEventHandler() actual = self.part_output_to_actual(inst.output) diff --git a/tests/unit/utils.py b/tests/unit/utils.py index f27b9232b..146db78cd 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -12,7 +12,7 @@ from hotsos.core.issues import IssuesManager # disable for stestr otherwise output is much too verbose from hotsos.core.log import log, logging, LoggingManager -from hotsos.core.ycheck.events import EventsPreloader +from hotsos.core.ycheck.common import GLOBAL_SEARCH_REGISTRY from hotsos.core.ycheck.scenarios import YScenarioChecker # Must be set prior to other imports @@ -461,7 +461,7 @@ def setUp(self): # here and defer the loading of searches and execution of the search # to happen as part of test so that any env changes can be consumed # properly. - EventsPreloader.reset() + GLOBAL_SEARCH_REGISTRY.reset() def _addDuration(self, *args, **kwargs): # pylint: disable=invalid-name """ Python >=3.12 needs subclasses of unittest.TestCase to implement