Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions modules/analysis/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,17 @@ pub struct CacheStatusEntry {
pub struct BaseSummary {
pub sbom_id: String,
pub node_id: String,
pub purl: Vec<Purl>,
pub cpe: Vec<Cpe>,
pub purl: Arc<[Purl]>,
pub cpe: Arc<[Cpe]>,
pub name: String,
pub version: String,
pub published: String,
pub document_id: String,
pub product_name: String,
pub product_version: String,
#[schema(value_type = String)]
pub document_id: Arc<String>,
#[schema(value_type = String)]
pub product_name: Arc<String>,
#[schema(value_type = String)]
pub product_version: Arc<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, ToSchema)]
Expand Down
65 changes: 25 additions & 40 deletions modules/analysis/src/model/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,33 +103,30 @@ fn published_to_string(value: OffsetDateTime) -> String {
value.format(&format).unwrap_or_else(|_| value.to_string())
}

static EMPTY_ARC_STRING: std::sync::LazyLock<Arc<String>> =
std::sync::LazyLock::new(|| Arc::new(String::new()));

fn arc_string_or_default(opt: &Option<Arc<String>>) -> Arc<String> {
opt.as_ref()
.cloned()
.unwrap_or_else(|| EMPTY_ARC_STRING.clone())
}

impl From<&Node> for BaseSummary {
fn from(value: &Node) -> Self {
match value {
Node::Package(value) => BaseSummary::from(value),
_ => Self {
sbom_id: value.sbom_id.to_string(),
node_id: value.node_id.to_string(),
purl: vec![],
cpe: vec![],
name: value.name.to_string(),
version: "".to_string(),
node_id: value.node_id.clone(),
purl: Arc::from([]),
cpe: Arc::from([]),
name: value.name.clone(),
version: String::new(),
published: published_to_string(value.published),
document_id: value
.document_id
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
product_name: value
.product_name
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
product_version: value
.product_version
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
document_id: arc_string_or_default(&value.document_id),
product_name: arc_string_or_default(&value.product_name),
product_version: arc_string_or_default(&value.product_version),
},
}
}
Expand All @@ -139,27 +136,15 @@ impl From<&PackageNode> for BaseSummary {
fn from(value: &PackageNode) -> Self {
Self {
sbom_id: value.sbom_id.to_string(),
node_id: value.node_id.to_string(),
purl: value.purl.to_vec(),
cpe: value.cpe.to_vec(),
name: value.name.to_string(),
version: value.version.to_string(),
node_id: value.node_id.clone(),
purl: Arc::clone(&value.purl),
cpe: Arc::clone(&value.cpe),
name: value.name.clone(),
version: value.version.clone(),
published: published_to_string(value.published),
document_id: value
.document_id
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
product_name: value
.product_name
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
product_version: value
.product_version
.as_ref()
.map(|s| s.to_string())
.unwrap_or_default(),
document_id: arc_string_or_default(&value.document_id),
product_name: arc_string_or_default(&value.product_name),
product_version: arc_string_or_default(&value.product_version),
}
}
}
19 changes: 10 additions & 9 deletions modules/analysis/src/model/roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,21 @@ impl<'a> RootTraces for &'a Vec<Node> {
mod test {
use super::*;
use crate::model::BaseSummary;
use std::sync::Arc;
use trustify_entity::relationship::Relationship;

fn base(node_id: &str) -> BaseSummary {
BaseSummary {
sbom_id: "".to_string(),
sbom_id: String::new(),
node_id: node_id.to_string(),
purl: vec![],
cpe: vec![],
name: "".to_string(),
version: "".to_string(),
published: "".to_string(),
document_id: "".to_string(),
product_name: "".to_string(),
product_version: "".to_string(),
purl: Arc::from([]),
cpe: Arc::from([]),
name: String::new(),
version: String::new(),
published: String::new(),
document_id: Arc::new(String::new()),
product_name: Arc::new(String::new()),
product_version: Arc::new(String::new()),
}
}

Expand Down
52 changes: 51 additions & 1 deletion modules/analysis/src/service/collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,48 @@ type AncestorResult = Arc<Vec<ResolvedSbom>>;
type AncestorCell = Arc<tokio::sync::OnceCell<AncestorResult>>;
type AncestorMap = HashMap<(Uuid, String), AncestorCell>;

type ExternalSbomResult = Arc<Option<ResolvedSbom>>;
type ExternalSbomCell = Arc<tokio::sync::OnceCell<ExternalSbomResult>>;
type ExternalSbomMap = HashMap<String, ExternalSbomCell>;

/// Request-scoped cache for [`resolve_external_sbom`] results.
///
/// During descendant traversal, every `ExternalNode` triggers a
/// `resolve_external_sbom` DB query. The same external reference
/// can appear across multiple SBOMs in the result set, causing
/// redundant queries. This cache deduplicates them.
///
/// Concurrent callers for the same `node_id` are coalesced via
/// `OnceCell` — only the first executes the query.
#[derive(Default, Clone)]
pub struct ExternalSbomCache {
cache: Arc<Mutex<ExternalSbomMap>>,
}

impl ExternalSbomCache {
/// Resolve an external SBOM reference, returning a cached result
/// when available.
async fn resolve<C: ConnectionTrait>(
&self,
node_id: &str,
connection: &C,
) -> Result<Option<ResolvedSbom>, Error> {
let cell = {
let mut map = self.cache.lock();
map.entry(node_id.to_string()).or_default().clone()
};

let result = cell
.get_or_try_init(|| async {
let resolved = resolve_external_sbom(node_id, connection).await?;
Ok::<_, Error>(Arc::new(resolved))
})
.await?;

Ok((**result).clone())
}
}

/// Coalescing barrier for [`AncestorCache::prefetch`].
///
/// When multiple concurrent tasks call `prefetch` for the same
Expand Down Expand Up @@ -205,6 +247,7 @@ pub struct Collector<'a, C: ConnectionTrait> {
discovered: DiscoveredTracker,
loaded_graphs: Arc<Mutex<HashMap<Uuid, Arc<PackageGraph>>>>,
ancestor_cache: AncestorCache,
external_sbom_cache: ExternalSbomCache,
relationships: &'a HashSet<Relationship>,
connection: &'a C,
concurrency: usize,
Expand All @@ -224,6 +267,7 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> {
discovered: self.discovered.clone(),
loaded_graphs: self.loaded_graphs.clone(),
ancestor_cache: self.ancestor_cache.clone(),
external_sbom_cache: self.external_sbom_cache.clone(),
relationships: self.relationships,
connection: self.connection,
concurrency: self.concurrency,
Expand All @@ -246,6 +290,7 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> {
concurrency: usize,
loader: &'a GraphLoader,
ancestor_cache: AncestorCache,
external_sbom_cache: ExternalSbomCache,
) -> Self {
Self {
graph_cache,
Expand All @@ -258,6 +303,7 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> {
discovered: Default::default(),
loaded_graphs: Default::default(),
ancestor_cache,
external_sbom_cache,
relationships,
connection,
concurrency,
Expand Down Expand Up @@ -293,6 +339,7 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> {
discovered: self.discovered.clone(),
loaded_graphs: self.loaded_graphs.clone(),
ancestor_cache: self.ancestor_cache.clone(),
external_sbom_cache: self.external_sbom_cache.clone(),
relationships: self.relationships,
connection: self.connection,
concurrency: self.concurrency,
Expand Down Expand Up @@ -365,7 +412,10 @@ impl<'a, C: ConnectionTrait> Collector<'a, C> {
sbom_id: external_sbom_id,
node_id: external_node_id,
..
}) = resolve_external_sbom(&external_node.node_id, self.connection).await?
}) = self
.external_sbom_cache
.resolve(&external_node.node_id, self.connection)
.await?
else {
return Ok((
None,
Expand Down
13 changes: 4 additions & 9 deletions modules/analysis/src/service/load/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,10 @@ impl Context {
}

pub fn intern(&mut self, s: String) -> Arc<String> {
if self.strings.contains_key(&s)
&& let Some(s) = self.strings.get(&s)
{
return s.clone();
}

let a = Arc::new(s.clone());
self.strings.insert(s, a.clone());
a
self.strings
.entry(s)
.or_insert_with_key(|k| Arc::new(k.clone()))
.clone()
}
}

Expand Down
Loading
Loading