Skip to content

Commit

Permalink
Make common global searcher (#887)
Browse files Browse the repository at this point in the history
First part of changes to make a common global searcher that
can be used for both events and scenarios. This patch also
fixes the double searching of events in openstack
AgentEventChecks.

This patches make use of a context manager that manipulates
a module global that will eventaully be removedin future
patches.
  • Loading branch information
dosaboy authored May 30, 2024
1 parent f59e827 commit 5f31acc
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 340 deletions.
5 changes: 5 additions & 0 deletions hotsos/core/plugintools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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).
Expand Down
217 changes: 217 additions & 0 deletions hotsos/core/ycheck/common.py
Original file line number Diff line number Diff line change
@@ -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()
29 changes: 21 additions & 8 deletions hotsos/core/ycheck/engine/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ytype>
"""
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')
Expand Down Expand Up @@ -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. """
Expand All @@ -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. """
Loading

0 comments on commit 5f31acc

Please sign in to comment.