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

mrhhsg 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 cbfb86b3585 [improvement](spill) Monitor and log inode usage during 
spill gc (#67498)
cbfb86b3585 is described below

commit cbfb86b3585728538f6139c7a2b235af69375eee
Author: Jerry Hu <[email protected]>
AuthorDate: Fri Oct 9 15:34:13 2026 +0800

    [improvement](spill) Monitor and log inode usage during spill gc (#67498)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Problem Summary:
    
    Spill creates one directory per query and per operator plus one file per
    part, so under high concurrency a spill disk can run out of inodes long
    before it runs out of bytes. The spill capacity checks and the periodic
    `spill gc time` log only report bytes, so an inode leak or a growing
    `spill_gc` backlog cannot be diagnosed from the BE log or metrics.
    
    This PR adds inode monitoring to the spill file manager:
    
    - `LocalFileSystem::get_inode_info()` returns the total and available
      inode count of the file system holding a path via `statvfs`.
    - `SpillDataDir::update_capacity()` refreshes the inode statistics and
      exposes them as `spill_disk_inode_total` and
      `spill_disk_inode_available` metrics. A throttled warning is logged
      when the inode usage reaches `storage_flood_stage_usage_percent`.
    - The spill gc summary log now prints, per spill store, the inode usage,
      the number of query directories still pending under `spill_gc`, the
      number of directories/files deleted in this round and the number of
      failed deletions. Deletion failures are no longer silently discarded
      but logged with a throttled warning.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test:
        - Unit Test: `LocalFileSystemTest.GetInodeInfo`,
          `SpillFileTest.UpdateCapacityTracksInodeUsage`,
          `SpillFileTest.GCCleansUpGcRootBacklog`
        - Regression test: No
    - Behavior changed: No
    - Does this need documentation: No
    
    ### Check List (For Reviewer who merge this PR)
    
    - Confirm the release note
    - Confirm test cases
    - Confirm document
    - Add branch pick label
    
    
    https://claude.ai/code/session_0149x6DMH4C9RUmHLjNKDFxS
---
 be/src/exec/spill/spill_file_manager.cpp | 159 +++++++++++++++++++++++--------
 be/src/exec/spill/spill_file_manager.h   |  22 +++++
 be/src/io/fs/local_file_system.cpp       |  16 ++++
 be/src/io/fs/local_file_system.h         |   6 ++
 be/test/io/fs/local_file_system_test.cpp |  21 ++++
 be/test/vec/spill/spill_file_test.cpp    |  70 ++++++++++++++
 6 files changed, 253 insertions(+), 41 deletions(-)

diff --git a/be/src/exec/spill/spill_file_manager.cpp 
b/be/src/exec/spill/spill_file_manager.cpp
index f5629d3b501..c04cb3c3410 100644
--- a/be/src/exec/spill/spill_file_manager.cpp
+++ b/be/src/exec/spill/spill_file_manager.cpp
@@ -26,6 +26,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "common/logging.h"
 #include "common/metrics/doris_metrics.h"
@@ -410,19 +411,21 @@ void 
SpillFileManager::_retry_pending_query_spill_directories() {
 }
 
 void SpillFileManager::gc(int32_t max_work_time_ms) {
-    bool exists = true;
     bool has_work = false;
     int64_t max_work_time_ns = max_work_time_ms * 1000L * 1000L;
     MonotonicStopWatch watch;
     watch.start();
+    // One summary line per spill store, printed together with the inode usage 
of each store so
+    // that an inode leak or a growing gc backlog can be diagnosed from the BE 
log alone.
+    std::vector<std::string> store_summaries;
     Defer defer {[&]() {
         if (has_work) {
             std::string msg(
                     fmt::format("spill gc time: {}",
                                 PrettyPrinter::print(watch.elapsed_time(), 
TUnit::TIME_NS)));
             msg += ", spill storage:\n";
-            for (const auto& [path, store_dir] : _spill_store_map) {
-                msg += "    " + store_dir->debug_string();
+            for (const auto& summary : store_summaries) {
+                msg += "    " + summary;
                 msg += "\n";
             }
             LOG(INFO) << msg;
@@ -430,48 +433,74 @@ void SpillFileManager::gc(int32_t max_work_time_ms) {
     }};
     _retry_pending_query_spill_directories();
     for (const auto& [path, store_dir] : _spill_store_map) {
-        std::string gc_root_dir = store_dir->get_spill_data_gc_path();
+        SpillGcStats stats;
+        _gc_spill_store(store_dir.get(), watch, max_work_time_ns, &stats);
+        has_work |= stats.has_work;
+        store_summaries.emplace_back(fmt::format(
+                "{}, gc backlog: {} query dirs, deleted this round: {} dirs, 
{} files, failed: {}",
+                store_dir->debug_string(), stats.backlog_dirs, 
stats.deleted_dirs,
+                stats.deleted_files, stats.failed_deletes));
+    }
+}
+
+void SpillFileManager::_gc_spill_store(SpillDataDir* store_dir, const 
MonotonicStopWatch& watch,
+                                       int64_t max_work_time_ns, SpillGcStats* 
stats) {
+    std::string gc_root_dir = store_dir->get_spill_data_gc_path();
+
+    std::error_code ec;
+    bool exists = std::filesystem::exists(gc_root_dir, ec);
+    if (ec || !exists) {
+        return;
+    }
+    // dirs of queries
+    std::vector<io::FileInfo> dirs;
+    auto st = io::global_local_filesystem()->list(gc_root_dir, false, &dirs, 
&exists);
+    if (!st.ok()) {
+        return;
+    }
+
+    auto delete_entry = [&](const std::string& abs_path, bool is_file) {
+        Status delete_st = is_file ? 
io::global_local_filesystem()->delete_file(abs_path)
+                                   : 
io::global_local_filesystem()->delete_directory(abs_path);
+        if (!delete_st.ok()) {
+            ++stats->failed_deletes;
+            LOG_EVERY_T(WARNING, 60) << fmt::format("failed to delete spill gc 
entry {}: {}",
+                                                    abs_path, 
delete_st.to_string());
+            return false;
+        }
+        if (is_file) {
+            ++stats->deleted_files;
+        } else {
+            ++stats->deleted_dirs;
+        }
+        return true;
+    };
 
-        std::error_code ec;
-        exists = std::filesystem::exists(gc_root_dir, ec);
-        if (ec || !exists) {
+    for (const auto& dir : dirs) {
+        stats->has_work = true;
+        if (dir.is_file) {
             continue;
         }
-        // dirs of queries
-        std::vector<io::FileInfo> dirs;
-        auto st = io::global_local_filesystem()->list(gc_root_dir, false, 
&dirs, &exists);
+        ++stats->backlog_dirs;
+        std::string abs_dir = fmt::format("{}/{}", gc_root_dir, dir.file_name);
+        // operator spill sub dirs of a query
+        std::vector<io::FileInfo> files;
+        st = io::global_local_filesystem()->list(abs_dir, false, &files, 
&exists);
         if (!st.ok()) {
             continue;
         }
-
-        for (const auto& dir : dirs) {
-            has_work = true;
-            if (dir.is_file) {
-                continue;
-            }
-            std::string abs_dir = fmt::format("{}/{}", gc_root_dir, 
dir.file_name);
-            // operator spill sub dirs of a query
-            std::vector<io::FileInfo> files;
-            st = io::global_local_filesystem()->list(abs_dir, false, &files, 
&exists);
-            if (!st.ok()) {
-                continue;
-            }
-            if (files.empty()) {
-                
static_cast<void>(io::global_local_filesystem()->delete_directory(abs_dir));
-                continue;
+        if (files.empty()) {
+            if (delete_entry(abs_dir, false)) {
+                --stats->backlog_dirs;
             }
+            continue;
+        }
 
-            for (const auto& file : files) {
-                auto abs_file_path = fmt::format("{}/{}", abs_dir, 
file.file_name);
-                if (file.is_file) {
-                    
static_cast<void>(io::global_local_filesystem()->delete_file(abs_file_path));
-                } else {
-                    static_cast<void>(
-                            
io::global_local_filesystem()->delete_directory(abs_file_path));
-                }
-                if (watch.elapsed_time() > max_work_time_ns) {
-                    break;
-                }
+        for (const auto& file : files) {
+            auto abs_file_path = fmt::format("{}/{}", abs_dir, file.file_name);
+            delete_entry(abs_file_path, file.is_file);
+            if (watch.elapsed_time() > max_work_time_ns) {
+                break;
             }
         }
     }
@@ -481,6 +510,8 @@ DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_capacity, 
MetricUnit::BYTES);
 DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_limit, MetricUnit::BYTES);
 DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_avail_capacity, 
MetricUnit::BYTES);
 DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_data_size, MetricUnit::BYTES);
+DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_inode_total, MetricUnit::NOUNIT);
+DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_inode_available, 
MetricUnit::NOUNIT);
 DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_has_spill_data, 
MetricUnit::BYTES);
 DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_has_spill_gc_data, 
MetricUnit::BYTES);
 
@@ -495,10 +526,27 @@ SpillDataDir::SpillDataDir(std::string path, int64_t 
capacity_bytes,
     INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_limit);
     INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, 
spill_disk_avail_capacity);
     INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, 
spill_disk_data_size);
+    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, 
spill_disk_inode_total);
+    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, 
spill_disk_inode_available);
     INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, 
spill_disk_has_spill_data);
     INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, 
spill_disk_has_spill_gc_data);
 }
 
+namespace {
+// Fraction of inodes in use, 0 when the file system does not report a fixed 
inode count.
+double inode_usage(size_t total, size_t available) {
+    return total == 0 ? 0 : (double)(total - available) / (double)total;
+}
+
+std::string inode_debug_string(size_t total, size_t available) {
+    if (total == 0) {
+        return "inodes: unknown";
+    }
+    return fmt::format("inodes: total: {}, used: {}, available: {}, used pct: 
{:.2f}%", total,
+                       total - available, available, inode_usage(total, 
available) * 100);
+}
+} // namespace
+
 bool is_directory_empty(const std::filesystem::path& dir) {
     // Spill cleanup may delete the directory while the iterator is 
constructed or advanced. Treat
     // that race as empty for these presence metrics.
@@ -550,6 +598,7 @@ Status SpillDataDir::update_capacity() {
                                                                   
&_available_bytes));
     spill_disk_capacity->set_value(_disk_capacity_bytes);
     spill_disk_avail_capacity->set_value(_available_bytes);
+    _update_inode_usage();
     auto disk_use_max_bytes =
             (int64_t)(_disk_capacity_bytes * 
config::storage_flood_stage_usage_percent / 100);
     bool is_percent = true;
@@ -577,6 +626,32 @@ Status SpillDataDir::update_capacity() {
     return Status::OK();
 }
 
+// Inode statistics are for monitoring only, so a failure to read them never 
fails
+// update_capacity(); the previous gauge values are kept and a throttled 
warning is logged.
+void SpillDataDir::_update_inode_usage() {
+    size_t inode_total = 0;
+    size_t inode_available = 0;
+    auto st = io::global_local_filesystem()->get_inode_info(_path, 
&inode_total, &inode_available);
+    if (!st.ok()) {
+        LOG_EVERY_T(WARNING, 60) << fmt::format("failed to get inode info of 
spill path {}: {}",
+                                                _path, st.to_string());
+        return;
+    }
+    spill_disk_inode_total->set_value(inode_total);
+    spill_disk_inode_available->set_value(inode_available);
+
+    if (inode_total == 0) {
+        return;
+    }
+    if (inode_usage(inode_total, inode_available) >=
+        config::storage_flood_stage_usage_percent / 100.0) {
+        LOG_EVERY_T(WARNING, 60) << fmt::format(
+                "spill disk inode usage is high, path: {}, {}. Too many spill 
files or a "
+                "large spill gc backlog may exhaust inodes before disk space 
runs out",
+                _path, inode_debug_string(inode_total, inode_available));
+    }
+}
+
 bool SpillDataDir::_reach_disk_capacity_limit(int64_t incoming_data_size) {
     double used_pct = _get_disk_usage(incoming_data_size);
     int64_t left_bytes = _available_bytes - incoming_data_size;
@@ -610,12 +685,14 @@ bool SpillDataDir::reach_capacity_limit(int64_t 
incoming_data_size) {
     return false;
 }
 std::string SpillDataDir::debug_string() {
+    std::lock_guard<std::mutex> l(_mutex);
     return fmt::format(
-            "path: {}, capacity: {}, limit: {}, used: {}, available: "
-            "{}",
-            _path, PrettyPrinter::print_bytes(_disk_capacity_bytes),
+            "path: {}, capacity: {}, limit: {}, used: {}, available: {}, {}", 
_path,
+            PrettyPrinter::print_bytes(_disk_capacity_bytes),
             PrettyPrinter::print_bytes(_spill_data_limit_bytes),
             PrettyPrinter::print_bytes(_spill_data_bytes),
-            PrettyPrinter::print_bytes(_available_bytes));
+            PrettyPrinter::print_bytes(_available_bytes),
+            
inode_debug_string(static_cast<size_t>(spill_disk_inode_total->value()),
+                               
static_cast<size_t>(spill_disk_inode_available->value())));
 }
 } // namespace doris
diff --git a/be/src/exec/spill/spill_file_manager.h 
b/be/src/exec/spill/spill_file_manager.h
index db6f2dced91..137c4c40917 100644
--- a/be/src/exec/spill/spill_file_manager.h
+++ b/be/src/exec/spill/spill_file_manager.h
@@ -28,6 +28,7 @@
 #include "common/status.h"
 #include "exec/spill/spill_file.h"
 #include "storage/options.h"
+#include "util/stopwatch.hpp"
 #include "util/threadpool.h"
 
 namespace doris {
@@ -85,6 +86,7 @@ public:
 
 private:
     bool _reach_disk_capacity_limit(int64_t incoming_data_size);
+    void _update_inode_usage();
     double _get_disk_usage(int64_t incoming_data_size) const {
         return _disk_capacity_bytes == 0
                        ? 0
@@ -110,6 +112,12 @@ private:
     IntGauge* spill_disk_limit = nullptr;
     IntGauge* spill_disk_avail_capacity = nullptr;
     IntGauge* spill_disk_data_size = nullptr;
+    // inode statistics of the disk of this data dir, refreshed by 
update_capacity(). Spill creates
+    // one directory per query and per operator plus one file per part, so 
inodes may be exhausted
+    // long before bytes are. The gauges are the only storage of these values; 
the total is 0 when
+    // the file system does not report a fixed inode count.
+    IntGauge* spill_disk_inode_total = nullptr;
+    IntGauge* spill_disk_inode_available = nullptr;
     // for test
     IntGauge* spill_disk_has_spill_data = nullptr;
     IntGauge* spill_disk_has_spill_gc_data = nullptr;
@@ -191,6 +199,16 @@ private:
         std::string query_dir;
     };
 
+    // Per-store statistics of one gc round, logged in the gc summary.
+    struct SpillGcStats {
+        bool has_work = false;
+        // Query directories still under the gc root after this round, i.e. 
the gc backlog.
+        size_t backlog_dirs = 0;
+        size_t deleted_dirs = 0;
+        size_t deleted_files = 0;
+        size_t failed_deletes = 0;
+    };
+
     void _init_metrics();
     Status _init_spill_store_map();
     void _spill_gc_thread_callback();
@@ -198,6 +216,10 @@ private:
     void _retry_pending_query_spill_directories();
     Status _initialize_external_spill_session(ExternalSpillSession* 
spill_session);
     void _release_external_spill_session(ExternalSpillSession* spill_session);
+
+    // Delete the gc backlog of one spill store until `max_work_time_ns` of 
`watch` has elapsed.
+    void _gc_spill_store(SpillDataDir* store_dir, const MonotonicStopWatch& 
watch,
+                         int64_t max_work_time_ns, SpillGcStats* stats);
     std::vector<SpillDataDir*> _get_stores_for_spill(TStorageMedium::type 
storage_medium);
     SpillDataDir* _get_store_for_spill();
 
diff --git a/be/src/io/fs/local_file_system.cpp 
b/be/src/io/fs/local_file_system.cpp
index 4f9b6baf2a7..421fb71bbbd 100644
--- a/be/src/io/fs/local_file_system.cpp
+++ b/be/src/io/fs/local_file_system.cpp
@@ -24,6 +24,7 @@
 #include <openssl/md5.h>
 #include <sys/mman.h>
 #include <sys/stat.h>
+#include <sys/statvfs.h>
 #include <unistd.h>
 
 #include <filesystem>
@@ -387,6 +388,21 @@ Status LocalFileSystem::get_space_info_impl(const Path& 
path, size_t* capacity,
     return Status::OK();
 }
 
+Status LocalFileSystem::get_inode_info(const Path& path, size_t* total, 
size_t* available) {
+    FILESYSTEM_M(get_inode_info_impl(path, total, available));
+}
+
+Status LocalFileSystem::get_inode_info_impl(const Path& path, size_t* total, 
size_t* available) {
+    struct statvfs vfs {};
+    if (::statvfs(path.c_str(), &vfs) != 0) {
+        return localfs_error(errno,
+                             fmt::format("failed to get inode info for path 
{}", path.native()));
+    }
+    *total = vfs.f_files;
+    *available = vfs.f_favail;
+    return Status::OK();
+}
+
 Status LocalFileSystem::copy_path(const Path& src, const Path& dest) {
     FILESYSTEM_M(copy_path_impl(src, dest));
 }
diff --git a/be/src/io/fs/local_file_system.h b/be/src/io/fs/local_file_system.h
index b1546bc4734..b381e960a7b 100644
--- a/be/src/io/fs/local_file_system.h
+++ b/be/src/io/fs/local_file_system.h
@@ -55,6 +55,11 @@ public:
                              const std::function<bool(const FileInfo&)>& cb);
     // return disk available space where the given path is.
     Status get_space_info(const Path& path, size_t* capacity, size_t* 
available);
+    // return inode statistics of the file system where the given path is.
+    // `total` is the number of inodes in the file system and `available` is 
the number of free
+    // inodes usable by unprivileged processes. Some file systems (e.g. btrfs) 
allocate inodes
+    // dynamically and report `total` as 0, callers must treat that as 
"unknown".
+    Status get_inode_info(const Path& path, size_t* total, size_t* available);
     // Copy src path to dest path. If `src` is a directory, this method will 
call recursively for each directory entry.
     Status copy_path(const Path& src, const Path& dest);
     // return true if parent path contain sub path
@@ -101,6 +106,7 @@ protected:
     Status iterate_directory_impl(const std::string& dir,
                                   const std::function<bool(const FileInfo&)>& 
cb);
     Status get_space_info_impl(const Path& path, size_t* capacity, size_t* 
available);
+    Status get_inode_info_impl(const Path& path, size_t* total, size_t* 
available);
     Status copy_path_impl(const Path& src, const Path& dest);
     Status permission_impl(const Path& file, std::filesystem::perms prms);
 
diff --git a/be/test/io/fs/local_file_system_test.cpp 
b/be/test/io/fs/local_file_system_test.cpp
index 4321a2ae562..a3b5549f98e 100644
--- a/be/test/io/fs/local_file_system_test.cpp
+++ b/be/test/io/fs/local_file_system_test.cpp
@@ -22,6 +22,7 @@
 #include <gtest/gtest-test-part.h>
 #include <gtest/gtest.h>
 #include <sys/stat.h>
+#include <sys/statvfs.h>
 #include <sys/uio.h>
 #include <unistd.h>
 
@@ -283,6 +284,26 @@ TEST_F(LocalFileSystemTest, Exist) {
     ASSERT_TRUE(check_exist(fname));
 }
 
+TEST_F(LocalFileSystemTest, GetInodeInfo) {
+    size_t total = 0;
+    size_t available = 0;
+    auto st = io::global_local_filesystem()->get_inode_info(test_dir, &total, 
&available);
+    ASSERT_TRUE(st.ok()) << st;
+    // Some file systems (e.g. btrfs) allocate inodes dynamically and report a 
total of 0, so only
+    // the relation between the two values is portable.
+    EXPECT_GE(total, available);
+
+    struct statvfs vfs {};
+    ASSERT_EQ(::statvfs(std::string(test_dir).c_str(), &vfs), 0);
+    // The free inode count changes concurrently, so only cross-check the 
total.
+    EXPECT_EQ(total, vfs.f_files);
+
+    st = 
io::global_local_filesystem()->get_inode_info(fmt::format("{}/not_exist", 
test_dir),
+                                                       &total, &available);
+    ASSERT_FALSE(st.ok());
+    EXPECT_TRUE(st.is<ErrorCode::NOT_FOUND>()) << st;
+}
+
 TEST_F(LocalFileSystemTest, List) {
     io::FileWriterPtr file_writer;
     auto fname = fmt::format("{}/abc", test_dir);
diff --git a/be/test/vec/spill/spill_file_test.cpp 
b/be/test/vec/spill/spill_file_test.cpp
index d3d251fe147..d826649725b 100644
--- a/be/test/vec/spill/spill_file_test.cpp
+++ b/be/test/vec/spill/spill_file_test.cpp
@@ -18,6 +18,7 @@
 #include "exec/spill/spill_file.h"
 
 #include <gtest/gtest.h>
+#include <sys/statvfs.h>
 
 #include <algorithm>
 #include <filesystem>
@@ -29,6 +30,8 @@
 #include <vector>
 
 #include "common/config.h"
+#include "common/metrics/doris_metrics.h"
+#include "common/metrics/metrics.h"
 #include "core/block/block.h"
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_string.h"
@@ -42,6 +45,7 @@
 #include "runtime/exec_env.h"
 #include "runtime/fragment_mgr.h"
 #include "runtime/runtime_profile.h"
+#include "storage/olap_define.h"
 #include "testutil/column_helper.h"
 #include "testutil/mock/mock_query_context.h"
 #include "testutil/mock/mock_runtime_state.h"
@@ -962,6 +966,72 @@ TEST_F(SpillFileTest, GCCleansUpFiles) {
     ASSERT_FALSE(exists);
 }
 
+TEST_F(SpillFileTest, UpdateCapacityTracksInodeUsage) {
+    auto st = _data_dir_ptr->update_capacity();
+    ASSERT_TRUE(st.ok()) << st.to_string();
+
+    struct statvfs vfs {};
+    ASSERT_EQ(::statvfs(_data_dir_ptr->path().c_str(), &vfs), 0);
+
+    auto entity = DorisMetrics::instance()->metric_registry()->get_entity(
+            "spill_data_dir." + _spill_dir, {{"path", _spill_dir + "/" + 
SPILL_DIR_PREFIX}});
+    ASSERT_NE(entity, nullptr);
+    auto* inode_total = 
dynamic_cast<IntGauge*>(entity->get_metric("spill_disk_inode_total"));
+    auto* inode_available =
+            
dynamic_cast<IntGauge*>(entity->get_metric("spill_disk_inode_available"));
+    ASSERT_NE(inode_total, nullptr);
+    ASSERT_NE(inode_available, nullptr);
+    // The free inode count changes concurrently, so only cross-check the 
total.
+    EXPECT_EQ(inode_total->value(), static_cast<int64_t>(vfs.f_files));
+    EXPECT_LE(inode_available->value(), inode_total->value());
+
+    auto debug_string = _data_dir_ptr->debug_string();
+    if (vfs.f_files == 0) {
+        EXPECT_NE(debug_string.find("inodes: unknown"), std::string::npos) << 
debug_string;
+    } else {
+        EXPECT_NE(debug_string.find(fmt::format("inodes: total: {}, used: ", 
vfs.f_files)),
+                  std::string::npos)
+                << debug_string;
+        EXPECT_NE(debug_string.find("used pct: "), std::string::npos) << 
debug_string;
+    }
+}
+
+TEST_F(SpillFileTest, GCCleansUpGcRootBacklog) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    // A query directory holding both an operator directory with a part file 
and a loose file,
+    // plus an already empty query directory, mirror what init() and 
SpillFile::gc() leave behind.
+    const auto gc_root = _data_dir_ptr->get_spill_data_gc_path();
+    const auto pending_query_dir = 
_data_dir_ptr->get_spill_data_gc_path("pending-query");
+    const auto empty_query_dir = 
_data_dir_ptr->get_spill_data_gc_path("empty-query");
+    _create_residual_file(pending_query_dir + "/sort-1-2-3/0");
+    _create_residual_file(pending_query_dir + "/loose-file");
+    auto st = io::global_local_filesystem()->create_directory(empty_query_dir, 
false);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+
+    ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+
+    bool exists = false;
+    st = io::global_local_filesystem()->exists(pending_query_dir + 
"/sort-1-2-3", &exists);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_FALSE(exists);
+    st = io::global_local_filesystem()->exists(pending_query_dir + 
"/loose-file", &exists);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_FALSE(exists);
+    st = io::global_local_filesystem()->exists(empty_query_dir, &exists);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_FALSE(exists);
+
+    // The emptied query directory is removed by the next round, the gc root 
itself stays.
+    ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+    st = io::global_local_filesystem()->exists(pending_query_dir, &exists);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_FALSE(exists);
+    st = io::global_local_filesystem()->exists(gc_root, &exists);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_TRUE(exists);
+}
+
 TEST_F(SpillFileTest, QueryContextDeletesEmptySpillDirectory) {
     ExecEnv::GetInstance()->spill_file_mgr()->stop();
 


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

Reply via email to