Skip to content

fix(pd): keep RaftEngineReadIndexTest stubs clear of the alive-peers refresher - #3385

Open
Mihir-Rabari wants to merge 2 commits into
apache:masterfrom
Mihir-Rabari:fix/raft-readindex-stubbing-race
Open

Mihir-Rabari wants to merge 2 commits into
apache:masterfrom
Mihir-Rabari:fix/raft-readindex-stubbing-race

Conversation

@Mihir-Rabari

@Mihir-Rabari Mihir-Rabari commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #3384

RaftEngineReadIndexTest swaps a mock into the RaftEngine singleton and re-stubs
readIndex on it inside its tests. Two background callers can reach that mock
from other threads, and a call arriving while a
doAnswer(...).when(mock).readIndex(...) is in progress is what Mockito reports
as an UnfinishedStubbingException:

  • the pd-raft-alive-peers refresher, which calls listAlivePeers() on the
    singleton's node whenever the state machine reports a leader;
  • the TaskScheduleService pollers that PDCoreTestBase starts and never stops:
    shutdownService() tore down the raft engine only, so each of the eight core
    suite classes that run before RaftEngineReadIndexTest leaked a pool that
    keeps polling RaftEngine.isLeader() every second.

The first commit keeps the refresher away from the mock: setUp announces no
leader term before installing the mock, and the test that needs a term
announces it only after its re-stubbing has completed. The refresher itself is
untouched — it still reads the node and the term exactly as before whenever
the singleton leads.

The second commit closes the poller leak pointed out in review:
PDCoreTestBase.shutdownService() now stops the TaskScheduleService it started,
before the engine it polls, the same order ShutdownHook.closeOwners() uses,
and RaftEngineReadIndexTest gains a test that fails while any leaked poller
reaches the node mock. On the first commit alone that test goes red within
0.2 s (NeverWantedButInvoked: node.isLeader(true), from RaftEngine.isLeader);
with the leak closed the suite runs clean.

Verification: mvn test -pl hugegraph-pd/hg-pd-test -P pd-core-test twice on
the branch head (Tests run: 191, Failures: 0, Errors: 0 both runs),
editorconfig:check and the apache-rat license check clean.

RaftEngineReadIndexTest swaps a mock into the RaftEngine singleton and re-stubs readIndex on it inside its tests. When an earlier suite test leaves the singleton started and leading, the pd-raft-alive-peers refresher calls listAlivePeers() on that same mock whenever the state machine reports a leader, and a call from that thread while a doAnswer(...).when(mock).readIndex(...) is in progress is what Mockito reports as an UnfinishedStubbingException, so the suite fails now and then.

Announce no leader term in setUp, before the mock is installed, so the refresher leaves the mock alone, and announce a term in testReadIndexInTermKeepsTheTermTakenBeforeTheWait only after its re-stubbing has completed. The refresher itself is untouched: it still reads the node and the term exactly as before whenever the singleton leads.

Fixes apache#3384
@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 55.94%. Comparing base (c2ec570) to head (20a44c3).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3385      +/-   ##
============================================
+ Coverage     55.93%   55.94%   +0.01%     
- Complexity     2277     2309      +32     
============================================
  Files           774      776       +2     
  Lines         68534    68865     +331     
  Branches       9333     9414      +81     
============================================
+ Hits          38334    38527     +193     
- Misses        26634    26746     +112     
- Partials       3566     3592      +26     
Flag Coverage Δ *Carryforward flag
commons 96.37% <ø> (ø) Carriedforward from af1b483
hstore 35.99% <ø> (+0.19%) ⬆️ Carriedforward from af1b483
pd 49.41% <ø> (+0.23%) ⬆️
server_hbase 41.84% <ø> (-0.06%) ⬇️ Carriedforward from af1b483
server_memory 54.29% <ø> (-0.04%) ⬇️ Carriedforward from af1b483
server_rocksdb 42.29% <ø> (-0.06%) ⬇️ Carriedforward from af1b483
store 31.93% <ø> (ø)

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 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.

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

Blocking: yes. Summary: The change keeps the alive-peers refresher away from the mock, but other threads still call it, so #3384 can still happen: the TaskScheduleService executors that PDCoreTestBase leaves running call isLeader(true) on the installed mock while leaderTerm is -1. Evidence: RaftEngine.isLeader(), TaskScheduleService.init() and PDCoreTestBase.shutdownService() at af1b483. I also ran mvn test -pl hugegraph-pd/hg-pd-test -P pd-core-test on this head with a probe added just before this class. The mock got 42 isLeader(true) calls from 8 pools in 5 s. A loop of the answerShutdown stubbing against a thread calling RaftEngine.getInstance().isLeader() failed with UnfinishedStubbingException after 13 re-stubs, and the same loop without that thread stayed clean for 2,000,000.

// unfinished stubbing. Keep the machine not-leader while the mock is
// installed: the refresher then leaves the mock alone, and the tests below
// announce a term only once every stubbing has completed.
this.leaderTerm.set(-1L);

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.

Important: leaderTerm at -1 keeps the refresher off this mock, but other threads still call it. RaftEngine.isLeader() calls raftNode.isLeader(true) without reading leaderTerm. PDCoreTestBase.initService() starts a TaskScheduleService for each of the 8 suite classes before this one, and shutdownService() never stops it, so their 1 s clearTTLData task (if (isLeader())) keeps calling isLeader(true) on this mock. A call from another thread between doAnswer(...).when(this.mockNode) and .readIndex(...) takes the pending answer, which gives the #3384 UnfinishedStubbingException. A probe placed just before this class in PDCoreSuiteTest saw 42 such calls in 5 s from 8 executor pools.

Please stub readIndex only once, in setUp before the mock is installed, with an answer that delegates to a volatile Answer<Void> field. The tests that now call doAnswer/doThrow(...).when(this.mockNode) would assign that field instead. The order of the leaderTerm writes and the setUp comment then no longer matter.

PDCoreTestBase.shutdownService() tears down the raft engine but not the TaskScheduleService it started, so every core suite class leaves a pool behind that keeps polling RaftEngine.isLeader() every second. The polls land on the node mock RaftEngineReadIndexTest installs, and one arriving from that thread while a test re-stubs the mock is again an UnfinishedStubbingException, so guarding the refresher alone does not close the flake.

Stop the pollers before the engine, the same order the server's shutdown hook closes them in, and add a test to RaftEngineReadIndexTest that fails while any such poller is left running.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants