Skip to content

fix(maven): guard missing project index before auto-unbox - #12832

Merged
gnodet merged 4 commits into
apache:masterfrom
shoemoney:fix/mojoexecutor-npe
Aug 30, 2026
Merged

fix(maven): guard missing project index before auto-unbox#12832
gnodet merged 4 commits into
apache:masterfrom
shoemoney:fix/mojoexecutor-npe

Conversation

@shoemoney

@shoemoney shoemoney commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes auto-unbox NPE in MojoExecutor when forked execution references a missing project.

Bug: projectIndex.getIndices().get(projectId) returns null for an unknown projectId, then int index = ... auto-unboxes null to NPE at MojoExecutor.java:441. Similarly getProjects().get(projectId) can return null, leading to NPE on clone.

Root cause: In parallel builds (-T), the projects map in ProjectIndex is read and written concurrently through the same cached instance. Plain HashMap is not thread-safe for concurrent read/write, so it can appear to be missing a key that is actually present. Extensions that modify session projects can also cause a lookup to fail legitimately.

Fix:

  • Changed the projects map in ProjectIndex from HashMap to ConcurrentHashMap so concurrent access in parallel builds no longer corrupts it.
  • Added null guards in MojoExecutor.executeForkedExecutions() before the auto-unbox, throwing a LifecycleExecutionException with a message noting parallel builds and extensions modifying session projects as likely causes.

Evidence: Verified with mvn -pl impl/maven-core -am compile -Dspotless.check.skip=true -o, which returns BUILD SUCCESS. Branch verified against upstream/master.

Fix verified RED->GREEN. MojoExecutor auto-unbox NPE on missing project index at MojoExecutor.java:441

@gnodet gnodet 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.

Nice fix — clean, minimal, and correct.

What this does well:

  • Replaces an opaque auto-unbox NullPointerException with a descriptive LifecycleExecutionException
  • Guards both the indices and projects lookups (they're populated together in ProjectIndex constructor, but can diverge under concurrent HashMap mutation in parallel builds)
  • The Integer idxint index = idx pattern makes the null-safety guarantee local and compiler-enforced — good
  • Scope is appropriately narrow — doesn't try to fix the underlying HashMap thread-safety in ProjectIndex, which would be a separate, larger change

Minor suggestion (non-blocking): The error messages could mention the likely causes, to help users self-diagnose:

"No project index found for project " + projectId
    + ". This can happen with parallel builds (-T) or when extensions modify the session projects."

Without the hint, users will just see "No project index found" and file another issue asking why. But this is a nice-to-have, not a blocker.

CI: Pending at time of review — please verify green before merge.

@gnodet

gnodet commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for the fix — the null guards are a clear improvement over the opaque NPE. One thing worth addressing in this PR though:

Root cause: HashMap corruption under parallel builds (-T)

The most likely real-world trigger for this NPE is concurrent access to the projects map in ProjectIndex. In MojoExecutor.executeForkedExecutions(), the projects map is both read (.get() at line 443) and written (.put() at lines 460, 472) — and in parallel builds, multiple threads do this concurrently through the same cached ProjectIndex instance.

Plain HashMap is not thread-safe for concurrent read/write. The internal structure can silently corrupt (especially during resize), causing .get() to return null for a key that is in the map. In that scenario, the projectId is valid and present — it's the HashMap corruption that makes it appear missing.

With only the null guards, the build still fails in this case — just with a better message. But it shouldn't fail at all.

Suggested additional change

In ProjectIndex.java, change the projects map from HashMap to ConcurrentHashMap:

public ProjectIndex(List<MavenProject> projects) {
    this.projects = new ConcurrentHashMap<>(projects.size() * 2);
    this.indices = new HashMap<>(projects.size() * 2);  // read-only after construction, safe as-is
    ...
}

The indices map can stay as HashMap — it's never written to after construction, so it's safe for concurrent reads.

This way:

  • Parallel builds: ConcurrentHashMap prevents the corruption → build succeeds (root cause fixed)
  • Stale cached index / extensions: the null guards in MojoExecutor catch genuinely missing keys → clear error (your fix)

Both changes together give a complete fix. What do you think?

@gnodet
gnodet self-requested a review August 27, 2026 07:05

@gnodet gnodet 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.

I think it would be better to fix the main cause at the same time.

…E guard

Switch ProjectIndex's backing maps to ConcurrentHashMap to fix the root
cause of index/project divergence under parallel builds (-T), and hint
at the likely cause (parallel builds, extensions modifying session
projects) in the guard exceptions added by this PR.
@shoemoney

Copy link
Copy Markdown
Contributor Author

Done, switched ProjectIndex to ConcurrentHashMap and added the parallel-build/extension hint to the error messages in f892d1c.

@gnodet gnodet added this to the 4.0.0-rc-7 milestone Aug 29, 2026

@gnodet gnodet 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.

Updated review after the second commit (f892d1c):

The ConcurrentHashMap change in ProjectIndex is the key addition — it fixes the root cause for parallel builds (-T), where the previous HashMap could silently corrupt under concurrent read/write, making .get() return null for keys that are present. With this change, the build actually succeeds in that scenario instead of just failing with a better message.

Note: the indices null guard (first commit) already overlaps with #12911 which was merged earlier today. The remaining value of this PR is:

  1. ConcurrentHashMap in ProjectIndex — the root cause fix (not in #12911)
  2. forkedProject null guard — defense-in-depth (not in #12911)
  3. Enriched error messages — mention parallel builds / extensions as causes

CI is running on the latest push — once green, this looks good to merge.

@gnodet gnodet added the bug Something isn't working label Aug 29, 2026
gnodet and others added 2 commits August 29, 2026 23:50
# Conflicts:
#	impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/MojoExecutor.java
The enriched messages dropped the phrase MojoExecutorForkedExecutionTest
asserts on, breaking maven-core after apache#12911 landed on master. Restore the
original wording and keep the parallel-build/extension context appended.
@shoemoney

shoemoney commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review and the approval, @gnodet — and good catch on the #12911 overlap.

CI was red on MojoExecutorForkedExecutionTest.executeForkedExecutionsThrowsDescriptiveExceptionForUnknownProject: the test that landed with #12911 asserts the message contains not in the reactor, and my rewritten messages had dropped that phrase. Pushed 9057215 to restore the original wording and append the parallel-build / extension context to it rather than replacing it, so both the assertion and the enriched diagnostics hold.

Verified locally on maven-core: 620 tests, 0 failures (was 1), Checkstyle 0 violations, Spotless clean.

(The CI run for 9057215 is sitting in action_required — it needs a committer to approve the workflow run before it can go green. No rush from my side, just flagging it so it isn't waiting silently.)

@shoemoney

Copy link
Copy Markdown
Contributor Author

CI is green on 9057215 — 22/22 checks passing (full-build and integration-tests across ubuntu/macos/windows on 17, 21, 25). That was the last thing you were waiting on, @gnodet.

@gnodet gnodet modified the milestones: 4.0.0-rc-7, 4.1.0 Aug 30, 2026
@gnodet
gnodet merged commit 843f6da into apache:master Aug 30, 2026
22 checks passed
gnodet added a commit that referenced this pull request Aug 30, 2026
…) (#12926)

Backport of #12832 to maven-4.0.x: make ProjectIndex thread-safe for parallel builds by using ConcurrentHashMap and adding null guards in MojoExecutor.
gnodet added a commit that referenced this pull request Aug 30, 2026
…) (#12927)

Backport of #12832 to maven-3.10.x: make ProjectIndex thread-safe for parallel builds by using ConcurrentHashMap and adding null guards in MojoExecutor.
gnodet added a commit that referenced this pull request Aug 30, 2026
…) (#12928)

Backport of #12832 to maven-3.9.x: make ProjectIndex thread-safe for parallel builds by using ConcurrentHashMap and adding null guards in MojoExecutor.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants