Skip to content

Conversation

@mrizzi
Copy link
Contributor

@mrizzi mrizzi commented Nov 4, 2025

Problem

The original PURL ingestion code used a SELECT-then-INSERT pattern that caused race conditions during parallel ingestion.
When multiple concurrent tasks try to ingest the same PURL:

  1. Time T0: Transaction A checks if PURL exists → Not found
  2. Time T1: Transaction B checks if PURL exists → Not found
  3. Time T2: Transaction A attempts INSERT → Success
  4. Time T3: Transaction B attempts INSERT → UNIQUE CONSTRAINT VIOLATION

This happened because:

  • The SELECT and INSERT are not atomic
  • Multiple transactions can pass the SELECT check before any INSERT completes

The advisories_parallel test was failing with duplicate key value violates unique constraint "package_pkey".

Solution

Introduced PurlStatusCreator for safe and performant purl_status ingestion.

Bonus fix: the assert_all_ok function respects the input ordering using num as first parameter in the asserts reflecting num being the first parameter in the function's signature.

Related to https://issues.redhat.com/browse/TC-3152

Summary by Sourcery

Improve package ingestion reliability and performance by switching to atomic upserts, introducing batch insertion utilities for base PURLs and package statuses, refactoring loaders to use these utilities, and adding a parallel ingestion test to ensure thread-safety.

New Features:

  • Introduce PurlStatusCreator for efficient batch insertion of package status entries
  • Add batch_create_base_purls helper to atomically upsert base PURLs in bulk
  • Provide build_package_status and build_package_status_versions functions to generate status entries

Bug Fixes:

  • Fix race condition in PURL ingestion by replacing SELECT-then-INSERT with atomic INSERT … ON CONFLICT DO NOTHING

Enhancements:

  • Refactor OSV and CVE loaders to collect status entries and base PURLs before batch creation
  • Optimize package ingestion happy path for single-query insertion when PURLs don’t exist

Tests:

  • Add parallel advisories ingestion test to verify thread-safety
  • Correct assert_all_ok to respect expected ordering of results

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 4, 2025

Reviewer's Guide

This PR eliminates race conditions in PURL ingestion by replacing non-atomic SELECT-then-INSERT sequences with PostgreSQL’s atomic INSERT…ON CONFLICT DO NOTHING pattern (with fallback SELECT), introduces batch and deduplication helpers for base PURLs and PURL status entries, refactors status-entry builders into pure functions, and updates tests to validate parallel ingestion and correct helper ordering.

Sequence diagram for atomic PURL ingestion with ON CONFLICT DO NOTHING

sequenceDiagram
    participant Loader
    participant DB
    Loader->>DB: INSERT base_purl ... ON CONFLICT DO NOTHING RETURNING row
    alt Insert successful
        DB-->>Loader: Return new row
    else Conflict (row exists)
        DB-->>Loader: Return NULL
        Loader->>DB: SELECT base_purl WHERE id = uuid
        DB-->>Loader: Return existing row
    end
Loading

ER diagram for atomic base_purl and purl_status ingestion

erDiagram
    BASE_PURL {
        id UUID PK
        type TEXT
        namespace TEXT
        name TEXT
    }
    VERSION_RANGE {
        id UUID PK
    }
    PURL_STATUS {
        id UUID PK
        advisory_id UUID FK
        vulnerability_id TEXT
        status_id UUID FK
        base_purl_id UUID FK
        version_range_id UUID FK
        context_cpe_id UUID FK
    }
    BASE_PURL ||--o{ PURL_STATUS : "base_purl_id"
    VERSION_RANGE ||--o{ PURL_STATUS : "version_range_id"
    PURL_STATUS }o--|| BASE_PURL : "base_purl_id"
    PURL_STATUS }o--|| VERSION_RANGE : "version_range_id"
Loading

Class diagram for new and refactored PURL status ingestion helpers

classDiagram
    class PurlStatusEntry {
        +advisory_id: Uuid
        +vulnerability_id: String
        +purl: Purl
        +status: String
        +version_info: VersionInfo
        +context_cpe: Option<Cpe>
    }
    class PurlStatusCreator {
        -entries: Vec<PurlStatusEntry>
        +new()
        +add(entry: &PurlStatusEntry)
        +create(connection: &C)
    }
    class PurlCreator {
        +create(db)
    }
    class Purl {
        +package_uuid()
        +clone()
    }
    PurlStatusCreator "1" *-- "*" PurlStatusEntry
    PurlStatusEntry "1" *-- "1" Purl
    PurlStatusEntry "1" *-- "1" VersionInfo
    PurlStatusEntry "1" *-- "0..1" Cpe
    PurlCreator "1" *-- "*" Purl
Loading

Class diagram for refactored status entry builder functions

classDiagram
    class AdvisoryVulnerabilityContext {
        +advisory: Advisory
        +advisory_vulnerability: AdvisoryVulnerability
    }
    class Purl {
    }
    class Range {
        +events: Vec<Event>
    }
    class PurlStatusEntry {
    }
    class VersionScheme {
    }
    class VersionSpec {
    }
    class VersionInfo {
    }
    AdvisoryVulnerabilityContext "1" *-- "1" Advisory
    AdvisoryVulnerabilityContext "1" *-- "1" AdvisoryVulnerability
    Range "1" *-- "*" Event
    PurlStatusEntry "1" *-- "1" Purl
    PurlStatusEntry "1" *-- "1" VersionInfo
    VersionInfo "1" *-- "1" VersionScheme
    VersionInfo "1" *-- "1" VersionSpec
Loading

File-Level Changes

Change Details Files
Replace non-atomic SELECT-then-INSERT patterns with atomic INSERT…ON CONFLICT DO NOTHING and fallback lookup
  • Introduced batch_create_base_purls helper for batching base_purl inserts with ON CONFLICT DO NOTHING
  • Updated PurlCreator.create to invoke batch_create_base_purls instead of manual package inserts
  • Modified OSV and CVE loaders to call batch_create_base_purls before creating status entries
modules/ingestor/src/graph/purl/mod.rs
modules/ingestor/src/graph/purl/creator.rs
modules/ingestor/src/service/advisory/osv/loader.rs
modules/ingestor/src/service/advisory/cve/loader.rs
Introduce PurlStatusCreator for batch, deduplicated insertion of PURL status records
  • Added status_creator.rs with PurlStatusCreator and PurlStatusEntry to collect and clone status data
  • Implemented batch lookup of status slugs, deduplication of version_ranges and purl_status entries
  • Replaced per-call ingest_package_status in loaders with PurlStatusCreator.add and .create
modules/ingestor/src/graph/purl/status_creator.rs
modules/ingestor/src/service/advisory/osv/loader.rs
modules/ingestor/src/service/advisory/cve/loader.rs
Refactor package-status assembly into pure builder functions
  • Extracted build_package_status, build_package_status_versions, and build_range_from functions returning PurlStatusEntry lists
  • Removed now-redundant async helpers ingest_exact, create_package_status, ingest_range_from
  • Streamlined OSV loader loops to accumulate entries via the new builders
modules/ingestor/src/service/advisory/osv/loader.rs
Fix test helper ordering and add parallel ingestion test
  • Corrected assert_all_ok to compare expected num against result.len() and ok count
  • Added advisories_parallel multi-threaded test to validate thread-safe advisory ingestion
  • Ensured assert_all_ok logs and handles errors in parallel scenarios
modules/ingestor/tests/parallel.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

Copy link
Contributor

@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 there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `modules/ingestor/tests/parallel.rs:301-302` </location>
<code_context>
+    // progress ingestion tasks
+    let result = futures::future::join_all(tasks).await;
+
+    // now test
+    assert_all_ok(data.len(), result);
+
+    // done
</code_context>

<issue_to_address>
**question (testing):** Question: Is there coverage for concurrent ingestion of identical advisories?

If not, please add a test for concurrent ingestion of the same advisory to verify the race condition fix and ON CONFLICT handling.
</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.

// now test
assert_all_ok(data.len(), result);
Copy link
Contributor

Choose a reason for hiding this comment

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

question (testing): Question: Is there coverage for concurrent ingestion of identical advisories?

If not, please add a test for concurrent ingestion of the same advisory to verify the race condition fix and ON CONFLICT handling.

@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

❌ Patch coverage is 88.31169% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.24%. Comparing base (80947f5) to head (8d1df91).

Files with missing lines Patch % Lines
...odules/ingestor/src/service/advisory/osv/loader.rs 85.98% 13 Missing and 2 partials ⚠️
modules/ingestor/src/graph/purl/status_creator.rs 91.66% 2 Missing ⚠️
modules/ingestor/src/graph/purl/mod.rs 95.65% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2080      +/-   ##
==========================================
+ Coverage   68.16%   68.24%   +0.08%     
==========================================
  Files         368      369       +1     
  Lines       20682    20730      +48     
  Branches    20682    20730      +48     
==========================================
+ Hits        14097    14147      +50     
+ Misses       5747     5745       -2     
  Partials      838      838              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@ctron ctron left a comment

Choose a reason for hiding this comment

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

For creating packages, we do have the PackageCreator. Which should actually take care of all of this. Bundling the logic. Using the same approach.

I'd hate to see another place, with a different style implementing this. So unless it's impossible to re-use that logic, or not able to extend, we should re-use it.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 5, 2025

For creating packages, we do have the PackageCreator. Which should actually take care of all of this. Bundling the logic. Using the same approach.

I'd hate to see another place, with a different style implementing this. So unless it's impossible to re-use that logic, or not able to extend, we should re-use it.

Not so sure but I can not see any usage of PackageCreator in the old code changed in this PR. Was there a reason for this inconsistency?

@ctron
Copy link
Contributor

ctron commented Nov 5, 2025

For creating packages, we do have the PackageCreator. Which should actually take care of all of this. Bundling the logic. Using the same approach.
I'd hate to see another place, with a different style implementing this. So unless it's impossible to re-use that logic, or not able to extend, we should re-use it.

Not so sure but I can not see any usage of PackageCreator in the old code changed in this PR. Was there a reason for this inconsistency?

Most likely because most of the graph creation code wasn't used anymore. There is some inconsistency with all the *Context stuff. That's partly removed, but some is still there.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 5, 2025

I think the situation is more nuanced than the comments suggest.

First of all, to my understanding, here is not a matter of adopting PackageCreator but rather PurlCreator considering the advisory ingestion creates PURLs rather than SBOM packages within the DB.
To confirm, the issue reproducer, i.e. advisories_parallel test, invokes the OsvLoader.load which already uses the PurlCreator batch approach.

The usage of the old (decomissioned?) *Context approach has been introduced in #2022 (~1 month ago) with the usage of ingest_exact one-by-one function (rather than batch) which caused OsvLoader.load to have this mixed pattern between PurlCreator and *Context approaches which should have been avoided.

I've anyway looking into PurlCreator implementation and I found that PurlCreator also has queries that would generate duplicate key issues when concurrent ingestions happen.
So I searched for a test covering the parallel usage of PurlCreator and found purl_creator which still has ignore even if PurlCreator is currently adopted.
Running it, as expected, I got the expected duplicate key value violates unique constraint "by_pid_v" error.
This is a bug affecting PurlCreator, totally disjoined from this PR, that I'll fix in a dedicated PR immediately (EDIT: #2082).

Now, going back to this PR, are we saying that the long-term solution should be to eliminate this mixed PurlCreator + *Context pattern entirely (fixing the approach from PR #2022) in favor of a pure "Creator-only" approach?

@ctron
Copy link
Contributor

ctron commented Nov 5, 2025

Now, going back to this PR, are we saying that the long-term solution should be to eliminate this mixed PurlCreator + *Context pattern entirely (fixing the approach from PR #2022) in favor of a pure "Creator-only" approach?

Yes, I think that's the way forward.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 5, 2025

Now, going back to this PR, are we saying that the long-term solution should be to eliminate this mixed PurlCreator + *Context pattern entirely (fixing the approach from PR #2022) in favor of a pure "Creator-only" approach?

Yes, I think that's the way forward.

Can this PR leverage the same exception applied to #2022 that allowed the *Context pattern to be applied?

@ctron
Copy link
Contributor

ctron commented Nov 5, 2025

Now, going back to this PR, are we saying that the long-term solution should be to eliminate this mixed PurlCreator + *Context pattern entirely (fixing the approach from PR #2022) in favor of a pure "Creator-only" approach?

Yes, I think that's the way forward.

Can this PR leverage the same exception applied to #2022 that allowed the *Context pattern to be applied?

Sorry, I don't understand.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 5, 2025

Can this PR leverage the same exception applied to #2022 that allowed the *Context pattern to be applied?

Sorry, I don't understand.

  1. fix(ingestor): also consider direct versions #2022 has been merged a month ago introducing the *Context pattern with no objections on using it
  2. this PR is fixing a bug in that code, not introducing the *Context pattern itself, just fixing it

What's wrong with this PR and its usage of the *Context pattern that wasn't wrong with #2022, considering both are "operating" in the *Context pattern realm and while the latter been merged the former can't?

@ctron
Copy link
Contributor

ctron commented Nov 6, 2025

So unless there's a very good reason not to use PurlCreator, I don't see a reason duplicate code. Especially it if deviates that much from the pattern we already have.

Taking a closer look at this. There seems to be exactly one call, outside of a lot of test code, which uses that code path. It looking at it, the whole logic is useless there anyway, because there is no need to return the ID, as is pre-known.

So why do we keep all of this alive? What is the benefit of keeping quite a good portion of code alive just for a single case which can be replaced with code that is known to work in all other places?

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 6, 2025

What is the benefit of keeping quite a good portion of code alive just for a single case which can be replaced with code that is known to work in all other places?

I don't know the benefit but it's the same benefit we had for merging one month ago #2022: which was the benefit?

@ctron
Copy link
Contributor

ctron commented Nov 6, 2025

The benefit was to solve https://issues.redhat.com/browse/TC-2983.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 6, 2025

The benefit was to solve https://issues.redhat.com/browse/TC-2983.

Great, this PR is solving an issue as well, https://issues.redhat.com/browse/TC-3152.
So, why didn't #2022 solve the bug using the proper *Creator approach but, instead, used a solution based on the *Context approach?

@ctron
Copy link
Contributor

ctron commented Nov 6, 2025

#2022 re-uses existing code. It's a 3 line change, calling into existing code.

This PR creates some new. Rewriting stuff that is currently broken. I am against creating new code or rewriting existing for stuff that already exists.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 6, 2025

#2022 re-uses existing code. It's a 3 line change, calling into existing code.

This PR creates some new. Rewriting stuff that is currently broken. I am against creating new code or rewriting existing for stuff that already exists.

Sorry, I don't understand how this explains why not having used the proper *Creator approach in #2022.
Why has the existing code using the *Context approach being re-used instead of the one with the *Creator approach in #2022?
Once we know this together with the right *Creator approach that should have been applied in #2022, then the same can be applied here.
Is it PurlCreator? Then why PurlCreator has not been applied in #2022?
Is it another Creator? Which one? And why it has not been applied to in #2022?

@ctron
Copy link
Contributor

ctron commented Nov 6, 2025

ingest_exact is existing code. Neither SQL nor PurlCreator is directly involved.

It is in this PR. Back to my question: what are reasons not to use PurlCreator?

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 6, 2025

ingest_exact is existing code. Neither SQL nor PurlCreator is directly involved.

It is in this PR. Back to my question: what are reasons not to use PurlCreator?

This PR is not using PurlCreator because #2022 added the invocation to ingest_exact that is not using PurlCreator.
If #2022 would have used PurlCreator then this PR would be using it as well.

I'm just asking for you to share how PurlCreator should have been used in #2022 instead of ingest_exact.

@ctron
Copy link
Contributor

ctron commented Nov 6, 2025

I don't see why 2022 should its using an existing function.

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 6, 2025

I don't see why 2022 should its using an existing function.

And this PR is fixing the very same existing function 😉
But your comment made me wonder if we have the same understanding of the deep effects of having used ingest_exact in #2022.

Before #2022, OsvLoader::load was running the PURL creations using the PurlCreator with a batch approach.

With #2022, the ingest_exact addition removed all the advantages of the the PurlCreator batch approach because it runs a one-by-one insert for PURLs before purl_creator.create() is invoked.

My questions have all been meant to understand the reason for having introduced ingest_exact with its one-by-one *Context based approach instead of fixing https://issues.redhat.com/browse/TC-2983 with a *Creator based approach.
Then, given the PurlCreator is already used in OsvLoader::load, I've asked if any other Creator should have been used.

@ctron
Copy link
Contributor

ctron commented Nov 6, 2025

That's the change of #2022:

image

@mrizzi mrizzi added the backport release/0.4.z Backport (0.4.z) label Nov 7, 2025
Signed-off-by: mrizzi <mrizzi@redhat.com>
Assisted-by: Claude Code
…3152)

Signed-off-by: mrizzi <mrizzi@redhat.com>
Assisted-by: Claude Code
@mrizzi mrizzi force-pushed the fix-concurrent-ingestion branch from 724557b to 8d1df91 Compare November 7, 2025 18:01
@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 7, 2025

@sourcery-ai summary

@mrizzi
Copy link
Contributor Author

mrizzi commented Nov 7, 2025

@sourcery-ai review

@mrizzi mrizzi requested a review from ctron November 7, 2025 18:02
Copy link
Contributor

@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 there - I've reviewed your changes - here's some feedback:

  • Consider moving the pure helper functions (build_package_status, build_range_from, build_package_status_versions) into their own module to declutter the loader and improve separation of concerns.
  • The PurlStatusCreator collects all entries into a single Vec before inserting—if you expect very large ingestions, you may want to chunk or stream entries into the creator to bound memory usage.
  • There’s a lot of boilerplate around constructing PurlStatusEntry in different branches—consider a small builder or helper to DRY up the repeated entry‐construction logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider moving the pure helper functions (build_package_status, build_range_from, build_package_status_versions) into their own module to declutter the loader and improve separation of concerns.
- The PurlStatusCreator collects all entries into a single Vec before inserting—if you expect very large ingestions, you may want to chunk or stream entries into the creator to bound memory usage.
- There’s a lot of boilerplate around constructing PurlStatusEntry in different branches—consider a small builder or helper to DRY up the repeated entry‐construction logic.

## Individual Comments

### Comment 1
<location> `modules/ingestor/src/graph/purl/status_creator.rs:79` </location>
<code_context>
+
+        for entry in self.entries {
+            // Validate status exists
+            let status_id = *status_map
+                .get(&entry.status)
+                .ok_or_else(|| Error::InvalidStatus(entry.status.clone()))?;
</code_context>

<issue_to_address>
**suggestion:** Error variant Error::InvalidStatus may benefit from more context.

Consider adding relevant fields (e.g., advisory_id or purl) to Error::InvalidStatus to make debugging easier.

Suggested implementation:

```rust
                .get(&entry.status)
                .ok_or_else(|| Error::InvalidStatus {
                    status: entry.status.clone(),
                    advisory_id: entry.advisory_id.clone(),
                    purl: entry.purl.clone(),
                })?;

```

You will also need to:
1. Update the definition of `Error::InvalidStatus` in your error enum (likely in a file like `modules/ingestor/src/error.rs` or similar) to be a struct variant with fields: `status: String`, `advisory_id: AdvisoryIdType`, `purl: PurlType` (replace types as appropriate).
2. Update any `Display`, `Debug`, or error handling implementations to format and use these new fields.
3. Update any other code that constructs or matches on `Error::InvalidStatus` to use the new struct variant.
</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.


for entry in self.entries {
// Validate status exists
let status_id = *status_map
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: Error variant Error::InvalidStatus may benefit from more context.

Consider adding relevant fields (e.g., advisory_id or purl) to Error::InvalidStatus to make debugging easier.

Suggested implementation:

                .get(&entry.status)
                .ok_or_else(|| Error::InvalidStatus {
                    status: entry.status.clone(),
                    advisory_id: entry.advisory_id.clone(),
                    purl: entry.purl.clone(),
                })?;

You will also need to:

  1. Update the definition of Error::InvalidStatus in your error enum (likely in a file like modules/ingestor/src/error.rs or similar) to be a struct variant with fields: status: String, advisory_id: AdvisoryIdType, purl: PurlType (replace types as appropriate).
  2. Update any Display, Debug, or error handling implementations to format and use these new fields.
  3. Update any other code that constructs or matches on Error::InvalidStatus to use the new struct variant.

@mrizzi mrizzi requested a review from dejanb November 7, 2025 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport release/0.4.z Backport (0.4.z)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants