Skip to content

Fix #12912: assign FastTerminal before starting the build thread - #12961

Merged
gnodet merged 1 commit into
apache:maven-4.0.xfrom
gnodet:fix/12912-race
Aug 31, 2026
Merged

Fix #12912: assign FastTerminal before starting the build thread#12961
gnodet merged 1 commit into
apache:maven-4.0.xfrom
gnodet:fix/12912-race

Conversation

@gnodet

@gnodet gnodet commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a race condition introduced in #12921: the FastTerminal constructor started its build thread before returning, so MessageUtils.terminal was still null when the build thread ran the builder callback — a race between the constructor returning and the thread scheduling. This caused a NullPointerException in CI (example):

NullPointerException: Cannot invoke "org.jline.terminal.Terminal.getSize()" because "t" is null

The fix splits construction from start: MessageUtils now assigns the field before calling FastTerminal.start(), and Thread.start() provides the happens-before edge that makes the assignment visible to the build thread without additional synchronization.

  • FastTerminal: extract start() method from constructor, document the publish-before-start contract
  • MessageUtils.systemInstall(): assign terminal field, then call start()
  • New test terminalAssignmentIsVisibleFromBuilderCallback that asserts MessageUtils.getTerminal() is non-null from the builder callback

Test plan

  • FastTerminalReentrancyTest — all 5 tests pass (4 existing + 1 new)
  • Full maven-jline module test suite passes
  • CI validates on Java 17/21/25

🤖 Generated with Claude Code

The FastTerminal constructor started the build thread before
returning, so MessageUtils.terminal was still null when the build
thread ran the builder callback — a race between the constructor
returning and the thread scheduling.

Split construction from start: MessageUtils now assigns the field
before calling FastTerminal.start(), and Thread.start() provides
the happens-before edge that makes the assignment visible to the
build thread.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet added this to the 4.0.0-rc-7 milestone Aug 30, 2026
@gnodet gnodet added the bug Something isn't working label Aug 30, 2026

@gnodet gnodet left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct fix for a real race condition. The constructor-starts-thread-before-field-assignment pattern was a textbook publish-before-init bug, and separating construction from start() is the right approach.

Highlights:

  • The JMM analysis is sound: Thread.start() establishes a happens-before edge (JLS 17.4.4), so the prior assignment to MessageUtils.terminal is guaranteed visible to the build thread without volatile.
  • The test terminalAssignmentIsVisibleFromBuilderCallback correctly exercises the invariant.
  • The diff is identical to the forward port #12962 targeting master — the two PRs are in sync.

Minor suggestion (non-blocking):

  • The new start() method is public, but its only caller is MessageUtils.systemInstall() in the same package. Package-private visibility would narrow the API surface. (LookupInvoker only calls getTerminal(), not start().)

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

Claude Code on behalf of Guillaume Nodet

@gnodet
gnodet merged commit 04668c6 into apache:maven-4.0.x Aug 31, 2026
22 checks passed
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.

1 participant