This is an automated email from the ASF dual-hosted git repository.
gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 8b77d1aaf5a [fix](cloud) Trigger periodic cloud checkpoints through
edit log rolls (#66154)
8b77d1aaf5a is described below
commit 8b77d1aaf5af4f049c8b993ccc91f54c39766913
Author: meiyi <[email protected]>
AuthorDate: Tue Aug 18 02:30:26 2026 +0800
[fix](cloud) Trigger periodic cloud checkpoints through edit log rolls
(#66154)
fix the wrong checkPointVersion when enable
cloud_checkpoint_image_stale_threshold_seconds
```
2026-07-24 04:19:43,353 INFO (leaderCheckpointer|165)
[Checkpoint.doCheckpoint():119] last checkpoint journal id: 497, create
timestamp: 1784863172078. current finalized journal id: 0
2026-07-24 04:19:43,353 INFO (leaderCheckpointer|165)
[Checkpoint.doCheckpoint():130] Trigger checkpoint in cloud mode because latest
image is expired. latestImageSeq: 497, latestImageCreateTime: 1784863172078
2026-07-24 04:19:43,353 INFO (leaderCheckpointer|165)
[Checkpoint.doCheckpoint():155] begin to generate new image: image.0
2026-07-24 04:19:43,394 WARN (leaderCheckpointer|165)
[Checkpoint.doCheckpoint():191] Save image failed: checkpoint version should be
0, actual replayed journal id is 497
org.apache.doris.common.CheckpointException: checkpoint version should be
0, actual replayed journal id is 497
at
org.apache.doris.master.Checkpoint.doCheckpoint(Checkpoint.java:166)
at
org.apache.doris.master.Checkpoint.runAfterCatalogReady(Checkpoint.java:91)
at
org.apache.doris.common.util.MasterDaemon.runOneCycle(MasterDaemon.java:58)
at org.apache.doris.common.util.Daemon.run(Daemon.java:119)
```
---
.../main/java/org/apache/doris/common/Config.java | 11 +-
.../java/org/apache/doris/master/Checkpoint.java | 8 --
.../java/org/apache/doris/persist/EditLog.java | 24 +++-
.../java/org/apache/doris/persist/EditLogTest.java | 159 +++++++++++++++++++++
.../test_cloud_edit_log_roll_checkpoint.groovy | 65 +++++++++
5 files changed, 245 insertions(+), 22 deletions(-)
diff --git a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
index ed6f21c1685..e1d25c8d945 100644
--- a/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
+++ b/fe/fe-common/src/main/java/org/apache/doris/common/Config.java
@@ -224,6 +224,10 @@ public class Config extends ConfigBase {
+ "entries exceeds this value, the log will be rolled")
public static int edit_log_roll_num = 50000;
+ @ConfField(mutable = true, masterOnly = true, description = "The maximum
interval in seconds between edit log "
+ + "rolls in cloud mode. A non-positive value disables time-based
edit log rolling")
+ public static int cloud_edit_log_roll_interval_second = 3600;
+
@ConfField(mutable = true, masterOnly = true, description = "The max
number of log entries for batching BDBJE")
public static int batch_edit_log_max_item_num = 100;
@@ -589,13 +593,6 @@ public class Config extends ConfigBase {
+ "tables in fuzzy tests to increase coverage")
public static boolean random_use_v3_storage_format = true;
- @ConfField(mutable = true, masterOnly = true, description = "The stale
threshold of checkpoint image file in "
- + "cloud mode (in seconds). If the image file is older " + "than
this threshold, a new checkpoint will be "
- + "triggered even if there are no new journals. This " + "helps
keep table version, partition version, and "
- + "tablet stats in the image up-to-date. If the value "
- + "is less than or equal to 0, this feature is disabled.")
- public static long cloud_checkpoint_image_stale_threshold_seconds = 3600;
-
@ConfField(mutable = true, masterOnly = true, description = "Wait for the
internal batch to be written before "
+ "returning; insert into and stream load use group " + "commit by
default.")
public static boolean wait_internal_group_commit_finish = false;
diff --git a/fe/fe-core/src/main/java/org/apache/doris/master/Checkpoint.java
b/fe/fe-core/src/main/java/org/apache/doris/master/Checkpoint.java
index 2b933034eeb..41d86851582 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/master/Checkpoint.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/master/Checkpoint.java
@@ -120,14 +120,6 @@ public class Checkpoint extends MasterDaemon {
if (imageVersion < checkPointVersion) {
LOG.info("Trigger checkpoint since last checkpoint journal id:
{} is less than "
+ "current finalized journal id: {}", imageVersion,
checkPointVersion);
- } else if (Config.isCloudMode() &&
Config.cloud_checkpoint_image_stale_threshold_seconds > 0
- && latestImageCreateTime > 0 &&
((System.currentTimeMillis() - latestImageCreateTime)
- >= Config.cloud_checkpoint_image_stale_threshold_seconds *
1000L)) {
- // No new finalized journals beyond the latest image.
- // But in cloud mode, we may still want to force a checkpoint
if the latest image file is expired.
- // This helps that image can keep the newer table version,
partition version, tablet stats.
- LOG.info("Trigger checkpoint in cloud mode because latest
image is expired. "
- + "latestImageSeq: {}, latestImageCreateTime: {}",
imageVersion, latestImageCreateTime);
} else {
return;
}
diff --git a/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java
b/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java
index 9730d1d571e..1c7c9738415 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/persist/EditLog.java
@@ -174,7 +174,8 @@ public class EditLog {
private EditLogOutputStream editStream = null;
private long txId = 0;
-
+ // This best-effort timer starts when EditLog is created and resets after
every roll.
+ private volatile long lastEditLogRollTimeMs = System.currentTimeMillis();
private AtomicLong numTransactions = new AtomicLong(0);
private AtomicLong totalTimeTransactions = new AtomicLong(0);
@@ -270,9 +271,10 @@ public class EditLog {
txId += batch.size();
// update statistics, etc. (optional, can be added as needed)
- if (txId >= Config.edit_log_roll_num) {
- LOG.info("txId {} is equal to or larger than edit_log_roll_num {},
will roll edit.", txId,
- Config.edit_log_roll_num);
+ if (txId >= Config.edit_log_roll_num || exceedEditLogRollInterval()) {
+ LOG.info("edit log roll condition met. txId: {}, edit log roll
num: {}, "
+ + "cloud edit log roll interval: {} seconds",
+ txId, Config.edit_log_roll_num,
Config.cloud_edit_log_roll_interval_second);
rollEditLog();
txId = 0;
}
@@ -1555,6 +1557,7 @@ public class EditLog {
*/
public void rollEditLog() {
journal.rollJournal();
+ lastEditLogRollTimeMs = System.currentTimeMillis();
}
// NOTICE: No guarantee atomicity of entries
@@ -1683,9 +1686,10 @@ public class EditLog {
// get a new transactionId
txId++;
- if (txId >= Config.edit_log_roll_num) {
- LOG.info("txId {} is equal to or larger than edit_log_roll_num {},
will roll edit.", txId,
- Config.edit_log_roll_num);
+ if (txId >= Config.edit_log_roll_num || exceedEditLogRollInterval()) {
+ LOG.info("edit log roll condition met. txId: {}, edit log roll
num: {}, "
+ + "cloud edit log roll interval: {} seconds",
+ txId, Config.edit_log_roll_num,
Config.cloud_edit_log_roll_interval_second);
rollEditLog();
txId = 0;
}
@@ -1693,6 +1697,12 @@ public class EditLog {
return logId;
}
+ private boolean exceedEditLogRollInterval() {
+ return Config.isCloudMode() &&
Config.cloud_edit_log_roll_interval_second > 0
+ && System.currentTimeMillis() - lastEditLogRollTimeMs
+ >=
TimeUnit.SECONDS.toMillis(Config.cloud_edit_log_roll_interval_second);
+ }
+
/**
* Write an operation to the edit log. Do not sync to persistent store yet.
*/
diff --git a/fe/fe-core/src/test/java/org/apache/doris/persist/EditLogTest.java
b/fe/fe-core/src/test/java/org/apache/doris/persist/EditLogTest.java
index e1c56810db8..61b4b786d8c 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/persist/EditLogTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/persist/EditLogTest.java
@@ -17,14 +17,58 @@
package org.apache.doris.persist;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.journal.bdbje.Timestamp;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
+import java.util.concurrent.TimeUnit;
public class EditLogTest {
private String meta = "editLogTestDir/";
+ private String originalEditLogType;
+ private int originalEditLogRollNum;
+ private int originalCloudEditLogRollIntervalSecond;
+ private String originalDeployMode;
+ private String originalCloudUniqueId;
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Before
+ public void setUpEditLogRollConfig() {
+ originalEditLogType = Config.edit_log_type;
+ originalEditLogRollNum = Config.edit_log_roll_num;
+ originalCloudEditLogRollIntervalSecond =
Config.cloud_edit_log_roll_interval_second;
+ originalDeployMode = Config.deploy_mode;
+ originalCloudUniqueId = Config.cloud_unique_id;
+
+ Config.edit_log_type = "local";
+ Config.edit_log_roll_num = Integer.MAX_VALUE;
+ Config.cloud_edit_log_roll_interval_second = 3600;
+ Config.cloud_unique_id = "";
+ }
+
+ @After
+ public void restoreEditLogRollConfig() {
+ Config.edit_log_type = originalEditLogType;
+ Config.edit_log_roll_num = originalEditLogRollNum;
+ Config.cloud_edit_log_roll_interval_second =
originalCloudEditLogRollIntervalSecond;
+ Config.deploy_mode = originalDeployMode;
+ Config.cloud_unique_id = originalCloudUniqueId;
+ }
public void mkdir() {
File dir = new File(meta);
@@ -102,4 +146,119 @@ public class EditLogTest {
public void test() {
}
+
+ @Test
+ public void testCloudModeTimeBasedEditLogRoll() throws Exception {
+ Config.deploy_mode = "cloud";
+
+ File imageDir = temporaryFolder.newFolder("time_based_roll");
+ Env env = Mockito.mock(Env.class);
+ Mockito.when(env.getImageDir()).thenReturn(imageDir.getAbsolutePath());
+ try (MockedStatic<Env> envStatic = Mockito.mockStatic(Env.class)) {
+ envStatic.when(Env::getCurrentEnv).thenReturn(env);
+ EditLog editLog = new EditLog("test");
+ editLog.open();
+ try {
+ Deencapsulation.setField(editLog, "lastEditLogRollTimeMs",
+ System.currentTimeMillis() -
TimeUnit.HOURS.toMillis(2));
+
+ editLog.logTimestamp(new Timestamp());
+
+ Assert.assertTrue(new File(imageDir, "edits.2").exists());
+ long txId = Deencapsulation.getField(editLog, "txId");
+ Assert.assertEquals(0L, txId);
+ } finally {
+ editLog.close();
+ }
+ }
+ }
+
+ @Test
+ public void testNonCloudModeDoesNotRollEditLogByTime() throws Exception {
+ Config.deploy_mode = "share_nothing";
+
+ File imageDir = temporaryFolder.newFolder("non_cloud_time_based_roll");
+ Env env = Mockito.mock(Env.class);
+ Mockito.when(env.getImageDir()).thenReturn(imageDir.getAbsolutePath());
+ try (MockedStatic<Env> envStatic = Mockito.mockStatic(Env.class)) {
+ envStatic.when(Env::getCurrentEnv).thenReturn(env);
+ EditLog editLog = new EditLog("test");
+ editLog.open();
+ try {
+ Deencapsulation.setField(editLog, "lastEditLogRollTimeMs",
+ System.currentTimeMillis() -
TimeUnit.HOURS.toMillis(2));
+
+ editLog.logTimestamp(new Timestamp());
+
+ Assert.assertFalse(new File(imageDir, "edits.2").exists());
+
+ Config.edit_log_roll_num = 2;
+ editLog.logTimestamp(new Timestamp());
+
+ Assert.assertTrue(new File(imageDir, "edits.3").exists());
+ } finally {
+ editLog.close();
+ }
+ }
+ }
+
+ @Test
+ public void testRollEditLogResetsCloudRollTime() throws Exception {
+ Config.deploy_mode = "cloud";
+
+ File imageDir = temporaryFolder.newFolder("reset_time_after_roll");
+ Env env = Mockito.mock(Env.class);
+ Mockito.when(env.getImageDir()).thenReturn(imageDir.getAbsolutePath());
+ try (MockedStatic<Env> envStatic = Mockito.mockStatic(Env.class)) {
+ envStatic.when(Env::getCurrentEnv).thenReturn(env);
+ EditLog editLog = new EditLog("test");
+ editLog.open();
+ try {
+ editLog.logTimestamp(new Timestamp());
+ Deencapsulation.setField(editLog, "lastEditLogRollTimeMs",
+ System.currentTimeMillis() -
TimeUnit.HOURS.toMillis(2));
+
+ editLog.rollEditLog();
+ editLog.logTimestamp(new Timestamp());
+
+ Assert.assertTrue(new File(imageDir, "edits.2").exists());
+ Assert.assertFalse(new File(imageDir, "edits.3").exists());
+ } finally {
+ editLog.close();
+ }
+ }
+ }
+
+ @Test
+ public void testNonPositiveCloudEditLogRollIntervalDisablesTimeBasedRoll()
throws Exception {
+ Config.deploy_mode = "cloud";
+ int[] disabledIntervals = {0, -1};
+ for (int i = 0; i < disabledIntervals.length; i++) {
+ Config.cloud_edit_log_roll_interval_second = disabledIntervals[i];
+ File imageDir =
temporaryFolder.newFolder("disabled_time_based_roll_" + i);
+ Env env = Mockito.mock(Env.class);
+
Mockito.when(env.getImageDir()).thenReturn(imageDir.getAbsolutePath());
+ try (MockedStatic<Env> envStatic = Mockito.mockStatic(Env.class)) {
+ envStatic.when(Env::getCurrentEnv).thenReturn(env);
+ EditLog editLog = new EditLog("test");
+ editLog.open();
+ try {
+ Deencapsulation.setField(editLog, "lastEditLogRollTimeMs",
+ System.currentTimeMillis() -
TimeUnit.HOURS.toMillis(2));
+
+ editLog.logTimestamp(new Timestamp());
+
+ Assert.assertFalse(new File(imageDir, "edits.2").exists());
+
+ Config.edit_log_roll_num = 2;
+ editLog.logTimestamp(new Timestamp());
+
+ Assert.assertTrue(new File(imageDir, "edits.3").exists());
+ Config.edit_log_roll_num = Integer.MAX_VALUE;
+ } finally {
+ editLog.close();
+ }
+ }
+ }
+ }
}
diff --git
a/regression-test/suites/cloud_p0/test_cloud_edit_log_roll_checkpoint.groovy
b/regression-test/suites/cloud_p0/test_cloud_edit_log_roll_checkpoint.groovy
new file mode 100644
index 00000000000..d800cbb872e
--- /dev/null
+++ b/regression-test/suites/cloud_p0/test_cloud_edit_log_roll_checkpoint.groovy
@@ -0,0 +1,65 @@
+// 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
+
+suite("test_cloud_edit_log_roll_checkpoint", "docker") {
+ if (!isCloudMode()) {
+ return
+ }
+
+ def options = new ClusterOptions()
+ options.feNum = 1
+ options.beNum = 1
+ options.cloudMode = true
+ options.feConfigs += [
+ "cloud_edit_log_roll_interval_second=30"
+ ]
+
+ docker(options) {
+ def config = sql_return_maparray """
+ ADMIN SHOW FRONTEND CONFIG LIKE
'cloud_edit_log_roll_interval_second'
+ """
+ assertEquals(1, config.size())
+ assertEquals("30", config[0].Value)
+
+ def getCheckpointVersion = {
+ def masterFe = cluster.getMasterFe()
+ def response = parseJson(new URL(
+
"http://${masterFe.host}:${masterFe.httpPort}/api/show_meta_info?action=SHOW_HA").text)
+ assertEquals(0, response.code)
+ return response.data.last_checkpoint_version as long
+ }
+
+ long initialCheckpointVersion = getCheckpointVersion()
+ logger.info("Initial checkpoint version: {}", initialCheckpointVersion)
+
+ long previousCheckpointVersion = initialCheckpointVersion
+ int checkpointCount = 0
+ awaitUntil(180, 5) {
+ long currentCheckpointVersion = getCheckpointVersion()
+ logger.info("Current checkpoint version: {}",
currentCheckpointVersion)
+ if (currentCheckpointVersion > previousCheckpointVersion) {
+ checkpointCount++
+ previousCheckpointVersion = currentCheckpointVersion
+ logger.info("Observed checkpoint image advancement {},
version: {}",
+ checkpointCount, currentCheckpointVersion)
+ }
+ return checkpointCount >= 2
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]