Coverage Report

Created: 2026-08-06 18:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/runtime/small_file_mgr.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 "runtime/small_file_mgr.h"
19
20
// IWYU pragma: no_include <bthread/errno.h>
21
#include <absl/strings/str_split.h>
22
#include <errno.h> // IWYU pragma: keep
23
#include <gen_cpp/HeartbeatService_types.h>
24
#include <gen_cpp/Types_types.h>
25
#include <glog/logging.h>
26
#include <stdint.h>
27
#include <stdio.h>
28
#include <unistd.h>
29
30
#include <cstring>
31
#include <memory>
32
#include <sstream>
33
#include <utility>
34
#include <vector>
35
36
#include "common/metrics/doris_metrics.h"
37
#include "common/metrics/metrics.h"
38
#include "common/status.h"
39
#include "io/fs/file_system.h"
40
#include "io/fs/local_file_system.h"
41
#include "runtime/cluster_info.h"
42
#include "runtime/exec_env.h"
43
#include "service/http/http_client.h"
44
#include "util/md5.h"
45
#include "util/string_util.h"
46
47
namespace doris {
48
49
DEFINE_GAUGE_METRIC_PROTOTYPE_2ARG(small_file_cache_count, MetricUnit::NOUNIT);
50
51
SmallFileMgr::SmallFileMgr(ExecEnv* env, const std::string& local_path)
52
0
        : _exec_env(env), _local_path(local_path) {
53
0
    REGISTER_HOOK_METRIC(small_file_cache_count, [this]() {
54
        // std::lock_guard<std::mutex> l(_lock);
55
0
        return _file_cache.size();
56
0
    });
57
0
}
58
59
0
SmallFileMgr::~SmallFileMgr() {
60
0
    DEREGISTER_HOOK_METRIC(small_file_cache_count);
61
0
}
62
63
0
Status SmallFileMgr::init() {
64
0
    RETURN_IF_ERROR(_load_local_files());
65
0
    return Status::OK();
66
0
}
67
68
0
Status SmallFileMgr::_load_local_files() {
69
0
    RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(_local_path));
70
71
0
    auto scan_cb = [this](const io::FileInfo& file) {
72
0
        if (!file.is_file) {
73
0
            return true;
74
0
        }
75
0
        auto st = _load_single_file(_local_path, file.file_name);
76
0
        if (!st.ok()) {
77
0
            LOG(WARNING) << "load small file failed: " << st;
78
0
        }
79
0
        return true;
80
0
    };
81
82
0
    RETURN_IF_ERROR(io::global_local_filesystem()->iterate_directory(_local_path, scan_cb));
83
0
    return Status::OK();
84
0
}
85
86
0
Status SmallFileMgr::_load_single_file(const std::string& path, const std::string& file_name) {
87
    // file name format should be like:
88
    // file_id.md5
89
0
    std::vector<std::string> parts = absl::StrSplit(file_name, ".");
90
0
    if (parts.size() != 2) {
91
0
        return Status::InternalError("Not a valid file name: {}", file_name);
92
0
    }
93
0
    int64_t file_id = std::stol(parts[0]);
94
0
    std::string md5 = parts[1];
95
96
0
    if (_file_cache.find(file_id) != _file_cache.end()) {
97
0
        return Status::InternalError("File with same id is already been loaded: {}", file_id);
98
0
    }
99
100
0
    std::string file_md5;
101
0
    RETURN_IF_ERROR(io::global_local_filesystem()->md5sum(path + "/" + file_name, &file_md5));
102
0
    if (file_md5 != md5) {
103
0
        return Status::InternalError("Invalid md5 of file: {}", file_name);
104
0
    }
105
106
0
    CacheEntry entry;
107
0
    entry.path = path + "/" + file_name;
108
0
    entry.md5 = file_md5;
109
110
0
    _file_cache.emplace(file_id, entry);
111
0
    return Status::OK();
112
0
}
113
114
0
Status SmallFileMgr::get_file(int64_t file_id, const std::string& md5, std::string* file_path) {
115
0
    std::unique_lock<std::mutex> l(_lock);
116
    // find in cache
117
0
    auto it = _file_cache.find(file_id);
118
0
    if (it != _file_cache.end()) {
119
        // find the cached file, check it
120
0
        CacheEntry& entry = it->second;
121
0
        Status st = _check_file(entry, md5);
122
0
        if (!st.ok()) {
123
            // check file failed, we should remove this cache and download it from FE again
124
0
            if (remove(entry.path.c_str()) != 0) {
125
0
                return Status::InternalError("failed to remove file: {}, err: {}", file_id,
126
0
                                             std::strerror(errno));
127
0
            }
128
0
            _file_cache.erase(it);
129
0
        } else {
130
            // check ok, return the path
131
0
            *file_path = entry.path;
132
0
            return Status::OK();
133
0
        }
134
0
    }
135
136
    // file not found in cache. download it from FE
137
0
    RETURN_IF_ERROR(_download_file(file_id, md5, file_path));
138
139
0
    return Status::OK();
140
0
}
141
142
0
Status SmallFileMgr::_check_file(const CacheEntry& entry, const std::string& md5) {
143
0
    bool exists;
144
0
    RETURN_IF_ERROR(io::global_local_filesystem()->exists(entry.path, &exists));
145
0
    if (!exists) {
146
0
        return Status::InternalError("file not exist: {}", entry.path);
147
0
    }
148
0
    if (!iequal(md5, entry.md5)) {
149
0
        return Status::InternalError("invalid MD5 of file: {}", entry.path);
150
0
    }
151
0
    return Status::OK();
152
0
}
153
154
Status SmallFileMgr::_download_file(int64_t file_id, const std::string& md5,
155
0
                                    std::string* file_path) {
156
0
    std::stringstream ss;
157
0
    ss << _local_path << "/" << file_id << ".tmp";
158
0
    std::string tmp_file = ss.str();
159
0
    bool should_delete = true;
160
0
    auto fp_closer = [&tmp_file, &should_delete](FILE* fp) {
161
0
        fclose(fp);
162
0
        if (should_delete) remove(tmp_file.c_str());
163
0
    };
164
165
0
    std::unique_ptr<FILE, decltype(fp_closer)> fp(fopen(tmp_file.c_str(), "w"), fp_closer);
166
0
    if (fp == nullptr) {
167
0
        LOG(WARNING) << "fail to open file, file=" << tmp_file;
168
0
        return Status::InternalError("fail to open file");
169
0
    }
170
171
0
    ClusterInfo* cluster_info = _exec_env->cluster_info();
172
    // Small file download is the only BE→FE path that uses HTTP (not Thrift/RPC).
173
    // master_fe_http_port is set to https_port when enable_https=true (see HeartbeatMgr).
174
    // The ~1ms fallback overhead is acceptable; small file downloads are infrequent.
175
0
    const std::string host_port = cluster_info->master_fe_addr.hostname + ":" +
176
0
                                  std::to_string(cluster_info->master_fe_http_port);
177
0
    const std::string query = "/api/get_small_file?file_id=" + std::to_string(file_id) +
178
0
                              "&token=" + cluster_info->token;
179
180
0
    Status status;
181
0
    Md5Digest digest;
182
0
    auto download_cb = [&status, &tmp_file, &fp, &digest](const void* data, size_t length) {
183
0
        digest.update(data, length);
184
0
        auto res = fwrite(data, length, 1, fp.get());
185
0
        if (res != 1) {
186
0
            LOG(WARNING) << "fail to write data to file, file=" << tmp_file
187
0
                         << ", error=" << ferror(fp.get());
188
0
            status = Status::InternalError("fail to write data when download");
189
0
            return false;
190
0
        }
191
0
        return true;
192
0
    };
193
194
0
    std::string url = "http://" + host_port + query;
195
0
    LOG(INFO) << "download file from: " << url;
196
0
    HttpClient client;
197
0
    RETURN_IF_ERROR(client.init(url));
198
0
    Status execute_status = client.execute(download_cb);
199
200
0
    if (!execute_status.ok()) {
201
0
        rewind(fp.get());
202
0
        if (ftruncate(fileno(fp.get()), 0) != 0) {
203
0
            LOG(WARNING) << "fail to truncate temp file for https retry, errno=" << errno;
204
0
        }
205
0
        status = Status::OK();
206
0
        digest = Md5Digest();
207
208
0
        url = "https://" + host_port + query;
209
0
        LOG(INFO) << "HTTP failed, retrying with HTTPS: " << url;
210
0
        HttpClient https_client;
211
0
        RETURN_IF_ERROR(https_client.init(url));
212
        // Skip TLS cert verification: internal cluster traffic only; file integrity
213
        // is guaranteed independently by MD5 checksum verification below.
214
0
        https_client.use_untrusted_ssl();
215
0
        execute_status = https_client.execute(download_cb);
216
0
    }
217
218
0
    RETURN_IF_ERROR(execute_status);
219
0
    RETURN_IF_ERROR(status);
220
0
    digest.digest();
221
222
0
    if (!iequal(digest.hex(), md5)) {
223
0
        LOG(WARNING) << "file's checksum is not equal, download: " << digest.hex()
224
0
                     << ", expected: " << md5 << ", file: " << file_id;
225
0
        return Status::InternalError("download with invalid md5");
226
0
    }
227
228
    // close this file
229
0
    should_delete = false;
230
0
    fp.reset();
231
232
    // rename temporary file to library file
233
0
    std::stringstream real_ss;
234
0
    real_ss << _local_path << "/" << file_id << "." << md5;
235
0
    std::string real_file_path = real_ss.str();
236
0
    auto ret = rename(tmp_file.c_str(), real_file_path.c_str());
237
0
    if (ret != 0) {
238
0
        char buf[64];
239
0
        LOG(WARNING) << "fail to rename file from=" << tmp_file << ", to=" << real_file_path
240
0
                     << ", errno=" << errno << ", errmsg=" << strerror_r(errno, buf, 64);
241
0
        remove(tmp_file.c_str());
242
0
        remove(real_file_path.c_str());
243
0
        return Status::InternalError("fail to rename file");
244
0
    }
245
246
    // add to file cache
247
0
    CacheEntry entry;
248
0
    entry.path = real_file_path;
249
0
    entry.md5 = md5;
250
0
    _file_cache.emplace(file_id, entry);
251
252
0
    *file_path = real_file_path;
253
254
    LOG(INFO) << "finished to download file: " << file_path;
255
0
    return Status::OK();
256
0
}
257
258
} // end namespace doris