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

yiguolei 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 3dff55a962e [fix](be) Clean up spill directories during query teardown 
(#66328)
3dff55a962e is described below

commit 3dff55a962e4609e6f0ad8f731183b8f9d88ea91
Author: Jerry Hu <[email protected]>
AuthorDate: Wed Aug 5 09:03:58 2026 +0800

    [fix](be) Clean up spill directories during query teardown (#66328)
    
    ## What problem does this PR solve?
    
    Per-operator spill cleanup can leave empty per-query directories or
    residual files. `QueryContext` had no reliable cleanup path for all
    spill roots touched by a query. In addition, rerunnable recursive CTE
    entries retained a shared `QueryContext` reference after successful
    `FINAL_CLOSE`, so normal completion could not release the `QueryContext`
    and trigger spill cleanup.
    
    This change:
    
    - records each spill data root before the query opens its first spill
    part;
    - recursively deletes the touched per-query spill directories
    synchronously during `QueryContext` teardown;
    - retains failed deletions and retries them during later spill GC and
    shutdown paths without a fixed retry limit;
    - appends retry failures under the queue lock so directories enqueued
    while a retry batch is processed are not lost;
    - detaches recursive CTE rerunnable entries on `FINAL_CLOSE`, query
    removal, and `FragmentMgr` shutdown, and destroys them after releasing
    `_rerunnable_params_lock`.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - `./run-be-ut.sh --run
    
--filter='SpillFileTest.GCCleansUpFiles:SpillFileTest.QueryContextDeletesEmptySpillDirectory:SpillFileTest.QueryContextCleansUpNestedSpillDirectory:SpillFileTest.QueryContextDeletesResidualSpillDirectory:SpillFileTest.QueryContextCleansUpAllTouchedSpillDirectories:SpillFileTest.QueryContextContinuesCleanupAfterRootFailure:SpillFileTest.QueryContextRetriesSpillDirectoryDeletionUntilSuccess:SpillFileTest.RetryPreservesDirectoryQueuedAfterPendingDrain:SpillFileTest.FinalCloseReleasesReru
 [...]
    (11/11 passed)
    - `./run-be-ut.sh --run
    
--filter='FragmentMgrRerunnableParamsTest.StopReleasesLastQueryContextRefOutsideLock'`
    (1/1 passed)
      - `build-support/check-format.sh` (passed)
      - `git diff --check` (passed)
    - Static analysis:
    - `build-support/run-clang-tidy.sh --build-dir be/ut_build_ASAN` was
    attempted, but the local analysis was blocked by the toolchain failing
    to find `stddef.h` and an existing unmatched `NOLINTEND` in
    `be/src/core/types.h`.
    - Behavior changed: Yes. Touched per-query spill directories are removed
    synchronously during `QueryContext` teardown; transient deletion
    failures are retried, and successful recursive CTE final close releases
    rerunnable state.
    - Does this need documentation: No
---
 be/src/exec/operator/rec_cte_source_operator.h     |   8 +-
 be/src/exec/pipeline/pipeline_fragment_context.cpp |   8 +-
 be/src/exec/spill/spill_file.cpp                   |   4 +-
 be/src/exec/spill/spill_file_manager.cpp           |  85 +++-
 be/src/exec/spill/spill_file_manager.h             |  25 +-
 be/src/exec/spill/spill_file_writer.cpp            |   3 +
 be/src/runtime/fragment_mgr.cpp                    |  34 +-
 be/src/runtime/fragment_mgr.h                      |   6 +-
 be/src/runtime/query_context.cpp                   |  11 +
 be/src/runtime/query_context.h                     |   9 +
 .../fragment_mgr_cross_cluster_cancel_test.cpp     |  37 ++
 be/test/vec/spill/spill_file_test.cpp              | 497 ++++++++++++++++++++-
 12 files changed, 691 insertions(+), 36 deletions(-)

diff --git a/be/src/exec/operator/rec_cte_source_operator.h 
b/be/src/exec/operator/rec_cte_source_operator.h
index 284f754f97d..7f12f254922 100644
--- a/be/src/exec/operator/rec_cte_source_operator.h
+++ b/be/src/exec/operator/rec_cte_source_operator.h
@@ -158,16 +158,14 @@ private:
 //   │ final_close (=4)                   │
 //   │  • listen_wait_close(guard,        │
 //   │    need_send_report = true)        │
-//   │  • notify_close() on PFC           │
 //   │  • send final status report        │
-//   │  • clean up completely             │
+//   │  • remove saved rerunnable params  │
+//   │  • notify_close() on PFC           │
 //   └──────────────┬─────────────────────┘
 //                  │
 //                  ▼
 //   ┌────────────────────────────────────┐
 //   │       Fragment Destroyed           │
-//   │  rerunnable_params removed on      │
-//   │  query end                         │
 //   └────────────────────────────────────┘
 //
 class RecCTESourceOperatorX : public OperatorX<RecCTESourceLocalState> {
@@ -326,4 +324,4 @@ private:
     bool _is_used_by_other_rec_cte = false;
 };
 
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git a/be/src/exec/pipeline/pipeline_fragment_context.cpp 
b/be/src/exec/pipeline/pipeline_fragment_context.cpp
index def2cf473ae..91991ce3aa2 100644
--- a/be/src/exec/pipeline/pipeline_fragment_context.cpp
+++ b/be/src/exec/pipeline/pipeline_fragment_context.cpp
@@ -173,8 +173,8 @@ bool PipelineFragmentContext::is_timeout(timespec now) 
const {
 }
 
 // notify_close() transitions the PFC from "waiting for external close 
notification" to
-// "self-managed close". For recursive CTE fragments, the old PFC is kept 
alive until
-// the rerun_fragment(wait_for_destroy) RPC calls this to trigger shutdown.
+// "self-managed close". A recursive CTE PFC normally remains registered until 
rerun_fragment()
+// calls this for WAIT_FOR_DESTROY or FINAL_CLOSE; cancellation can also call 
it.
 // Returns true if all tasks have already closed (i.e., the PFC can be safely 
destroyed).
 bool PipelineFragmentContext::notify_close() {
     bool all_closed = false;
@@ -183,7 +183,7 @@ bool PipelineFragmentContext::notify_close() {
         std::lock_guard<std::mutex> l(_task_mutex);
         if (_closed_tasks >= _total_tasks) {
             if (_need_notify_close) {
-                // Fragment was cancelled and waiting for notify to close.
+                // The fragment finished while waiting for the external close 
notification.
                 // Record that we need to remove from fragment mgr, but do it
                 // after releasing _task_mutex to avoid ABBA deadlock with
                 // dump_pipeline_tasks() (which acquires _pipeline_map lock
@@ -192,7 +192,7 @@ bool PipelineFragmentContext::notify_close() {
             }
             all_closed = true;
         }
-        // make fragment release by self after cancel
+        // Allow the fragment to be removed now or after its remaining tasks 
close.
         _need_notify_close = false;
     }
     if (need_remove) {
diff --git a/be/src/exec/spill/spill_file.cpp b/be/src/exec/spill/spill_file.cpp
index d49d5aead1d..be9a32b61f3 100644
--- a/be/src/exec/spill/spill_file.cpp
+++ b/be/src/exec/spill/spill_file.cpp
@@ -57,8 +57,8 @@ void SpillFile::gc() {
                                                    _spill_dir, 
status.to_string());
         }
     }
-    // decrease spill data usage anyway, since in ~QueryContext() spill data 
of the query will be
-    // clean up as a last resort
+    // Decrease spill data usage even if per-file cleanup failed. QueryContext 
teardown deletes the
+    // whole query spill directory and retains failures for later retries.
     _data_dir->update_spill_data_usage(-_total_written_bytes);
     _total_written_bytes = 0;
 }
diff --git a/be/src/exec/spill/spill_file_manager.cpp 
b/be/src/exec/spill/spill_file_manager.cpp
index c74eeee9d02..eb56fb14a13 100644
--- a/be/src/exec/spill/spill_file_manager.cpp
+++ b/be/src/exec/spill/spill_file_manager.cpp
@@ -24,6 +24,7 @@
 #include <filesystem>
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "common/logging.h"
 #include "common/metrics/doris_metrics.h"
@@ -31,6 +32,7 @@
 #include "io/fs/file_system.h"
 #include "io/fs/local_file_system.h"
 #include "storage/olap_define.h"
+#include "util/debug_points.h"
 #include "util/parse_util.h"
 #include "util/pretty_printer.h"
 #include "util/time.h"
@@ -38,6 +40,11 @@
 namespace doris {
 
 SpillFileManager::~SpillFileManager() {
+    // QueryContext destruction can still queue failed deletions after stop(), 
for example while
+    // VDataStreamMgr is being destroyed. Retry them once more before dropping 
the in-memory state.
+    // Any directory that still cannot be deleted remains under the active 
spill root and will be
+    // moved to the GC root by init() after restart.
+    _retry_pending_query_spill_directories();
     DorisMetrics::instance()->metric_registry()->deregister_entity(_entity);
 }
 
@@ -45,6 +52,17 @@ SpillFileManager::SpillFileManager(
         std::unordered_map<std::string, std::unique_ptr<SpillDataDir>>&& 
spill_store_map)
         : _spill_store_map(std::move(spill_store_map)), 
_stop_background_threads_latch(1) {}
 
+void SpillFileManager::stop() {
+    _stop_background_threads_latch.count_down();
+    if (_spill_gc_thread) {
+        _spill_gc_thread->join();
+    }
+    // The GC thread may observe the stop latch before processing a recently 
queued failed deletion.
+    // Retry the pending directories after the thread exits; later failures 
get one final retry in
+    // the destructor.
+    _retry_pending_query_spill_directories();
+}
+
 Status SpillFileManager::init() {
     LOG(INFO) << "init spill stream manager";
     RETURN_IF_ERROR(_init_spill_store_map());
@@ -97,7 +115,7 @@ void SpillFileManager::_init_metrics() {
             _spill_read_bytes_metric.get()));
 }
 
-// clean up stale spilled files
+// Retry failed query-directory deletions and clean up stale spill files.
 void SpillFileManager::_spill_gc_thread_callback() {
     while (!_stop_background_threads_latch.wait_for(
             std::chrono::milliseconds(config::spill_gc_interval_ms))) {
@@ -162,6 +180,66 @@ void SpillFileManager::delete_spill_file(SpillFileSPtr 
spill_file) {
     spill_file->gc();
 }
 
+void SpillFileManager::delete_query_spill_directory(const std::string& 
query_id,
+                                                    SpillDataDir* data_dir) {
+    PendingQuerySpillDirectory pending_directory {
+            .query_dir = data_dir->get_spill_data_path(query_id),
+    };
+
+    auto status = _try_delete_query_spill_directory(pending_directory);
+    if (!status.ok()) {
+        std::lock_guard lock(_pending_query_spill_directories_mutex);
+        ++pending_directory.failed_count;
+        
_pending_query_spill_directories.emplace_back(std::move(pending_directory));
+    }
+}
+
+Status SpillFileManager::_try_delete_query_spill_directory(
+        const PendingQuerySpillDirectory& pending_directory) {
+    
DBUG_EXECUTE_IF("fault_inject::spill_file_manager::delete_query_spill_directory",
 {
+        return Status::Error<INTERNAL_ERROR>("injected query spill directory 
deletion failure");
+    });
+    const auto& fs = io::global_local_filesystem();
+    return fs->delete_directory(pending_directory.query_dir);
+}
+
+void SpillFileManager::_retry_pending_query_spill_directories() {
+    std::vector<PendingQuerySpillDirectory> pending_directories;
+    {
+        std::lock_guard lock(_pending_query_spill_directories_mutex);
+        pending_directories.swap(_pending_query_spill_directories);
+    }
+    DBUG_EXECUTE_IF(
+            
"fault_inject::spill_file_manager::retry_pending_query_spill_directories_after_drain",
+            { DBUG_RUN_CALLBACK(); });
+
+    // Limit repeated warnings for a persistently unavailable directory while 
retaining it for
+    // every subsequent retry.
+    constexpr int log_interval = 5;
+    std::vector<PendingQuerySpillDirectory> failed_directories;
+    for (auto& pending_directory : pending_directories) {
+        auto status = _try_delete_query_spill_directory(pending_directory);
+        if (status.ok()) {
+            continue;
+        }
+
+        ++pending_directory.failed_count;
+        if (pending_directory.failed_count % log_interval == 0) {
+            LOG(WARNING) << fmt::format(
+                    "failed to retry deleting spill query directory, dir {}, 
error: {}",
+                    pending_directory.query_dir, status.to_string());
+        }
+        failed_directories.emplace_back(std::move(pending_directory));
+    }
+
+    if (!failed_directories.empty()) {
+        std::lock_guard lock(_pending_query_spill_directories_mutex);
+        for (auto& pending_directory : failed_directories) {
+            
_pending_query_spill_directories.emplace_back(std::move(pending_directory));
+        }
+    }
+}
+
 void SpillFileManager::gc(int32_t max_work_time_ms) {
     bool exists = true;
     bool has_work = false;
@@ -181,6 +259,7 @@ void SpillFileManager::gc(int32_t max_work_time_ms) {
             LOG(INFO) << msg;
         }
     }};
+    _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();
 
@@ -252,12 +331,12 @@ SpillDataDir::SpillDataDir(std::string path, int64_t 
capacity_bytes,
 }
 
 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.
     try {
         return std::filesystem::is_directory(dir) &&
                std::filesystem::directory_iterator(dir) ==
                        
std::filesystem::end(std::filesystem::directory_iterator {});
-        // this method is not thread safe, the file referenced by 
directory_iterator
-        // maybe moved to spill_gc dir during this function call, so need to 
catch expection
     } catch (const std::filesystem::filesystem_error&) {
         return true;
     }
diff --git a/be/src/exec/spill/spill_file_manager.h 
b/be/src/exec/spill/spill_file_manager.h
index b90e1760236..1e3042d6ad3 100644
--- a/be/src/exec/spill/spill_file_manager.h
+++ b/be/src/exec/spill/spill_file_manager.h
@@ -20,10 +20,12 @@
 #include <atomic>
 #include <memory>
 #include <mutex>
+#include <string>
 #include <unordered_map>
 #include <vector>
 
 #include "common/metrics/metrics.h"
+#include "common/status.h"
 #include "exec/spill/spill_file.h"
 #include "storage/options.h"
 #include "util/threadpool.h"
@@ -118,12 +120,7 @@ public:
 
     Status init();
 
-    void stop() {
-        _stop_background_threads_latch.count_down();
-        if (_spill_gc_thread) {
-            _spill_gc_thread->join();
-        }
-    }
+    void stop();
 
     // Create SpillFile and register it
     // @param relative_path  Operator-formatted path under the spill root,
@@ -133,9 +130,13 @@ public:
     /// Get a unique ID for constructing spill file paths.
     uint64_t next_id() { return id_++; }
 
-    // Mark SpillFile for deletion; asynchronously delete spill files in the 
GC thread
+    // Delete SpillFile data synchronously.
     void delete_spill_file(SpillFileSPtr spill_file);
 
+    // Recursively delete a per-query spill directory during query teardown. 
Failed deletions are
+    // retained by the manager and retried by its GC and shutdown paths.
+    void delete_query_spill_directory(const std::string& query_id, 
SpillDataDir* data_dir);
+
     void gc(int32_t max_work_time_ms);
 
     void update_spill_write_bytes(int64_t bytes) { 
_spill_write_bytes_counter->increment(bytes); }
@@ -143,9 +144,16 @@ public:
     void update_spill_read_bytes(int64_t bytes) { 
_spill_read_bytes_counter->increment(bytes); }
 
 private:
+    struct PendingQuerySpillDirectory {
+        int failed_count {0};
+        std::string query_dir;
+    };
+
     void _init_metrics();
     Status _init_spill_store_map();
     void _spill_gc_thread_callback();
+    Status _try_delete_query_spill_directory(const PendingQuerySpillDirectory& 
pending_directory);
+    void _retry_pending_query_spill_directories();
     std::vector<SpillDataDir*> _get_stores_for_spill(TStorageMedium::type 
storage_medium);
 
     std::unordered_map<std::string, std::unique_ptr<SpillDataDir>> 
_spill_store_map;
@@ -153,6 +161,9 @@ private:
     CountDownLatch _stop_background_threads_latch;
     std::shared_ptr<Thread> _spill_gc_thread;
 
+    std::mutex _pending_query_spill_directories_mutex;
+    std::vector<PendingQuerySpillDirectory> _pending_query_spill_directories;
+
     std::atomic_uint64_t id_ = 0;
 
     std::shared_ptr<MetricEntity> _entity {nullptr};
diff --git a/be/src/exec/spill/spill_file_writer.cpp 
b/be/src/exec/spill/spill_file_writer.cpp
index d68d52c9640..a2dde22ddf1 100644
--- a/be/src/exec/spill/spill_file_writer.cpp
+++ b/be/src/exec/spill/spill_file_writer.cpp
@@ -149,6 +149,9 @@ Status SpillFileWriter::write_block(RuntimeState* state, 
const Block& block) {
 
     // Lazily open the first part
     if (!_file_writer) {
+        if (_current_part_index == 0) {
+            state->get_query_ctx()->record_spill_data_dir(_data_dir);
+        }
         RETURN_IF_ERROR(_open_next_part());
     }
 
diff --git a/be/src/runtime/fragment_mgr.cpp b/be/src/runtime/fragment_mgr.cpp
index 44c407875c5..ca2a700900b 100644
--- a/be/src/runtime/fragment_mgr.cpp
+++ b/be/src/runtime/fragment_mgr.cpp
@@ -353,10 +353,14 @@ void FragmentMgr::stop() {
     // destructred and remove it from _query_ctx_map_delay_delete which is 
destructring. it's UB.
     _query_ctx_map_delay_delete.clear();
     _pipeline_map.clear();
+    decltype(_rerunnable_params_map) rerunnable_params_map;
     {
         std::lock_guard<std::mutex> lk(_rerunnable_params_lock);
-        _rerunnable_params_map.clear();
+        rerunnable_params_map.swap(_rerunnable_params_map);
     }
+    // RerunableFragmentInfo holds QueryContext. Destroy it after releasing 
the lock because
+    // QueryContext::~QueryContext() can re-enter remove_query_context().
+    rerunnable_params_map.clear();
 }
 
 static void empty_function(RuntimeState*, Status*) {}
@@ -429,18 +433,23 @@ void 
FragmentMgr::remove_pipeline_context(std::pair<TUniqueId, int> key) {
 }
 
 void FragmentMgr::remove_query_context(const TUniqueId& key) {
-    // Clean up any saved rerunnable params for this query to avoid memory 
leaks.
-    // This covers both cancel and normal destruction paths.
+    // Successful FINAL_CLOSE removes rerunnable entries one fragment at a 
time. Detach any entries
+    // that remain when the query is removed, such as on cancellation.
+    decltype(_rerunnable_params_map) rerunnable_params_map;
     {
         std::lock_guard<std::mutex> lk(_rerunnable_params_lock);
         for (auto it = _rerunnable_params_map.begin(); it != 
_rerunnable_params_map.end();) {
             if (it->first.first == key) {
-                it = _rerunnable_params_map.erase(it);
+                auto current = it++;
+                
rerunnable_params_map.insert(_rerunnable_params_map.extract(current));
             } else {
                 ++it;
             }
         }
     }
+    // Destroy detached entries outside _rerunnable_params_lock. Releasing 
their last QueryContext
+    // reference can run QueryContext::~QueryContext(), which calls this 
method.
+    rerunnable_params_map.clear();
     _query_ctx_map_delay_delete.erase(key);
 #ifndef BE_TEST
     _query_ctx_map.erase(key);
@@ -1253,10 +1262,10 @@ Status FragmentMgr::transmit_rec_cte_block(
 // wait_for_destroy: collect deregister RF IDs, store brpc closure, trigger 
old PFC close
 // rebuild: increment stage, deregister old RFs, create+prepare new PFC from 
saved params
 // submit: submit the new PFC's pipeline tasks for execution
-// final_close: async wait for close, send final report, clean up (last round 
only)
+// final_close: send final report, detach saved params, trigger the last PFC 
close
 //
-// The brpc ClosureGuard is stored in the PFC so the RPC response is deferred 
until
-// the PFC is fully destroyed. This gives the caller (RecCTESourceOperatorX) a
+// During wait_for_destroy, the brpc ClosureGuard is stored in the PFC so the 
RPC response is
+// deferred until the PFC is fully destroyed. This gives the caller 
(RecCTESourceOperatorX) a
 // synchronization point to know when the old PFC has finished all its tasks.
 Status FragmentMgr::rerun_fragment(const std::shared_ptr<brpc::ClosureGuard>& 
guard,
                                    const TUniqueId& query_id, int fragment_id,
@@ -1294,6 +1303,17 @@ Status FragmentMgr::rerun_fragment(const 
std::shared_ptr<brpc::ClosureGuard>& gu
         SCOPED_ATTACH_TASK(query_ctx);
         RETURN_IF_ERROR(
                 fragment_ctx->listen_wait_close(guard, stage == 
PRerunFragmentParams::FINAL_CLOSE));
+
+        if (stage == PRerunFragmentParams::FINAL_CLOSE) {
+            // Detach the entry under the lock and destroy it afterward. This 
keeps the invariant
+            // that RerunableFragmentInfo, which owns QueryContext, is never 
destroyed while holding
+            // _rerunnable_params_lock.
+            decltype(_rerunnable_params_map)::node_type final_close_info;
+            {
+                std::lock_guard<std::mutex> lk(_rerunnable_params_lock);
+                final_close_info = _rerunnable_params_map.extract({query_id, 
fragment_id});
+            }
+        }
         fragment_ctx->notify_close();
         return Status::OK();
     } else if (stage == PRerunFragmentParams::REBUILD) {
diff --git a/be/src/runtime/fragment_mgr.h b/be/src/runtime/fragment_mgr.h
index ab78c18555a..9065ae9e5a8 100644
--- a/be/src/runtime/fragment_mgr.h
+++ b/be/src/runtime/fragment_mgr.h
@@ -228,8 +228,10 @@ private:
 
     // Saved params and callback for rerunnable (recursive CTE) fragments.
     // Only populated when need_notify_close == true during exec_plan_fragment.
-    // Lifecycle: created in exec_plan_fragment(), used in 
rerun_fragment(rebuild)
-    // to recreate PFC with fresh state, cleaned up in remove_query_context().
+    // Lifecycle: created in exec_plan_fragment(), retained across 
wait/rebuild/submit rounds,
+    // and removed after a successful final_close. remove_query_context() and 
stop() provide
+    // fallback cleanup. Entries are detached under _rerunnable_params_lock 
and destroyed afterward;
+    // releasing their last QueryContext reference can call 
FragmentMgr::remove_query_context().
     struct RerunableFragmentInfo {
         // Runtime filter IDs registered by the old PFC, collected during 
wait_for_destroy.
         // These are deregistered from the RuntimeFilterMgr before the new PFC 
is created.
diff --git a/be/src/runtime/query_context.cpp b/be/src/runtime/query_context.cpp
index bc0e7ed1502..3f7ef26e40c 100644
--- a/be/src/runtime/query_context.cpp
+++ b/be/src/runtime/query_context.cpp
@@ -225,6 +225,11 @@ void QueryContext::init_query_task_controller() {
 #endif
 }
 
+void QueryContext::record_spill_data_dir(SpillDataDir* data_dir) {
+    std::lock_guard lock(_spill_data_dirs_mutex);
+    _spill_data_dirs.emplace(data_dir);
+}
+
 QueryContext::~QueryContext() {
     SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(query_mem_tracker());
     // query mem tracker consumption is equal to 0, it means that after 
QueryContext is created,
@@ -253,6 +258,12 @@ QueryContext::~QueryContext() {
     obj_pool.clear();
     _merge_controller_handler.reset();
 
+    if (auto* spill_file_mgr = _exec_env->spill_file_mgr()) {
+        for (auto* data_dir : _spill_data_dirs) {
+            spill_file_mgr->delete_query_spill_directory(print_id(_query_id), 
data_dir);
+        }
+    }
+
     DorisMetrics::instance()->query_ctx_cnt->increment(-1);
     // fragment_mgr is nullptr in unittest
     if (ExecEnv::GetInstance()->fragment_mgr()) {
diff --git a/be/src/runtime/query_context.h b/be/src/runtime/query_context.h
index 2dedffb7576..5d327c2f348 100644
--- a/be/src/runtime/query_context.h
+++ b/be/src/runtime/query_context.h
@@ -28,6 +28,7 @@
 #include <mutex>
 #include <string>
 #include <unordered_map>
+#include <unordered_set>
 
 #include "common/config.h"
 #include "common/factory_creator.h"
@@ -55,6 +56,7 @@ class PipelineTask;
 class QueryTaskController;
 class Dependency;
 class RecCTEScanLocalState;
+class SpillDataDir;
 
 struct ReportStatusRequest {
     const Status status;
@@ -203,6 +205,10 @@ public:
 
     TUniqueId query_id() const { return _query_id; }
 
+    // Record a spill data directory before opening the first spill part so 
teardown only visits
+    // touched roots.
+    void record_spill_data_dir(SpillDataDir* data_dir);
+
     // Expose task-level query progress counters for runtime statistics 
reporting.
     void add_total_task_num(int delta);
     void inc_finished_task_num();
@@ -329,6 +335,9 @@ private:
     MonotonicStopWatch _query_watcher;
     bool _is_nereids = false;
 
+    std::mutex _spill_data_dirs_mutex;
+    std::unordered_set<SpillDataDir*> _spill_data_dirs;
+
     std::shared_ptr<ResourceContext> _resource_ctx;
 
     void _init_resource_context();
diff --git a/be/test/runtime/fragment_mgr_cross_cluster_cancel_test.cpp 
b/be/test/runtime/fragment_mgr_cross_cluster_cancel_test.cpp
index 70dd2e874ba..85de9266100 100644
--- a/be/test/runtime/fragment_mgr_cross_cluster_cancel_test.cpp
+++ b/be/test/runtime/fragment_mgr_cross_cluster_cancel_test.cpp
@@ -183,4 +183,41 @@ TEST(FragmentMgrDelayDeleteMapTest, 
ClearShouldNotAbortWhenReleasingLastQueryCon
     exec_env->_fragment_mgr = previous_fragment_mgr;
 }
 
+TEST(FragmentMgrRerunnableParamsTest, 
StopReleasesLastQueryContextRefOutsideLock) {
+    auto* exec_env = ExecEnv::GetInstance();
+    auto* previous_fragment_mgr = exec_env->_fragment_mgr;
+    auto* fragment_mgr = new FragmentMgr(exec_env);
+    exec_env->_fragment_mgr = fragment_mgr;
+
+    TUniqueId query_id;
+    query_id.__set_hi(303);
+    query_id.__set_lo(404);
+
+    TQueryOptions query_options;
+    query_options.__set_query_type(TQueryType::SELECT);
+    query_options.__set_execution_timeout(60);
+    query_options.__set_mem_limit(64L * 1024 * 1024);
+
+    TNetworkAddress fe_addr;
+    fe_addr.hostname = "127.0.0.1";
+    fe_addr.port = 9030;
+
+    auto query_ctx =
+            QueryContext::create(query_id, exec_env, query_options, fe_addr,
+                                 /*is_nereids*/ true, fe_addr, 
QuerySource::INTERNAL_FRONTEND);
+    std::weak_ptr<QueryContext> weak_query_ctx = query_ctx;
+    {
+        std::lock_guard<std::mutex> 
lock(fragment_mgr->_rerunnable_params_lock);
+        fragment_mgr->_rerunnable_params_map[{query_id, 1}].query_ctx = 
query_ctx;
+    }
+    query_ctx.reset();
+
+    EXPECT_FALSE(weak_query_ctx.expired());
+    fragment_mgr->stop();
+    EXPECT_TRUE(weak_query_ctx.expired());
+
+    exec_env->_fragment_mgr = previous_fragment_mgr;
+    delete fragment_mgr;
+}
+
 } // namespace doris
diff --git a/be/test/vec/spill/spill_file_test.cpp 
b/be/test/vec/spill/spill_file_test.cpp
index 09173a7a343..9df4bf626f3 100644
--- a/be/test/vec/spill/spill_file_test.cpp
+++ b/be/test/vec/spill/spill_file_test.cpp
@@ -21,22 +21,32 @@
 
 #include <algorithm>
 #include <filesystem>
+#include <functional>
 #include <memory>
 #include <numeric>
+#include <set>
+#include <string>
 #include <vector>
 
 #include "common/config.h"
 #include "core/block/block.h"
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_string.h"
+#include "exec/pipeline/pipeline_fragment_context.h"
 #include "exec/spill/spill_file_manager.h"
 #include "exec/spill/spill_file_reader.h"
 #include "exec/spill/spill_file_writer.h"
+#include "io/fs/file_writer.h"
 #include "io/fs/local_file_system.h"
 #include "runtime/exec_env.h"
+#include "runtime/fragment_mgr.h"
 #include "runtime/runtime_profile.h"
 #include "testutil/column_helper.h"
+#include "testutil/mock/mock_query_context.h"
 #include "testutil/mock/mock_runtime_state.h"
+#include "util/debug_points.h"
+#include "util/defer_op.h"
+#include "util/uid_util.h"
 
 namespace doris::vectorized {
 
@@ -78,13 +88,21 @@ protected:
         _profile->add_child(_common_profile.get(), true);
 
         _spill_dir = "./ut_dir/spill_file_test";
-        auto spill_data_dir = std::make_unique<SpillDataDir>(_spill_dir, 1024L 
* 1024 * 128);
+        _second_spill_dir = "./ut_dir/spill_file_test_second";
+        auto spill_data_dir =
+                std::make_unique<SpillDataDir>(_spill_dir, 1024L * 1024 * 128, 
TStorageMedium::SSD);
         auto st = 
io::global_local_filesystem()->create_directory(spill_data_dir->path(), false);
         ASSERT_TRUE(st.ok()) << "create directory failed: " << st.to_string();
+        auto second_spill_data_dir = std::make_unique<SpillDataDir>(
+                _second_spill_dir, 1024L * 1024 * 128, TStorageMedium::HDD);
+        st = 
io::global_local_filesystem()->create_directory(second_spill_data_dir->path(), 
false);
+        ASSERT_TRUE(st.ok()) << "create directory failed: " << st.to_string();
 
         std::unordered_map<std::string, std::unique_ptr<SpillDataDir>> 
data_map;
         _data_dir_ptr = spill_data_dir.get();
+        _second_data_dir_ptr = second_spill_data_dir.get();
         data_map.emplace("test", std::move(spill_data_dir));
+        data_map.emplace("test_second", std::move(second_spill_data_dir));
         auto* spill_file_manager = new SpillFileManager(std::move(data_map));
         ExecEnv::GetInstance()->_spill_file_mgr = spill_file_manager;
         st = spill_file_manager->init();
@@ -93,11 +111,13 @@ protected:
 
     void TearDown() override {
         ExecEnv::GetInstance()->spill_file_mgr()->stop();
+        _runtime_state.reset();
         SAFE_DELETE(ExecEnv::GetInstance()->_spill_file_mgr);
         // Clean up test directory
         auto st = io::global_local_filesystem()->delete_directory(_spill_dir);
         (void)st;
-        _runtime_state.reset();
+        st = 
io::global_local_filesystem()->delete_directory(_second_spill_dir);
+        (void)st;
     }
 
     Block _create_int_block(const std::vector<int32_t>& data) {
@@ -111,12 +131,58 @@ protected:
         return block;
     }
 
+    void _write_and_release_spill_file(const TUniqueId& query_id, 
QueryContext* query_ctx,
+                                       SpillDataDir* data_dir, const 
std::string& relative_path) {
+        TQueryGlobals query_globals;
+        auto runtime_state = std::make_unique<MockRuntimeState>(
+                query_id, 0, query_ctx->query_options(), query_globals, 
ExecEnv::GetInstance(),
+                query_ctx);
+
+        auto spill_file = std::make_shared<SpillFile>(
+                data_dir, fmt::format("{}/{}", print_id(query_id), 
relative_path));
+
+        SpillFileWriterSPtr writer;
+        auto st = spill_file->create_writer(runtime_state.get(), 
_profile.get(), writer);
+        ASSERT_TRUE(st.ok());
+        auto block = _create_int_block({1, 2, 3});
+        st = writer->write_block(runtime_state.get(), block);
+        ASSERT_TRUE(st.ok());
+        st = writer->close();
+        ASSERT_TRUE(st.ok());
+        writer.reset();
+        spill_file.reset();
+    }
+
+    void _create_residual_file(const std::string& file_path) {
+        auto st = io::global_local_filesystem()->create_directory(
+                std::filesystem::path(file_path).parent_path(), false);
+        ASSERT_TRUE(st.ok()) << st.to_string();
+        io::FileWriterPtr writer;
+        st = io::global_local_filesystem()->create_file(file_path, &writer);
+        ASSERT_TRUE(st.ok()) << st.to_string();
+        st = writer->close();
+        ASSERT_TRUE(st.ok()) << st.to_string();
+    }
+
+    std::set<std::string> _gc_subdirectories(SpillDataDir* data_dir) {
+        std::set<std::string> subdirectories;
+        for (const auto& entry :
+             
std::filesystem::directory_iterator(data_dir->get_spill_data_gc_path())) {
+            if (entry.is_directory()) {
+                subdirectories.emplace(entry.path().filename().string());
+            }
+        }
+        return subdirectories;
+    }
+
     std::unique_ptr<MockRuntimeState> _runtime_state;
     std::unique_ptr<RuntimeProfile> _profile;
     std::unique_ptr<RuntimeProfile> _custom_profile;
     std::unique_ptr<RuntimeProfile> _common_profile;
     std::string _spill_dir;
+    std::string _second_spill_dir;
     SpillDataDir* _data_dir_ptr = nullptr;
+    SpillDataDir* _second_data_dir_ptr = nullptr;
 };
 
 // ═══════════════════════════════════════════════════════════════════════
@@ -889,7 +955,420 @@ TEST_F(SpillFileTest, GCCleansUpFiles) {
     ASSERT_FALSE(exists);
 }
 
-TEST_F(SpillFileTest, DeleteSpillFileThroughManager) {
+TEST_F(SpillFileTest, QueryContextDeletesEmptySpillDirectory) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    TUniqueId query_id;
+    query_id.hi = 1;
+    query_id.lo = 2;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+
+    auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr);
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, 
"query_context_gc");
+
+    bool exists = false;
+    auto st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+
+    query_ctx.reset();
+
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_FALSE(exists);
+    st = 
io::global_local_filesystem()->exists(_data_dir_ptr->get_spill_data_path(), 
&exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+    EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before);
+}
+
+TEST_F(SpillFileTest, QueryContextCleansUpNestedSpillDirectory) {
+    TUniqueId query_id;
+    query_id.hi = 3;
+    query_id.lo = 4;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+
+    auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    auto nested_dir = query_dir + "/nested";
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr,
+                                  "nested/query_context_gc");
+
+    bool exists = false;
+    auto st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+    st = io::global_local_filesystem()->exists(nested_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+
+    query_ctx.reset();
+
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_FALSE(exists);
+}
+
+TEST_F(SpillFileTest, QueryContextDeletesResidualSpillDirectory) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    TUniqueId query_id;
+    query_id.hi = 5;
+    query_id.lo = 6;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+
+    auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr);
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, 
"query_context_gc");
+
+    auto residual_file = query_dir + "/residual/temporary-data";
+    _create_residual_file(residual_file);
+
+    bool exists = false;
+    auto st = io::global_local_filesystem()->exists(residual_file, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+
+    query_ctx.reset();
+
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_FALSE(exists);
+
+    EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before);
+}
+
+TEST_F(SpillFileTest, QueryContextCleansUpAllTouchedSpillDirectories) {
+    TUniqueId query_id;
+    query_id.hi = 9;
+    query_id.lo = 10;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+
+    auto first_query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    auto second_query_dir = 
_second_data_dir_ptr->get_spill_data_path(query_id_str);
+    const auto first_gc_subdirectories_before = 
_gc_subdirectories(_data_dir_ptr);
+    const auto second_gc_subdirectories_before = 
_gc_subdirectories(_second_data_dir_ptr);
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, 
"first");
+    _write_and_release_spill_file(query_id, query_ctx.get(), 
_second_data_dir_ptr, "second");
+
+    bool first_exists = false;
+    bool second_exists = false;
+    auto st = io::global_local_filesystem()->exists(first_query_dir, 
&first_exists);
+    ASSERT_TRUE(st.ok());
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(first_exists);
+    ASSERT_TRUE(second_exists);
+
+    query_ctx.reset();
+
+    st = io::global_local_filesystem()->exists(first_query_dir, &first_exists);
+    ASSERT_TRUE(st.ok());
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_FALSE(first_exists);
+    ASSERT_FALSE(second_exists);
+    EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), 
first_gc_subdirectories_before);
+    EXPECT_EQ(_gc_subdirectories(_second_data_dir_ptr), 
second_gc_subdirectories_before);
+}
+
+TEST_F(SpillFileTest, QueryContextContinuesCleanupAfterRootFailure) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    TUniqueId query_id;
+    query_id.hi = 11;
+    query_id.lo = 12;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+
+    auto first_query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    auto second_query_dir = 
_second_data_dir_ptr->get_spill_data_path(query_id_str);
+    const auto first_gc_subdirectories_before = 
_gc_subdirectories(_data_dir_ptr);
+    const auto second_gc_subdirectories_before = 
_gc_subdirectories(_second_data_dir_ptr);
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, 
"first");
+    _write_and_release_spill_file(query_id, query_ctx.get(), 
_second_data_dir_ptr, "second");
+    _create_residual_file(first_query_dir + "/residual/temporary-data");
+    _create_residual_file(second_query_dir + "/residual/temporary-data");
+
+    const auto live_query_dir = 
_data_dir_ptr->get_spill_data_path("live-query");
+    const auto live_query_file = live_query_dir + "/sentinel";
+    _create_residual_file(live_query_file);
+
+    bool first_exists = false;
+    bool second_exists = false;
+    auto st = io::global_local_filesystem()->exists(first_query_dir, 
&first_exists);
+    ASSERT_TRUE(st.ok());
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(first_exists);
+    ASSERT_TRUE(second_exists);
+
+    const bool previous_enable_debug_points = config::enable_debug_points;
+    constexpr auto debug_point_name =
+            "fault_inject::spill_file_manager::delete_query_spill_directory";
+    Defer restore_debug_point([&] {
+        DebugPoints::instance()->remove(debug_point_name);
+        config::enable_debug_points = previous_enable_debug_points;
+    });
+    auto debug_point = std::make_shared<DebugPoint>();
+    debug_point->execute_limit = 1;
+    config::enable_debug_points = true;
+    DebugPoints::instance()->add(debug_point_name, debug_point);
+
+    query_ctx.reset();
+
+    st = io::global_local_filesystem()->exists(first_query_dir, &first_exists);
+    ASSERT_TRUE(st.ok());
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_NE(first_exists, second_exists);
+    ASSERT_EQ(debug_point->execute_num.load(), 2);
+
+    ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+
+    st = io::global_local_filesystem()->exists(first_query_dir, &first_exists);
+    ASSERT_TRUE(st.ok());
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    EXPECT_FALSE(first_exists);
+    EXPECT_FALSE(second_exists);
+
+    bool live_query_exists = false;
+    st = io::global_local_filesystem()->exists(live_query_file, 
&live_query_exists);
+    ASSERT_TRUE(st.ok());
+    EXPECT_TRUE(live_query_exists);
+
+    EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), 
first_gc_subdirectories_before);
+    EXPECT_EQ(_gc_subdirectories(_second_data_dir_ptr), 
second_gc_subdirectories_before);
+}
+
+TEST_F(SpillFileTest, QueryContextRetriesSpillDirectoryDeletionUntilSuccess) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    TUniqueId query_id;
+    query_id.hi = 15;
+    query_id.lo = 16;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+
+    auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr);
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, 
"retry_cleanup");
+    _create_residual_file(query_dir + "/residual/temporary-data");
+
+    const bool previous_enable_debug_points = config::enable_debug_points;
+    constexpr auto debug_point_name =
+            "fault_inject::spill_file_manager::delete_query_spill_directory";
+    Defer restore_debug_point([&] {
+        DebugPoints::instance()->remove(debug_point_name);
+        config::enable_debug_points = previous_enable_debug_points;
+    });
+    auto debug_point = std::make_shared<DebugPoint>();
+    debug_point->execute_limit = 5;
+    config::enable_debug_points = true;
+    DebugPoints::instance()->add(debug_point_name, debug_point);
+
+    query_ctx.reset();
+
+    bool exists = false;
+    auto st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+
+    for (int i = 0; i < 4; ++i) {
+        ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+        st = io::global_local_filesystem()->exists(query_dir, &exists);
+        ASSERT_TRUE(st.ok());
+        ASSERT_TRUE(exists);
+    }
+
+    ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    EXPECT_FALSE(exists);
+    EXPECT_EQ(debug_point->execute_num.load(), 6);
+    EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before);
+}
+
+TEST_F(SpillFileTest, RetryPreservesDirectoryQueuedAfterPendingDrain) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    TUniqueId first_query_id;
+    first_query_id.hi = 17;
+    first_query_id.lo = 18;
+    auto first_query_ctx = MockQueryContext::create(first_query_id);
+    auto first_query_dir = 
_data_dir_ptr->get_spill_data_path(print_id(first_query_id));
+    _write_and_release_spill_file(first_query_id, first_query_ctx.get(), 
_data_dir_ptr,
+                                  "first_retry_cleanup");
+    _create_residual_file(first_query_dir + "/residual/temporary-data");
+
+    TUniqueId second_query_id;
+    second_query_id.hi = 19;
+    second_query_id.lo = 20;
+    auto second_query_ctx = MockQueryContext::create(second_query_id);
+    auto second_query_dir = 
_data_dir_ptr->get_spill_data_path(print_id(second_query_id));
+    _write_and_release_spill_file(second_query_id, second_query_ctx.get(), 
_data_dir_ptr,
+                                  "second_retry_cleanup");
+    _create_residual_file(second_query_dir + "/residual/temporary-data");
+
+    const bool previous_enable_debug_points = config::enable_debug_points;
+    constexpr auto delete_debug_point_name =
+            "fault_inject::spill_file_manager::delete_query_spill_directory";
+    constexpr auto after_drain_debug_point_name =
+            
"fault_inject::spill_file_manager::retry_pending_query_spill_directories_after_drain";
+    Defer restore_debug_points([&] {
+        DebugPoints::instance()->remove(after_drain_debug_point_name);
+        DebugPoints::instance()->remove(delete_debug_point_name);
+        config::enable_debug_points = previous_enable_debug_points;
+    });
+    config::enable_debug_points = true;
+    DebugPoints::instance()->add(delete_debug_point_name);
+
+    first_query_ctx.reset();
+
+    auto after_drain_debug_point = std::make_shared<DebugPoint>();
+    after_drain_debug_point->execute_limit = 1;
+    after_drain_debug_point->callback = std::function<void()>([&]() { 
second_query_ctx.reset(); });
+    DebugPoints::instance()->add(after_drain_debug_point_name, 
after_drain_debug_point);
+
+    ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+
+    bool first_exists = false;
+    auto st = io::global_local_filesystem()->exists(first_query_dir, 
&first_exists);
+    ASSERT_TRUE(st.ok());
+    bool second_exists = false;
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(first_exists);
+    ASSERT_TRUE(second_exists);
+    ASSERT_EQ(after_drain_debug_point->execute_num.load(), 1);
+
+    DebugPoints::instance()->remove(after_drain_debug_point_name);
+    DebugPoints::instance()->remove(delete_debug_point_name);
+    ExecEnv::GetInstance()->spill_file_mgr()->gc(10000);
+
+    st = io::global_local_filesystem()->exists(first_query_dir, &first_exists);
+    ASSERT_TRUE(st.ok());
+    st = io::global_local_filesystem()->exists(second_query_dir, 
&second_exists);
+    ASSERT_TRUE(st.ok());
+    EXPECT_FALSE(first_exists);
+    EXPECT_FALSE(second_exists);
+}
+
+TEST_F(SpillFileTest, 
FinalCloseReleasesRerunnableQueryContextAndDeletesSpillDirectory) {
+    ExecEnv::GetInstance()->spill_file_mgr()->stop();
+
+    auto* exec_env = ExecEnv::GetInstance();
+    auto* previous_fragment_mgr = exec_env->_fragment_mgr;
+    auto* fragment_mgr = new FragmentMgr(exec_env);
+    exec_env->_fragment_mgr = fragment_mgr;
+
+    TUniqueId query_id;
+    query_id.hi = 13;
+    query_id.lo = 14;
+    constexpr int first_fragment_id = 1;
+    constexpr int second_fragment_id = 2;
+    Defer restore_fragment_mgr([&] {
+        fragment_mgr->remove_query_context(query_id);
+        fragment_mgr->remove_pipeline_context({query_id, first_fragment_id});
+        fragment_mgr->remove_pipeline_context({query_id, second_fragment_id});
+        fragment_mgr->stop();
+        delete fragment_mgr;
+        exec_env->_fragment_mgr = previous_fragment_mgr;
+    });
+
+    auto query_ctx = MockQueryContext::create(query_id);
+    std::weak_ptr<QueryContext> weak_query_ctx = query_ctx;
+    const auto query_id_str = print_id(query_id);
+    const auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+    const auto gc_subdirectories_before = _gc_subdirectories(_data_dir_ptr);
+    _write_and_release_spill_file(query_id, query_ctx.get(), _data_dir_ptr, 
"recursive_cte");
+    _create_residual_file(query_dir + "/residual/temporary-data");
+
+    auto create_fragment_context = [&](int fragment_id) {
+        TPipelineFragmentParams params;
+        params.__set_query_id(query_id);
+        params.__set_fragment_id(fragment_id);
+        params.__set_need_notify_close(true);
+        auto context = std::make_shared<PipelineFragmentContext>(
+                query_id, params, query_ctx, exec_env, [](RuntimeState*, 
Status*) {});
+        fragment_mgr->_pipeline_map.insert({query_id, fragment_id}, context);
+        g_fragment_executing_count << 1;
+        query_ctx->set_pipeline_context(fragment_id, context);
+        {
+            std::lock_guard lock(fragment_mgr->_rerunnable_params_lock);
+            auto& info = fragment_mgr->_rerunnable_params_map[{query_id, 
fragment_id}];
+            info.params = params;
+            info.query_ctx = query_ctx;
+        }
+        return context;
+    };
+
+    auto first_fragment_ctx = create_fragment_context(first_fragment_id);
+    auto second_fragment_ctx = create_fragment_context(second_fragment_id);
+    query_ctx.reset();
+    first_fragment_ctx.reset();
+    second_fragment_ctx.reset();
+
+    ASSERT_FALSE(weak_query_ctx.expired());
+    auto st = fragment_mgr->rerun_fragment({}, query_id, first_fragment_id,
+                                           PRerunFragmentParams::FINAL_CLOSE);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_EQ(fragment_mgr->_pipeline_map.num_items(), 1);
+    {
+        std::lock_guard lock(fragment_mgr->_rerunnable_params_lock);
+        EXPECT_FALSE(fragment_mgr->_rerunnable_params_map.contains({query_id, 
first_fragment_id}));
+        EXPECT_TRUE(fragment_mgr->_rerunnable_params_map.contains({query_id, 
second_fragment_id}));
+    }
+    EXPECT_FALSE(weak_query_ctx.expired());
+
+    bool exists = false;
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    EXPECT_TRUE(exists);
+
+    st = fragment_mgr->rerun_fragment({}, query_id, second_fragment_id,
+                                      PRerunFragmentParams::FINAL_CLOSE);
+    ASSERT_TRUE(st.ok()) << st.to_string();
+    EXPECT_EQ(fragment_mgr->_pipeline_map.num_items(), 0);
+    {
+        std::lock_guard lock(fragment_mgr->_rerunnable_params_lock);
+        EXPECT_TRUE(fragment_mgr->_rerunnable_params_map.empty());
+    }
+    EXPECT_TRUE(weak_query_ctx.expired());
+
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    EXPECT_FALSE(exists);
+
+    EXPECT_EQ(_gc_subdirectories(_data_dir_ptr), gc_subdirectories_before);
+}
+
+TEST_F(SpillFileTest, QueryContextSkipsCleanupWithoutSpill) {
+    TUniqueId query_id;
+    query_id.hi = 7;
+    query_id.lo = 8;
+    auto query_id_str = print_id(query_id);
+    auto query_ctx = MockQueryContext::create(query_id);
+    auto query_dir = _data_dir_ptr->get_spill_data_path(query_id_str);
+
+    // No spill root was recorded for this query, so teardown must leave this 
untracked directory.
+    auto st = io::global_local_filesystem()->create_directory(query_dir, 
false);
+    ASSERT_TRUE(st.ok());
+
+    query_ctx.reset();
+
+    bool exists = false;
+    st = io::global_local_filesystem()->exists(query_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+}
+
+TEST_F(SpillFileTest, DeleteSpillFileThroughManagerSynchronously) {
     SpillFileSPtr spill_file;
     auto st = 
ExecEnv::GetInstance()->spill_file_mgr()->create_spill_file("test_query/mgr_delete",
                                                                           
spill_file);
@@ -906,11 +1385,17 @@ TEST_F(SpillFileTest, DeleteSpillFileThroughManager) {
     st = writer->close();
     ASSERT_TRUE(st.ok());
 
-    // Delete through manager (async GC)
+    auto spill_file_dir = 
_data_dir_ptr->get_spill_data_path("test_query/mgr_delete");
+    bool exists = false;
+    st = io::global_local_filesystem()->exists(spill_file_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_TRUE(exists);
+
     ExecEnv::GetInstance()->spill_file_mgr()->delete_spill_file(spill_file);
 
-    // Run GC to process the deletion
-    ExecEnv::GetInstance()->spill_file_mgr()->gc(1000);
+    st = io::global_local_filesystem()->exists(spill_file_dir, &exists);
+    ASSERT_TRUE(st.ok());
+    ASSERT_FALSE(exists);
 }
 
 // ═══════════════════════════════════════════════════════════════════════


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

Reply via email to