Coverage Report

Created: 2026-09-02 08:44

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/io/fs/file_handle_cache.h
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
// This file is copied from
19
// https://github.com/apache/impala/blob/master/be/src/runtime/io/handle-cache.h
20
// and modified by Doris
21
22
#pragma once
23
24
#include <array>
25
#include <cstdint>
26
#include <list>
27
#include <map>
28
#include <memory>
29
#include <mutex>
30
#include <string>
31
#include <utility>
32
33
#include "common/status.h"
34
#include "io/fs/file_system.h"
35
#include "io/fs/hdfs.h"
36
#include "util/aligned_new.h"
37
#include "util/lru_multi_cache.inline.h"
38
#include "util/thread.h"
39
40
namespace doris::io {
41
42
/// This abstract class is a small wrapper around the hdfsFile handle and the file system
43
/// instance which is needed to close the file handle. The handle incorporates
44
/// the last modified time of the file when it was opened. This is used to distinguish
45
/// between file handles for files that can be updated or overwritten.
46
/// This is used only through its subclasses, CachedHdfsFileHandle and
47
/// ExclusiveHdfsFileHandle.
48
class HdfsFileHandle {
49
public:
50
    /// Destructor will close the file handle
51
    ~HdfsFileHandle();
52
53
    /// Init only sets file_size (from param or hdfsGetPathInfo), does NOT open the file.
54
    Status init(int64_t file_size);
55
56
    /// Lazily opens the file handle on first read. Thread-safe via std::call_once.
57
    Status ensure_open();
58
59
0
    hdfsFS fs() const { return _fs; }
60
15
    hdfsFile file() const { return _hdfs_file; }
61
0
    int64_t mtime() const { return _mtime; }
62
4
    int64_t file_size() const { return _file_size; }
63
64
protected:
65
    HdfsFileHandle(const hdfsFS& fs, const std::string& fname, int64_t mtime)
66
16
            : _fs(fs), _fname(fname), _mtime(mtime) {}
67
68
private:
69
    hdfsFS _fs;
70
    const std::string _fname;
71
    hdfsFile _hdfs_file = nullptr;
72
    int64_t _mtime;
73
    int64_t _file_size = -1;
74
    std::once_flag _open_once;
75
    Status _open_status;
76
};
77
78
/// CachedHdfsFileHandles are owned by the file handle cache and are used for no
79
/// other purpose.
80
class CachedHdfsFileHandle : public HdfsFileHandle {
81
public:
82
    CachedHdfsFileHandle(const hdfsFS& fs, const std::string& fname, int64_t mtime);
83
    ~CachedHdfsFileHandle();
84
};
85
86
/// ExclusiveHdfsFileHandles are used for all purposes where a CachedHdfsFileHandle
87
/// is not appropriate.
88
class ExclusiveHdfsFileHandle : public HdfsFileHandle {
89
public:
90
    ExclusiveHdfsFileHandle(const hdfsFS& fs, const std::string& fname, int64_t mtime)
91
            : HdfsFileHandle(fs, fname, mtime) {}
92
};
93
94
/// The FileHandleCache is a data structure that owns HdfsFileHandles to share between
95
/// threads. The HdfsFileHandles are hash partitioned across NUM_PARTITIONS partitions.
96
/// Each partition operates independently with its own locks, reducing contention
97
/// between concurrent threads. The `capacity` is split between the partitions and is
98
/// enforced independently.
99
///
100
/// Threads check out a file handle for exclusive access, released automatically by RAII
101
/// accessor. If the file handle is not already present in the cache or all file handles
102
/// for this file are checked out, the file handle is emplaced in the cache. The cache can
103
/// contain multiple file handles for the same file. If a file handle is checked out, it
104
/// cannot be evicted from the cache. In this case, a cache can exceed the specified
105
/// capacity.
106
///
107
/// Remote file systems could keep a connection as part of the file handle without support
108
/// for unbuffering. The file handle cache is not suitable for those systems, as the cache
109
/// size can exceed the limit on the number of concurrent connections. HDFS does not
110
/// maintain a connection in the file handle, S3A client supports unbuffering since
111
/// IMPALA-8428, so those do not have this restriction.
112
///
113
/// If there is a file handle in the cache and the underlying file is deleted,
114
/// the file handle might keep the file from being deleted at the OS level. This can
115
/// take up disk space and impact correctness. To avoid this, the cache will evict any
116
/// file handle that has been unused for longer than threshold specified by
117
/// `unused_handle_timeout_secs`. Eviction is disabled when the threshold is 0.
118
///
119
/// TODO: The cache should also evict file handles more aggressively if the file handle's
120
/// mtime is older than the file's current mtime.
121
class FileHandleCache {
122
private:
123
    using CacheKey = std::pair<hdfsFS, std::pair<std::string, int64_t>>;
124
125
    /// Each partition operates independently, and thus has its own thread-safe cache.
126
    /// To avoid contention on the lock_ due to false sharing the partitions are
127
    /// aligned to cache line boundaries.
128
    struct FileHandleCachePartition : public CacheLineAligned {
129
        // The same HDFS path can be opened through different hdfsFS instances with
130
        // different authentication contexts, so the filesystem handle is part of the key.
131
        typedef LruMultiCache<CacheKey, CachedHdfsFileHandle> CacheType;
132
        CacheType cache;
133
    };
134
135
public:
136
    /// RAII accessor built over LruMultiCache::Accessor to handle metrics and unbuffering.
137
    /// Composition is used instead of inheritance to support the usage as in/out parameter
138
    class Accessor {
139
    public:
140
        Accessor();
141
        Accessor(FileHandleCachePartition::CacheType::Accessor&& cache_accessor);
142
4
        Accessor(Accessor&&) = default;
143
        Accessor& operator=(Accessor&&) = default;
144
145
        DISALLOW_COPY_AND_ASSIGN(Accessor);
146
147
        /// Handles metrics and unbuffering
148
        ~Accessor();
149
150
        /// Set function can be used if the Accessor is used as in/out parameter.
151
        void set(FileHandleCachePartition::CacheType::Accessor&& cache_accessor);
152
153
        /// Interface mimics LruMultiCache::Accessor's interface, handles metrics
154
        CachedHdfsFileHandle* get();
155
        void release();
156
        void destroy();
157
158
    private:
159
        FileHandleCachePartition::CacheType::Accessor _cache_accessor;
160
    };
161
162
    /// Instantiates the cache with `capacity` split evenly across NUM_PARTITIONS
163
    /// partitions. If the capacity does not split evenly, then the capacity is rounded
164
    /// up. The cache will age out any file handle that is unused for
165
    /// `unused_handle_timeout_secs` seconds. Age out is disabled if this is set to zero.
166
    FileHandleCache(size_t capacity, size_t num_partitions, uint64_t unused_handle_timeout_secs);
167
168
    /// Destructor is only called for backend tests
169
    ~FileHandleCache();
170
171
    /// Starts up a thread that monitors the age of file handles and evicts any that
172
    /// exceed the limit.
173
    Status init() WARN_UNUSED_RESULT;
174
175
    /// Get a file handle accessor from the cache for the specified filename (fname) and
176
    /// last modification time (mtime). This will hash the filename to determine
177
    /// which partition to use for this file handle.
178
    ///
179
    /// If 'require_new_handle' is false and the partition contains an available handle,
180
    /// an accessor is returned and cache_hit is set to true. Otherwise, the partition will
181
    /// emplace file handle, an accessor to it will be returned with cache_hit set to false.
182
    /// On failure, empty accessor will be returned. In either case, the partition may evict
183
    /// a file handle to make room for the new file handle.
184
    ///
185
    /// This obtains exclusive control over the returned file handle.
186
    Status get_file_handle(const hdfsFS& fs, const std::string& fname, int64_t mtime,
187
                           int64_t file_size, bool require_new_handle, Accessor* accessor,
188
                           bool* cache_hit) WARN_UNUSED_RESULT;
189
190
#ifdef BE_TEST
191
    static bool same_cache_key_for_test(const hdfsFS& lhs_fs, const std::string& lhs_fname,
192
                                        int64_t lhs_mtime, const hdfsFS& rhs_fs,
193
                                        const std::string& rhs_fname, int64_t rhs_mtime);
194
#endif
195
196
private:
197
15
    static CacheKey make_cache_key(const hdfsFS& fs, const std::string& fname, int64_t mtime) {
198
15
        return {fs, {fname, mtime}};
199
15
    }
200
201
    /// Periodic check to evict unused file handles. Only executed by _eviction_thread.
202
    void _evict_handles_loop();
203
204
    std::vector<FileHandleCachePartition> _cache_partitions;
205
206
    /// Maximum time before an unused file handle is aged out of the cache.
207
    /// Aging out is disabled if this is set to 0.
208
    uint64_t _unused_handle_timeout_secs;
209
210
    /// Thread to check for unused file handles to evict. This thread will exit when
211
    /// the _shut_down_promise is set.
212
    std::shared_ptr<Thread> _eviction_thread;
213
    std::atomic<bool> _is_shut_down = {false};
214
};
215
216
} // namespace doris::io