Skip to content

fix: dispatch metrics update for collection parent on project delete#6067

Draft
valentijnscholten wants to merge 5 commits into
DependencyTrack:masterfrom
valentijnscholten:fix/collection-project-metrics-on-child-delete
Draft

fix: dispatch metrics update for collection parent on project delete#6067
valentijnscholten wants to merge 5 commits into
DependencyTrack:masterfrom
valentijnscholten:fix/collection-project-metrics-on-child-delete

Conversation

@valentijnscholten
Copy link
Copy Markdown
Contributor

@valentijnscholten valentijnscholten commented Apr 24, 2026

Problem

In my DT instance I am observing that collection projects without any children retain their metrics. Although I will probably update my integration scripts to remove these empty projects, I also think DT should handle this correctly w.r.t. the metrics.

When child projects are deleted, any parent collection project retains stale metrics indefinitely — showing non-zero vulnerability counts, risk scores, etc. even after the collection is empty.

Root cause

Two separate bugs, both in ProjectQueryManager:

1. recursivelyDelete (single delete path)

recursivelyDelete captures project.getParent() before the cascade, but collectionLogic is @Persistent without defaultFetchGroup = "true" — it is not eagerly loaded. By the time parent.getCollectionLogic() is checked at the end of the method, the PM state has been heavily modified by cascading pm.deletePersistentAll calls (all joining the same outer transaction from ProjectResource). The lazy load silently fails, getCollectionLogic() normalises null to NONE, the condition is false, and ProjectMetricsUpdateEvent is never dispatched for the parent collection.

2. deleteProjectsByUUIDs (bulk delete path)

Uses bulk SQL to remove projects but never dispatches ProjectMetricsUpdateEvent for parent collection projects at all.

Fix

recursivelyDelete: Reload the parent with the METRICS_UPDATE fetch group (which explicitly includes collectionLogic) before the deletion cascade begins, so the field is in memory and does not depend on lazy loading through a modified PM state.

deleteProjectsByUUIDs: Before the SQL deletion runs, collect the UUIDs of any parent collection projects whose children are being deleted (excluding parents that are themselves in the delete set). After the transaction completes, dispatch ProjectMetricsUpdateEvent for each of them.

Why it does not self-correct

One might expect the periodic portfolio metrics recalculation to fix this automatically, but it explicitly skips collection projects (PortfolioMetricsUpdateTask:179):

query.setFilter("active && (collectionLogic == null || collectionLogic == 'NONE')");

This exclusion is intentional — collection project numbers are derived from their children, not counted independently at portfolio level, to avoid double-counting. The side effect is that a collection project's own ProjectMetrics record is only updated when a ProjectMetricsUpdateEvent is explicitly dispatched for it.

This exclusion is doubtful as I think collection projects also need to have their metrics updated periodically. But it's not straightforward to fix as projects can be nested multiple levels deep.

Tests

Three new tests added to ProjectQueryManagerTest, following the existing MockedStatic<Event> + ArgumentCaptor pattern:

  • testRecursivelyDeleteDispatchesMetricsUpdateForCollectionParent — verifies that single-deleting a child dispatches a metrics update event for the collection parent.
  • testDeleteProjectsByUUIDsDispatchesMetricsUpdateForCollectionParent — verifies that bulk-deleting a child dispatches a metrics update event for the collection parent.
  • testDeleteProjectsByUUIDsDoesNotDispatchMetricsUpdateWhenParentAlsoDeleted — verifies that no spurious event is dispatched when the parent is also included in the same delete batch.

Disclaimer

Claude Code was used in the process of creating this PR.

@owasp-dt-bot
Copy link
Copy Markdown

owasp-dt-bot commented Apr 24, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 24, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@valentijnscholten valentijnscholten force-pushed the fix/collection-project-metrics-on-child-delete branch from 84b6b6a to 6f830f0 Compare April 24, 2026 20:53
@nscuro nscuro requested a review from Copilot April 24, 2026 21:10
@nscuro nscuro added the defect Something isn't working label Apr 24, 2026
@valentijnscholten
Copy link
Copy Markdown
Contributor Author

Looks like that when a project is updated, for example when the parent is changes, the metrics aren't always updated as the parent is not always fetched from the DB resutling in an "always NONE" value for collection logic. Committed a possible fix for that.

@valentijnscholten valentijnscholten force-pushed the fix/collection-project-metrics-on-child-delete branch from 874fd69 to 732e3fd Compare April 24, 2026 21:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes stale metrics on parent collection projects when deleting child projects via the bulk delete endpoint by dispatching ProjectMetricsUpdateEvent for impacted parents after the SQL deletion completes.

Changes:

  • Collect UUIDs of parent collection projects affected by bulk project deletion (excluding parents included in the delete set).
  • Dispatch ProjectMetricsUpdateEvent for those parents after the bulk deletion transaction.
  • Add unit tests to verify event dispatch behavior for parent collections (and non-dispatch when parent is also deleted).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java Collects impacted parent collection UUIDs and dispatches metrics update events post bulk-delete transaction.
src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java Adds tests covering metrics update event dispatch for collection parents during bulk delete.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/test/java/org/dependencytrack/persistence/ProjectQueryManagerTest.java Outdated
@valentijnscholten valentijnscholten changed the title fix: dispatch metrics update for collection parent on bulk project delete fix: dispatch metrics update for collection parent on project delete Apr 24, 2026
@valentijnscholten valentijnscholten force-pushed the fix/collection-project-metrics-on-child-delete branch from fce1cdb to fef6694 Compare April 24, 2026 21:28
valentijnscholten and others added 5 commits April 24, 2026 23:28
…lete

When child projects are deleted via deleteProjectsByUUIDs, their parent
collection project's metrics are now recalculated. Previously the stale
aggregated metrics (vuln counts, risk scores, etc.) would persist on the
collection project indefinitely after all children were removed.

The single-delete path (recursivelyDelete) already handled this correctly;
this aligns the bulk-delete path with the same behaviour.

Signed-off-by: Valentijn Scholten <valentijnscholten@gmail.com>
Signed-off-by: Valentijn Scholten <valentijnscholten@gmail.com>
…gerTest.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: valentijnscholten <valentijnscholten@gmail.com>
Signed-off-by: Valentijn Scholten <valentijnscholten@gmail.com>
recursivelyDelete captures project.getParent() before any deletions, but
collectionLogic is not in the default fetch group. After numerous
pm.deletePersistentAll calls modify PM state, the subsequent lazy load of
collectionLogic on the parent silently returns null (normalised to NONE),
so the ProjectMetricsUpdateEvent for the collection parent is never
dispatched and stale metrics persist indefinitely.

Fix by reloading the parent with the METRICS_UPDATE fetch group (which
includes collectionLogic) before the deletion cascade begins.

Signed-off-by: Valentijn Scholten <valentijnscholten@gmail.com>
…n bulk delete

Signed-off-by: Valentijn Scholten <valentijnscholten@gmail.com>
@valentijnscholten valentijnscholten force-pushed the fix/collection-project-metrics-on-child-delete branch from fef6694 to 2e3bbdf Compare April 24, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

defect Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants