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

bobhan1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 1f5d8bcb350 [fix](be) Fix file descriptor leak when resuming log files 
(#68926)
1f5d8bcb350 is described below

commit 1f5d8bcb35008ff285d2ec3aa4f6e4e384bff49d
Author: bobhan1 <[email protected]>
AuthorDate: Wed Oct 14 18:34:48 2026 +0800

    [fix](be) Fix file descriptor leak when resuming log files (#68926)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: #3649 (the existing-log resume path)
    
    Problem Summary:
    
    When a BE restarts with `log_split_method=size` and existing
    INFO/WARNING logs, Doris's glog patch opens each resumed file with
    `open()` and then opens it again with `fopen()`. The first descriptor
    has no owner on the success path. Rotation and shutdown close only the
    `FILE*`; once the file quota unlinks the old log, the leaked descriptor
    keeps its disk blocks allocated until the BE exits.
    
    Use `fdopen(fd, "a")` in both the glog 0.4.0 and 0.6.0 patches so the
    stream owns the existing append-only, close-on-exec descriptor.
    Initialize the saved file length only after `fdopen()` succeeds, and
    close the descriptor on failure before using the existing new-file path.
    Skip resume when the file list is empty, including when a quota of one
    removes the last existing file.
    
    Add a standalone regression test against the actual static glog library
    and run it after the Linux third-party build. It covers INFO and WARNING
    logs, append behavior, rotation using the saved file length, repeated
    quota deletion, shutdown, empty directories, quota one, and injected
    `fdopen()` failure.
    
    ### Release note
    
    Fix leaked file descriptors that could retain disk space for deleted BE
    logs after restarting with size-based log splitting.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [ ] Unit Test
    - [x] Manual test (standalone third-party library regression; details
    below)
        - [ ] No need to test or manual test.
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Resumed log descriptors are released during rotation and
    shutdown; an empty resume list and failed `fdopen()` use the normal
    new-file path.
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    Validation:
    
    - Applied both updated patches to clean upstream source archives with
    `patch --dry-run --fuzz=0`, then rebuilt and installed the static
    libraries in isolated directories (0.4.0 using autotools, 0.6.0 using
    CMake; optional gflags and libunwind disabled).
    - Ran `TP_INSTALL_DIR=<rebuilt-glog-install> CXX=/usr/bin/c++ bash
    thirdparty/test/glog-logfile-resume-test.sh` for each version: all four
    scenarios passed on each version. Two active log descriptors, zero
    descriptors for deleted logs through quota rotations, and zero log
    descriptors after shutdown.
    - The same test against the existing unmodified Doris glog failed with
    `initial write: held=4 expected=2`, confirming that it detects the
    original leak.
    - `git diff --check`, Bash syntax, ShellCheck 0.8.0, shfmt 3.5.1,
    embedded C++ formatting with clang-format 16, and workflow YAML
    validation passed.
    - A full BE build, BE unit tests, and cluster regression were not run;
    validation directly rebuilt and exercised the patched dependency.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .github/workflows/build-thirdparty.yml             |   3 +
 .../pipeline/nonConcurrent/conf/fe.conf            |   2 +-
 thirdparty/patches/glog-0.4.0.patch                |  35 ++--
 thirdparty/patches/glog-0.6.0.patch                |  35 ++--
 thirdparty/test/glog-logfile-resume-test.sh        | 191 +++++++++++++++++++++
 5 files changed, 233 insertions(+), 33 deletions(-)

diff --git a/.github/workflows/build-thirdparty.yml 
b/.github/workflows/build-thirdparty.yml
index 5e25d5fdc87..a6ee808c293 100644
--- a/.github/workflows/build-thirdparty.yml
+++ b/.github/workflows/build-thirdparty.yml
@@ -201,6 +201,9 @@ jobs:
           cd "${GITHUB_WORKSPACE}"
           thirdparty/test/libunwind-page-size-test.sh
 
+          # Check descriptor ownership when resuming and rotating existing 
logs.
+          thirdparty/test/glog-logfile-resume-test.sh
+
   build_macos:
     name: Build Third Party Libraries (macOS)
     needs: changes
diff --git a/regression-test/pipeline/nonConcurrent/conf/fe.conf 
b/regression-test/pipeline/nonConcurrent/conf/fe.conf
index d65d213f0de..c0e849cf21f 100644
--- a/regression-test/pipeline/nonConcurrent/conf/fe.conf
+++ b/regression-test/pipeline/nonConcurrent/conf/fe.conf
@@ -30,7 +30,7 @@ LOG_DIR = ${DORIS_HOME}/log
 JAVA_OPTS="-Djavax.security.auth.useSubjectCredsOnly=false -Xss4m -Xmx4096m 
-XX:+HeapDumpOnOutOfMemoryError -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC 
-XX:MaxGCPauseMillis=200 -XX:+PrintGCDateStamps -XX:+PrintGCDetails 
-XX:+PrintClassHistogramAfterFullGC -Xloggc:$DORIS_HOME/log/fe.gc.log.$CUR_DATE 
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=50M 
-Dlog4j2.formatMsgNoLookups=true 
-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true"
 
 # For jdk 17, this JAVA_OPTS will be used as default JVM options
-JAVA_OPTS_FOR_JDK_17="-Djavax.security.auth.useSubjectCredsOnly=false 
-Xmx8192m -Xms8192m -XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=$DORIS_HOME/log/ 
-Xlog:gc*,classhisto*=trace:$DORIS_HOME/log/fe.gc.log.$CUR_DATE:time,uptime:filecount=10,filesize=50M
 -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true 
--add-opens=java.base/java.nio=ALL-UNNAMED 
--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED"
+JAVA_OPTS_FOR_JDK_17="-Djavax.security.auth.useSubjectCredsOnly=false 
-Xmx8192m -Xms8192m -XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=$DORIS_HOME/log/ 
-Xlog:gc*,classhisto*=trace:$DORIS_HOME/log/fe.gc.log.$CUR_DATE:time,uptime:filecount=10,filesize=50M
 -Dcom.mysql.cj.disableAbandonedConnectionCleanup=true 
-Darrow.adbc.driver.jni.library.path=${DORIS_HOME}/lib 
--add-opens=java.base/java.nio=ALL-UNNAMED 
--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED"
 
 sys_log_level = INFO
 sys_log_mode = NORMAL
diff --git a/thirdparty/patches/glog-0.4.0.patch 
b/thirdparty/patches/glog-0.4.0.patch
index 1ba0af223ce..01fea4a5bf4 100644
--- a/thirdparty/patches/glog-0.4.0.patch
+++ b/thirdparty/patches/glog-0.4.0.patch
@@ -194,7 +194,7 @@ diff -uprN a/src/logging.cc b/src/logging.cc
  void LogFileObject::Write(bool force_flush,
                            time_t timestamp,
                            const char* message,
-@@ -972,14 +1055,55 @@ void LogFileObject::Write(bool force_flu
+@@ -972,14 +1055,58 @@ void LogFileObject::Write(bool force_flu
      return;
    }
  
@@ -232,27 +232,30 @@ diff -uprN a/src/logging.cc b/src/logging.cc
  
 +  if ((file_ == NULL) && (!inited_) && (FLAGS_log_split_method == "size")) {
 +    CheckFileNumQuota();
-+    const char* filename = file_list_.back().name.c_str();
-+    int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 
FLAGS_logfile_mode);
-+    if (fd != -1) {
++    if (!file_list_.empty()) {
++      const char* filename = file_list_.back().name.c_str();
++      int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 
FLAGS_logfile_mode);
++      if (fd != -1) {
 +#ifdef HAVE_FCNTL
-+      // Mark the file close-on-exec. We don't really care if this fails
-+      fcntl(fd, F_SETFD, FD_CLOEXEC);
++        // Mark the file close-on-exec. We don't really care if this fails
++        fcntl(fd, F_SETFD, FD_CLOEXEC);
 +#endif
-+      file_ = fopen(filename, "a+"); // Read and append a FILE*.
-+      if (file_ == NULL) {      // Man, we're screwed!, try to create new log 
file
-+        close(fd);
++        file_ = fdopen(fd, "a");
++        if (file_ == NULL) {
++          close(fd);
++        } else {
++          fseek(file_, 0, SEEK_END);
++          file_length_ = bytes_since_flush_ = ftell(file_);
++          inited_ = true;
++        }
 +      }
-+      fseek(file_, 0, SEEK_END);
-+      file_length_ = bytes_since_flush_ = ftell(file_);
-+      inited_ = true;
 +    }
 +  }
 +
    // If there's no destination file, make one before outputting
    if (file_ == NULL) {
      // Try to rollover the log file every 32 log messages.  The only time
-@@ -988,7 +1112,16 @@ void LogFileObject::Write(bool force_flu
+@@ -988,7 +1115,16 @@ void LogFileObject::Write(bool force_flu
      if (++rollover_attempt_ != kRolloverAttemptFrequency) return;
      rollover_attempt_ = 0;
  
@@ -270,7 +273,7 @@ diff -uprN a/src/logging.cc b/src/logging.cc
      localtime_r(&timestamp, &tm_time);
  
      // The logfile's filename will have the date/time & pid in it
-@@ -996,13 +1129,19 @@ void LogFileObject::Write(bool force_flu
+@@ -996,13 +1132,19 @@ void LogFileObject::Write(bool force_flu
      time_pid_stream.fill('0');
      time_pid_stream << 1900+tm_time.tm_year
                      << setw(2) << 1+tm_time.tm_mon
@@ -297,7 +300,7 @@ diff -uprN a/src/logging.cc b/src/logging.cc
      const string& time_pid_string = time_pid_stream.str();
  
      if (base_filename_selected_) {
-@@ -1035,10 +1174,7 @@ void LogFileObject::Write(bool force_flu
+@@ -1035,10 +1177,7 @@ void LogFileObject::Write(bool force_flu
        // attempt to hold on to the same mutex, and get into a
        // deadlock. Simply use a name like invalid-user.
        if (uidname.empty()) uidname = "invalid-user";
@@ -309,7 +312,7 @@ diff -uprN a/src/logging.cc b/src/logging.cc
        // We're going to (potentially) try to put logs in several different 
dirs
        const vector<string> & log_dirs = GetLoggingDirectories();
  
-@@ -1062,28 +1198,6 @@ void LogFileObject::Write(bool force_flu
+@@ -1062,28 +1201,6 @@ void LogFileObject::Write(bool force_flu
          return;
        }
      }
diff --git a/thirdparty/patches/glog-0.6.0.patch 
b/thirdparty/patches/glog-0.6.0.patch
index 31cf60a2858..879c2cc884f 100644
--- a/thirdparty/patches/glog-0.6.0.patch
+++ b/thirdparty/patches/glog-0.6.0.patch
@@ -214,7 +214,7 @@ index 4028ccc..e8d9e32 100644
  void LogFileObject::Write(bool force_flush,
                            time_t timestamp,
                            const char* message,
-@@ -1149,13 +1238,55 @@ void LogFileObject::Write(bool force_flush,
+@@ -1149,13 +1238,58 @@ void LogFileObject::Write(bool force_flush,
      return;
    }
  
@@ -251,27 +251,30 @@ index 4028ccc..e8d9e32 100644
  
 +  if ((file_ == NULL) && (!inited_) && (FLAGS_log_split_method == "size")) {
 +    CheckFileNumQuota();
-+    const char* filename = file_list_.back().name.c_str();
-+    int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 
FLAGS_logfile_mode);
-+    if (fd != -1) {
++    if (!file_list_.empty()) {
++      const char* filename = file_list_.back().name.c_str();
++      int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 
FLAGS_logfile_mode);
++      if (fd != -1) {
 +#ifdef HAVE_FCNTL
-+      // Mark the file close-on-exec. We don't really care if this fails
-+      fcntl(fd, F_SETFD, FD_CLOEXEC);
++        // Mark the file close-on-exec. We don't really care if this fails
++        fcntl(fd, F_SETFD, FD_CLOEXEC);
 +#endif
-+      file_ = fopen(filename, "a+"); // Read and append a FILE*.
-+      if (file_ == NULL) {      // Man, we're screwed!, try to create new log 
file
-+        close(fd);
++        file_ = fdopen(fd, "a");
++        if (file_ == NULL) {
++          close(fd);
++        } else {
++          fseek(file_, 0, SEEK_END);
++          file_length_ = bytes_since_flush_ = ftell(file_);
++          inited_ = true;
++        }
 +      }
-+      fseek(file_, 0, SEEK_END);
-+      file_length_ = bytes_since_flush_ = ftell(file_);
-+      inited_ = true;
 +    }
 +  }
 +
    // If there's no destination file, make one before outputting
    if (file_ == NULL) {
      // Try to rollover the log file every 32 log messages.  The only time
-@@ -1164,7 +1295,19 @@ void LogFileObject::Write(bool force_flush,
+@@ -1164,7 +1298,19 @@ void LogFileObject::Write(bool force_flush,
      if (++rollover_attempt_ != kRolloverAttemptFrequency) return;
      rollover_attempt_ = 0;
  
@@ -292,7 +295,7 @@ index 4028ccc..e8d9e32 100644
      if (FLAGS_log_utc_time) {
        gmtime_r(&timestamp, &tm_time);
      } else {
-@@ -1176,13 +1319,19 @@ void LogFileObject::Write(bool force_flush,
+@@ -1176,13 +1322,19 @@ void LogFileObject::Write(bool force_flush,
      time_pid_stream.fill('0');
      time_pid_stream << 1900+tm_time.tm_year
                      << setw(2) << 1+tm_time.tm_mon
@@ -319,7 +322,7 @@ index 4028ccc..e8d9e32 100644
      const string& time_pid_string = time_pid_stream.str();
  
      if (base_filename_selected_) {
-@@ -1216,9 +1365,7 @@ void LogFileObject::Write(bool force_flush,
+@@ -1216,9 +1368,7 @@ void LogFileObject::Write(bool force_flush,
        // deadlock. Simply use a name like invalid-user.
        if (uidname.empty()) uidname = "invalid-user";
  
@@ -330,7 +333,7 @@ index 4028ccc..e8d9e32 100644
        // We're going to (potentially) try to put logs in several different 
dirs
        const vector<string> & log_dirs = GetLoggingDirectories();
  
-@@ -1243,36 +1390,6 @@ void LogFileObject::Write(bool force_flush,
+@@ -1243,36 +1393,6 @@ void LogFileObject::Write(bool force_flush,
        }
      }
  
diff --git a/thirdparty/test/glog-logfile-resume-test.sh 
b/thirdparty/test/glog-logfile-resume-test.sh
new file mode 100755
index 00000000000..d78b7f62b9f
--- /dev/null
+++ b/thirdparty/test/glog-logfile-resume-test.sh
@@ -0,0 +1,191 @@
+#!/usr/bin/env bash
+# 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.
+
+# Run after rebuilding the patched glog with build-thirdparty.sh glog.
+# TP_INSTALL_DIR can point to an isolated install of either patched glog 
version.
+# Exercise the real static library, including fdopen failure via the GNU 
linker.
+
+set -eo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)"
+TP_INSTALL_DIR="${TP_INSTALL_DIR:-${ROOT}/installed}"
+
+if [[ "$(uname -s)" != 'Linux' ]]; then
+    echo "SKIP: this test requires /proc/self/fd and GNU ld --wrap"
+    exit 0
+fi
+
+[[ -f "${TP_INSTALL_DIR}/lib/libglog.a" ]] || {
+    echo "FAIL: ${TP_INSTALL_DIR}/lib/libglog.a not found; rebuild glog first" 
>&2
+    exit 1
+}
+
+tmpdir="$(mktemp -d)"
+trap 'rm -rf "${tmpdir}"' EXIT
+
+cat >"${tmpdir}/test.cc" <<'EOF'
+#include <dirent.h>
+#include <fcntl.h>
+#include <glog/logging.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <cerrno>
+#include <cstdio>
+#include <cstdlib>
+#include <fstream>
+#include <string>
+
+static bool fail_next_fdopen = false;
+static int failed_fdopens = 0;
+
+extern "C" FILE* __real_fdopen(int fd, const char* mode);
+extern "C" FILE* __wrap_fdopen(int fd, const char* mode) {
+    if (fail_next_fdopen) {
+        fail_next_fdopen = false;
+        ++failed_fdopens;
+        errno = ENOMEM;
+        return nullptr;
+    }
+    return __real_fdopen(fd, mode);
+}
+
+static void require(bool condition, const char* message) {
+    if (!condition) {
+        fprintf(stderr, "FAIL: %s\n", message);
+        exit(1);
+    }
+}
+
+static int check_fds(const std::string& log_dir, const char* stage, int 
expected) {
+    DIR* dir = opendir("/proc/self/fd");
+    require(dir != nullptr, "cannot inspect /proc/self/fd");
+    int held = 0;
+    int seeds = 0;
+    int deleted = 0;
+    bool close_on_exec = true;
+    while (dirent* entry = readdir(dir)) {
+        const std::string path = std::string("/proc/self/fd/") + entry->d_name;
+        char target[PATH_MAX];
+        const ssize_t len = readlink(path.c_str(), target, sizeof(target) - 1);
+        if (len < 0) continue;
+        target[len] = '\0';
+        const std::string name(target);
+        if (name.find(log_dir + "/be.") != 0) continue;
+        const int fd = atoi(entry->d_name);
+        struct stat st;
+        require(fstat(fd, &st) == 0, "fstat failed");
+        if (st.st_nlink == 0) ++deleted;
+        close_on_exec = close_on_exec && (fcntl(fd, F_GETFD) & FD_CLOEXEC) != 
0;
+        ++held;
+        if (name.find(".seed") != std::string::npos) ++seeds;
+    }
+    closedir(dir);
+    printf("%s: held=%d expected=%d deleted=%d\n", stage, held, expected, 
deleted);
+    require(held == expected, "unexpected number of log fds");
+    require(deleted == 0, "deleted log still has an open fd");
+    require(close_on_exec, "log fd lost FD_CLOEXEC");
+    return seeds;
+}
+
+int main(int argc, char** argv) {
+    require(argc == 3, "usage: test LOG_DIRECTORY MODE");
+    const std::string log_dir = argv[1];
+    const std::string mode = argv[2];
+    const bool seeded = mode != "empty";
+    const google::LogSeverity severities[] = {google::GLOG_INFO, 
google::GLOG_WARNING};
+    const char* names[] = {"INFO", "WARNING"};
+    const std::string large(1024 * 1024, 's');
+    const std::string marker = "resume\n";
+    std::string seeds[2];
+    for (int i = 0; i < 2; ++i) {
+        seeds[i] = log_dir + "/be." + names[i] + ".log.seed";
+        if (seeded) {
+            std::ofstream seed(seeds[i]);
+            seed << large;
+            require(seed.good(), "cannot seed log");
+        }
+    }
+
+    FLAGS_log_dir = log_dir;
+    FLAGS_log_split_method = "size";
+    FLAGS_log_filenum_quota = mode == "quota-one" ? 1 : 2;
+    FLAGS_max_log_size = 1;
+    google::InitGoogleLogging("be");
+    time_t timestamp = 1800000000;
+    for (int i = 0; i < 2; ++i) {
+        fail_next_fdopen = mode == "fdopen-failure";
+        google::base::GetLogger(severities[i])
+                ->Write(true, timestamp, marker.data(), marker.size());
+        if (mode == "resume") {
+            struct stat st;
+            require(stat(seeds[i].c_str(), &st) == 0, "existing log 
disappeared");
+            require(st.st_size == static_cast<off_t>(large.size() + 
marker.size()),
+                    "existing log was not appended");
+        }
+    }
+    const int held_seeds = check_fds(log_dir, "initial write", 2);
+    if (mode == "resume") {
+        require(held_seeds == 2, "did not resume both existing logs");
+        // The seeded files already exceed max_log_size. Their saved length 
must
+        // trigger rotation on the next write, even though this message is 
tiny.
+        for (auto severity : severities) {
+            google::base::GetLogger(severity)->Write(true, timestamp + 1, 
marker.data(),
+                                                     marker.size());
+        }
+        require(check_fds(log_dir, "rotation from saved length", 2) == 0,
+                "resumed file length was not preserved");
+    }
+    if (mode == "fdopen-failure") {
+        require(failed_fdopens == 2, "did not inject both resume failures");
+    }
+
+    // Rotate enough times for the quota to unlink both seeded inodes. A leaked
+    // raw fd would keep the deleted files open even after closing each FILE*.
+    for (int round = 0; round < 4; ++round) {
+        timestamp += 2;
+        for (auto severity : severities) {
+            google::base::GetLogger(severity)->Write(true, timestamp, 
large.data(), large.size());
+            google::base::GetLogger(severity)->Write(true, timestamp + 1, 
marker.data(),
+                                                     marker.size());
+        }
+        check_fds(log_dir, "quota rotation", 2);
+    }
+    for (const auto& seed : seeds) {
+        require(access(seed.c_str(), F_OK) != 0 && errno == ENOENT,
+                "quota did not remove seeded log");
+    }
+    google::ShutdownGoogleLogging();
+    check_fds(log_dir, "shutdown", 0);
+    printf("PASS: %s\n", mode.c_str());
+}
+EOF
+
+extra_libs=()
+[[ -f "${TP_INSTALL_DIR}/lib/libgflags.a" ]] && 
extra_libs+=("${TP_INSTALL_DIR}/lib/libgflags.a")
+
+"${CXX:-c++}" -std=c++11 -O0 -g -Wall -Wextra -Werror \
+    -I"${TP_INSTALL_DIR}/include" "${tmpdir}/test.cc" \
+    "${TP_INSTALL_DIR}/lib/libglog.a" "${extra_libs[@]}" \
+    -Wl,--wrap=fdopen -pthread -ldl -o "${tmpdir}/test"
+
+for mode in resume empty quota-one fdopen-failure; do
+    mkdir "${tmpdir}/${mode}"
+    "${tmpdir}/test" "${tmpdir}/${mode}" "${mode}"
+done


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

Reply via email to