Skip to content

perf: Rules lazy cache evaluator#2325

Open
pindo696 wants to merge 3 commits into
RedHatInsights:masterfrom
pindo696:rules-lazy-cache-evaluator
Open

perf: Rules lazy cache evaluator#2325
pindo696 wants to merge 3 commits into
RedHatInsights:masterfrom
pindo696:rules-lazy-cache-evaluator

Conversation

@pindo696
Copy link
Copy Markdown
Contributor

@pindo696 pindo696 commented May 8, 2026

Use local cache for rules and reuse it while the cache is still warm (ttl). After cache ttl expires, preload rules from DB cache.

Secure Coding Practices Checklist GitHub Link

Secure Coding Checklist

  • Input Validation
  • Output Encoding
  • Authentication and Password Management
  • Session Management
  • Access Control
  • Cryptographic Practices
  • Error Handling and Logging
  • Data Protection
  • Communication Security
  • System Configuration
  • Database Security
  • File Management
  • Memory Management
  • General Coding Practices

Summary by Sourcery

Introduce a TTL-based in-memory cache for insights rules and lazy loading of missing rule metadata during advisor evaluation to reduce repeated full-table database queries.

Enhancements:

  • Add a configurable TTL-controlled rule cache with automatic warmup and reload behavior.
  • Support lazy per-rule database lookups when advisor evaluation encounters rules missing from the current cache snapshot while tracking negative lookups to avoid repeated queries.

Build:

  • Expose INSIGHTS_LOAD_RULE_CACHE_TTL_SEC configuration for controlling evaluator rule cache behavior.

Tests:

  • Add async tests covering rule cache TTL semantics and lazy rule loading behavior during advisor/advisor+vmaas merge paths.

pindo696 added 2 commits May 8, 2026 19:26
use local cache with ttl or warm cache time instead of lazy db cache load on each evaluation.
Lets say a part of RHINENG-24321
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 8, 2026

Reviewer's Guide

Introduces an in-memory, TTL-based cache for insights_rule metadata with lazy per-rule loading during advisor evaluation, configurable via environment variable, and adds tests to validate cache behavior and advisor merge semantics.

Sequence diagram for advisor evaluation using lazy TTL rule cache

sequenceDiagram
    participant Evaluator
    participant Config
    participant DBPool
    participant DB

    Note over Evaluator,DB: Initialization warmup
    Evaluator->>DBPool: connection()
    DBPool-->>Evaluator: AsyncConnection
    Evaluator->>DB: _load_rule_cache(conn)
    DB-->>Evaluator: all insights_rule rows
    Evaluator->>Evaluator: _set_rule_cache_expiry()
    Evaluator->>Evaluator: store rule_cache

    Note over Evaluator,DB: Per-system evaluation
    Evaluator->>DBPool: connection()
    DBPool-->>Evaluator: AsyncConnection conn
    Evaluator->>Evaluator: _get_rule_cache(conn)
    Evaluator->>Config: read insights_load_rule_cache_ttl_sec
    Config-->>Evaluator: ttl
    Evaluator->>Evaluator: _rule_cache_expired()
    alt cache expired
        Evaluator->>DB: _load_rule_cache(conn)
        DB-->>Evaluator: refreshed insights_rule rows
        Evaluator->>Evaluator: _set_rule_cache_expiry()
    else cache valid
        Evaluator->>Evaluator: reuse existing rule_cache
    end
    Evaluator->>Evaluator: snapshot = dict(rule_cache)

    loop for each rule hit in rule_results
        Evaluator->>Evaluator: resolve_rule_cache(rule_name)
        alt rule in snapshot
            Evaluator-->>Evaluator: return cached RuleCache
        else rule missing in snapshot
            alt rule_name in missing_rules_not_found
                Evaluator-->>Evaluator: return None
            else
                Evaluator->>DB: _load_rule_cache_for_name(conn, rule_name)
                DB-->>Evaluator: RuleCache or None
                alt rule found
                    Evaluator->>Evaluator: update rule_cache and snapshot
                    Evaluator-->>Evaluator: return RuleCache
                else rule not found
                    Evaluator->>Evaluator: add to missing_rules_not_found
                    Evaluator-->>Evaluator: return None
                end
            end
        end
        Evaluator->>Evaluator: merge advisor result using RuleCache
    end
Loading

Class diagram for evaluator rule cache and config changes

classDiagram
    class EvaluatorLogic {
        - db_pool AsyncConnectionPool
        - module_cache Dict~str, ModuleCache~
        - vulnerable_package_cache Dict~(int, int, Optional~int~), VulnerablePackageCache~
        - skipped_rules List~str~
        - rule_cache Dict~str, RuleCache~
        - rule_cache_expires_at Optional~datetime~
        + __init__(db_pool AsyncConnectionPool)
        + init() Async
        - _load_cve_impact_cache() Dict~str, CveImpactCache~
        - _load_cve_cache() Dict~str, CveCache~
        - _load_rule_cache(conn AsyncConnection) Dict~str, RuleCache~
        - _set_rule_cache_expiry() None
        - _rule_cache_expired() bool
        - _load_rule_cache_for_name(conn AsyncConnection, name str) Optional~RuleCache~
        - _get_rule_cache(conn Optional~AsyncConnection~) Dict~str, RuleCache~
        - _load_package_name_cache() Dict~str, PackageNameCache~
        - _evaluate_advisor_res(rule_results Dict, sys_vuln_rows Dict~str, SystemVulnerabilitiesRow~, rule_cache Dict~str, RuleCache~, system_platform SystemPlatform, unpatched_cves Set~str~, conn AsyncConnection) Dict~str, SystemVulnerabilitiesRow~
        + evaluate_vulnerabilities(system_platform SystemPlatform, conn AsyncConnection) Dict~str, SystemVulnerabilitiesRow~
    }

    class RuleCache {
        + id int
        + playbook_count int
    }

    class Config {
        + insights_load_rule_cache_ttl_sec int
    }

    EvaluatorLogic --> RuleCache : uses
    EvaluatorLogic --> Config : reads TTL from
Loading

File-Level Changes

Change Details Files
Add TTL-based in-memory cache for insights_rule metadata with lazy per-rule DB lookups during advisor evaluation.
  • Introduce EvaluatorLogic.rule_cache and rule_cache_expires_at fields and warm the cache during init via _get_rule_cache.
  • Implement _set_rule_cache_expiry and _rule_cache_expired to control when the cache is considered stale based on CFG.insights_load_rule_cache_ttl_sec.
  • Add _get_rule_cache to return a snapshot of the rule cache and refresh from DB when TTL has expired, with optional reuse of an existing DB connection.
  • Add _load_rule_cache_for_name and resolve_rule_cache helper inside _evaluate_advisor_res to lazily fetch missing rule entries from DB and backfill both the instance cache and the snapshot used during merge.
  • Change evaluate_vulnerabilities and _evaluate_advisor_res signatures and call sites to pass a DB connection and use resolve_rule_cache instead of direct dict access for rule metadata lookups.
evaluator/logic.py
Make rule cache TTL configurable via environment variable.
  • Add insights_load_rule_cache_ttl_sec configuration option sourced from INSIGHTS_LOAD_RULE_CACHE_TTL_SEC, with 0 meaning cache disabled and always reload from DB.
  • Document default behavior (0 -> local cache disabled) in config.
common/config.py
conf/evaluator.env
Add tests covering rule cache TTL behavior and lazy-load semantics in advisor merge.
  • Create FakeAsyncPool to stand in for the async DB pool in tests.
  • Add tests validating that _get_rule_cache calls the full load only once while cache is warm, always reloads when TTL is disabled, and reloads after TTL expiry.
  • Add tests verifying that advisor merge lazily loads missing rule metadata when not present in a warm cache and that lazy vs preloaded rule metadata produce identical vulnerability rows.
tests/common_tests/test_evaluator_rule_cache.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

SC Environment Impact Assessment

Overall Impact:NONE

No SC Environment-specific impacts detected in this PR.

What was checked

This PR was automatically scanned for:

  • Database migrations
  • ClowdApp configuration changes
  • Kessel integration changes
  • AWS service integrations (S3, RDS, ElastiCache)
  • Kafka topic changes
  • Secrets management changes
  • External dependencies

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new rule cache (self.rule_cache / self.rule_cache_expires_at) is mutated from multiple async call paths without any locking, so under concurrent evaluations you may see races (e.g., redundant reloads or partially updated caches); consider adding an asyncio.Lock around cache refresh/mutation to make cache updates atomic per process.
  • In _get_rule_cache you re-read CFG.insights_load_rule_cache_ttl_sec and recompute expiry on every access; if runtime reconfiguration is not required, caching the TTL on the EvaluatorLogic instance at init time would simplify the logic and avoid subtle behavior changes if CFG is mutated later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new rule cache (`self.rule_cache` / `self.rule_cache_expires_at`) is mutated from multiple async call paths without any locking, so under concurrent evaluations you may see races (e.g., redundant reloads or partially updated caches); consider adding an `asyncio.Lock` around cache refresh/mutation to make cache updates atomic per process.
- In `_get_rule_cache` you re-read `CFG.insights_load_rule_cache_ttl_sec` and recompute expiry on every access; if runtime reconfiguration is not required, caching the TTL on the `EvaluatorLogic` instance at init time would simplify the logic and avoid subtle behavior changes if `CFG` is mutated later.

## Individual Comments

### Comment 1
<location path="evaluator/logic.py" line_range="60" />
<code_context>
         self.module_cache: Dict[str, ModuleCache] = {}
         self.vulnerable_package_cache: Dict[(int, int, Optional[int]), VulnerablePackageCache] = {}
         self.skipped_rules = ["CVE_2017_5715_cpu_virt|VIRT_CVE_2017_5715_CPU_3_ONLYKERNEL", "CVE_2017_5715_cpu_virt"]
+        self.rule_cache: Dict[str, RuleCache] = {}
+        self.rule_cache_expires_at: Optional[datetime] = None

</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the rule caching logic to use a single canonical cache with helper methods instead of a local snapshot and inner async closure.

A lot of the complexity comes from maintaining both `self.rule_cache` and a separate `rule_cache` snapshot plus the inner `resolve_rule_cache` closure. You can keep the same behavior (TTL + lazy load on miss) with a single canonical cache and a small helper method, which removes duplication and makes ownership clearer.

### 1. Make `_get_rule_cache` return the canonical cache (no copy)

```python
async def _get_rule_cache(
    self, conn: Optional[AsyncConnection] = None
) -> Dict[str, RuleCache]:
    """Return the rule cache, refreshing from DB when cache TTL expired."""
    if self._rule_cache_expired():
        if conn is None:
            async with self.db_pool.connection() as conn:
                self.rule_cache = await self._load_rule_cache(conn)
        else:
            self.rule_cache = await self._load_rule_cache(conn)
        self._set_rule_cache_expiry()
    return self.rule_cache  # <- no dict(...) copy
```

This removes the need to keep a snapshot in sync with `self.rule_cache`.

### 2. Extract `resolve_rule_cache` into a dedicated method operating only on `self.rule_cache`

```python
async def _get_rule_from_cache(
    self,
    conn: AsyncConnection,
    rule_name: str,
    missing_rules_not_found: Set[str],
) -> Optional[RuleCache]:
    cache = await self._get_rule_cache(conn)

    cached = cache.get(rule_name)
    if cached:
        return cached

    if rule_name in missing_rules_not_found:
        return None

    fetched = await self._load_rule_cache_for_name(conn, rule_name)
    if fetched:
        # update canonical cache only
        self.rule_cache[rule_name] = fetched
        return fetched

    missing_rules_not_found.add(rule_name)
    return None
```

Now all cache logic (TTL + lazy load) is encapsulated, and there is a single source of truth: `self.rule_cache`.

### 3. Simplify `_evaluate_advisor_res` to use the helper (no local cache or inner async)

Change the signature to drop `rule_cache` (internal method, so safe to adjust call sites):

```python
async def _evaluate_advisor_res(
    self,
    rule_results: dict,
    sys_vuln_rows: Dict[str, SystemVulnerabilitiesRow],
    system_platform: SystemPlatform,
    unpatched_cves: Set[str],
    conn: AsyncConnection,
) -> Dict[str, SystemVulnerabilitiesRow]:
    """Merge results from vmaas package evaluation with advisor rule evaluation"""

    missing_rules_not_found: Set[str] = set()

    for cve, hit_details in rule_results["rule_hits"].items():
        ...
        rule = hit_details["rule_id"]
        if rule in self.skipped_rules:
            continue

        if not isinstance(hit_details["details"], dict):
            hit_details["details"] = json.loads(hit_details["details"])

        rule_db = await self._get_rule_from_cache(conn, rule, missing_rules_not_found)
        if not rule_db:
            continue
        ...
```

And in the second loop:

```python
rule = pass_details["rule_id"]
if rule in self.skipped_rules:
    continue

rule_db = await self._get_rule_from_cache(conn, rule, missing_rules_not_found)
if not rule_db:
    continue
...
```

### 4. Adjust the call site

```python
with RULES_EVAL_TIME.time():
    # warm/refresh cache; returned dict is `self.rule_cache`, but caller
    # no longer needs to keep its own snapshot
    await self._get_rule_cache(conn)
    sys_vuln_rows = await self._evaluate_advisor_res(
        system_platform.rule_results,
        sys_vuln_rows,
        system_platform,
        unpatched_cves_set,
        conn,
    )
```

Outcome:

- Single cache (`self.rule_cache`), no snapshot that needs manual synchronization.
- No inner async closure capturing multiple mutable structures.
- TTL and lazy loading remain, but are hidden behind `_get_rule_cache` and `_get_rule_from_cache`, making `_evaluate_advisor_res` easier to read and reason about.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread evaluator/logic.py
Return rule cache from get rule cache (no dict copy), inner closure to class fnc, test updated
@pindo696
Copy link
Copy Markdown
Contributor Author

/retest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant