This is an automated email from the ASF dual-hosted git repository.

wuchong pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new fca305f9e [server] Add disk-usage write protection to TabletServer 
(#3340)
fca305f9e is described below

commit fca305f9e69618a8f6d0df2a6319e581e97ab353
Author: yunhong <[email protected]>
AuthorDate: Mon May 25 00:48:27 2026 +0800

    [server] Add disk-usage write protection to TabletServer (#3340)
---
 .../fluss/client/admin/FlussAdminITCase.java       |  73 +++++++
 .../org/apache/fluss/config/ConfigOptions.java     |  22 ++
 .../fluss/exception/DiskWriteLockedException.java  |  43 ++++
 .../java/org/apache/fluss/metrics/MetricNames.java |   4 +
 .../java/org/apache/fluss/rpc/protocol/Errors.java |   7 +-
 .../apache/fluss/server/DynamicServerConfig.java   |   4 +-
 .../server/coordinator/CoordinatorServer.java      |   4 +
 .../fluss/server/replica/ReplicaManager.java       |  22 ++
 .../fluss/server/storage/DiskUsageCollector.java   |  94 ++++++++
 .../fluss/server/storage/DiskUsageMonitor.java     | 164 ++++++++++++++
 .../storage/DiskWriteLimitRatioValidator.java      |  60 ++++++
 .../fluss/server/storage/LocalDiskManager.java     | 152 ++++++++++++-
 .../apache/fluss/server/tablet/TabletServer.java   |   2 +
 .../fluss/server/DynamicConfigChangeTest.java      |  90 ++++++++
 .../fluss/server/replica/ReplicaManagerTest.java   |  61 ++++++
 .../server/storage/DiskUsageCollectorTest.java     |  88 ++++++++
 .../fluss/server/storage/DiskUsageMonitorTest.java | 238 +++++++++++++++++++++
 website/docs/maintenance/configuration.md          |  30 +--
 18 files changed, 1141 insertions(+), 17 deletions(-)

diff --git 
a/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
 
b/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
index 4b1dc28ac..2cda18398 100644
--- 
a/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
+++ 
b/fluss-client/src/test/java/org/apache/fluss/client/admin/FlussAdminITCase.java
@@ -1546,6 +1546,79 @@ class FlussAdminITCase extends ClientToServerITCaseBase {
                 .hasMessageContaining("not.exist.key");
     }
 
+    @Test
+    void testDynamicDiskWriteLimitRatio() throws Exception {
+        // Valid value should succeed
+        admin.alterClusterConfigs(
+                        Collections.singletonList(
+                                new AlterConfig(
+                                        
ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(),
+                                        "0.15",
+                                        AlterConfigOpType.SET)))
+                .get();
+        assertConfigEntry(
+                ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(),
+                "0.15",
+                ConfigEntry.ConfigSource.DYNAMIC_SERVER_CONFIG);
+
+        // Invalid value: 0.0 (must be > 0.1)
+        assertThatThrownBy(
+                        () ->
+                                admin.alterClusterConfigs(
+                                                Collections.singletonList(
+                                                        new AlterConfig(
+                                                                ConfigOptions
+                                                                        
.SERVER_DATA_DISK_WRITE_LIMIT_RATIO
+                                                                        .key(),
+                                                                "0.0",
+                                                                
AlterConfigOpType.SET)))
+                                        .get())
+                .cause()
+                .isInstanceOf(ConfigException.class)
+                .hasMessageContaining("must be within (0.1, 1.0]");
+
+        // Invalid value: 0.1 (boundary, must be > 0.1)
+        assertThatThrownBy(
+                        () ->
+                                admin.alterClusterConfigs(
+                                                Collections.singletonList(
+                                                        new AlterConfig(
+                                                                ConfigOptions
+                                                                        
.SERVER_DATA_DISK_WRITE_LIMIT_RATIO
+                                                                        .key(),
+                                                                "0.1",
+                                                                
AlterConfigOpType.SET)))
+                                        .get())
+                .cause()
+                .isInstanceOf(ConfigException.class)
+                .hasMessageContaining("must be within (0.1, 1.0]");
+
+        // Invalid value: 1.5 (must be <= 1.0)
+        assertThatThrownBy(
+                        () ->
+                                admin.alterClusterConfigs(
+                                                Collections.singletonList(
+                                                        new AlterConfig(
+                                                                ConfigOptions
+                                                                        
.SERVER_DATA_DISK_WRITE_LIMIT_RATIO
+                                                                        .key(),
+                                                                "1.5",
+                                                                
AlterConfigOpType.SET)))
+                                        .get())
+                .cause()
+                .isInstanceOf(ConfigException.class)
+                .hasMessageContaining("must be within (0.1, 1.0]");
+
+        // Reset should succeed (restores default)
+        admin.alterClusterConfigs(
+                        Collections.singletonList(
+                                new AlterConfig(
+                                        
ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(),
+                                        null,
+                                        AlterConfigOpType.DELETE)))
+                .get();
+    }
+
     private void assertConfigEntry(
             String key, @Nullable String value, ConfigEntry.ConfigSource 
source)
             throws ExecutionException, InterruptedException {
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java 
b/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
index 1818e16ef..18f425f89 100644
--- a/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
+++ b/fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java
@@ -335,6 +335,28 @@ public class ConfigOptions {
                                     + "The default value is 10.")
                     .withDeprecatedKeys("coordinator.io-pool.size");
 
+    public static final ConfigOption<Double> 
SERVER_DATA_DISK_WRITE_LIMIT_RATIO =
+            key("server.data-disk.write-limit-ratio")
+                    .doubleType()
+                    .defaultValue(0.85)
+                    .withDescription(
+                            "Reject writes when the tablet server data disk 
usage exceeds this ratio. "
+                                    + "Writes resume after the usage drops 
below (ratio - 0.10). "
+                                    + "Set to 1.0 to disable the disk-usage 
protection entirely. "
+                                    + "The valid range is (0.1, 1.0].");
+
+    public static final ConfigOption<Duration> SERVER_DATA_DISK_CHECK_INTERVAL 
=
+            key("server.data-disk.check-interval")
+                    .durationType()
+                    .defaultValue(Duration.ofSeconds(30))
+                    .withDescription(
+                            "The interval at which the tablet server samples 
the local data disk "
+                                    + "usage for the write-protection state 
machine. A shorter interval "
+                                    + "narrows the time window during which 
writes can still flow in "
+                                    + "after the disk crosses the limit ratio, 
at the cost of slightly "
+                                    + "more frequent statvfs calls (which are 
in-memory and cheap). "
+                                    + "The default 30s is suitable for typical 
write workloads.");
+
     // ------------------------------------------------------------------------
     //  ConfigOptions for Coordinator Server
     // ------------------------------------------------------------------------
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/exception/DiskWriteLockedException.java
 
b/fluss-common/src/main/java/org/apache/fluss/exception/DiskWriteLockedException.java
new file mode 100644
index 000000000..cce9c3a74
--- /dev/null
+++ 
b/fluss-common/src/main/java/org/apache/fluss/exception/DiskWriteLockedException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.fluss.exception;
+
+import org.apache.fluss.annotation.PublicEvolving;
+
+/**
+ * Thrown by a tablet server to reject writes when its local data disk usage 
has reached the
+ * configured write-limit ratio. The exception is retriable so that clients 
can retry once the
+ * server frees up enough disk space and resumes accepting writes.
+ */
+@PublicEvolving
+public class DiskWriteLockedException extends RetriableException {
+
+    private static final long serialVersionUID = 1L;
+
+    public DiskWriteLockedException(String message) {
+        super(message);
+    }
+
+    public DiskWriteLockedException(int serverId, double usageRatio, double 
limit) {
+        super(
+                String.format(
+                        "TabletServer %d has rejected writes because the data 
disk usage "
+                                + "reached %.2f%% (limit: %.2f%%). Free up 
space or scale the cluster.",
+                        serverId, usageRatio * 100, limit * 100));
+    }
+}
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java 
b/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java
index 8f48ccfd3..07ebf57b6 100644
--- a/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java
+++ b/fluss-common/src/main/java/org/apache/fluss/metrics/MetricNames.java
@@ -94,6 +94,10 @@ public class MetricNames {
     public static final String SERVER_PHYSICAL_STORAGE_LOCAL_SIZE = 
"localSize";
     public static final String SERVER_PHYSICAL_STORAGE_REMOTE_LOG_SIZE = 
"remoteLogSize";
 
+    // for tablet server data disk write protection
+    public static final String DISK_USAGE_RATIO = "diskUsageRatio";
+    public static final String DISK_WRITE_LOCKED = "diskWriteLocked";
+
     // 
--------------------------------------------------------------------------------------------
     // metrics for user
     // 
--------------------------------------------------------------------------------------------
diff --git a/fluss-rpc/src/main/java/org/apache/fluss/rpc/protocol/Errors.java 
b/fluss-rpc/src/main/java/org/apache/fluss/rpc/protocol/Errors.java
index c72012a0a..0307932ec 100644
--- a/fluss-rpc/src/main/java/org/apache/fluss/rpc/protocol/Errors.java
+++ b/fluss-rpc/src/main/java/org/apache/fluss/rpc/protocol/Errors.java
@@ -27,6 +27,7 @@ import 
org.apache.fluss.exception.DatabaseAlreadyExistException;
 import org.apache.fluss.exception.DatabaseNotEmptyException;
 import org.apache.fluss.exception.DatabaseNotExistException;
 import org.apache.fluss.exception.DeletionDisabledException;
+import org.apache.fluss.exception.DiskWriteLockedException;
 import org.apache.fluss.exception.DuplicateSequenceException;
 import org.apache.fluss.exception.FencedLeaderEpochException;
 import org.apache.fluss.exception.FencedTieringEpochException;
@@ -265,7 +266,11 @@ public enum Errors {
     TOO_MANY_SCANNERS(
             69,
             "The per-bucket or per-server scanner session limit has been 
reached.",
-            TooManyScannersException::new);
+            TooManyScannersException::new),
+    DISK_WRITE_LOCKED(
+            70,
+            "The tablet server has rejected writes because its data disk usage 
reached the configured write-limit ratio.",
+            DiskWriteLockedException::new);
 
     private static final Logger LOG = LoggerFactory.getLogger(Errors.class);
 
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java 
b/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java
index dcec31b46..e130c392d 100644
--- 
a/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java
@@ -45,6 +45,7 @@ import static 
org.apache.fluss.config.ConfigOptions.DATALAKE_FORMAT;
 import static 
org.apache.fluss.config.ConfigOptions.KV_SHARED_RATE_LIMITER_BYTES_PER_SEC;
 import static org.apache.fluss.config.ConfigOptions.KV_SNAPSHOT_INTERVAL;
 import static 
org.apache.fluss.config.ConfigOptions.LOG_REPLICA_MIN_IN_SYNC_REPLICAS_NUMBER;
+import static 
org.apache.fluss.config.ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO;
 import static org.apache.fluss.utils.concurrent.LockUtils.inReadLock;
 import static org.apache.fluss.utils.concurrent.LockUtils.inWriteLock;
 
@@ -64,7 +65,8 @@ class DynamicServerConfig {
                             DATALAKE_FORMAT.key(),
                             LOG_REPLICA_MIN_IN_SYNC_REPLICAS_NUMBER.key(),
                             KV_SHARED_RATE_LIMITER_BYTES_PER_SEC.key(),
-                            KV_SNAPSHOT_INTERVAL.key()));
+                            KV_SNAPSHOT_INTERVAL.key(),
+                            SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key()));
     private static final Set<String> ALLOWED_CONFIG_PREFIXES = 
Collections.singleton("datalake.");
 
     private final ReadWriteLock lock = new ReentrantReadWriteLock();
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorServer.java
 
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorServer.java
index 10ba95c05..c3094c7ad 100644
--- 
a/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorServer.java
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/coordinator/CoordinatorServer.java
@@ -39,6 +39,7 @@ import org.apache.fluss.server.metadata.ServerMetadataCache;
 import org.apache.fluss.server.metrics.ServerMetricUtils;
 import org.apache.fluss.server.metrics.group.CoordinatorMetricGroup;
 import org.apache.fluss.server.metrics.group.LakeTieringMetricGroup;
+import org.apache.fluss.server.storage.DiskWriteLimitRatioValidator;
 import org.apache.fluss.server.zk.ZkEpoch;
 import org.apache.fluss.server.zk.ZooKeeperClient;
 import org.apache.fluss.server.zk.ZooKeeperUtils;
@@ -229,6 +230,9 @@ public class CoordinatorServer extends ServerBase {
             // Register server reconfigurable components
             dynamicConfigManager.register(lakeCatalogDynamicLoader);
 
+            // Register stateless validators for coordinator-side upfront 
validation
+            dynamicConfigManager.registerValidator(new 
DiskWriteLimitRatioValidator());
+
             dynamicConfigManager.startup();
 
             this.metadataCache = new CoordinatorMetadataCache();
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/replica/ReplicaManager.java
 
b/fluss-server/src/main/java/org/apache/fluss/server/replica/ReplicaManager.java
index 71fd233b6..0aaea033a 100644
--- 
a/fluss-server/src/main/java/org/apache/fluss/server/replica/ReplicaManager.java
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/replica/ReplicaManager.java
@@ -106,6 +106,7 @@ import 
org.apache.fluss.server.replica.delay.DelayedTableBucketKey;
 import org.apache.fluss.server.replica.delay.DelayedWrite;
 import org.apache.fluss.server.replica.fetcher.InitialFetchStatus;
 import org.apache.fluss.server.replica.fetcher.ReplicaFetcherManager;
+import org.apache.fluss.server.storage.DiskUsageMonitor;
 import org.apache.fluss.server.storage.LocalDiskManager;
 import org.apache.fluss.server.utils.FatalErrorHandler;
 import org.apache.fluss.server.zk.ZooKeeperClient;
@@ -332,6 +333,7 @@ public class ReplicaManager implements ServerReconfigurable 
{
         this.ioExecutor = ioExecutor;
         this.minInSyncReplicas = 
conf.get(ConfigOptions.LOG_REPLICA_MIN_IN_SYNC_REPLICAS_NUMBER);
         this.scannerManager = checkNotNull(scannerManager, "scannerManager");
+
         registerMetrics();
     }
 
@@ -344,6 +346,9 @@ public class ReplicaManager implements ServerReconfigurable 
{
                 this::maybeShrinkIsr,
                 0L,
                 conf.get(ConfigOptions.LOG_REPLICA_MAX_LAG_TIME).toMillis() / 
2);
+
+        // Start periodic disk usage monitoring (initial + periodic sampling)
+        localDiskManager.startDiskUsageMonitor(scheduler);
     }
 
     public RemoteLogManager getRemoteLogManager() {
@@ -423,6 +428,11 @@ public class ReplicaManager implements 
ServerReconfigurable {
         physicalStorage.gauge(
                 MetricNames.SERVER_PHYSICAL_STORAGE_REMOTE_LOG_SIZE,
                 this::physicalStorageRemoteLogSize);
+
+        serverMetricGroup.gauge(
+                MetricNames.DISK_USAGE_RATIO, 
localDiskManager::getLastDiskUsageRatio);
+        serverMetricGroup.gauge(
+                MetricNames.DISK_WRITE_LOCKED, () -> 
localDiskManager.isDiskWriteLocked() ? 1 : 0);
     }
 
     @VisibleForTesting
@@ -592,6 +602,16 @@ public class ReplicaManager implements 
ServerReconfigurable {
         }
     }
 
+    @VisibleForTesting
+    public boolean isDiskWriteLocked() {
+        return localDiskManager.isDiskWriteLocked();
+    }
+
+    @VisibleForTesting
+    public DiskUsageMonitor getDiskUsageMonitor() {
+        return localDiskManager.getDiskUsageMonitor();
+    }
+
     /**
      * Append log records to leader replicas of the buckets, and wait for them 
to be replicated to
      * other replicas.
@@ -609,6 +629,7 @@ public class ReplicaManager implements ServerReconfigurable 
{
         if (isRequiredAcksInvalid(requiredAcks)) {
             throw new InvalidRequiredAcksException("Invalid required acks: " + 
requiredAcks);
         }
+        localDiskManager.ensureWritable();
 
         long startTime = System.currentTimeMillis();
         Map<TableBucket, ProduceLogResultForBucket> appendResult =
@@ -662,6 +683,7 @@ public class ReplicaManager implements ServerReconfigurable 
{
         if (isRequiredAcksInvalid(requiredAcks)) {
             throw new InvalidRequiredAcksException("Invalid required acks: " + 
requiredAcks);
         }
+        localDiskManager.ensureWritable();
 
         long startTime = System.currentTimeMillis();
         Map<TableBucket, PutKvResultForBucket> kvPutResult =
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskUsageCollector.java
 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskUsageCollector.java
new file mode 100644
index 000000000..332d29ea9
--- /dev/null
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskUsageCollector.java
@@ -0,0 +1,94 @@
+/*
+ * 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.fluss.server.storage;
+
+import org.apache.fluss.annotation.Internal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.apache.fluss.utils.Preconditions.checkNotNull;
+
+/**
+ * Collects the local data disk usage ratio for the tablet server. The 
reported ratio is the
+ * <b>maximum</b> usage across all distinct {@link FileStore}s backing the 
configured data
+ * directories. A per-disk maximum (rather than a weighted average over 
total/used bytes) is used so
+ * that a single nearly-full disk cannot be masked by other low-usage disks in 
a multi-disk
+ * deployment: any single disk crossing the limit ratio must trip the write 
protection, because
+ * partitions pinned to that disk would otherwise fail to write. Multiple data 
directories sharing
+ * the same physical {@link FileStore} are still counted only once.
+ */
+@Internal
+public final class DiskUsageCollector {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(DiskUsageCollector.class);
+
+    private final List<File> dataDirs;
+
+    public DiskUsageCollector(List<File> dataDirs) {
+        checkNotNull(dataDirs, "dataDirs");
+        this.dataDirs = Collections.unmodifiableList(dataDirs);
+    }
+
+    /**
+     * Collects the current disk usage ratio in the range {@code [0.0, 1.0]}, 
defined as the maximum
+     * usage across all distinct {@link FileStore}s. Returns {@code 0.0} when 
no data directory is
+     * configured or every reachable file store reports a non-positive total 
space.
+     *
+     * <p>Individual directories that fail (e.g. deleted at runtime, or whose 
{@link FileStore}
+     * throws during {@code getTotalSpace}/{@code getUsableSpace}) are skipped 
with a warning so
+     * that one unhealthy directory does not prevent monitoring of the 
remaining disks. An {@link
+     * IOException} is thrown only when <b>all</b> directories fail.
+     */
+    public double collect() throws IOException {
+        double maxRatio = 0.0;
+        Set<FileStore> counted = new HashSet<>();
+        int failures = 0;
+        for (File dir : dataDirs) {
+            try {
+                FileStore fs = Files.getFileStore(dir.toPath());
+                if (counted.add(fs)) {
+                    long total = fs.getTotalSpace();
+                    if (total <= 0L) {
+                        continue;
+                    }
+                    double ratio = (double) (total - fs.getUsableSpace()) / 
total;
+                    if (ratio > maxRatio) {
+                        maxRatio = ratio;
+                    }
+                }
+            } catch (IOException e) {
+                LOG.warn("Failed to stat FileStore for data directory {}; 
skipping.", dir, e);
+                failures++;
+            }
+        }
+        if (failures > 0 && failures == dataDirs.size()) {
+            throw new IOException("All " + failures + " data directories 
failed FileStore lookup.");
+        }
+        return maxRatio;
+    }
+}
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskUsageMonitor.java
 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskUsageMonitor.java
new file mode 100644
index 000000000..8a27c1ef3
--- /dev/null
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskUsageMonitor.java
@@ -0,0 +1,164 @@
+/*
+ * 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.fluss.server.storage;
+
+import org.apache.fluss.annotation.Internal;
+import org.apache.fluss.annotation.VisibleForTesting;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+import static org.apache.fluss.utils.Preconditions.checkArgument;
+import static org.apache.fluss.utils.Preconditions.checkNotNull;
+
+/**
+ * Periodically samples the local data disk usage ratio and toggles the tablet 
server write-lock
+ * state with a fixed 10% hysteresis: writes are locked when the usage reaches 
the configured
+ * write-limit ratio and resume only after the usage drops below {@code (limit 
- 0.10)}. The monitor
+ * is single-state and intended to be driven by a scheduler thread; it never 
blocks.
+ */
+@Internal
+public final class DiskUsageMonitor {
+
+    /** Fixed hysteresis between the lock and unlock thresholds. */
+    public static final double RECOVER_GAP = 0.10;
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(DiskUsageMonitor.class);
+
+    private final int serverId;
+    private final DiskUsageCollector collector;
+    private volatile double writeLimitRatio;
+    private volatile double recoverThreshold;
+    private final Listener listener;
+
+    private volatile boolean locked;
+    private volatile double lastUsageRatio;
+
+    public DiskUsageMonitor(
+            int serverId, DiskUsageCollector collector, double 
writeLimitRatio, Listener listener) {
+        checkArgument(
+                writeLimitRatio > 0.0 && writeLimitRatio <= 1.0,
+                "%s must be within (0.0, 1.0], but was %s",
+                "server.data-disk.write-limit-ratio",
+                writeLimitRatio);
+        this.serverId = serverId;
+        this.collector = checkNotNull(collector, "collector");
+        this.writeLimitRatio = writeLimitRatio;
+        this.recoverThreshold = Math.max(0.0, writeLimitRatio - RECOVER_GAP);
+        this.listener = checkNotNull(listener, "listener");
+    }
+
+    /** Samples the disk usage once and updates the lock state. Never throws. 
*/
+    public void runOnce() {
+        double usage;
+        try {
+            usage = collector.collect();
+        } catch (IOException e) {
+            LOG.warn(
+                    "Failed to collect disk usage for tablet server {}; "
+                            + "keep the previous write-lock state {} (last 
usage {}%).",
+                    serverId, locked, String.format("%.2f", lastUsageRatio * 
100), e);
+            return;
+        }
+
+        if (LOG.isDebugEnabled()) {
+            LOG.debug(
+                    "[DISK-MONITOR-DEBUG] TabletServer {} disk usage: {}% | 
limit: {}% | "
+                            + "recover: {}% | locked: {}",
+                    serverId,
+                    String.format("%.4f", usage * 100),
+                    String.format("%.2f", writeLimitRatio * 100),
+                    String.format("%.2f", recoverThreshold * 100),
+                    locked);
+        }
+        update(usage);
+    }
+
+    @VisibleForTesting
+    public void update(double usage) {
+        lastUsageRatio = usage;
+        // writeLimitRatio == 1.0 means the protection is disabled; skip state 
transitions
+        // but still keep sampling for metrics.
+        if (Double.compare(writeLimitRatio, 1.0) >= 0) {
+            if (locked) {
+                locked = false;
+            }
+            listener.onSample(lastUsageRatio, false);
+            return;
+        }
+        boolean wasLocked = locked;
+        if (!wasLocked && usage >= writeLimitRatio) {
+            locked = true;
+            LOG.warn(
+                    "TabletServer {} disk usage reached {}% (limit {}%); 
rejecting writes "
+                            + "until usage drops below {}%.",
+                    serverId,
+                    String.format("%.2f", usage * 100),
+                    String.format("%.2f", writeLimitRatio * 100),
+                    String.format("%.2f", recoverThreshold * 100));
+        } else if (wasLocked && usage <= recoverThreshold) {
+            locked = false;
+            LOG.info(
+                    "TabletServer {} disk usage dropped to {}% (recover 
threshold {}%); "
+                            + "resuming writes.",
+                    serverId,
+                    String.format("%.2f", usage * 100),
+                    String.format("%.2f", recoverThreshold * 100));
+        }
+        listener.onSample(lastUsageRatio, locked);
+    }
+
+    public boolean isLocked() {
+        return locked;
+    }
+
+    public double getLastUsageRatio() {
+        return lastUsageRatio;
+    }
+
+    public double getWriteLimitRatio() {
+        return writeLimitRatio;
+    }
+
+    public double getRecoverThreshold() {
+        return recoverThreshold;
+    }
+
+    /**
+     * Dynamically updates the write-limit ratio and the derived recover 
threshold. The new ratio
+     * takes effect on the next {@link #runOnce()} invocation or {@link 
#update(double)} call.
+     *
+     * @param newRatio the new write-limit ratio, must be within (0.0, 1.0]
+     */
+    public void updateWriteLimitRatio(double newRatio) {
+        checkArgument(
+                newRatio > 0.0 && newRatio <= 1.0,
+                "server.data-disk.write-limit-ratio must be within (0.0, 1.0], 
but was %s",
+                newRatio);
+        this.writeLimitRatio = newRatio;
+        this.recoverThreshold = Math.max(0.0, newRatio - RECOVER_GAP);
+    }
+
+    /** Receives every sample for downstream state synchronization (e.g. 
metrics gauges). */
+    @FunctionalInterface
+    public interface Listener {
+        void onSample(double usageRatio, boolean locked);
+    }
+}
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskWriteLimitRatioValidator.java
 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskWriteLimitRatioValidator.java
new file mode 100644
index 000000000..14fbbaa2b
--- /dev/null
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/DiskWriteLimitRatioValidator.java
@@ -0,0 +1,60 @@
+/*
+ * 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.fluss.server.storage;
+
+import org.apache.fluss.config.ConfigOptions;
+import org.apache.fluss.config.cluster.ConfigValidator;
+import org.apache.fluss.exception.ConfigException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Stateless validator for {@link 
ConfigOptions#SERVER_DATA_DISK_WRITE_LIMIT_RATIO}.
+ *
+ * <p>This validator ensures the disk write-limit ratio stays within the valid 
range (0.1, 1.0]. The
+ * lower bound of 0.1 (exclusive) guarantees the recover threshold (ratio - 
0.10) remains positive,
+ * ensuring the system can always recover from a write-locked state. It is 
registered on the
+ * CoordinatorServer so that invalid values are rejected upfront during {@code 
AlterConfigs}, before
+ * being persisted to ZooKeeper.
+ *
+ * <p>The same range check is also performed on the TabletServer side by 
{@link LocalDiskManager}
+ * (via {@link 
org.apache.fluss.config.cluster.ServerReconfigurable#validate}), providing
+ * defense-in-depth.
+ */
+public class DiskWriteLimitRatioValidator implements ConfigValidator<Double> {
+
+    @Override
+    public String configKey() {
+        return ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key();
+    }
+
+    @Override
+    public void validate(@Nullable Double oldValue, @Nullable Double newValue)
+            throws ConfigException {
+        if (newValue == null) {
+            // Deletion (reset to default) is always valid.
+            return;
+        }
+        if (newValue <= 0.1 || newValue > 1.0) {
+            throw new ConfigException(
+                    String.format(
+                            "Invalid %s: must be within (0.1, 1.0], but was 
%s",
+                            
ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(), newValue));
+        }
+    }
+}
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/storage/LocalDiskManager.java
 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/LocalDiskManager.java
index f7a8d6c44..a1afe73bc 100644
--- 
a/fluss-server/src/main/java/org/apache/fluss/server/storage/LocalDiskManager.java
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/storage/LocalDiskManager.java
@@ -20,10 +20,14 @@ package org.apache.fluss.server.storage;
 import org.apache.fluss.annotation.VisibleForTesting;
 import org.apache.fluss.config.ConfigOptions;
 import org.apache.fluss.config.Configuration;
+import org.apache.fluss.config.cluster.ServerReconfigurable;
+import org.apache.fluss.exception.ConfigException;
+import org.apache.fluss.exception.DiskWriteLockedException;
 import org.apache.fluss.exception.IllegalConfigurationException;
 import org.apache.fluss.exception.LogStorageException;
 import org.apache.fluss.utils.FileUtils;
 import org.apache.fluss.utils.IOUtils;
+import org.apache.fluss.utils.concurrent.Scheduler;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -67,8 +71,12 @@ import java.util.UUID;
  * After that, the mutable state is limited to in-memory bucket counters and 
shutdown cleanup. The
  * public runtime APIs synchronize on the manager instance so directory 
lookups, counter updates,
  * and {@link #close()} observe a consistent view.
+ *
+ * <p>The disk usage write-protection state ({@link #isDiskWriteLocked()} / 
{@link
+ * #getLastDiskUsageRatio()}) is updated by the scheduler thread started via 
{@link
+ * #startDiskUsageMonitor(Scheduler)} and read by request handlers via {@code 
volatile} fields.
  */
-public final class LocalDiskManager implements Closeable {
+public final class LocalDiskManager implements Closeable, ServerReconfigurable 
{
 
     private static final Logger LOG = 
LoggerFactory.getLogger(LocalDiskManager.class);
 
@@ -84,6 +92,32 @@ public final class LocalDiskManager implements Closeable {
     private final Map<File, DiskInfo> diskInfosByDir = new LinkedHashMap<>();
     private final Map<File, DirectoryLock> directoryLocks = new 
LinkedHashMap<>();
 
+    // ------------------------------------------------------------------------
+    // Disk usage write protection
+    // ------------------------------------------------------------------------
+
+    private final long diskCheckIntervalMs;
+    private final DiskUsageMonitor diskUsageMonitor;
+
+    /**
+     * Configured high-water-mark ratio: writes are rejected once any single 
backing {@link
+     * java.nio.file.FileStore}'s usage ratio reaches this value. Writes 
resume after the usage
+     * drops below {@code diskWriteLimitRatio - 0.10} (the recover gap is 
owned by {@link
+     * DiskUsageMonitor}). This field is volatile because it can be changed at 
runtime via dynamic
+     * reconfiguration.
+     */
+    private volatile double diskWriteLimitRatio;
+
+    /**
+     * Whether the local tablet server is currently rejecting writes because 
the data disk usage has
+     * reached {@link #diskWriteLimitRatio}. Updated by the {@link 
DiskUsageMonitor} scheduler
+     * thread, read by append/put handlers.
+     */
+    private volatile boolean diskWriteLocked;
+
+    /** Last observed disk usage ratio in range {@code [0.0, 1.0]}. */
+    private volatile double lastDiskUsageRatio;
+
     // ------------------------------------------------------------------------
     // Initialization
     // ------------------------------------------------------------------------
@@ -99,6 +133,33 @@ public final class LocalDiskManager implements Closeable {
             throw throwable;
         }
         this.dataDirs = Collections.unmodifiableList(initializedDataDirs);
+
+        this.diskWriteLimitRatio = 
conf.get(ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO);
+        if (diskWriteLimitRatio <= 0.0 || diskWriteLimitRatio > 1.0) {
+            throw new IllegalConfigurationException(
+                    String.format(
+                            "%s must be within (0.0, 1.0], but was %s",
+                            
ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(),
+                            diskWriteLimitRatio));
+        }
+        this.diskCheckIntervalMs =
+                
conf.get(ConfigOptions.SERVER_DATA_DISK_CHECK_INTERVAL).toMillis();
+        if (diskCheckIntervalMs <= 0L) {
+            throw new IllegalConfigurationException(
+                    String.format(
+                            "%s must be a positive duration, but was %s ms",
+                            
ConfigOptions.SERVER_DATA_DISK_CHECK_INTERVAL.key(),
+                            diskCheckIntervalMs));
+        }
+        this.diskUsageMonitor =
+                new DiskUsageMonitor(
+                        serverId,
+                        new DiskUsageCollector(this.dataDirs),
+                        diskWriteLimitRatio,
+                        (usage, locked) -> {
+                            this.lastDiskUsageRatio = usage;
+                            this.diskWriteLocked = locked;
+                        });
     }
 
     public static LocalDiskManager create(Configuration conf) throws 
IOException {
@@ -402,6 +463,95 @@ public final class LocalDiskManager implements Closeable {
         return dataDirs;
     }
 
+    // ------------------------------------------------------------------------
+    // Disk usage write protection
+    // ------------------------------------------------------------------------
+
+    /**
+     * Schedules the periodic disk usage sampling on the given {@link 
Scheduler}. An immediate
+     * synchronous sample is performed first so that the write-lock state is 
meaningful before the
+     * first scheduled tick.
+     */
+    public void startDiskUsageMonitor(Scheduler scheduler) {
+        scheduler.schedule("disk-usage-monitor", diskUsageMonitor::runOnce, 0, 
diskCheckIntervalMs);
+    }
+
+    /**
+     * @return whether write requests are currently being rejected due to disk 
pressure.
+     */
+    public boolean isDiskWriteLocked() {
+        return diskWriteLocked;
+    }
+
+    /**
+     * @return the most recent observed disk usage ratio in {@code [0.0, 1.0]}.
+     */
+    public double getLastDiskUsageRatio() {
+        return lastDiskUsageRatio;
+    }
+
+    /**
+     * @return the configured high-water-mark ratio that triggers the write 
lock.
+     */
+    public double getDiskWriteLimitRatio() {
+        return diskWriteLimitRatio;
+    }
+
+    /**
+     * Throws {@link DiskWriteLockedException} when the local data disk usage 
has crossed the
+     * configured write-limit ratio. Only client-driven writes ({@code 
appendLog} / {@code putKv})
+     * should call this; follower replication paths must bypass this check to 
preserve replica
+     * consistency.
+     */
+    public void ensureWritable() {
+        if (diskWriteLocked) {
+            throw new DiskWriteLockedException(serverId, lastDiskUsageRatio, 
diskWriteLimitRatio);
+        }
+    }
+
+    @VisibleForTesting
+    public DiskUsageMonitor getDiskUsageMonitor() {
+        return diskUsageMonitor;
+    }
+
+    // ------------------------------------------------------------------------
+    // ServerReconfigurable: dynamic write-limit-ratio update
+    // ------------------------------------------------------------------------
+
+    @Override
+    public void validate(Configuration newConfig) throws ConfigException {
+        double newRatio = 
newConfig.get(ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO);
+        if (newRatio <= 0.1 || newRatio > 1.0) {
+            throw new ConfigException(
+                    String.format(
+                            "Invalid %s: must be within (0.1, 1.0], but was 
%s",
+                            
ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(), newRatio));
+        }
+    }
+
+    @Override
+    public void reconfigure(Configuration newConfig) {
+        double newRatio = 
newConfig.get(ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO);
+        if (Double.compare(newRatio, diskWriteLimitRatio) == 0) {
+            LOG.debug(
+                    "{} unchanged: {}",
+                    ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(),
+                    newRatio);
+            return;
+        }
+        double oldRatio = diskWriteLimitRatio;
+        diskWriteLimitRatio = newRatio;
+        diskUsageMonitor.updateWriteLimitRatio(newRatio);
+        // Trigger an immediate check so the new threshold takes effect 
without waiting
+        // for the next scheduled tick.
+        diskUsageMonitor.runOnce();
+        LOG.info(
+                "{} reconfigured: {} -> {} (immediate check triggered)",
+                ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key(),
+                oldRatio,
+                newRatio);
+    }
+
     /**
      * Resolves a local path back to the configured data directory that owns 
it.
      *
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/tablet/TabletServer.java 
b/fluss-server/src/main/java/org/apache/fluss/server/tablet/TabletServer.java
index 4ec7df46e..74afa8337 100644
--- 
a/fluss-server/src/main/java/org/apache/fluss/server/tablet/TabletServer.java
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/tablet/TabletServer.java
@@ -299,6 +299,8 @@ public class TabletServer extends ServerBase {
             
dynamicConfigManager.register(replicaManager.getKvSnapshotContext());
             // Register replicaManager to dynamicConfigManager for dynamic 
config
             dynamicConfigManager.register(replicaManager);
+            // Register localDiskManager for dynamic 
server.data-disk.write-limit-ratio
+            dynamicConfigManager.register(localDiskManager);
             // Start dynamicConfigManager after all reconfigurable components 
are registered
             dynamicConfigManager.startup();
 
diff --git 
a/fluss-server/src/test/java/org/apache/fluss/server/DynamicConfigChangeTest.java
 
b/fluss-server/src/test/java/org/apache/fluss/server/DynamicConfigChangeTest.java
index 3ef221eca..39db82daf 100644
--- 
a/fluss-server/src/test/java/org/apache/fluss/server/DynamicConfigChangeTest.java
+++ 
b/fluss-server/src/test/java/org/apache/fluss/server/DynamicConfigChangeTest.java
@@ -24,6 +24,7 @@ import org.apache.fluss.config.cluster.AlterConfigOpType;
 import org.apache.fluss.config.cluster.ServerReconfigurable;
 import org.apache.fluss.exception.ConfigException;
 import org.apache.fluss.server.coordinator.LakeCatalogDynamicLoader;
+import org.apache.fluss.server.storage.LocalDiskManager;
 import org.apache.fluss.server.zk.NOPErrorHandler;
 import org.apache.fluss.server.zk.ZooKeeperClient;
 import org.apache.fluss.server.zk.ZooKeeperExtension;
@@ -36,7 +37,9 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
 
+import java.io.File;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collections;
@@ -53,6 +56,7 @@ import static 
org.apache.fluss.testutils.common.CommonTestUtils.retry;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.within;
 
 /** Test for {@link DynamicConfigManager}. */
 public class DynamicConfigChangeTest {
@@ -552,4 +556,90 @@ public class DynamicConfigChangeTest {
                             "'datalake.format' must be configured when 
'datalake.enabled' is explicitly set to true.");
         }
     }
+
+    @Test
+    void testDynamicDiskWriteLimitRatioChange(@TempDir File tempDir) throws 
Exception {
+        File dataDir = new File(tempDir, "data-0");
+        assertThat(dataDir.mkdirs()).isTrue();
+
+        Configuration configuration = new Configuration();
+        configuration.setInt(ConfigOptions.TABLET_SERVER_ID, 0);
+        configuration.setString(ConfigOptions.DATA_DIR, 
dataDir.getAbsolutePath());
+        configuration.set(ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO, 
0.85);
+
+        DynamicConfigManager dynamicConfigManager =
+                new DynamicConfigManager(zookeeperClient, configuration, true);
+
+        // Create LocalDiskManager and register it
+        try (LocalDiskManager localDiskManager = 
LocalDiskManager.create(configuration)) {
+            dynamicConfigManager.register(localDiskManager);
+            dynamicConfigManager.startup();
+
+            // Verify initial state
+            
assertThat(localDiskManager.getDiskWriteLimitRatio()).isEqualTo(0.85);
+            
assertThat(localDiskManager.getDiskUsageMonitor().getWriteLimitRatio()).isEqualTo(0.85);
+            
assertThat(localDiskManager.getDiskUsageMonitor().getRecoverThreshold())
+                    .isEqualTo(0.75);
+
+            // Lower the limit to 0.70 via dynamic config
+            assertThatCode(
+                            () ->
+                                    dynamicConfigManager.alterConfigs(
+                                            Collections.singletonList(
+                                                    new AlterConfig(
+                                                            ConfigOptions
+                                                                    
.SERVER_DATA_DISK_WRITE_LIMIT_RATIO
+                                                                    .key(),
+                                                            "0.70",
+                                                            
AlterConfigOpType.SET))))
+                    .doesNotThrowAnyException();
+
+            // Verify the new ratio took effect immediately (reconfigure 
triggers runOnce)
+            
assertThat(localDiskManager.getDiskWriteLimitRatio()).isEqualTo(0.70);
+            
assertThat(localDiskManager.getDiskUsageMonitor().getWriteLimitRatio()).isEqualTo(0.70);
+            
assertThat(localDiskManager.getDiskUsageMonitor().getRecoverThreshold())
+                    .isCloseTo(0.60, within(1e-9));
+
+            // Verify config was persisted to ZK
+            Map<String, String> zkConfig = zookeeperClient.fetchEntityConfig();
+            
assertThat(zkConfig.get(ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO.key()))
+                    .isEqualTo("0.70");
+        }
+    }
+
+    @Test
+    void testPreventInvalidDiskWriteLimitRatio(@TempDir File tempDir) throws 
Exception {
+        File dataDir = new File(tempDir, "data-0");
+        assertThat(dataDir.mkdirs()).isTrue();
+
+        Configuration configuration = new Configuration();
+        configuration.setInt(ConfigOptions.TABLET_SERVER_ID, 0);
+        configuration.setString(ConfigOptions.DATA_DIR, 
dataDir.getAbsolutePath());
+        configuration.set(ConfigOptions.SERVER_DATA_DISK_WRITE_LIMIT_RATIO, 
0.85);
+
+        DynamicConfigManager dynamicConfigManager =
+                new DynamicConfigManager(zookeeperClient, configuration, true);
+
+        try (LocalDiskManager localDiskManager = 
LocalDiskManager.create(configuration)) {
+            dynamicConfigManager.register(localDiskManager);
+            dynamicConfigManager.startup();
+
+            // Try to set to invalid value 0.0 - should be rejected
+            assertThatThrownBy(
+                            () ->
+                                    dynamicConfigManager.alterConfigs(
+                                            Collections.singletonList(
+                                                    new AlterConfig(
+                                                            ConfigOptions
+                                                                    
.SERVER_DATA_DISK_WRITE_LIMIT_RATIO
+                                                                    .key(),
+                                                            "0.0",
+                                                            
AlterConfigOpType.SET))))
+                    .isInstanceOf(ConfigException.class)
+                    .hasMessageContaining("must be within (0.1, 1.0]");
+
+            // Verify the ratio was NOT changed
+            
assertThat(localDiskManager.getDiskWriteLimitRatio()).isEqualTo(0.85);
+        }
+    }
 }
diff --git 
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
 
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
index 14e24c18c..eea0d5eb1 100644
--- 
a/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
+++ 
b/fluss-server/src/test/java/org/apache/fluss/server/replica/ReplicaManagerTest.java
@@ -21,6 +21,7 @@ import org.apache.fluss.cluster.Endpoint;
 import org.apache.fluss.cluster.ServerNode;
 import org.apache.fluss.cluster.ServerType;
 import org.apache.fluss.config.ConfigOptions;
+import org.apache.fluss.exception.DiskWriteLockedException;
 import org.apache.fluss.exception.InvalidCoordinatorException;
 import org.apache.fluss.exception.InvalidRequiredAcksException;
 import org.apache.fluss.exception.NotLeaderOrFollowerException;
@@ -491,6 +492,66 @@ class ReplicaManagerTest extends ReplicaTestBase {
         }
     }
 
+    @Test
+    void testAppendRejectedWhenDiskLocked() throws Exception {
+        TableBucket tb = new TableBucket(DATA1_TABLE_ID, 1);
+        makeLogTableAsLeader(tb.getBucket());
+
+        // simulate disk usage breaching the write-limit ratio (default 0.85)
+        replicaManager.getDiskUsageMonitor().update(0.95);
+        assertThat(replicaManager.isDiskWriteLocked()).isTrue();
+
+        assertThatThrownBy(
+                        () ->
+                                replicaManager.appendRecordsToLog(
+                                        20000,
+                                        1,
+                                        Collections.singletonMap(
+                                                tb, 
genMemoryLogRecordsByObject(DATA1)),
+                                        null,
+                                        (result) -> {}))
+                .isInstanceOf(DiskWriteLockedException.class)
+                .hasMessageContaining("data disk usage");
+
+        // recover when usage drops below (limit - 0.10) -> 0.75
+        replicaManager.getDiskUsageMonitor().update(0.50);
+        assertThat(replicaManager.isDiskWriteLocked()).isFalse();
+
+        CompletableFuture<List<ProduceLogResultForBucket>> future = new 
CompletableFuture<>();
+        replicaManager.appendRecordsToLog(
+                20000,
+                1,
+                Collections.singletonMap(tb, 
genMemoryLogRecordsByObject(DATA1)),
+                null,
+                future::complete);
+        assertThat(future.get()).containsOnly(new 
ProduceLogResultForBucket(tb, 0, 10L));
+    }
+
+    @Test
+    void testPutKvRejectedWhenDiskLocked() {
+        TableBucket tb = new TableBucket(DATA1_TABLE_ID_PK, 1);
+        makeKvTableAsLeader(DATA1_TABLE_ID_PK, DATA1_TABLE_PATH_PK, 
tb.getBucket());
+
+        replicaManager.getDiskUsageMonitor().update(0.99);
+        assertThat(replicaManager.isDiskWriteLocked()).isTrue();
+
+        assertThatThrownBy(
+                        () ->
+                                replicaManager.putRecordsToKv(
+                                        20000,
+                                        1,
+                                        Collections.singletonMap(
+                                                tb, 
genKvRecordBatch(DATA_1_WITH_KEY_AND_VALUE)),
+                                        null,
+                                        MergeMode.DEFAULT,
+                                        PUT_KV_VERSION,
+                                        (result) -> {}))
+                .isInstanceOf(DiskWriteLockedException.class);
+
+        // unlock for any subsequent tests on the shared replicaManager 
instance
+        replicaManager.getDiskUsageMonitor().update(0.10);
+    }
+
     @Test
     void testPutKv() throws Exception {
         TableBucket tb = new TableBucket(DATA1_TABLE_ID_PK, 1);
diff --git 
a/fluss-server/src/test/java/org/apache/fluss/server/storage/DiskUsageCollectorTest.java
 
b/fluss-server/src/test/java/org/apache/fluss/server/storage/DiskUsageCollectorTest.java
new file mode 100644
index 000000000..86b09c6fa
--- /dev/null
+++ 
b/fluss-server/src/test/java/org/apache/fluss/server/storage/DiskUsageCollectorTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.fluss.server.storage;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Test for {@link DiskUsageCollector}. */
+class DiskUsageCollectorTest {
+
+    @TempDir private File tempDir;
+
+    @Test
+    void testCollectReturnsRatioInRange() throws Exception {
+        File dataDir = new File(tempDir, "data-0");
+        assertThat(dataDir.mkdirs()).isTrue();
+
+        DiskUsageCollector collector = new 
DiskUsageCollector(Collections.singletonList(dataDir));
+
+        double ratio = collector.collect();
+        assertThat(ratio).isBetween(0.0, 1.0);
+    }
+
+    @Test
+    void testMultipleDirsOnSameFileStoreDeduplicated() throws Exception {
+        File dataDir1 = new File(tempDir, "data-1");
+        File dataDir2 = new File(tempDir, "data-2");
+        assertThat(dataDir1.mkdirs()).isTrue();
+        assertThat(dataDir2.mkdirs()).isTrue();
+
+        DiskUsageCollector twoDirs = new 
DiskUsageCollector(Arrays.asList(dataDir1, dataDir2));
+        DiskUsageCollector oneDir = new 
DiskUsageCollector(Collections.singletonList(dataDir1));
+
+        // both directories share the same FileStore -> result should match a 
single-dir collector
+        assertThat(twoDirs.collect()).isEqualTo(oneDir.collect());
+    }
+
+    @Test
+    void testEmptyDataDirsReturnsZero() throws Exception {
+        DiskUsageCollector collector = new 
DiskUsageCollector(Collections.emptyList());
+        assertThat(collector.collect()).isEqualTo(0.0);
+    }
+
+    @Test
+    void testPartialFailureSkipsBadDirAndReportsRemaining() throws Exception {
+        File goodDir = new File(tempDir, "good");
+        assertThat(goodDir.mkdirs()).isTrue();
+        File badDir = new File(tempDir, "does-not-exist/nested");
+
+        DiskUsageCollector collector = new 
DiskUsageCollector(Arrays.asList(badDir, goodDir));
+        double ratio = collector.collect();
+        assertThat(ratio).isBetween(0.0, 1.0);
+    }
+
+    @Test
+    void testAllDirsFailThrowsIOException() {
+        File bad1 = new File("/__fluss_non_existent_1__/x");
+        File bad2 = new File("/__fluss_non_existent_2__/y");
+
+        DiskUsageCollector collector = new 
DiskUsageCollector(Arrays.asList(bad1, bad2));
+        assertThatThrownBy(collector::collect)
+                .isInstanceOf(IOException.class)
+                .hasMessageContaining("All 2 data directories failed");
+    }
+}
diff --git 
a/fluss-server/src/test/java/org/apache/fluss/server/storage/DiskUsageMonitorTest.java
 
b/fluss-server/src/test/java/org/apache/fluss/server/storage/DiskUsageMonitorTest.java
new file mode 100644
index 000000000..dc1291d08
--- /dev/null
+++ 
b/fluss-server/src/test/java/org/apache/fluss/server/storage/DiskUsageMonitorTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.fluss.server.storage;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.within;
+
+/** Test for {@link DiskUsageMonitor}. */
+class DiskUsageMonitorTest {
+
+    private static final int SERVER_ID = 7;
+
+    @Test
+    void testInvalidLimitRatioRejected() {
+        DiskUsageCollector collector = new 
DiskUsageCollector(Collections.emptyList());
+        assertThatThrownBy(
+                        () ->
+                                new DiskUsageMonitor(
+                                        SERVER_ID, collector, 0.0, (usage, 
locked) -> {}))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(
+                        () ->
+                                new DiskUsageMonitor(
+                                        SERVER_ID, collector, 1.5, (usage, 
locked) -> {}))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void testStaysUnlockedBelowLimit() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(0.85, recorder);
+
+        monitor.update(0.50);
+        assertThat(monitor.isLocked()).isFalse();
+        assertThat(monitor.getLastUsageRatio()).isEqualTo(0.50);
+        assertThat(recorder.lastLocked.get()).isFalse();
+        assertThat(recorder.lastUsage.get()).isEqualTo(0.50);
+    }
+
+    @Test
+    void testLockedWhenReachingLimit() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(0.85, recorder);
+
+        monitor.update(0.85);
+        assertThat(monitor.isLocked()).isTrue();
+        assertThat(recorder.lastLocked.get()).isTrue();
+
+        monitor.update(0.90);
+        assertThat(monitor.isLocked()).isTrue();
+    }
+
+    @Test
+    void testStaysLockedAboveRecoverThreshold() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(0.85, recorder);
+
+        monitor.update(0.86);
+        assertThat(monitor.isLocked()).isTrue();
+
+        // recover threshold is 0.75, 0.80 still above it -> stay locked
+        monitor.update(0.80);
+        assertThat(monitor.isLocked()).isTrue();
+    }
+
+    @Test
+    void testUnlockedAtRecoverThreshold() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(0.85, recorder);
+
+        monitor.update(0.90);
+        assertThat(monitor.isLocked()).isTrue();
+
+        monitor.update(0.75);
+        assertThat(monitor.isLocked()).isFalse();
+        assertThat(recorder.lastLocked.get()).isFalse();
+    }
+
+    @Test
+    void testRecoverThresholdNeverNegative() {
+        DiskUsageMonitor monitor = newMonitor(0.05, new Recorder());
+        assertThat(monitor.getRecoverThreshold()).isEqualTo(0.0);
+        assertThat(monitor.getWriteLimitRatio()).isEqualTo(0.05);
+    }
+
+    @Test
+    void testRunOncePreservesStateWhenCollectorThrows() {
+        // Pointing the collector at a non-existent path makes 
Files.getFileStore raise
+        // NoSuchFileException -> IOException, which runOnce must swallow 
without flipping state.
+        DiskUsageCollector failing =
+                new DiskUsageCollector(
+                        Collections.singletonList(
+                                new 
File("/__fluss_disk_monitor_does_not_exist__/x")));
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = new DiskUsageMonitor(SERVER_ID, failing, 
0.85, recorder);
+
+        // First put the monitor into the locked state via update().
+        monitor.update(0.95);
+        assertThat(monitor.isLocked()).isTrue();
+        double usageBefore = monitor.getLastUsageRatio();
+        recorder.lastUsage.set(Double.NaN);
+        recorder.lastLocked.set(false);
+
+        // runOnce should swallow the IOException, keep locked=true and not 
invoke the listener.
+        monitor.runOnce();
+        assertThat(monitor.isLocked()).isTrue();
+        assertThat(monitor.getLastUsageRatio()).isEqualTo(usageBefore);
+        assertThat(Double.isNaN(recorder.lastUsage.get())).isTrue();
+        assertThat(recorder.lastLocked.get()).isFalse();
+    }
+
+    @Test
+    void testRunOnceUpdatesUsageWhenCollectorReturnsValue() {
+        // Reuse a real collector backed by an empty data dirs list -> always 
returns 0.0.
+        DiskUsageCollector collector = new 
DiskUsageCollector(Collections.emptyList());
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = new DiskUsageMonitor(SERVER_ID, collector, 
0.85, recorder);
+
+        monitor.runOnce();
+        assertThat(monitor.isLocked()).isFalse();
+        assertThat(monitor.getLastUsageRatio()).isEqualTo(0.0);
+        assertThat(recorder.lastUsage.get()).isEqualTo(0.0);
+        assertThat(recorder.lastLocked.get()).isFalse();
+    }
+
+    private DiskUsageMonitor newMonitor(double limit, 
DiskUsageMonitor.Listener listener) {
+        return new DiskUsageMonitor(
+                SERVER_ID, new DiskUsageCollector(Collections.emptyList()), 
limit, listener);
+    }
+
+    @Test
+    void testUpdateWriteLimitRatioChangesThresholds() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(0.85, recorder);
+
+        // Initially ratio=0.85, recover=0.75
+        assertThat(monitor.getWriteLimitRatio()).isEqualTo(0.85);
+        assertThat(monitor.getRecoverThreshold()).isEqualTo(0.75);
+
+        // Simulate usage at 0.82 — should NOT lock (below 0.85)
+        monitor.update(0.82);
+        assertThat(monitor.isLocked()).isFalse();
+
+        // Lower the limit to 0.80 — now 0.82 exceeds the new limit
+        monitor.updateWriteLimitRatio(0.80);
+        assertThat(monitor.getWriteLimitRatio()).isEqualTo(0.80);
+        assertThat(monitor.getRecoverThreshold()).isCloseTo(0.70, 
within(1e-9));
+
+        // Re-evaluate with same usage — should lock
+        monitor.update(0.82);
+        assertThat(monitor.isLocked()).isTrue();
+        assertThat(recorder.lastLocked.get()).isTrue();
+
+        // Raise the limit to 0.90 and recover threshold becomes 0.80
+        // 0.82 > 0.80 so should remain locked
+        monitor.updateWriteLimitRatio(0.90);
+        monitor.update(0.82);
+        assertThat(monitor.isLocked()).isTrue();
+
+        // Drop usage to 0.79 — now below new recover threshold 0.80, should 
unlock
+        monitor.update(0.79);
+        assertThat(monitor.isLocked()).isFalse();
+    }
+
+    @Test
+    void testDisabledWhenRatioIsOne() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(1.0, recorder);
+
+        // Even at 100% disk usage, should NOT lock when ratio = 1.0
+        monitor.update(1.0);
+        assertThat(monitor.isLocked()).isFalse();
+        assertThat(recorder.lastLocked.get()).isFalse();
+        assertThat(recorder.lastUsage.get()).isEqualTo(1.0);
+
+        // Any usage below 1.0 should also remain unlocked
+        monitor.update(0.99);
+        assertThat(monitor.isLocked()).isFalse();
+
+        // If previously locked (via dynamic update), switching to 1.0 should 
unlock immediately
+        DiskUsageMonitor monitor2 = newMonitor(0.80, recorder);
+        monitor2.update(0.85); // lock it
+        assertThat(monitor2.isLocked()).isTrue();
+
+        monitor2.updateWriteLimitRatio(1.0);
+        monitor2.update(0.85); // same usage, but ratio=1.0 -> should unlock
+        assertThat(monitor2.isLocked()).isFalse();
+        assertThat(recorder.lastLocked.get()).isFalse();
+    }
+
+    @Test
+    void testUpdateWriteLimitRatioRejectsInvalidValues() {
+        Recorder recorder = new Recorder();
+        DiskUsageMonitor monitor = newMonitor(0.85, recorder);
+
+        assertThatThrownBy(() -> monitor.updateWriteLimitRatio(0.0))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> monitor.updateWriteLimitRatio(1.1))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> monitor.updateWriteLimitRatio(-0.5))
+                .isInstanceOf(IllegalArgumentException.class);
+    }
+
+    /** Captures the latest sample observed by the listener. */
+    private static final class Recorder implements DiskUsageMonitor.Listener {
+        final AtomicReference<Double> lastUsage = new 
AtomicReference<>(Double.NaN);
+        final AtomicBoolean lastLocked = new AtomicBoolean(false);
+
+        @Override
+        public void onSample(double usageRatio, boolean locked) {
+            lastUsage.set(usageRatio);
+            lastLocked.set(locked);
+        }
+    }
+}
diff --git a/website/docs/maintenance/configuration.md 
b/website/docs/maintenance/configuration.md
index 2f77fb567..d681ae0bd 100644
--- a/website/docs/maintenance/configuration.md
+++ b/website/docs/maintenance/configuration.md
@@ -64,20 +64,22 @@ during the Fluss cluster working.
 
 ## TabletServer
 
-| Option                                           | Type       | Default      
   | Description                                                                
                                                                                
                                                                                
                                                                        |
-|--------------------------------------------------|------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 
-| tablet-server.id                                 | Integer    | (None)       
   | The id for the tablet server.                                              
                                                                                
                                                                                
                                                                        |
-| tablet-server.rack                               | String     | (None)       
   | The rack for the TabletServer. This will be used in rack aware bucket 
assignment for fault tolerance. Examples: `RACK1`, `cn-hangzhou-server10`       
                                                                                
                                                                             |
-| data.dir                                         | String     | 
/tmp/fluss-data | This configuration controls the directory where Fluss will 
store its data. The default value is /tmp/fluss-data                            
                                                                                
                                                                                
        |
-| server.writer-id.expiration-time                 | Duration   | 7d           
   | The time that the tablet server will wait without receiving any write 
request from a client before expiring the related status. The default value is 
7 days.                                                                         
                                                                              |
-| server.writer-id.expiration-check-interval       | Duration   | 10min        
   | The interval at which to remove writer ids that have expired due to 
`server.writer-id.expiration-time passing. The default value is 10 minutes.     
                                                                                
                                                                               |
-| server.background.threads                        | Integer    | 10           
   | The number of threads to use for various background processing tasks. The 
default value is 10.                                                            
                                                                                
                                                                         |
-| server.buffer.memory-size                        | MemorySize | 256mb        
   | The total bytes of memory the server can use, e.g, buffer write-ahead-log 
rows.                                                                           
                                                                                
                                                                         |
-| server.buffer.page-size                          | MemorySize | 128kb        
   | Size of every page in memory buffers (`server.buffer.memory-size`).        
                                                                                
                                                                                
                                                                        |
-| server.buffer.per-request-memory-size            | MemorySize | 16mb         
   | The minimum number of bytes that will be allocated by the writer rounded 
down to the closest multiple of server.buffer.page-size. It must be greater 
than or equal to server.buffer.page-size. This option allows to allocate memory 
in batches to have better CPU-cached friendliness due to contiguous segments. | 
                                                                      |
-| server.buffer.wait-timeout                       | Duration   | 2^(63)-1ns   
   | Defines how long the buffer pool will block when waiting for segments.     
                                                                                
                                                                                
                                                                        |
-| tablet-server.controlled-shutdown.max-retries    | Integer    | 3            
   | Maximum number of attempts to transfer leadership before proceeding with 
an unclean shutdown during a controlled shutdown procedure.                     
                                                                                
                                                                          |
-| tablet-server.controlled-shutdown.retry-interval | Duration   | 1000ms       
   | Time interval between retry attempts when trying to transfer leadership 
during controlled shutdown.                                                     
                                                                                
                                                                           |
+| Option                                           | Type       | Default      
   | Description                                                                
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+|--------------------------------------------------|------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [...]
+| tablet-server.id                                 | Integer    | (None)       
   | The id for the tablet server.                                              
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+| tablet-server.rack                               | String     | (None)       
   | The rack for the TabletServer. This will be used in rack aware bucket 
assignment for fault tolerance. Examples: `RACK1`, `cn-hangzhou-server10`       
                                                                                
                                                                                
                                                                                
                   [...]
+| data.dir                                         | String     | 
/tmp/fluss-data | This configuration controls the directory where Fluss will 
store its data. The default value is /tmp/fluss-data                            
                                                                                
                                                                                
                                                                                
                              [...]
+| server.writer-id.expiration-time                 | Duration   | 7d           
   | The time that the tablet server will wait without receiving any write 
request from a client before expiring the related status. The default value is 
7 days.                                                                         
                                                                                
                                                                                
                    [...]
+| server.writer-id.expiration-check-interval       | Duration   | 10min        
   | The interval at which to remove writer ids that have expired due to 
`server.writer-id.expiration-time passing. The default value is 10 minutes.     
                                                                                
                                                                                
                                                                                
                     [...]
+| server.background.threads                        | Integer    | 10           
   | The number of threads to use for various background processing tasks. The 
default value is 10.                                                            
                                                                                
                                                                                
                                                                                
               [...]
+| server.buffer.memory-size                        | MemorySize | 256mb        
   | The total bytes of memory the server can use, e.g, buffer write-ahead-log 
rows.                                                                           
                                                                                
                                                                                
                                                                                
               [...]
+| server.buffer.page-size                          | MemorySize | 128kb        
   | Size of every page in memory buffers (`server.buffer.memory-size`).        
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+| server.buffer.per-request-memory-size            | MemorySize | 16mb         
   | The minimum number of bytes that will be allocated by the writer rounded 
down to the closest multiple of server.buffer.page-size. It must be greater 
than or equal to server.buffer.page-size. This option allows to allocate memory 
in batches to have better CPU-cached friendliness due to contiguous segments.   
                                                                                
                    [...]
+| server.buffer.wait-timeout                       | Duration   | 2^(63)-1ns   
   | Defines how long the buffer pool will block when waiting for segments.     
                                                                                
                                                                                
                                                                                
                                                                                
              [...]
+| tablet-server.controlled-shutdown.max-retries    | Integer    | 3            
   | Maximum number of attempts to transfer leadership before proceeding with 
an unclean shutdown during a controlled shutdown procedure.                     
                                                                                
                                                                                
                                                                                
                [...]
+| tablet-server.controlled-shutdown.retry-interval | Duration   | 1000ms       
   | Time interval between retry attempts when trying to transfer leadership 
during controlled shutdown.                                                     
                                                                                
                                                                                
                                                                                
                 [...]
+| server.data-disk.write-limit-ratio               | Double     | 0.85         
   | Reject writes when the tablet server data disk usage exceeds this ratio. 
Writes resume after the usage drops below `(ratio - 0.10)`. The monitor reports 
the maximum usage across all distinct file stores so that a single nearly-full 
disk is never masked by other low-usage disks. Set to `1.0` to disable the 
disk-usage protection entirely. The valid range is `(0.1, 1.0]`. This 
configuration can be updated dy [...]
+| server.data-disk.check-interval                  | Duration   | 30s          
   | The interval at which the tablet server samples the local data disk usage 
for the write-protection state machine. A shorter interval narrows the time 
window during which writes can still flow in after the disk crosses the limit 
ratio, at the cost of slightly more frequent `statvfs` calls (which are 
in-memory and cheap). The default 30s is suitable for typical write workloads.  
                             [...]
 
 ## Zookeeper
 

Reply via email to