This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 7674d494cc5 branch-4.1: [fix](transaction) Fail fast when a backend
restarts during insert #65525 (#65566)
7674d494cc5 is described below
commit 7674d494cc5b9e0491821a66e3587ef4db7e7c82
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Jul 14 17:45:41 2026 +0800
branch-4.1: [fix](transaction) Fail fast when a backend restarts during
insert #65525 (#65566)
Cherry-picked from #65525
Co-authored-by: hui lai <[email protected]>
---
be/src/exec/sink/writer/vtablet_writer.cpp | 5 +-
be/src/exec/sink/writer/vtablet_writer_v2.cpp | 5 +-
.../org/apache/doris/qe/runtime/LoadProcessor.java | 22 ++--
.../qe/runtime/SingleFragmentPipelineTask.java | 19 ++-
.../qe/runtime/SingleFragmentPipelineTaskTest.java | 96 ++++++++++++++++
.../test_insert_fail_fast_after_be_restart.groovy | 128 +++++++++++++++++++++
6 files changed, 253 insertions(+), 22 deletions(-)
diff --git a/be/src/exec/sink/writer/vtablet_writer.cpp
b/be/src/exec/sink/writer/vtablet_writer.cpp
index 2ac24fbf402..2ba4d139016 100644
--- a/be/src/exec/sink/writer/vtablet_writer.cpp
+++ b/be/src/exec/sink/writer/vtablet_writer.cpp
@@ -2071,8 +2071,9 @@ Status VTabletWriter::close(Status exec_status) {
TEST_INJECTION_POINT("VOlapTableSink::close");
DBUG_EXECUTE_IF("VTabletWriter.close.sleep", {
- auto sleep_sec =
DebugPoints::instance()->get_debug_param_or_default<int32_t>(
- "VTabletWriter.close.sleep", "sleep_sec", 1);
+ auto sleep_sec = dp->param<int32_t>("sleep_sec", 1);
+ auto token = dp->param<std::string>("token", "");
+ LOG(INFO) << "hit debug point VTabletWriter.close.sleep, token=" <<
token;
std::this_thread::sleep_for(std::chrono::seconds(sleep_sec));
});
DBUG_EXECUTE_IF("VTabletWriter.close.close_status_not_ok",
diff --git a/be/src/exec/sink/writer/vtablet_writer_v2.cpp
b/be/src/exec/sink/writer/vtablet_writer_v2.cpp
index c4708a01164..ce78cb3b270 100644
--- a/be/src/exec/sink/writer/vtablet_writer_v2.cpp
+++ b/be/src/exec/sink/writer/vtablet_writer_v2.cpp
@@ -671,8 +671,9 @@ Status VTabletWriterV2::close(Status exec_status) {
}
DBUG_EXECUTE_IF("VTabletWriterV2.close.sleep", {
- auto sleep_sec =
DebugPoints::instance()->get_debug_param_or_default<int32_t>(
- "VTabletWriterV2.close.sleep", "sleep_sec", 1);
+ auto sleep_sec = dp->param<int32_t>("sleep_sec", 1);
+ auto token = dp->param<std::string>("token", "");
+ LOG(INFO) << "hit debug point VTabletWriterV2.close.sleep, token=" <<
token;
std::this_thread::sleep_for(std::chrono::seconds(sleep_sec));
});
DBUG_EXECUTE_IF("VTabletWriterV2.close.cancel",
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/LoadProcessor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/LoadProcessor.java
index 45995a7aad7..a605f3039aa 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/LoadProcessor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/LoadProcessor.java
@@ -30,7 +30,6 @@ import org.apache.doris.qe.CoordinatorContext;
import org.apache.doris.qe.LoadContext;
import org.apache.doris.thrift.TFragmentInstanceReport;
import org.apache.doris.thrift.TReportExecStatusParams;
-import org.apache.doris.thrift.TStatusCode;
import org.apache.doris.thrift.TUniqueId;
import com.google.common.collect.Lists;
@@ -53,7 +52,6 @@ public class LoadProcessor extends AbstractJobProcessor {
// key: fragmentId, value: backendId
private volatile Optional<MarkedCountDownLatch<Integer, Long>> latch;
private volatile Optional<MarkedCountDownLatch<Integer, Long>>
topFragmentLatch;
- private volatile List<SingleFragmentPipelineTask> topFragmentTasks;
public LoadProcessor(CoordinatorContext coordinatorContext, long jobId) {
super(coordinatorContext);
@@ -76,8 +74,6 @@ public class LoadProcessor extends AbstractJobProcessor {
String.valueOf(jobId), coordinatorContext.scanRangeNum.get()
);
- topFragmentTasks = Lists.newArrayList();
-
LOG.info("dispatch load job: {} to {}",
DebugUtil.printId(queryId),
coordinatorContext.backends.get().keySet()
);
@@ -105,8 +101,6 @@ public class LoadProcessor extends AbstractJobProcessor {
}
}
}
- this.topFragmentTasks = topFragmentTasks;
-
// only wait top fragments
MarkedCountDownLatch<Integer, Long> topFragmentLatch = new
MarkedCountDownLatch<>(topFragmentTasks.size());
for (SingleFragmentPipelineTask topFragmentTask : topFragmentTasks) {
@@ -255,16 +249,14 @@ public class LoadProcessor extends AbstractJobProcessor {
}
}
- /*
- * Check the state of backends in needCheckBackendExecStates.
- * return true if all of them are OK. Otherwise, return false.
- */
+ // Check backend health for every unfinished load fragment task.
private boolean checkHealthy() {
- for (SingleFragmentPipelineTask topFragmentTask : topFragmentTasks) {
- if (!topFragmentTask.isBackendHealthy(jobId)) {
- long backendId = topFragmentTask.getBackend().getId();
- Status unhealthyStatus = new Status(
- TStatusCode.INTERNAL_ERROR, "backend " + backendId + "
is down");
+ for (SingleFragmentPipelineTask fragmentTask :
backendFragmentTasks.get().values()) {
+ if (fragmentTask.isDone()) {
+ continue;
+ }
+ Status unhealthyStatus =
fragmentTask.getBackendHealthStatus(jobId);
+ if (!unhealthyStatus.ok()) {
coordinatorContext.updateStatusIfOk(unhealthyStatus);
return false;
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTask.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTask.java
index 1aa4c7f0478..c6110d6a35b 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTask.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTask.java
@@ -17,10 +17,12 @@
package org.apache.doris.qe.runtime;
+import org.apache.doris.common.Status;
import org.apache.doris.qe.QueryStatisticsItem.FragmentInstanceInfo;
import org.apache.doris.system.Backend;
import org.apache.doris.thrift.TNetworkAddress;
import org.apache.doris.thrift.TReportExecStatusParams;
+import org.apache.doris.thrift.TStatusCode;
import org.apache.doris.thrift.TUniqueId;
import com.google.common.collect.Lists;
@@ -38,6 +40,7 @@ public class SingleFragmentPipelineTask extends
LeafRuntimeTask {
private final Backend backend;
private final int fragmentId;
private final long lastMissingHeartbeatTime;
+ private final long backendProcessEpoch;
private final Set<TUniqueId> instanceIds;
// mutate states
@@ -48,6 +51,7 @@ public class SingleFragmentPipelineTask extends
LeafRuntimeTask {
this.fragmentId = fragmentId;
this.instanceIds = instanceIds;
this.lastMissingHeartbeatTime = backend.getLastMissingHeartbeatTime();
+ this.backendProcessEpoch = backend.getProcessEpoch();
}
// update profile.
@@ -62,12 +66,21 @@ public class SingleFragmentPipelineTask extends
LeafRuntimeTask {
return this.done.compareAndSet(false, true);
}
- public boolean isBackendHealthy(long jobId) {
+ public Status getBackendHealthStatus(long jobId) {
if (backend.getLastMissingHeartbeatTime() > lastMissingHeartbeatTime
&& !backend.isAlive()) {
LOG.warn("backend {} is down while joining the coordinator. job
id: {}", backend.getId(), jobId);
- return false;
+ return new Status(TStatusCode.INTERNAL_ERROR, "backend {} is
down", backend.getId());
+ }
+
+ long currentProcessEpoch = backend.getProcessEpoch();
+ if (backendProcessEpoch > 0 && currentProcessEpoch > 0 &&
backendProcessEpoch != currentProcessEpoch) {
+ Status unhealthyStatus = new Status(TStatusCode.INTERNAL_ERROR,
+ "backend {} process epoch changed from {} to {},
indicating that the backend restarted",
+ backend.getId(), backendProcessEpoch, currentProcessEpoch);
+ LOG.warn("{} while joining the coordinator. job id: {}",
unhealthyStatus.getErrorMsg(), jobId);
+ return unhealthyStatus;
}
- return true;
+ return Status.OK;
}
public boolean isDone() {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTaskTest.java
b/fe/fe-core/src/test/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTaskTest.java
new file mode 100644
index 00000000000..31becae01dc
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/qe/runtime/SingleFragmentPipelineTaskTest.java
@@ -0,0 +1,96 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.qe.runtime;
+
+import org.apache.doris.common.Status;
+import org.apache.doris.system.Backend;
+import org.apache.doris.thrift.TStatusCode;
+import org.apache.doris.thrift.TUniqueId;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+class SingleFragmentPipelineTaskTest {
+ @Test
+ void backendWithUnchangedProcessEpochIsHealthy() {
+ Backend backend = createBackend(100L);
+ SingleFragmentPipelineTask task = createTask(backend);
+
+ Assertions.assertTrue(task.getBackendHealthStatus(-1L).ok());
+ }
+
+ @Test
+ void backendRestartIsUnhealthyEvenWhenBackendIsAlive() {
+ Backend backend = createBackend(100L);
+ SingleFragmentPipelineTask task = createTask(backend);
+
+ backend.setLastStartTime(200L);
+
+ Status status = task.getBackendHealthStatus(-1L);
+ Assertions.assertEquals(TStatusCode.INTERNAL_ERROR,
status.getErrorCode());
+ Assertions.assertEquals(
+ "backend 1 process epoch changed from 100 to 200, indicating
that the backend restarted",
+ status.getErrorMsg());
+ }
+
+ @Test
+ void zeroCurrentProcessEpochIsIgnoredForCompatibility() {
+ Backend backend = createBackend(100L);
+ SingleFragmentPipelineTask task = createTask(backend);
+
+ backend.setLastStartTime(0L);
+
+ Assertions.assertTrue(task.getBackendHealthStatus(-1L).ok());
+ }
+
+ @Test
+ void zeroCapturedProcessEpochIsIgnoredForCompatibility() {
+ Backend backend = createBackend(0L);
+ SingleFragmentPipelineTask task = createTask(backend);
+
+ backend.setLastStartTime(100L);
+
+ Assertions.assertTrue(task.getBackendHealthStatus(-1L).ok());
+ }
+
+ @Test
+ void backendDownAfterMissingHeartbeatIsUnhealthy() {
+ Backend backend = createBackend(100L);
+ SingleFragmentPipelineTask task = createTask(backend);
+
+ backend.setLastMissingHeartbeatTime(1L);
+ backend.setAlive(false);
+
+ Status status = task.getBackendHealthStatus(-1L);
+ Assertions.assertEquals(TStatusCode.INTERNAL_ERROR,
status.getErrorCode());
+ Assertions.assertEquals("backend 1 is down", status.getErrorMsg());
+ }
+
+ private static Backend createBackend(long processEpoch) {
+ Backend backend = new Backend(1L, "127.0.0.1", 9050);
+ backend.setAlive(true);
+ backend.setLastStartTime(processEpoch);
+ return backend;
+ }
+
+ private static SingleFragmentPipelineTask createTask(Backend backend) {
+ return new SingleFragmentPipelineTask(backend, 0,
Collections.<TUniqueId>emptySet());
+ }
+}
diff --git
a/regression-test/suites/load_p0/insert/test_insert_fail_fast_after_be_restart.groovy
b/regression-test/suites/load_p0/insert/test_insert_fail_fast_after_be_restart.groovy
new file mode 100644
index 00000000000..e194ebe3188
--- /dev/null
+++
b/regression-test/suites/load_p0/insert/test_insert_fail_fast_after_be_restart.groovy
@@ -0,0 +1,128 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import org.apache.doris.regression.suite.ClusterOptions
+
+import java.util.concurrent.TimeUnit
+
+suite("test_insert_fail_fast_after_be_restart", "docker") {
+ def options = new ClusterOptions()
+ options.enableDebugPoints()
+ options.setFeNum(1)
+ options.setBeNum(1)
+ options.cloudMode = false
+ // Keep the BE logically alive during restart so the test exercises the
process-epoch branch,
+ // rather than the existing backend-down branch.
+ options.feConfigs += ["max_backend_heartbeat_failure_tolerance_count=100"]
+
+ docker(options) {
+ GetDebugPoint().clearDebugPointsForAllBEs()
+
+ try {
+ sql "DROP TABLE IF EXISTS test_insert_fail_fast_after_be_restart"
+ sql """
+ CREATE TABLE test_insert_fail_fast_after_be_restart (
+ k BIGINT NOT NULL,
+ v BIGINT NOT NULL
+ )
+ DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ )
+ """
+
+ def backendBeforeRestart = sql_return_maparray("SHOW BACKENDS")[0]
+ def backendId = backendBeforeRestart.BackendId.toString()
+ def previousLastStartTime =
backendBeforeRestart.LastStartTime.toString()
+
assertTrue(backendBeforeRestart.Alive.toString().equalsIgnoreCase("true"))
+
+ // Hold the load fragment before it can report completion to FE.
Restarting the BE
+ // at this point drops that report while the replacement process
quickly becomes alive.
+ def debugPointToken = "insert_restart_${System.nanoTime()}"
+ def debugPointParams = [sleep_sec: 300, token: debugPointToken]
+
GetDebugPoint().enableDebugPointForAllBEs("VTabletWriter.close.sleep",
debugPointParams)
+
GetDebugPoint().enableDebugPointForAllBEs("VTabletWriterV2.close.sleep",
debugPointParams)
+
+ def insertFuture = thread {
+ sql "SET enable_nereids_planner = true"
+ sql "SET enable_fallback_to_original_planner = false"
+ sql "SET insert_timeout = 300"
+ try {
+ sql """
+ INSERT INTO test_insert_fail_fast_after_be_restart
+ SELECT number, number FROM numbers("number" = "1024")
+ """
+ return null
+ } catch (Throwable t) {
+ logger.info("INSERT failed after BE restart as expected:
${t.message}")
+ return t.message
+ }
+ }
+
+ def beLogFile = new File(cluster.getBeByIndex(1).getLogFilePath())
+ def debugPointMarkers = [
+ "hit debug point VTabletWriter.close.sleep,
token=${debugPointToken}",
+ "hit debug point VTabletWriterV2.close.sleep,
token=${debugPointToken}"
+ ]
+ def waitDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60)
+ def debugPointHit = false
+ while (System.nanoTime() < waitDeadline) {
+ if (beLogFile.exists()) {
+ def beLog = beLogFile.text
+ if (debugPointMarkers.any { beLog.contains(it) }) {
+ debugPointHit = true
+ break
+ }
+ }
+ sleep(200)
+ }
+ assertTrue(debugPointHit, "INSERT did not reach the writer close
debug point")
+
+ cluster.restartBackends()
+
+ def restartedBackendObserved = false
+ def heartbeatDeadline = System.nanoTime() +
TimeUnit.SECONDS.toNanos(60)
+ while (System.nanoTime() < heartbeatDeadline) {
+ def currentBackend = sql_return_maparray("SHOW BACKENDS")
+ .find { it.BackendId.toString() == backendId }
+ if (currentBackend != null
+ &&
currentBackend.Alive.toString().equalsIgnoreCase("true")
+ && currentBackend.LastStartTime.toString() !=
previousLastStartTime) {
+ restartedBackendObserved = true
+ break
+ }
+ sleep(200)
+ }
+ assertTrue(restartedBackendObserved,
+ "FE did not observe the restarted BE as alive with a
changed process epoch")
+
+ // LoadProcessor checks backend health every 30 seconds. The
restarted BE is alive,
+ // so this only finishes before insert_timeout when FE also
compares process epochs.
+ def errorMessage = insertFuture.get(60, TimeUnit.SECONDS)
+ assertNotNull(errorMessage, "INSERT should fail after its BE
process restarts")
+ assertTrue(errorMessage.contains("process epoch changed"),
+ "unexpected INSERT error after BE restart:
${errorMessage}")
+ assertTrue(errorMessage.contains("backend restarted"),
+ "INSERT error should explain that the backend restarted:
${errorMessage}")
+
+ assertEquals(0, sql("SELECT COUNT(*) FROM
test_insert_fail_fast_after_be_restart")[0][0] as int)
+ } finally {
+ GetDebugPoint().clearDebugPointsForAllBEs()
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]