Coverage Report

Created: 2026-08-07 13:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exec/spill/spill_file_manager.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "exec/spill/spill_file_manager.h"
19
20
#include <fmt/format.h>
21
#include <glog/logging.h>
22
23
#include <algorithm>
24
#include <filesystem>
25
#include <memory>
26
#include <string>
27
#include <utility>
28
29
#include "common/logging.h"
30
#include "common/metrics/doris_metrics.h"
31
#include "exec/spill/spill_file.h"
32
#include "io/fs/file_system.h"
33
#include "io/fs/local_file_system.h"
34
#include "storage/olap_define.h"
35
#include "util/debug_points.h"
36
#include "util/parse_util.h"
37
#include "util/pretty_printer.h"
38
#include "util/time.h"
39
40
namespace doris {
41
42
205
SpillFileManager::~SpillFileManager() {
43
    // QueryContext destruction can still queue failed deletions after stop(), for example while
44
    // VDataStreamMgr is being destroyed. Retry them once more before dropping the in-memory state.
45
    // Any directory that still cannot be deleted remains under the active spill root and will be
46
    // moved to the GC root by init() after restart.
47
205
    _retry_pending_query_spill_directories();
48
205
    DorisMetrics::instance()->metric_registry()->deregister_entity(_entity);
49
205
}
50
51
SpillFileManager::SpillFileManager(
52
        std::unordered_map<std::string, std::unique_ptr<SpillDataDir>>&& spill_store_map)
53
209
        : _spill_store_map(std::move(spill_store_map)), _stop_background_threads_latch(1) {}
54
55
211
void SpillFileManager::stop() {
56
211
    _stop_background_threads_latch.count_down();
57
211
    if (_spill_gc_thread) {
58
211
        _spill_gc_thread->join();
59
211
    }
60
    // The GC thread may observe the stop latch before processing a recently queued failed deletion.
61
    // Retry the pending directories after the thread exits; later failures get one final retry in
62
    // the destructor.
63
211
    _retry_pending_query_spill_directories();
64
211
}
65
66
209
Status SpillFileManager::init() {
67
209
    LOG(INFO) << "init spill stream manager";
68
209
    RETURN_IF_ERROR(_init_spill_store_map());
69
70
247
    for (const auto& [path, store] : _spill_store_map) {
71
247
        auto gc_dir_root_dir = store->get_spill_data_gc_path();
72
247
        bool exists = true;
73
247
        RETURN_IF_ERROR(io::global_local_filesystem()->exists(gc_dir_root_dir, &exists));
74
247
        if (!exists) {
75
126
            RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(gc_dir_root_dir));
76
126
        }
77
78
247
        auto spill_dir = store->get_spill_data_path();
79
247
        RETURN_IF_ERROR(io::global_local_filesystem()->exists(spill_dir, &exists));
80
247
        if (!exists) {
81
126
            RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(spill_dir));
82
126
        } else {
83
121
            auto suffix = ToStringFromUnixMillis(UnixMillis());
84
121
            auto gc_dir = store->get_spill_data_gc_path(suffix);
85
121
            if (std::filesystem::exists(gc_dir)) {
86
0
                LOG(WARNING) << "gc dir already exists: " << gc_dir;
87
0
            }
88
121
            (void)io::global_local_filesystem()->rename(spill_dir, gc_dir);
89
121
            RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(spill_dir));
90
121
        }
91
247
    }
92
93
209
    RETURN_IF_ERROR(Thread::create(
94
209
            "Spill", "spill_gc_thread", [this]() { this->_spill_gc_thread_callback(); },
95
209
            &_spill_gc_thread));
96
209
    LOG(INFO) << "spill gc thread started";
97
98
209
    _init_metrics();
99
100
209
    return Status::OK();
101
209
}
102
103
209
void SpillFileManager::_init_metrics() {
104
209
    _entity = DorisMetrics::instance()->metric_registry()->register_entity("spill",
105
209
                                                                           {{"name", "spill"}});
106
107
209
    _spill_write_bytes_metric = std::make_unique<doris::MetricPrototype>(
108
209
            doris::MetricType::COUNTER, doris::MetricUnit::BYTES, "spill_write_bytes");
109
209
    _spill_write_bytes_counter = (IntAtomicCounter*)(_entity->register_metric<IntAtomicCounter>(
110
209
            _spill_write_bytes_metric.get()));
111
112
209
    _spill_read_bytes_metric = std::make_unique<doris::MetricPrototype>(
113
209
            doris::MetricType::COUNTER, doris::MetricUnit::BYTES, "spill_read_bytes");
114
209
    _spill_read_bytes_counter = (IntAtomicCounter*)(_entity->register_metric<IntAtomicCounter>(
115
209
            _spill_read_bytes_metric.get()));
116
209
}
117
118
// Retry failed query-directory deletions and clean up stale spill files.
119
209
void SpillFileManager::_spill_gc_thread_callback() {
120
6.71k
    while (!_stop_background_threads_latch.wait_for(
121
6.71k
            std::chrono::milliseconds(config::spill_gc_interval_ms))) {
122
6.50k
        gc(config::spill_gc_work_time_ms);
123
7.08k
        for (auto& [path, dir] : _spill_store_map) {
124
7.08k
            static_cast<void>(dir->update_capacity());
125
7.08k
        }
126
6.50k
    }
127
209
}
128
129
209
Status SpillFileManager::_init_spill_store_map() {
130
247
    for (const auto& store : _spill_store_map) {
131
247
        RETURN_IF_ERROR(store.second->init());
132
247
    }
133
134
209
    return Status::OK();
135
209
}
136
137
std::vector<SpillDataDir*> SpillFileManager::_get_stores_for_spill(
138
626
        TStorageMedium::type storage_medium) {
139
626
    std::vector<std::pair<SpillDataDir*, double>> stores_with_usage;
140
654
    for (auto& [_, store] : _spill_store_map) {
141
654
        if (store->storage_medium() == storage_medium && !store->reach_capacity_limit(0)) {
142
327
            stores_with_usage.emplace_back(store.get(), store->_get_disk_usage(0));
143
327
        }
144
654
    }
145
626
    if (stores_with_usage.empty()) {
146
299
        return {};
147
299
    }
148
149
327
    std::ranges::sort(stores_with_usage, [](auto&& a, auto&& b) { return a.second < b.second; });
150
151
327
    std::vector<SpillDataDir*> stores;
152
327
    for (const auto& [store, _] : stores_with_usage) {
153
327
        stores.emplace_back(store);
154
327
    }
155
327
    return stores;
156
626
}
157
158
Status SpillFileManager::create_spill_file(const std::string& relative_path,
159
327
                                           SpillFileSPtr& spill_file) {
160
327
    auto data_dirs = _get_stores_for_spill(TStorageMedium::type::SSD);
161
327
    if (data_dirs.empty()) {
162
299
        data_dirs = _get_stores_for_spill(TStorageMedium::type::HDD);
163
299
    }
164
327
    if (data_dirs.empty()) {
165
0
        return Status::Error<ErrorCode::NO_AVAILABLE_ROOT_PATH>(
166
0
                "no available disk can be used for spill.");
167
0
    }
168
169
    // Select the first available data dir (sorted by usage ascending)
170
327
    SpillDataDir* data_dir = data_dirs.front();
171
327
    spill_file = std::make_shared<SpillFile>(data_dir, relative_path);
172
327
    return Status::OK();
173
327
}
174
175
140
void SpillFileManager::delete_spill_file(SpillFileSPtr spill_file) {
176
140
    if (!spill_file) {
177
0
        LOG(WARNING) << "[spill][delete] null spill_file";
178
0
        return;
179
0
    }
180
140
    spill_file->gc();
181
140
}
182
183
void SpillFileManager::delete_query_spill_directory(const std::string& query_id,
184
33
                                                    SpillDataDir* data_dir) {
185
33
    PendingQuerySpillDirectory pending_directory {
186
33
            .query_dir = data_dir->get_spill_data_path(query_id),
187
33
    };
188
189
33
    auto status = _try_delete_query_spill_directory(pending_directory);
190
33
    if (!status.ok()) {
191
4
        std::lock_guard lock(_pending_query_spill_directories_mutex);
192
4
        ++pending_directory.failed_count;
193
4
        _pending_query_spill_directories.emplace_back(std::move(pending_directory));
194
4
    }
195
33
}
196
197
Status SpillFileManager::_try_delete_query_spill_directory(
198
42
        const PendingQuerySpillDirectory& pending_directory) {
199
42
    DBUG_EXECUTE_IF("fault_inject::spill_file_manager::delete_query_spill_directory", {
200
42
        return Status::Error<INTERNAL_ERROR>("injected query spill directory deletion failure");
201
42
    });
202
33
    const auto& fs = io::global_local_filesystem();
203
33
    return fs->delete_directory(pending_directory.query_dir);
204
42
}
205
206
6.93k
void SpillFileManager::_retry_pending_query_spill_directories() {
207
6.93k
    std::vector<PendingQuerySpillDirectory> pending_directories;
208
6.93k
    {
209
6.93k
        std::lock_guard lock(_pending_query_spill_directories_mutex);
210
6.93k
        pending_directories.swap(_pending_query_spill_directories);
211
6.93k
    }
212
6.93k
    DBUG_EXECUTE_IF(
213
6.93k
            "fault_inject::spill_file_manager::retry_pending_query_spill_directories_after_drain",
214
6.93k
            { DBUG_RUN_CALLBACK(); });
215
216
    // Limit repeated warnings for a persistently unavailable directory while retaining it for
217
    // every subsequent retry.
218
6.93k
    constexpr int log_interval = 5;
219
6.93k
    std::vector<PendingQuerySpillDirectory> failed_directories;
220
6.93k
    for (auto& pending_directory : pending_directories) {
221
9
        auto status = _try_delete_query_spill_directory(pending_directory);
222
9
        if (status.ok()) {
223
4
            continue;
224
4
        }
225
226
5
        ++pending_directory.failed_count;
227
5
        if (pending_directory.failed_count % log_interval == 0) {
228
1
            LOG(WARNING) << fmt::format(
229
1
                    "failed to retry deleting spill query directory, dir {}, error: {}",
230
1
                    pending_directory.query_dir, status.to_string());
231
1
        }
232
5
        failed_directories.emplace_back(std::move(pending_directory));
233
5
    }
234
235
6.93k
    if (!failed_directories.empty()) {
236
5
        std::lock_guard lock(_pending_query_spill_directories_mutex);
237
5
        for (auto& pending_directory : failed_directories) {
238
5
            _pending_query_spill_directories.emplace_back(std::move(pending_directory));
239
5
        }
240
5
    }
241
6.93k
}
242
243
6.51k
void SpillFileManager::gc(int32_t max_work_time_ms) {
244
6.51k
    bool exists = true;
245
6.51k
    bool has_work = false;
246
6.51k
    int64_t max_work_time_ns = max_work_time_ms * 1000L * 1000L;
247
6.51k
    MonotonicStopWatch watch;
248
6.51k
    watch.start();
249
6.51k
    Defer defer {[&]() {
250
6.51k
        if (has_work) {
251
7
            std::string msg(
252
7
                    fmt::format("spill gc time: {}",
253
7
                                PrettyPrinter::print(watch.elapsed_time(), TUnit::TIME_NS)));
254
7
            msg += ", spill storage:\n";
255
11
            for (const auto& [path, store_dir] : _spill_store_map) {
256
11
                msg += "    " + store_dir->debug_string();
257
11
                msg += "\n";
258
11
            }
259
7
            LOG(INFO) << msg;
260
7
        }
261
6.51k
    }};
262
6.51k
    _retry_pending_query_spill_directories();
263
7.09k
    for (const auto& [path, store_dir] : _spill_store_map) {
264
7.09k
        std::string gc_root_dir = store_dir->get_spill_data_gc_path();
265
266
7.09k
        std::error_code ec;
267
7.09k
        exists = std::filesystem::exists(gc_root_dir, ec);
268
7.09k
        if (ec || !exists) {
269
0
            continue;
270
0
        }
271
        // dirs of queries
272
7.09k
        std::vector<io::FileInfo> dirs;
273
7.09k
        auto st = io::global_local_filesystem()->list(gc_root_dir, false, &dirs, &exists);
274
7.09k
        if (!st.ok()) {
275
0
            continue;
276
0
        }
277
278
7.09k
        for (const auto& dir : dirs) {
279
171
            has_work = true;
280
171
            if (dir.is_file) {
281
0
                continue;
282
0
            }
283
171
            std::string abs_dir = fmt::format("{}/{}", gc_root_dir, dir.file_name);
284
            // operator spill sub dirs of a query
285
171
            std::vector<io::FileInfo> files;
286
171
            st = io::global_local_filesystem()->list(abs_dir, false, &files, &exists);
287
171
            if (!st.ok()) {
288
0
                continue;
289
0
            }
290
171
            if (files.empty()) {
291
121
                static_cast<void>(io::global_local_filesystem()->delete_directory(abs_dir));
292
121
                continue;
293
121
            }
294
295
50
            for (const auto& file : files) {
296
50
                auto abs_file_path = fmt::format("{}/{}", abs_dir, file.file_name);
297
50
                if (file.is_file) {
298
0
                    static_cast<void>(io::global_local_filesystem()->delete_file(abs_file_path));
299
50
                } else {
300
50
                    static_cast<void>(
301
50
                            io::global_local_filesystem()->delete_directory(abs_file_path));
302
50
                }
303
50
                if (watch.elapsed_time() > max_work_time_ns) {
304
0
                    break;
305
0
                }
306
50
            }
307
50
        }
308
7.09k
    }
309
6.51k
}
310
311
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_capacity, MetricUnit::BYTES);
312
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_limit, MetricUnit::BYTES);
313
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_avail_capacity, MetricUnit::BYTES);
314
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_data_size, MetricUnit::BYTES);
315
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_has_spill_data, MetricUnit::BYTES);
316
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(spill_disk_has_spill_gc_data, MetricUnit::BYTES);
317
318
SpillDataDir::SpillDataDir(std::string path, int64_t capacity_bytes,
319
                           TStorageMedium::type storage_medium)
320
247
        : _path(std::move(path)),
321
247
          _disk_capacity_bytes(capacity_bytes),
322
247
          _storage_medium(storage_medium) {
323
247
    spill_data_dir_metric_entity = DorisMetrics::instance()->metric_registry()->register_entity(
324
247
            std::string("spill_data_dir.") + _path, {{"path", _path + "/" + SPILL_DIR_PREFIX}});
325
247
    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_capacity);
326
247
    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_limit);
327
247
    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_avail_capacity);
328
247
    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_data_size);
329
247
    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_has_spill_data);
330
247
    INT_GAUGE_METRIC_REGISTER(spill_data_dir_metric_entity, spill_disk_has_spill_gc_data);
331
247
}
332
333
14.6k
bool is_directory_empty(const std::filesystem::path& dir) {
334
    // Spill cleanup may delete the directory while the iterator is constructed or advanced. Treat
335
    // that race as empty for these presence metrics.
336
14.6k
    try {
337
14.6k
        return std::filesystem::is_directory(dir) &&
338
14.6k
               std::filesystem::directory_iterator(dir) ==
339
14.4k
                       std::filesystem::end(std::filesystem::directory_iterator {});
340
14.6k
    } catch (const std::filesystem::filesystem_error&) {
341
0
        return true;
342
0
    }
343
14.6k
}
344
345
247
Status SpillDataDir::init() {
346
247
    bool exists = false;
347
247
    RETURN_IF_ERROR(io::global_local_filesystem()->exists(_path, &exists));
348
247
    if (!exists) {
349
0
        RETURN_NOT_OK_STATUS_WITH_WARN(Status::IOError("opendir failed, path={}", _path),
350
0
                                       "check file exist failed");
351
0
    }
352
247
    RETURN_IF_ERROR(update_capacity());
353
247
    LOG(INFO) << fmt::format(
354
247
            "spill storage path: {}, capacity: {}, limit: {}, available: "
355
247
            "{}",
356
247
            _path, PrettyPrinter::print_bytes(_disk_capacity_bytes),
357
247
            PrettyPrinter::print_bytes(_spill_data_limit_bytes),
358
247
            PrettyPrinter::print_bytes(_available_bytes));
359
247
    return Status::OK();
360
247
}
361
362
7.96k
std::string SpillDataDir::get_spill_data_path(const std::string& query_id) const {
363
7.96k
    auto dir = fmt::format("{}/{}", _path, SPILL_DIR_PREFIX);
364
7.96k
    if (!query_id.empty()) {
365
47
        dir = fmt::format("{}/{}", dir, query_id);
366
47
    }
367
7.96k
    return dir;
368
7.96k
}
369
370
14.8k
std::string SpillDataDir::get_spill_data_gc_path(const std::string& sub_dir_name) const {
371
14.8k
    auto dir = fmt::format("{}/{}", _path, SPILL_GC_DIR_PREFIX);
372
14.8k
    if (!sub_dir_name.empty()) {
373
121
        dir = fmt::format("{}/{}", dir, sub_dir_name);
374
121
    }
375
14.8k
    return dir;
376
14.8k
}
377
378
7.32k
Status SpillDataDir::update_capacity() {
379
7.32k
    std::lock_guard<std::mutex> l(_mutex);
380
7.32k
    RETURN_IF_ERROR(io::global_local_filesystem()->get_space_info(_path, &_disk_capacity_bytes,
381
7.32k
                                                                  &_available_bytes));
382
7.32k
    spill_disk_capacity->set_value(_disk_capacity_bytes);
383
7.32k
    spill_disk_avail_capacity->set_value(_available_bytes);
384
7.32k
    auto disk_use_max_bytes =
385
7.32k
            (int64_t)(_disk_capacity_bytes * config::storage_flood_stage_usage_percent / 100);
386
7.32k
    bool is_percent = true;
387
7.32k
    _spill_data_limit_bytes = ParseUtil::parse_mem_spec(config::spill_storage_limit, -1,
388
7.32k
                                                        _disk_capacity_bytes, &is_percent);
389
7.32k
    if (_spill_data_limit_bytes <= 0) {
390
0
        spill_disk_limit->set_value(_spill_data_limit_bytes);
391
0
        auto err_msg = fmt::format("Failed to parse spill storage limit from '{}'",
392
0
                                   config::spill_storage_limit);
393
0
        LOG(WARNING) << err_msg;
394
0
        return Status::InvalidArgument(err_msg);
395
0
    }
396
7.32k
    if (is_percent) {
397
7.32k
        _spill_data_limit_bytes = (int64_t)(_spill_data_limit_bytes *
398
7.32k
                                            config::storage_flood_stage_usage_percent / 100);
399
7.32k
    }
400
7.32k
    _spill_data_limit_bytes = std::min(_spill_data_limit_bytes, disk_use_max_bytes);
401
7.32k
    spill_disk_limit->set_value(_spill_data_limit_bytes);
402
403
7.32k
    std::string spill_root_dir = get_spill_data_path();
404
7.32k
    std::string spill_gc_root_dir = get_spill_data_gc_path();
405
7.32k
    spill_disk_has_spill_data->set_value(is_directory_empty(spill_root_dir) ? 0 : 1);
406
7.32k
    spill_disk_has_spill_gc_data->set_value(is_directory_empty(spill_gc_root_dir) ? 0 : 1);
407
408
7.32k
    return Status::OK();
409
7.32k
}
410
411
779
bool SpillDataDir::_reach_disk_capacity_limit(int64_t incoming_data_size) {
412
779
    double used_pct = _get_disk_usage(incoming_data_size);
413
779
    int64_t left_bytes = _available_bytes - incoming_data_size;
414
779
    if (used_pct >= config::storage_flood_stage_usage_percent / 100.0 &&
415
779
        left_bytes <= config::storage_flood_stage_left_capacity_bytes) {
416
0
        LOG(WARNING) << "reach capacity limit. used pct: " << used_pct
417
0
                     << ", left bytes: " << left_bytes << ", path: " << _path;
418
0
        return true;
419
0
    }
420
779
    return false;
421
779
}
422
779
bool SpillDataDir::reach_capacity_limit(int64_t incoming_data_size) {
423
779
    std::lock_guard<std::mutex> l(_mutex);
424
779
    if (_reach_disk_capacity_limit(incoming_data_size)) {
425
0
        return true;
426
0
    }
427
779
    if (_spill_data_bytes + incoming_data_size > _spill_data_limit_bytes) {
428
0
        LOG_EVERY_T(WARNING, 1) << fmt::format(
429
0
                "spill data reach limit, path: {}, capacity: {}, limit: {}, used: {}, "
430
0
                "available: "
431
0
                "{}, "
432
0
                "incoming "
433
0
                "bytes: {}",
434
0
                _path, PrettyPrinter::print_bytes(_disk_capacity_bytes),
435
0
                PrettyPrinter::print_bytes(_spill_data_limit_bytes),
436
0
                PrettyPrinter::print_bytes(_spill_data_bytes),
437
0
                PrettyPrinter::print_bytes(_available_bytes),
438
0
                PrettyPrinter::print_bytes(incoming_data_size));
439
0
        return true;
440
0
    }
441
779
    return false;
442
779
}
443
11
std::string SpillDataDir::debug_string() {
444
11
    return fmt::format(
445
11
            "path: {}, capacity: {}, limit: {}, used: {}, available: "
446
11
            "{}",
447
11
            _path, PrettyPrinter::print_bytes(_disk_capacity_bytes),
448
11
            PrettyPrinter::print_bytes(_spill_data_limit_bytes),
449
11
            PrettyPrinter::print_bytes(_spill_data_bytes),
450
11
            PrettyPrinter::print_bytes(_available_bytes));
451
11
}
452
} // namespace doris