github-actions[bot] commented on code in PR #66612:
URL: https://github.com/apache/doris/pull/66612#discussion_r3861337756


##########
be/src/exec/spill/spill_file_writer.cpp:
##########
@@ -91,12 +91,27 @@ Status SpillFileWriter::_close_current_part(const 
std::shared_ptr<SpillFile>& sp
     _part_meta.append((const char*)&_part_max_sub_block_size, 
sizeof(_part_max_sub_block_size));
     _part_meta.append((const char*)&_part_written_blocks, 
sizeof(_part_written_blocks));
 
+    int64_t meta_size = _part_meta.size();
+    if (!_data_dir->try_reserve_spill_data(meta_size)) {

Review Comment:
   [P1] Propagate footer reservation failure before publishing the spill
   
   This new capacity failure can occur after the data blocks consume the 
remaining quota but before the footer, file close, part-count update, and 
`finish_writing()`. The Iceberg `_do_spill()` and intermediate-merge paths 
never explicitly close their local writers; the destructor only logs this 
error, and intermediate merge deletes its input spill files before the output 
writer is destroyed. The method can therefore return success with a replacement 
whose part count is zero, which the reader treats as EOS, losing the spilled 
rows. Please explicitly close and propagate status before publishing/deleting 
inputs, make failed close state retry-safe, and cover footer-quota exhaustion.



##########
be/src/exec/spill/spill_file_manager.cpp:
##########
@@ -22,24 +22,238 @@
 
 #include <algorithm>
 #include <filesystem>
+#include <limits>
 #include <memory>
 #include <string>
+#include <unordered_set>
 #include <utility>
 
 #include "common/logging.h"
 #include "common/metrics/doris_metrics.h"
 #include "exec/spill/spill_file.h"
 #include "io/fs/file_system.h"
 #include "io/fs/local_file_system.h"
+#include "runtime/query_context.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"
+#include "util/uid_util.h"
 
 namespace doris {
 #include "common/compile_check_begin.h"
 
+ExternalSpillSession::ExternalSpillSession(SpillFileManager* manager, 
QueryContext* query_context,
+                                           std::string relative_path)
+        : _manager(manager),
+          _query_context(query_context->weak_from_this()),
+          _resource_context(query_context->resource_ctx()),
+          _query_id(print_id(query_context->query_id())),
+          _relative_path(std::move(relative_path)) {
+    DCHECK(_manager != nullptr);
+    DCHECK(!_query_context.expired());
+    DCHECK(_resource_context != nullptr);
+}
+
+ExternalSpillSession::~ExternalSpillSession() {
+    _manager->_release_external_spill_session(this);
+}
+
+Status ExternalSpillSession::get_paths(std::vector<std::string>* paths) {
+    if (paths == nullptr) {
+        return Status::InvalidArgument("External spill paths output must not 
be null");
+    }
+    std::lock_guard lock(_mutex);
+    if (_managed_paths.empty()) {
+        RETURN_IF_ERROR(_manager->_initialize_external_spill_session(this));
+    }
+    paths->clear();
+    for (const auto& managed_path : _managed_paths) {
+        paths->emplace_back(managed_path.path);
+    }
+    return Status::OK();
+}
+
+ExternalSpillSession::ManagedPath* ExternalSpillSession::_find_managed_path(
+        const std::string& path) {
+    for (auto& managed_path : _managed_paths) {
+        if (path == managed_path.path ||
+            (path.size() > managed_path.path.size() && 
path.starts_with(managed_path.path) &&
+             path[managed_path.path.size()] == '/')) {
+            return &managed_path;
+        }
+    }
+    return nullptr;
+}
+
+Status ExternalSpillSession::reserve(const std::string& path, int64_t bytes) {
+    if (bytes <= 0) {
+        return Status::InvalidArgument("External spill reservation must be 
positive: {}", bytes);
+    }
+
+    std::lock_guard lock(_mutex);
+    auto* managed_path = _find_managed_path(path);
+    if (managed_path == nullptr) {
+        return Status::InvalidArgument("External spill path is not managed by 
Doris: {}", path);
+    }
+    const int64_t accounted_bytes =
+            managed_path->buffer_accounted_bytes + 
managed_path->direct_file_accounted_bytes;
+    if (bytes > std::numeric_limits<int64_t>::max() - accounted_bytes) {
+        return Status::InvalidArgument("External spill reservation overflows: 
bytes={}", bytes);
+    }
+    if (!managed_path->data_dir->try_reserve_spill_data(bytes)) {
+        return Status::Error<ErrorCode::DISK_REACH_CAPACITY_LIMIT>(
+                "External spill write exceeds the Doris spill storage limit: 
path={}, bytes={}",
+                path, bytes);
+    }
+    managed_path->buffer_accounted_bytes += bytes;
+    managed_path->buffer_accounted_bytes_by_path[path] += bytes;
+    return Status::OK();
+}
+
+void ExternalSpillSession::update_accounting(const std::string& path, int64_t 
current_bytes_delta,
+                                             int64_t write_bytes, int64_t 
read_bytes) {
+    int64_t released_bytes = 0;
+    SpillDataDir* data_dir = nullptr;
+    {
+        std::lock_guard lock(_mutex);
+        auto* managed_path = _find_managed_path(path);
+        if (managed_path == nullptr) {
+            LOG(WARNING) << "Ignoring accounting for unmanaged external spill 
path: " << path;
+            return;
+        }
+        data_dir = managed_path->data_dir;
+        if (current_bytes_delta < 0) {
+            const int64_t requested_release =
+                    current_bytes_delta == std::numeric_limits<int64_t>::min()
+                            ? std::numeric_limits<int64_t>::max()
+                            : -current_bytes_delta;
+            auto path_it = 
managed_path->buffer_accounted_bytes_by_path.find(path);
+            if (path_it == managed_path->buffer_accounted_bytes_by_path.end()) 
{
+                return;
+            }
+            released_bytes = std::min(requested_release, path_it->second);
+            path_it->second -= released_bytes;
+            if (path_it->second == 0) {
+                managed_path->buffer_accounted_bytes_by_path.erase(path_it);
+            }
+            managed_path->buffer_accounted_bytes -= released_bytes;
+        }
+    }
+    if (released_bytes > 0) {
+        data_dir->update_spill_data_usage(-released_bytes);
+    }
+    if (write_bytes > 0) {
+        
_resource_context->io_context()->update_spill_write_bytes_to_local_storage(write_bytes);
+        _manager->update_spill_write_bytes(write_bytes);
+    }
+    if (read_bytes > 0) {
+        
_resource_context->io_context()->update_spill_read_bytes_from_local_storage(read_bytes);
+        _manager->update_spill_read_bytes(read_bytes);
+    }
+}
+
+Status ExternalSpillSession::reconcile_direct_file_usage(bool allow_release) {
+    struct ObservedPathUsage {
+        std::string path;
+        std::unordered_set<std::string> buffer_paths;
+        uintmax_t bytes = 0;
+    };
+    std::vector<ObservedPathUsage> observed_paths;
+    {
+        std::lock_guard lock(_mutex);
+        observed_paths.reserve(_managed_paths.size());
+        for (const auto& managed_path : _managed_paths) {
+            ObservedPathUsage observed_path {
+                    .path = managed_path.path,
+                    .buffer_paths = {},
+                    .bytes = 0,
+            };
+            for (const auto& buffer_entry : 
managed_path.buffer_accounted_bytes_by_path) {
+                observed_path.buffer_paths.emplace(buffer_entry.first);
+            }
+            observed_paths.emplace_back(std::move(observed_path));
+        }
+    }
+
+    // Do not hold the session mutex while walking the filesystem. Buffer 
callbacks can continue to
+    // reserve and release capacity while a rate-limited raw-file observation 
is in progress.
+    for (auto& observed_path : observed_paths) {
+        uintmax_t actual_bytes = 0;
+        std::error_code ec;
+        if (std::filesystem::exists(observed_path.path, ec)) {
+            std::filesystem::recursive_directory_iterator iterator(
+                    observed_path.path, 
std::filesystem::directory_options::skip_permission_denied,
+                    ec);
+            const std::filesystem::recursive_directory_iterator end;
+            while (!ec && iterator != end) {
+                if (iterator->is_regular_file(ec)) {
+                    const auto file_path = iterator->path().string();
+                    if (!observed_path.buffer_paths.contains(file_path)) {

Review Comment:
   [P1] Reclassify buffer paths after the unlocked scan
   
   This exclusion set is only a snapshot. While the mutex is dropped for the 
filesystem walk, Paimon's asynchronous lookup compaction can reserve and create 
a new callback-managed channel; the scan then counts that file as direct data, 
and the relocked code adds those bytes on top of the channel's existing 
reservation without checking the current buffer map. Ordinary reconciliations 
cannot release the overcount because `allow_release` is false, so a valid later 
write can fail with `DISK_REACH_CAPACITY_LIMIT`. Please make the observation 
generation-aware or reclassify observed paths after relocking, and add a 
barrier-controlled concurrent scan/reserve regression.



##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -576,13 +583,36 @@ private void closeWriter() throws Exception {
             throw new IllegalStateException(
                     "A previous Paimon SDK close failed; native memory cannot 
be released safely");
         }
-        Exception failure = closeResource(writer, null);
-        failure = closeResource(globalIndexAssigner, failure);
-        failure = closeResource(ioManager, failure);
+        Exception lifecycleFailure = closeResource(writer, null);
+        Exception compactionFailure = closeCompactionExecutor();
+        if (compactionFailure != null) {
+            lifecycleFailure = appendFailure(lifecycleFailure, 
compactionFailure);
+            // The task may still reference Doris-backed memory and spill 
files. Leave all dependent
+            // Java resources reachable and open; the native backend will 
retain their handles.
+            sdkCloseFailed = true;
+            throw lifecycleFailure;
+        }
+        lifecycleFailure = closeResource(globalIndexAssigner, 
lifecycleFailure);
+        Exception cleanupFailure = closeResource(ioManager, null);
+        boolean physicalCleanupFailure =
+                cleanupFailure instanceof 
DorisIOManager.SpillDirectoryCleanupException;
+        if (cleanupFailure != null && !physicalCleanupFailure) {

Review Comment:
   [P1] Abort prepared files without globally fencing the BE
   
   Native has already received the prepared payloads and passed its only abort 
branch before this close runs. If the final filesystem/accounting 
reconciliation returns a plain `IOException`, this branch clears Java's saved 
messages and throws; native drops its local payloads without aborting them, 
then sets `paimon_jni_close_failed` and rejects every later Paimon writer until 
restart even though the SDK writer is closed and its executor terminated. 
Please abort the prepared set on this close failure, transfer any residual 
session/accounting to retryable cleanup without setting the SDK-liveness fence, 
and test both file cleanup and subsequent writer admission.



##########
fe/be-java-extensions/paimon-connector/src/main/java/org/apache/doris/paimon/PaimonJniWriter.java:
##########
@@ -547,6 +553,7 @@ private List<CommitMessage> prepareCommitMessages() throws 
Exception {
         List<CommitMessage> messages = commitIdentifier > 0
                 ? writer.prepareCommit(true, commitIdentifier)
                 : writer.prepareCommit();
+        ioManager.reconcileNow(false);

Review Comment:
   [P1] Never let reconciliation skip aborting drained files
   
   `writer.prepareCommit()` drains Paimon's current increment before this new 
fallible call. On the normal prepare path, a reconciliation error occurs before 
the messages are saved, so the subsequent abort cannot recover them. A 
write-time reconciliation error is worse: native enters `abortWriter()` 
directly, its prepare reaches this same call, and another error skips 
`InnerTableCommit.abort` even if the messages were published first. In both 
cases the drained files are orphaned. Please publish the messages immediately 
after SDK prepare and structure abort so it consumes that saved set despite 
reconciliation failure; cover both normal prepare failure and 
write-error/abort-time failure.



##########
be/src/exec/spill/spill_file_manager.cpp:
##########
@@ -183,32 +493,70 @@ void SpillFileManager::delete_spill_file(SpillFileSPtr 
spill_file) {
 
 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 query_dir = data_dir->get_spill_data_path(query_id);
+    {
+        std::lock_guard lock(_query_spill_directories_mutex);
+        auto it = _query_spill_directories
+                          .try_emplace(query_dir,
+                                       QuerySpillDirectoryState {
+                                               .failed_count = 0,
+                                               .data_dir = data_dir,
+                                               .external_accounted_bytes = 0,
+                                               .external_leases = 0,
+                                               .delete_requested = false,
+                                       })
+                          .first;
+        DCHECK(it->second.data_dir == data_dir);
+        it->second.delete_requested = true;
+    }
 
-    auto status = _try_delete_query_spill_directory(pending_directory);
+    auto status = _try_delete_query_spill_directory(query_dir);
     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));
+        std::lock_guard lock(_query_spill_directories_mutex);
+        auto it = _query_spill_directories.find(query_dir);
+        if (it != _query_spill_directories.end()) {
+            ++it->second.failed_count;
+        }
     }
 }
 
-Status SpillFileManager::_try_delete_query_spill_directory(
-        const PendingQuerySpillDirectory& pending_directory) {
+Status SpillFileManager::_try_delete_query_spill_directory(const std::string& 
query_dir) {
+    std::unique_lock lock(_query_spill_directories_mutex);
+    auto it = _query_spill_directories.find(query_dir);
+    if (it == _query_spill_directories.end() || !it->second.delete_requested) {
+        return Status::OK();
+    }
+    if (it->second.external_leases > 0) {
+        return Status::InternalError("external spill directory is still in 
use: {}", query_dir);
+    }
     
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);
+    auto status = fs->delete_directory(query_dir);

Review Comment:
   [P2] Release the global registry lock during recursive deletion
   
   `_query_spill_directories_mutex` remains held while `delete_directory()` 
recursively removes every file under this query. The same global mutex is 
required to initialize or release every external spill session and to tear down 
every other query, so one large cleanup can stall unrelated Paimon writers and 
query cleanup for the full `remove_all` duration. Please mark this directory as 
deletion-in-progress, drop the global lock around filesystem I/O, and serialize 
only competing operations for the same directory.



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to