You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@celeborn.apache.org by za...@apache.org on 2026/10/10 08:33:30 UTC
(celeborn) branch branch-0.7 updated: [CELEBORN-2453] Master should fail fast when the Ratis server is closed unexpectedly
This is an automated email from the ASF dual-hosted git repository.
zaynt4606 pushed a commit to branch branch-0.7
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/branch-0.7 by this push:
new 493252df0d [CELEBORN-2453] Master should fail fast when the Ratis server is closed unexpectedly
493252df0d is described below
commit 493252df0d9fd8c806d20d177226fe2530035bb4
Author: yew1eb <ye...@gmail.com>
AuthorDate: Sat Oct 10 16:32:34 2026 +0800
[CELEBORN-2453] Master should fail fast when the Ratis server is closed unexpectedly
### What changes were proposed in this pull request?
When the embedded Ratis server is closed unexpectedly (e.g. by `JvmPauseMonitor` after a JVM pause longer than `raft.server.close.threshold`), the master process keeps running as a zombie: its RPC/HTTP ports stay alive but it has silently left the Raft group.
This PR makes the master fail fast in this situation:
1. Add a new config `celeborn.master.ha.ratis.unexpected.close.action` (`exit` / `none`, default `exit`). When the periodic role check detects the local `RaftServer` has reached a terminal lifecycle state (`CLOSED` / `EXCEPTION`), the master logs the root cause and exits, so the HA group keeps a healthy quorum and the node can be restarted by external supervision (systemd/k8s).
2. `HARaftServer.updateServerRole()` short-circuits when the Raft server is in a terminal state (`CLOSED` / `EXCEPTION`), avoiding repeated ERROR log flooding from subsequent role checks and metrics scrapes, while leaving transitional states (`STARTING` / `CLOSING`) on the existing code path.
### Why are the changes needed?
In our production, one master of a 3-node HA cluster got stuck for ~78s by `jmap -F -histo` (non-GC pause). Ratis `JvmPauseMonitor` then closed the local RaftServer:
<img width="2314" height="1124" alt="image" src="https://github.com/user-attachments/assets/1ed38a5b-e707-4a80-83fe-4aa9f802700d" />
After that the master process was still alive but out of the Raft group, role became `UNRECOGNIZED`, and every metrics scrape / role check printed a full ERROR stack (`ServerNotReadyException`, `SegmentedRaftLog is expected to be opened but it is CLOSED`). Nothing recovers the node except a manual restart — failing fast makes the failure explicit and lets supervision restart it.
### Does this PR resolve a correctness bug?
- [ ] Yes
### Does this PR introduce _any_ user-facing change?
- [ ] Yes
### How was this patch tested?
- New unit test `RatisMasterStatusSystemSuiteJ#testUnexpectedRaftServerClose`.
- Existing master module tests pass (`./build/mvn test -pl master`).
This patch had conflicts when merged, resolved by
Committer: zhengtao <sh...@alibaba-inc.com>
Closes #3838 from yew1eb/CELEBORN-2453.
Authored-by: yew1eb <ye...@gmail.com>
Signed-off-by: zhengtao <sh...@alibaba-inc.com>
AI-Contributed/Feature: 0/76
AI-Contributed/UT: 0/58
(cherry picked from commit abdfd05d804e8a128682d1d87c540501ed5f8be2)
Signed-off-by: zhengtao <sh...@alibaba-inc.com>
---
.../org/apache/celeborn/common/CelebornConf.scala | 14 +++++
docs/configuration/ha.md | 1 +
.../deploy/master/clustermeta/ha/HARaftServer.java | 61 ++++++++++++++++++++++
.../ha/RatisMasterStatusSystemSuiteJ.java | 58 ++++++++++++++++++++
4 files changed, 134 insertions(+)
diff --git a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
index 71f9b3d01d..1fadc77f23 100644
--- a/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
+++ b/common/src/main/scala/org/apache/celeborn/common/CelebornConf.scala
@@ -811,6 +811,7 @@ class CelebornConf(loadDefaults: Boolean) extends Cloneable with Logging with Se
get(HA_MASTER_RATIS_NOTIFICATION_NO_LEADER_TIMEOUT)
def haMasterRatisRpcSlownessTimeout: Long = get(HA_MASTER_RATIS_RPC_SLOWNESS_TIMEOUT)
def haMasterRatisRoleCheckInterval: Long = get(HA_MASTER_RATIS_ROLE_CHECK_INTERVAL)
+ def haMasterRatisUnexpectedCloseAction: String = get(HA_MASTER_RATIS_UNEXPECTED_CLOSE_ACTION)
def haMasterRatisSnapshotAutoTriggerEnabled: Boolean =
get(HA_MASTER_RATIS_SNAPSHOT_AUTO_TRIGGER_ENABLED)
def haMasterRatisSnapshotAutoTriggerThreshold: Long =
@@ -2976,6 +2977,19 @@ object CelebornConf extends Logging {
.timeConf(TimeUnit.SECONDS)
.createWithDefaultString("60s")
+ val HA_MASTER_RATIS_UNEXPECTED_CLOSE_ACTION: ConfigEntry[String] =
+ buildConf("celeborn.master.ha.ratis.unexpected.close.action")
+ .categories("ha")
+ .version("0.7.1")
+ .doc("Action when the local raft server is closed unexpectedly, i.e. not via the " +
+ "master stop flow, e.g. closed by the Ratis JvmPauseMonitor after a JVM pause " +
+ "longer than raft.server.close.threshold. 'exit' logs a fatal message and exits " +
+ "the master process so that supervisors (systemd/K8s) can restart it and the " +
+ "node rejoins the raft group; 'none' only logs.")
+ .stringConf
+ .checkValues(Set("exit", "none"))
+ .createWithDefault("exit")
+
val HA_MASTER_RATIS_SERVER_RETRY_CACHE_EXPIRY_TIME: ConfigEntry[Long] =
buildConf("celeborn.master.ha.ratis.raft.server.retrycache.expirytime")
.withAlternative("celeborn.ha.master.ratis.raft.server.retrycache.expirytime")
diff --git a/docs/configuration/ha.md b/docs/configuration/ha.md
index a249752fd6..0e2857951d 100644
--- a/docs/configuration/ha.md
+++ b/docs/configuration/ha.md
@@ -30,4 +30,5 @@ license: |
| celeborn.master.ha.ratis.raft.server.close.threshold | 60s | false | Threshold for the Ratis JvmPauseMonitor to close the local raft server after a JVM/host pause, e.g. long GC, VM live migration or jmap -F. Maps to Ratis raft.server.close.threshold. Once exceeded, the master leaves the raft group until the process is restarted. Takes precedence over the celeborn.ratis.raft.server.close.threshold passthrough when both are set. | 0.7.1 | |
| celeborn.master.ha.ratis.raft.server.storage.dir | /tmp/ratis | false | Root storage directory to hold RaftServer data. | 0.3.0 | celeborn.ha.master.ratis.raft.server.storage.dir |
| celeborn.master.ha.ratis.raft.server.storage.startup.option | RECOVER | false | Startup option of RaftServer storage. Available options: RECOVER, FORMAT. | 0.5.0 | |
+| celeborn.master.ha.ratis.unexpected.close.action | exit | false | Action when the local raft server is closed unexpectedly, i.e. not via the master stop flow, e.g. closed by the Ratis JvmPauseMonitor after a JVM pause longer than raft.server.close.threshold. 'exit' logs a fatal message and exits the master process so that supervisors (systemd/K8s) can restart it and the node rejoins the raft group; 'none' only logs. | 0.7.1 | |
<!--end-include-->
diff --git a/master/src/main/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/HARaftServer.java b/master/src/main/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/HARaftServer.java
index 597d6bf46a..c36aa321de 100644
--- a/master/src/main/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/HARaftServer.java
+++ b/master/src/main/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/HARaftServer.java
@@ -101,9 +101,16 @@ public class HARaftServer {
private final CelebornConf conf;
private final AtomicBoolean stopped = new AtomicBoolean(false);
+ private final AtomicBoolean unexpectedCloseHandled = new AtomicBoolean(false);
private long workerTimeoutDeadline;
private long appTimeoutDeadline;
+ private Runnable unexpectedCloseHandler =
+ () -> {
+ LOG.error("Exiting master process because the raft server was closed unexpectedly.");
+ System.exit(1);
+ };
+
/**
* Returns a Master Ratis server.
*
@@ -158,6 +165,7 @@ public class HARaftServer {
// Run a scheduler to check and update the server role on the leader periodically
this.scheduledRoleChecker.scheduleWithFixedDelay(
() -> {
+ checkRaftServerState();
// Run this check only on the leader OM
if (cachedPeerRole.isPresent()
&& cachedPeerRole.get() == RaftProtos.RaftPeerRole.LEADER) {
@@ -311,6 +319,46 @@ public class HARaftServer {
}
}
+ @VisibleForTesting
+ void checkRaftServerState() {
+ if (stopped.get() || unexpectedCloseHandled.get()) {
+ return;
+ }
+ handleRaftServerState(server.getLifeCycleState());
+ }
+
+ @VisibleForTesting
+ void handleRaftServerState(LifeCycle.State state) {
+ // Recheck stopped after the lifecycle state was observed: a concurrent stop() may
+ // have set stopped and closed the server in between. Treating an intentional
+ // shutdown as an unexpected close would exit the process as a failure and trigger
+ // a spurious restart under a restart-on-failure supervisor.
+ if (stopped.get() || unexpectedCloseHandled.get()) {
+ return;
+ }
+ if ((state == LifeCycle.State.CLOSED || state == LifeCycle.State.EXCEPTION)
+ && unexpectedCloseHandled.compareAndSet(false, true)) {
+ LOG.error(
+ "Raft server {} was closed unexpectedly (state {}), e.g. by the Ratis "
+ + "JvmPauseMonitor after a long JVM pause. Action: {}.",
+ server.getId(),
+ state,
+ conf.haMasterRatisUnexpectedCloseAction());
+ onUnexpectedRaftServerClose();
+ }
+ }
+
+ void onUnexpectedRaftServerClose() {
+ if ("exit".equals(conf.haMasterRatisUnexpectedCloseAction())) {
+ unexpectedCloseHandler.run();
+ }
+ }
+
+ @VisibleForTesting
+ void setUnexpectedCloseHandler(Runnable handler) {
+ this.unexpectedCloseHandler = handler;
+ }
+
private RaftProperties newRaftProperties(CelebornConf conf, RpcType rpc) {
final RaftProperties properties = new RaftProperties();
// Set RPC type
@@ -557,6 +605,19 @@ public class HARaftServer {
* Get the group info (peer role and leader peer id) from Ratis server and update the server role.
*/
public void updateServerRole() {
+ LifeCycle.State state = server.getLifeCycleState();
+ if (state != LifeCycle.State.RUNNING) {
+ // Only log on role change to avoid flooding logs on every metrics scrape.
+ if (cachedPeerRole.isPresent()) {
+ LOG.warn(
+ "Raft server {} is not RUNNING (state {}). Setting cached role to UNRECOGNIZED "
+ + "and resetting leader info.",
+ server.getId(),
+ state);
+ }
+ setServerRole(null, null, null);
+ return;
+ }
try {
GroupInfoReply groupInfo = getGroupInfo();
RaftProtos.RoleInfoProto roleInfoProto = groupInfo.getRoleInfoProto();
diff --git a/master/src/test/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/RatisMasterStatusSystemSuiteJ.java b/master/src/test/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/RatisMasterStatusSystemSuiteJ.java
index 6038ee4e15..7ca8a83cf9 100644
--- a/master/src/test/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/RatisMasterStatusSystemSuiteJ.java
+++ b/master/src/test/java/org/apache/celeborn/service/deploy/master/clustermeta/ha/RatisMasterStatusSystemSuiteJ.java
@@ -25,11 +25,13 @@ import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import scala.Tuple2;
import com.google.common.collect.Lists;
+import org.apache.ratis.util.LifeCycle;
import org.junit.*;
import org.mockito.Mockito;
@@ -2145,4 +2147,60 @@ public class RatisMasterStatusSystemSuiteJ {
assertEquals(statusSystem.applicationInfos.get(appId).userIdentifier(), userIdentifier);
assertEquals(statusSystem.applicationInfos.get(appId).extraInfo(), extraInfo);
}
+
+ @Test
+ public void testUnexpectedRaftServerClose() throws Exception {
+ HARaftServer nonLeader =
+ !RATISSERVER1.isLeader()
+ ? RATISSERVER1
+ : (!RATISSERVER2.isLeader() ? RATISSERVER2 : RATISSERVER3);
+ try {
+ AtomicBoolean handlerInvoked = new AtomicBoolean(false);
+ nonLeader.setUnexpectedCloseHandler(() -> handlerInvoked.set(true));
+
+ // Simulate the Ratis JvmPauseMonitor closing the local raft server.
+ nonLeader.getServer().close();
+ Assert.assertEquals(LifeCycle.State.CLOSED, nonLeader.getServerState());
+
+ long deadline = System.currentTimeMillis() + 30_000;
+ while (!handlerInvoked.get() && System.currentTimeMillis() < deadline) {
+ Thread.sleep(200L);
+ }
+ Assert.assertTrue(handlerInvoked.get());
+
+ nonLeader.updateServerRole();
+ Assert.assertFalse(nonLeader.getCachedLeaderPeerRpcEndpoint().isPresent());
+ } finally {
+ init();
+ }
+ }
+
+ @Test
+ public void testConcurrentStopNotTreatedAsUnexpectedClose() throws Exception {
+ HARaftServer nonLeader =
+ !RATISSERVER1.isLeader()
+ ? RATISSERVER1
+ : (!RATISSERVER2.isLeader() ? RATISSERVER2 : RATISSERVER3);
+ try {
+ AtomicBoolean handlerInvoked = new AtomicBoolean(false);
+ nonLeader.setUnexpectedCloseHandler(() -> handlerInvoked.set(true));
+
+ // Normal shutdown: sets stopped and closes the raft server.
+ nonLeader.stop();
+ Assert.assertEquals(LifeCycle.State.CLOSED, nonLeader.getServerState());
+
+ // Replay the interleaving described in the review: the checker passed the entry
+ // check while stopped was still false and read CLOSED; stop() then completed
+ // before the checker acted on the state. The checker must stay silent.
+ nonLeader.handleRaftServerState(LifeCycle.State.CLOSED);
+ Assert.assertFalse(
+ "A normal stop must not be treated as an unexpected close", handlerInvoked.get());
+
+ // The full check path must also stay silent after a normal stop.
+ nonLeader.checkRaftServerState();
+ Assert.assertFalse(handlerInvoked.get());
+ } finally {
+ init();
+ }
+ }
}