From af1b48398c336e6437d458b73458496eb9ab4408 Mon Sep 17 00:00:00 2001 From: Rabari Mihir <169349389+Mihir-Rabari@users.noreply.github.com> Date: Tue, 20 Oct 2026 19:57:40 +0000 Subject: [PATCH 1/2] fix(pd): keep RaftEngineReadIndexTest stubs clear of the refresher 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 #3384 --- .../pd/raft/RaftEngineReadIndexTest.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java index 85d250bc45..1d7e56855c 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java @@ -70,6 +70,17 @@ public class RaftEngineReadIndexTest { public void setUp() { RaftEngine engine = RaftEngine.getInstance(); this.originalRaftNode = engine.getRaftNode(); + RaftStateMachine machine = Whitebox.getInternalState(engine, "stateMachine"); + this.leaderTerm = Whitebox.getInternalState(machine, "leaderTerm"); + this.originalLeaderTerm = this.leaderTerm.get(); + // The engine's alive-peers refresher calls listAlivePeers() on whatever node + // the singleton holds whenever this machine reports a leader, and a call from + // that thread while a stubbing runs here is what Mockito reports as an + // 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); + this.mockNode = mock(Node.class); doAnswer(invocation -> { ReadIndexClosure closure = invocation.getArgument(1); @@ -87,9 +98,6 @@ public void setUp() { PDConfig.Raft config = new PDConfig().new Raft(); config.setRpcTimeout(1000); Whitebox.setInternalState(engine, "config", config); - RaftStateMachine machine = Whitebox.getInternalState(engine, "stateMachine"); - this.leaderTerm = Whitebox.getInternalState(machine, "leaderTerm"); - this.originalLeaderTerm = this.leaderTerm.get(); } @After @@ -206,8 +214,9 @@ public void testReadIndexInTermReturnsTheLeaderTerm() throws PDException { @Test public void testReadIndexInTermKeepsTheTermTakenBeforeTheWait() throws PDException { // The node loses the leadership of term 5 and wins term 7 during the wait. The - // caller must get 5: its write is then rejected, as the wait began in another term - this.leaderTerm.set(5L); + // caller must get 5: its write is then rejected, as the wait began in another + // term. Stub before announcing term 5: the refresher leaves the mock alone + // only while the machine reports no leader doAnswer(invocation -> { this.leaderTerm.set(7L); ReadIndexClosure closure = invocation.getArgument(1); @@ -215,6 +224,7 @@ public void testReadIndexInTermKeepsTheTermTakenBeforeTheWait() throws PDExcepti closure.run(Status.OK()); return null; }).when(this.mockNode).readIndex(any(byte[].class), any(ReadIndexClosure.class)); + this.leaderTerm.set(5L); Assert.assertEquals(5L, RaftEngine.getInstance().waitReadIndexInTerm()); } From 20a44c30d68301dc33d06abb2b9e7d848bccebf4 Mon Sep 17 00:00:00 2001 From: Rabari Mihir <169349389+Mihir-Rabari@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:16:26 +0000 Subject: [PATCH 2/2] fix(pd): stop the task pollers core tests leak into later raft tests 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. --- .../apache/hugegraph/pd/core/PDCoreTestBase.java | 7 +++++++ .../pd/raft/RaftEngineReadIndexTest.java | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreTestBase.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreTestBase.java index 9e7b03d98e..daa5fcedb3 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreTestBase.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/core/PDCoreTestBase.java @@ -182,6 +182,13 @@ public void onStoreRaftChanged(Metapb.Store store) { @AfterClass public static void shutdownService() { + // Stop the task pollers before the engine they poll, as the server's + // shutdown hook does. A poller left running keeps calling isLeader() on + // whatever node the singleton holds, which breaks tests that mock the + // node afterwards. + if (taskService != null) { + taskService.shutDown(); + } var instance = RaftEngine.getInstance(); if (instance != null) { instance.shutDown(); diff --git a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java index 1d7e56855c..e3cba398d1 100644 --- a/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java +++ b/hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/raft/RaftEngineReadIndexTest.java @@ -40,6 +40,7 @@ import com.alipay.sofa.jraft.error.RaftError; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -402,4 +403,18 @@ public void testMissingRaftNodeFailsAsNotLeader() { Assert.assertEquals(Pdpb.ErrorType.NOT_LEADER_VALUE, e.getErrorCode()); } + + @Test + public void testNoLeakedTaskPollerTouchesTheNodeMock() throws InterruptedException { + // A suite class that fails to stop its TaskScheduleService leaves a pool + // that polls RaftEngine.isLeader() every second, and a call from that + // thread while another test stubs this mock surfaces as Mockito's + // unfinished stubbing. No such call may reach the mock here. + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (System.nanoTime() < deadline) { + verify(this.mockNode, never()).isLeader(anyBoolean()); + Thread.sleep(20); + } + verify(this.mockNode, never()).isLeader(anyBoolean()); + } }