Ryan19929 commented on code in PR #68740:
URL: https://github.com/apache/doris/pull/68740#discussion_r4253915674


##########
be/src/storage/storage_engine.cpp:
##########
@@ -495,9 +495,15 @@ int64_t StorageEngine::get_file_or_directory_size(const 
std::string& file_path)
 
 void StorageEngine::_start_disk_stat_monitor() {
     for (auto& it : _store_map) {
+        // Include IO-error handling: persisting the broken path can also 
stall.
+        DiskHealthCheckWatchdog::ScopedCheck 
check(_disk_health_check_watchdog);

Review Comment:
   [P2] Document the semantic change and the restart behavior after a hang
   
   With the watchdog enabled, a single hung disk terminates the whole BE 
regardless of `max_percentage_of_error_disk` / `ignore_broken_disk`.
   
   Unlike EIO, a hang never reaches `add_broken_path()`, so the path is not 
persisted to `broken_storage_path`. After a restart, the startup 
`check_datapath_rw()` hangs on the same disk and exits with 74 again, so an 
auto-restarting BE crash-loops until the operator manually adds the path to 
`broken_storage_path`.
   
   That is a reasonable trade-off for a fail-fast option, but it should be in 
the release note and the config docs together with the recovery step.



##########
be/src/common/config.cpp:
##########
@@ -376,6 +376,12 @@ DEFINE_mInt32(cache_periodic_prune_stale_sweep_sec, "60");
 DEFINE_mInt32(tablet_lookup_cache_stale_sweep_time_sec, "30");
 DEFINE_mInt32(point_query_row_cache_stale_sweep_time_sec, "300");
 DEFINE_mInt32(disk_stat_monitor_interval, "5");
+// Maximum time for one startup disk/spill check, local-engine disk health 
check,

Review Comment:
   [P2] The probe includes fsync, so a small timeout can terminate a healthy BE
   
   `read_write_test_file()` ends with `LocalFileWriter::close()`, which (with 
the default `sync_file_on_close=true`) runs `fdatasync` on the test file and 
`sync_dir` on its parent. On a healthy disk under heavy compaction/load write 
pressure (e.g. ext4 `data=ordered`, where an fsync waits for the journal 
commit), one probe can take seconds to tens of seconds.
   
   If an operator sets this close to `disk_stat_monitor_interval` (5–10 s), 
healthy BEs get `_exit(74)`. Write pressure is often cluster-wide, so several 
BEs can exit at about the same time, which is worse than the stall this guards 
against. The final supervised phase also waits for `_store_lock`, which 
`get_all_data_dir_info()` holds across `update_capacity()` (statvfs) for every 
disk.
   
   Please state in this comment and in the config docs that the probe includes 
file + directory fsync, and recommend a conservative minimum (e.g. >= 60 s), or 
enforce a lower bound in the validator.



##########
be/src/storage/storage_engine.cpp:
##########
@@ -495,9 +495,15 @@ int64_t StorageEngine::get_file_or_directory_size(const 
std::string& file_path)
 
 void StorageEngine::_start_disk_stat_monitor() {
     for (auto& it : _store_map) {
+        // Include IO-error handling: persisting the broken path can also 
stall.
+        DiskHealthCheckWatchdog::ScopedCheck 
check(_disk_health_check_watchdog);
         it.second->health_check();
     }
 
+    // These steps acquire _store_lock and may log a fatal disk failure. Keep
+    // supervising them so a stalled report or fatal logger cannot stop probing
+    // forever after the last per-disk check has completed.
+    DiskHealthCheckWatchdog::ScopedCheck check(_disk_health_check_watchdog);

Review Comment:
   [P3] This scope also covers the `LOG(FATAL)` path
   
   `_exit_if_too_many_disks_are_failed()` may `LOG(FATAL)`. If the failure 
signal handler (stack symbolization and core dump) takes longer than the 
timeout, the intended SIGABRT + core is preempted by a silent `_exit(74)`, and 
the diagnostics are lost. The process exits either way, but consider ending the 
scope before the fatal path, or documenting this.



##########
be/src/service/doris_main.cpp:
##########
@@ -505,12 +515,14 @@ int main(int argc, char** argv) {
     }
 
     if (paths.empty()) {
+        doris::DiskHealthCheckWatchdog::ScopedCheck 
check(startup_disk_watchdog);

Review Comment:
   [P3] This scope supervises `LOG` + `exit()`, not a disk check
   
   The same applies at `:539`. Because `exit()` does not unwind locals, the 
scope stays armed through `atexit` handlers and static destructors, so a slow 
teardown changes the exit code from 255 to 74. A one-line comment on the intent 
would help future readers.



##########
be/src/storage/disk_health_check_watchdog.cpp:
##########
@@ -0,0 +1,128 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "storage/disk_health_check_watchdog.h"
+
+#include <unistd.h>
+
+#include <ctime>
+#include <limits>
+
+#include "common/check.h"
+#include "common/status.h"
+#include "util/time.h"
+
+namespace doris {
+
+static_assert(std::atomic<int64_t>::is_always_lock_free);
+static_assert(std::atomic<bool>::is_always_lock_free);
+
+DiskHealthCheckWatchdog::~DiskHealthCheckWatchdog() {
+    stop();
+}
+
+Status DiskHealthCheckWatchdog::start(std::chrono::milliseconds timeout) {
+    // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces): DORIS_CHECK 
expands to if/else.
+    DORIS_CHECK(!_started && !_stopped.load(std::memory_order_acquire));
+    if (timeout.count() < 0 || timeout.count() > 
std::numeric_limits<int32_t>::max() * 1000LL) {
+        return Status::InvalidArgument("Invalid disk health check timeout: {} 
ms", timeout.count());
+    }
+    _timeout_ms = timeout.count();
+    if (_timeout_ms == 0) {
+        return Status::OK();
+    }
+    // Unlike ordinary Doris workers, this thread must work before ExecEnv is
+    // initialized and cannot depend on ThreadMgr locks or memory-tracker 
setup.
+    // Its callback neither allocates nor logs and needs no Doris thread 
context.
+    int err = pthread_create(
+            &_thread, nullptr,
+            [](void* arg) -> void* {
+                static_cast<DiskHealthCheckWatchdog*>(arg)->_run();
+                return nullptr;
+            },
+            this);
+    if (err != 0) {
+        _timeout_ms = 0;
+        return Status::RuntimeError("Cannot start disk health check watchdog, 
pthread error {}",
+                                    err);
+    }
+    _started = true;
+    return Status::OK();
+}
+
+void DiskHealthCheckWatchdog::stop() {
+    if (_started) {
+        _stopped.store(true, std::memory_order_release);
+        int err = pthread_join(_thread, nullptr);
+        DORIS_CHECK_EQ(err, 0);
+        _started = false;
+    }
+}
+
+DiskHealthCheckWatchdog::ScopedCheck::ScopedCheck(DiskHealthCheckWatchdog& 
watchdog)
+        : _watchdog(watchdog), _deadline(watchdog._begin_check()) {}
+
+DiskHealthCheckWatchdog::ScopedCheck::~ScopedCheck() {
+    _watchdog._end_check(_deadline);
+}
+
+int64_t DiskHealthCheckWatchdog::_begin_check() {
+    if (_timeout_ms == 0) {
+        return 0;
+    }
+    const int64_t deadline = MonotonicMillis() + _timeout_ms;
+    int64_t idle = 0;
+    const bool armed = _deadline.compare_exchange_strong(idle, deadline, 
std::memory_order_acq_rel);
+    if (!armed && idle == kTimedOut) {
+        // The poller may have claimed the previous check and then been
+        // descheduled before _exit(). Do not enter logging/exception handling
+        // when this producer completes and advances to its next check.
+        _exit(kTimeoutExitCode);
+    }
+    // Checks from this producer must not overlap.
+    // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces): DORIS_CHECK 
expands to if/else.
+    DORIS_CHECK(armed);
+    return deadline;
+}
+
+void DiskHealthCheckWatchdog::_end_check(int64_t deadline) {
+    if (deadline != 0) {
+        // Never clear the terminal state after the watchdog has claimed a 
timeout.
+        _deadline.compare_exchange_strong(deadline, 0, 
std::memory_order_acq_rel);
+    }
+}
+
+bool DiskHealthCheckWatchdog::_claim_timeout(int64_t deadline, int64_t now) {
+    return deadline > 0 && now >= deadline &&
+           _deadline.compare_exchange_strong(deadline, kTimedOut, 
std::memory_order_acq_rel);
+}
+
+void DiskHealthCheckWatchdog::_run() {
+    while (!_stopped.load(std::memory_order_acquire)) {
+        const int64_t deadline = _deadline.load(std::memory_order_acquire);
+        if (_claim_timeout(deadline, MonotonicMillis())) {
+            // Do not log, flush, invoke fatal-signal handlers, destroy 
objects or
+            // join threads here: any of those can wait on the failed disk.
+            _exit(kTimeoutExitCode);

Review Comment:
   [P2] The timeout exit is silent under the default launcher
   
   On timeout the process leaves no log, no core and nothing in `be.out`. With 
`bin/start_be.sh --daemon` (`nohup ... &`, lines 681/683) nothing collects the 
exit status either, so the BE just disappears. This looks the same as an 
external `SIGKILL`, and the operator cannot tell which disk hung. The header 
comment "The process supervisor can report this exit status" 
(`disk_health_check_watchdog.h:34-36`) only holds under systemd/k8s/foreground 
runs.
   
   A way to keep the never-block guarantee:
   - Let `ScopedCheck` carry a stable label (e.g. the `DataDir` path's 
`c_str()`).
   - On timeout, hand a pre-formatted message to a helper thread created in 
`start()`; the helper `write()`s it to stderr.
   - The watchdog waits only a short bounded grace period (e.g. 200 ms), then 
calls `_exit(74)` unconditionally, even if the helper's write blocks.
   
   Setting `O_NONBLOCK` on a `dup(2)` would not work: it has no effect on 
regular files such as `be.out`, and it belongs to the shared open file 
description, so it would also make fd 2 non-blocking process-wide.
   
   At minimum, please qualify the header comment and document exit status 74 in 
the config reference.



##########
be/src/storage/disk_health_check_watchdog.cpp:
##########
@@ -0,0 +1,128 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "storage/disk_health_check_watchdog.h"
+
+#include <unistd.h>
+
+#include <ctime>
+#include <limits>
+
+#include "common/check.h"
+#include "common/status.h"
+#include "util/time.h"
+
+namespace doris {
+
+static_assert(std::atomic<int64_t>::is_always_lock_free);
+static_assert(std::atomic<bool>::is_always_lock_free);
+
+DiskHealthCheckWatchdog::~DiskHealthCheckWatchdog() {
+    stop();
+}
+
+Status DiskHealthCheckWatchdog::start(std::chrono::milliseconds timeout) {
+    // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces): DORIS_CHECK 
expands to if/else.
+    DORIS_CHECK(!_started && !_stopped.load(std::memory_order_acquire));
+    if (timeout.count() < 0 || timeout.count() > 
std::numeric_limits<int32_t>::max() * 1000LL) {
+        return Status::InvalidArgument("Invalid disk health check timeout: {} 
ms", timeout.count());
+    }
+    _timeout_ms = timeout.count();
+    if (_timeout_ms == 0) {
+        return Status::OK();
+    }
+    // Unlike ordinary Doris workers, this thread must work before ExecEnv is
+    // initialized and cannot depend on ThreadMgr locks or memory-tracker 
setup.
+    // Its callback neither allocates nor logs and needs no Doris thread 
context.
+    int err = pthread_create(

Review Comment:
   [P3] Name the poller thread
   
   The raw pthread inherits the process name, so in `top -H`, 
`/proc/<pid>/task` and gdb it shows up as `doris_be` and is hard to identify. A 
`pthread_setname_np` call at thread start has no blocking risk.



##########
be/src/storage/disk_health_check_watchdog.cpp:
##########
@@ -0,0 +1,128 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "storage/disk_health_check_watchdog.h"
+
+#include <unistd.h>
+
+#include <ctime>
+#include <limits>
+
+#include "common/check.h"
+#include "common/status.h"
+#include "util/time.h"
+
+namespace doris {
+
+static_assert(std::atomic<int64_t>::is_always_lock_free);
+static_assert(std::atomic<bool>::is_always_lock_free);
+
+DiskHealthCheckWatchdog::~DiskHealthCheckWatchdog() {
+    stop();
+}
+
+Status DiskHealthCheckWatchdog::start(std::chrono::milliseconds timeout) {
+    // NOLINTNEXTLINE(readability-inconsistent-ifelse-braces): DORIS_CHECK 
expands to if/else.
+    DORIS_CHECK(!_started && !_stopped.load(std::memory_order_acquire));
+    if (timeout.count() < 0 || timeout.count() > 
std::numeric_limits<int32_t>::max() * 1000LL) {

Review Comment:
   [P3] Defensive range check is unreachable from production callers
   
   Both callers pass 
`std::chrono::seconds(config::disk_health_check_timeout_seconds)`. The 
validator already guarantees `>= 0`, and an `int32` number of seconds cannot 
exceed the upper bound. Per AGENTS.md (no defensive `if` without a known 
failure path), consider a `DORIS_CHECK` here and dropping 
`RejectsInvalidTimeouts`. The `pthread_create` error check below is fine to 
keep, since `EAGAIN` can really happen.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to