Skip to content

fix: persist total_components count for EI analysis when job terminates - #2570

Merged
Strum355 merged 1 commit into
guacsec:mainfrom
Strum355:null-total-components
Aug 12, 2026
Merged

fix: persist total_components count for EI analysis when job terminates#2570
Strum355 merged 1 commit into
guacsec:mainfrom
Strum355:null-total-components

Conversation

@Strum355

@Strum355 Strum355 commented Aug 11, 2026

Copy link
Copy Markdown
Member

total_components was always set to NULL at job creation time. No code ever wrote it back to the DB during the polling/completion flow. The list endpoint masked this with a fallback that computed the count from component rows, but the details endpoint, which didn't have this fallback, returned the raw DB value of null.

The fix: update_job_completed and update_job_failed now accept an optional total_components parameter and write it to the DB when provided. The polling code passes the known count (Some(1) for single-component, Some(components.len()) for SPDX) at finalization time. The reason we do it when marking a job as failed/completed is that, to my understanding, EI doesn't give us a definitive list/number of components until EI itself marks a job as having terminated.

Summary by Sourcery

Persist and surface accurate component counts for exploit intelligence analysis jobs when they complete or fail.

Bug Fixes:

  • Ensure total_components is written to the database when EI jobs transition to completed or failed, so details responses no longer return null for terminated jobs.

Enhancements:

  • Pass known component totals from polling logic into job completion/failure updates for both single-component and multi-component (SPDX) flows.
  • Tighten exploit intelligence job details tests to validate product metadata, report URLs, component counters, and component statuses across success and failure scenarios.
  • Simplify summary construction by relying on entity-with-counts rather than recomputing total_components in the list endpoint.
  • Remove unused helper for creating jobs with preset product_id and total_components in the service layer.

Tests:

  • Expand EI runner, service, and endpoint tests to cover total_components persistence and full job/component counter invariants across various analysis outcomes.

@sourcery-ai

sourcery-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR ensures exploit intelligence jobs persist an accurate total_components count when they terminate, and tightens tests around job detail fields so the API returns consistent metadata for completed and failed analyses.

Sequence diagram for persisting total_components on EI job completion

sequenceDiagram
    participant RunnerPolling as poll_for_product_result
    participant EiService as ExploitIntelligenceService
    participant DB as Database

    RunnerPolling->>EiService: fetch_components(job_id, db)
    EiService-->>RunnerPolling: components (Vec<ExploitIntelligenceComponent>)
    RunnerPolling->>RunnerPolling: total = Some(components.len() as i32)
    alt has_finding or all_excluded
        RunnerPolling->>EiService: update_job_completed(job_id, total, db)
        EiService->>DB: ActiveModel{status=Completed, total_components=total}.update(db)
    else no findings and not all excluded
        RunnerPolling->>EiService: update_job_failed(job_id, error_message, total, db)
        EiService->>DB: ActiveModel{status=Failed, total_components=total}.update(db)
    end
Loading

Sequence diagram for persisting total_components on single-component EI job completion

sequenceDiagram
    participant RunnerPolling as poll_for_result
    participant EiService as ExploitIntelligenceService
    participant DB as Database

    RunnerPolling->>EiService: update_component_completed(component_id, finding, advisory_id, db)
    EiService-->>RunnerPolling: component updated
    RunnerPolling->>EiService: update_job_completed(job_id, Some(1), db)
    EiService->>DB: ActiveModel{status=Completed, total_components=Some(1)}.update(db)
Loading

File-Level Changes

Change Details Files
Persist total_components on job completion/failure and simplify summary construction.
  • Added optional total_components parameter to update_job_completed and update_job_failed and wired it into the ActiveModel updates using NotSet when absent.
  • Updated polling and worker flows to pass the correct total component count for single-component and multi-component product analyses at the point jobs are marked completed or failed.
  • Removed the list-endpoint fallback logic that derived total_components from component counts when the DB value was null, relying instead on persisted totals.
  • Deleted the create_job_with_product helper that pre-set total_components at job creation, aligning all flows to set the count at job termination.
modules/exploit-intelligence/src/service/mod.rs
modules/exploit-intelligence/src/runner/polling.rs
Update tests to cover persisted total_components and richer job detail expectations.
  • Extended runner tests to assert product_id, report_url, total_components and per-status component counters across various CDX/SPDX success and failure scenarios, replacing some vague collection-based assertions with precise index checks.
  • Adjusted service tests to verify total_components is persisted on completion and that update_job_failed behavior remains correct when the new parameter is None.
  • Updated endpoint tests to accept a total_components parameter when inserting test jobs, set appropriate values for different statuses, and pass None through to update_job_failed in timeout and other failure flows.
  • Ensured list and filter endpoint tests still behave as expected with jobs that may or may not have total_components set.
modules/exploit-intelligence/src/runner/test.rs
modules/exploit-intelligence/src/service/test.rs
modules/exploit-intelligence/src/endpoints/test.rs
modules/exploit-intelligence/src/runner/worker.rs

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

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 propagation of Some(1) for single-component jobs is repeated in multiple places; consider encapsulating this in a helper or constant to avoid duplication and make future changes to the default behavior easier.
  • When casting components.len() to i32 for total_components, it may be safer to use a fallible conversion (e.g., try_into()) or at least document the assumption about maximum component count to avoid potential overflow issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The propagation of `Some(1)` for single-component jobs is repeated in multiple places; consider encapsulating this in a helper or constant to avoid duplication and make future changes to the default behavior easier.
- When casting `components.len()` to `i32` for `total_components`, it may be safer to use a fallible conversion (e.g., `try_into()`) or at least document the assumption about maximum component count to avoid potential overflow issues.

## Individual Comments

### Comment 1
<location path="modules/exploit-intelligence/src/runner/polling.rs" line_range="292" />
<code_context>

-            // Check whether any component actually has a finding.
             let components = ei_service.fetch_components(job_id, db).await?;
+            let total = Some(components.len() as i32);

             let has_finding = components.iter().any(|c| {
</code_context>
<issue_to_address>
**issue (bug_risk):** Casting `components.len()` (usize) to `i32` can overflow for very large jobs.

The `as i32` cast will silently overflow if `components.len()` ever exceeds `i32::MAX`. To make this safer, consider clamping to `i32::MAX`, using `i32::try_from`, or using a wider integer type if the schema supports it.
</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 modules/exploit-intelligence/src/runner/polling.rs
self.ui_url(),
c,
);
if s.total_components.is_none() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

good to remove

tracing::warn!(job_id = %job_id, retry_count = job.retry_count, "failing job: max retries exhausted on claim");
self.ei_service
.update_job_failed(job_id, "max retries exhausted", &tx)
.update_job_failed(job_id, "max retries exhausted", None, &tx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe consider adding a code comment somewhere near here ...

//  total_components is None here because failure occurs before
// (or independently of) component creation. If components already exist
// in DB (e.g. retry-exhausted during SPDX polling)  stored
// total_components will remain NULL. 

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

On Phils recommendation, I'll follow up on non-functional changes like this in a separate PR so we don't have to wait the multiple hours for CI to finish before we can get this merged


// Check whether any component actually has a finding.
let components = ei_service.fetch_components(job_id, db).await?;
let total = Some(components.len() as i32);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

its unlikely any EI job would have 2b+ components ... though maybe add a source comment to inform future human/agents we ok with this

@rh-jfuller rh-jfuller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@Strum355
Strum355 added this pull request to the merge queue Aug 12, 2026
Merged via the queue into guacsec:main with commit 155a5c9 Aug 12, 2026
11 checks passed
@Strum355
Strum355 deleted the null-total-components branch August 12, 2026 11:58
@github-project-automation github-project-automation Bot moved this to Done in Trustify Aug 12, 2026
@trustify-ci-bot

Copy link
Copy Markdown

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants