GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/bundle_mgr.cc
Date: 2026-07-26 02:35:14
Exec Total Coverage
Lines: 147 212 69.3%
Branches: 113 304 37.2%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 */
4
5 #include "bundle_mgr.h"
6
7 #include <fcntl.h>
8 #include <pthread.h>
9 #include <unistd.h>
10
11 #include <cassert>
12 #include <cerrno>
13 #include <cstdlib>
14 #include <memory>
15 #include <string>
16 #include <vector>
17
18 #include "cache.h"
19 #include "catalog_mgr_client.h"
20 #include "fetch.h"
21 #include "file_chunk.h"
22 #include "json_document.h"
23 #include "mountpoint.h"
24 #include "options.h"
25 #include "shortstring.h"
26 #include "util/posix.h"
27
28 namespace {
29 constexpr size_t kDefaultBundlePoolSize = 8;
30
31 // Read the .cvmfsbundle-<basename> file via the cvmfs cache
32 190 BundleFileMgr *LoadBundleFromCvmfs(MountPoint *mp,
33 const PathString &bundle_file_path) {
34
1/2
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
190 catalog::DirectoryEntry dirent;
35
2/4
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 190 times.
190 if (!mp->catalog_mgr()->LookupPath(bundle_file_path, catalog::kLookupDefault,
36 &dirent)) {
37 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-LOAD: LookupPath failed for %s",
38 bundle_file_path.ToString().c_str());
39 return nullptr;
40 }
41 190 cvmfs::Fetcher *fetcher = mp->fetcher();
42
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 190 times.
190 if (fetcher == nullptr) {
43 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-LOAD: fetcher is null");
44 return nullptr;
45 }
46
47 190 CacheManager::Label label;
48
1/2
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
190 label.path = bundle_file_path.ToString();
49
1/2
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
190 label.size = dirent.size();
50 190 label.zip_algorithm = dirent.compression_algorithm();
51
2/4
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 190 times.
✗ Branch 6 not taken.
570 const int fd = fetcher->Fetch(
52
1/2
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
380 CacheManager::LabeledObject(dirent.checksum(), label));
53
1/2
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
190 if (fd < 0) {
54
1/2
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
190 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-LOAD: Fetch returned fd=%d", fd);
55 190 return nullptr;
56 }
57
58 CacheManager *cache_mgr = mp->file_system()->cache_mgr();
59 std::string content;
60 content.resize(static_cast<size_t>(dirent.size()));
61 const int64_t n = cache_mgr->Pread(fd, &content[0], content.size(), 0);
62 cache_mgr->Close(fd);
63 if (n < 0 || static_cast<size_t>(n) != content.size()) {
64 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-LOAD: Pread returned %ld want %zu",
65 static_cast<long>(n), content.size());
66 return nullptr;
67 }
68
69 // The bundle file may start with a "#%CVMFS_BUNDLE version=..." header
70 // line (per file_bundle.h); strip any leading lines beginning with '#'
71 // before handing off to the strict JSON parser.
72 size_t json_start = 0;
73 while (json_start < content.size() && content[json_start] == '#') {
74 const size_t nl = content.find('\n', json_start);
75 if (nl == std::string::npos) {
76 json_start = content.size();
77 break;
78 }
79 json_start = nl + 1;
80 }
81 const std::string json_text = (json_start == 0) ? content
82 : content.substr(json_start);
83
84 JsonDocument *doc = JsonDocument::Create(json_text);
85 if (doc == nullptr) {
86 LogCvmfs(kLogCvmfs, kLogDebug,
87 "BUNDLE-LOAD: JsonDocument::Create failed (size=%zu)",
88 json_text.size());
89 return nullptr;
90 }
91 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-LOAD: loaded bundle %s (%zu bytes)",
92 bundle_file_path.ToString().c_str(), content.size());
93 return new BundleFileMgr(doc);
94 190 }
95 } // namespace
96
97 190 BundleMgr::BundleMgr(MountPoint *mp, const PathString &path)
98 190 : mount_point_(mp)
99 190 , path_(path)
100 190 , fetcher_threads_()
101 190 , pool_size_(kDefaultBundlePoolSize) {
102
2/4
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
190 fname_ = GetFileName(path_);
103
2/4
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
190 parent_path_ = GetParentPath(path_);
104 // There is a naming convention regarding the name of the file with the
105 // contents of the bundle
106
3/6
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 190 times.
✗ Branch 8 not taken.
760 bundle_file_path_ = PathString(parent_path_.ToString() + "/.cvmfsbundle-"
107
3/6
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 190 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 190 times.
✗ Branch 8 not taken.
950 + fname_.ToString());
108
109 190 pipe_bm_[0] = pipe_bm_[1] = -1;
110 190 pthread_mutex_init(&worker_read_mutex_, nullptr);
111
112
1/2
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
190 bfm_ = LoadBundleFromCvmfs(mount_point_, bundle_file_path_);
113
1/2
✓ Branch 0 taken 190 times.
✗ Branch 1 not taken.
190 if (bfm_ == nullptr) {
114
1/2
✓ Branch 2 taken 190 times.
✗ Branch 3 not taken.
190 LogCvmfs(kLogCvmfs, kLogDebug, "BundleMgr: failed to load bundle file %s",
115
1/2
✓ Branch 1 taken 190 times.
✗ Branch 2 not taken.
380 bundle_file_path_.ToString().c_str());
116 190 is_valid_ = false;
117 190 return;
118 }
119
120 // Pool size override via CVMFS_BUNDLE_POOL_SIZE
121 if (mount_point_ != nullptr && mount_point_->file_system() != nullptr
122 && mount_point_->file_system()->options_mgr() != nullptr) {
123 std::string opt;
124 if (mount_point_->file_system()->options_mgr()->GetValue(
125 "CVMFS_BUNDLE_POOL_SIZE", &opt)) {
126 char *end = nullptr;
127 const unsigned long n = std::strtoul(opt.c_str(), &end, 10);
128 if (end != opt.c_str() && n >= 1) {
129 pool_size_ = static_cast<size_t>(n);
130 }
131 }
132 }
133
134 SpawnFetcherPool();
135 }
136
137 76 void BundleMgr::Fetch() {
138
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 76 times.
76 if (not is_valid_) {
139 LogCvmfs(kLogBundleMgr,
140 kLogDebug,
141 "BundleMgr is not in a valid state. Can't fetch!");
142 return;
143 }
144
145
4/6
✓ Branch 1 taken 304 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 304 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 76 times.
✓ Branch 7 taken 228 times.
304 while (auto file = bfm_->GetNext()) {
146
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 const PathString path = NormalizeDependencyPath(file);
147 // A TrySendPath() here is used as a profylaxis to a scenario where the pipe
148 // is currently blocked.
149
2/4
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 228 times.
228 while (not TrySendPath(back_channel_, path)) {
150 }
151
2/2
✓ Branch 2 taken 228 times.
✓ Branch 3 taken 76 times.
532 }
152 }
153
154 /**
155 * Dependency paths in a bundle are absolute from the repository root.
156 * Entries without a leading slash (optionally prefixed with "./") are
157 * resolved relative to the directory holding the bundle file.
158 */
159 228 PathString BundleMgr::NormalizeDependencyPath(const PathString &path) const {
160
2/4
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 228 times.
228 if (path.StartsWith(PathString("/", 1)))
161 return path;
162
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 const PathString relative = path.StartsWith(PathString("./", 2))
163 ? path.Suffix(2)
164
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 228 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 6 taken 228 times.
✗ Branch 7 not taken.
228 : path;
165
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 PathString normalized(parent_path_);
166
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 normalized.Append("/", 1);
167
1/2
✓ Branch 3 taken 228 times.
✗ Branch 4 not taken.
228 normalized.Append(relative.GetChars(), relative.GetLength());
168
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 return normalized;
169 228 }
170
171 266 void BundleMgr::JoinFetcherPool() {
172
2/2
✓ Branch 0 taken 76 times.
✓ Branch 1 taken 190 times.
266 if (pipe_bm_[1] < 0)
173 76 return;
174 // Send one kTerminate per worker. Workers drain all queued kFetch
175 // messages before reaching their kTerminate (FIFO pipe), so we can't
176 // just close the pipe — that would EOF some workers mid-drain.
177
2/2
✓ Branch 1 taken 1520 times.
✓ Branch 2 taken 190 times.
1710 for (size_t i = 0; i < fetcher_threads_.size(); ++i) {
178 1520 Command cmd = Command::kTerminate;
179 while (true) {
180
1/2
✓ Branch 1 taken 1520 times.
✗ Branch 2 not taken.
1520 const ssize_t n = ::write(pipe_bm_[1], &cmd, sizeof(Command));
181
1/2
✓ Branch 0 taken 1520 times.
✗ Branch 1 not taken.
1520 if (n == sizeof(Command))
182 1520 break;
183 if (errno != EAGAIN && errno != EWOULDBLOCK)
184 break;
185 }
186 }
187 // Wait for every worker to drain its share of the queue and exit.
188
2/2
✓ Branch 5 taken 1520 times.
✓ Branch 6 taken 190 times.
1710 for (auto &t : fetcher_threads_) {
189
1/2
✓ Branch 2 taken 1520 times.
✗ Branch 3 not taken.
1520 pthread_join(*t, nullptr);
190 }
191 190 fetcher_threads_.clear();
192 190 ClosePipe(pipe_bm_);
193 // Mark the pool as gone so that a subsequent call (e.g. the destructor
194 // running after an explicit JoinFetcherPool()) is a no-op instead of a
195 // double close/join.
196 190 pipe_bm_[0] = pipe_bm_[1] = -1;
197 }
198
199 190 void BundleMgr::SpawnFetcherPool() {
200 190 MakePipe(pipe_bm_);
201 190 back_channel_ = pipe_bm_[1];
202
203 // Non-blocking writes on the work-queue pipe so TrySendPath can poll.
204 // Per pipe(7), writes <= PIPE_BUF are atomic on non-blocking pipes:
205 // they either fully succeed or fail with EAGAIN.
206 190 const int flags = fcntl(back_channel_, F_GETFL);
207 190 fcntl(back_channel_, F_SETFL, flags | O_NONBLOCK);
208
209
2/2
✓ Branch 0 taken 1520 times.
✓ Branch 1 taken 190 times.
1710 for (size_t i = 0; i < pool_size_; ++i) {
210
1/2
✓ Branch 1 taken 1520 times.
✗ Branch 2 not taken.
1520 std::unique_ptr<pthread_t> thread(new pthread_t());
211 1520 const int res = pthread_create(thread.get(), nullptr, MainBundleMgrFetcher,
212 this);
213
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1520 times.
1520 if (res != 0) {
214 LogCvmfs(kLogBundleMgr, kLogDebug,
215 "Thread creation failed! pool_size_=%zu spawned=%zu", pool_size_,
216 i);
217 is_valid_ = false;
218 return;
219 }
220
1/2
✓ Branch 2 taken 1520 times.
✗ Branch 3 not taken.
1520 fetcher_threads_.emplace_back(std::move(thread));
221
1/2
✓ Branch 1 taken 1520 times.
✗ Branch 2 not taken.
1520 }
222 }
223
224 228 void BundleMgr::FetchPath(const PathString &path) {
225
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 catalog::DirectoryEntry dirent;
226
1/2
✓ Branch 2 taken 228 times.
✗ Branch 3 not taken.
228 const bool found = mount_point_->catalog_mgr()->LookupPath(
227 path, catalog::kLookupDefault, &dirent);
228 228 cvmfs::Fetcher *this_fetcher = dirent.IsExternalFile()
229
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 228 times.
228 ? mount_point_->external_fetcher()
230 228 : mount_point_->fetcher();
231
2/4
✓ Branch 0 taken 228 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 228 times.
228 if (not(found and this_fetcher)) {
232 LogCvmfs(kLogCvmfs, kLogDebug,
233 "BUNDLE-FETCH: lookup failed for %s (found=%d)",
234 path.ToString().c_str(), int(found));
235 return;
236 }
237
1/2
✓ Branch 2 taken 228 times.
✗ Branch 3 not taken.
228 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-FETCH: prefetching %s",
238
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
456 path.ToString().c_str());
239
240
2/2
✓ Branch 1 taken 114 times.
✓ Branch 2 taken 114 times.
228 if (dirent.IsChunkedFile()) {
241 // Files above the chunking threshold are stored as per-chunk objects;
242 // their bulk object only exists if the repository sets
243 // CVMFS_GENERATE_LEGACY_BULK_CHUNKS. Fetch the chunks, exactly like the
244 // read path does.
245
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
114 FileChunkList chunks;
246
1/2
✓ Branch 3 taken 114 times.
✗ Branch 4 not taken.
114 if (!mount_point_->catalog_mgr()->ListFileChunks(
247 path, dirent.hash_algorithm(), &chunks)
248
3/6
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 114 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 114 times.
114 || chunks.IsEmpty()) {
249 LogCvmfs(kLogCvmfs, kLogDebug, "BUNDLE-FETCH: no chunks found for %s",
250 path.ToString().c_str());
251 return;
252 }
253
2/2
✓ Branch 1 taken 342 times.
✓ Branch 2 taken 114 times.
456 for (unsigned i = 0; i < chunks.size(); ++i) {
254 342 CacheManager::Label label;
255
1/2
✓ Branch 1 taken 342 times.
✗ Branch 2 not taken.
342 label.path = path.ToString();
256 342 label.size = chunks.AtPtr(i)->size();
257 342 label.zip_algorithm = dirent.compression_algorithm();
258 342 label.flags |= CacheManager::kLabelChunked;
259
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 342 times.
342 if (mount_point_->catalog_mgr()->volatile_flag())
260 label.flags |= CacheManager::kLabelVolatile;
261
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 342 times.
342 if (dirent.IsExternalFile()) {
262 label.flags |= CacheManager::kLabelExternal;
263 label.range_offset = chunks.AtPtr(i)->offset();
264 }
265
2/4
✓ Branch 2 taken 342 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 342 times.
✗ Branch 6 not taken.
1026 const int fd = this_fetcher->Fetch(
266
1/2
✓ Branch 3 taken 342 times.
✗ Branch 4 not taken.
684 CacheManager::LabeledObject(chunks.AtPtr(i)->content_hash(), label));
267
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 342 times.
342 if (fd >= 0)
268 mount_point_->file_system()->cache_mgr()->Close(fd);
269 342 }
270 114 return;
271 114 }
272
273 114 CacheManager::Label label;
274
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
114 label.path = path.ToString();
275
1/2
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
114 label.size = dirent.size();
276 114 label.zip_algorithm = dirent.compression_algorithm();
277
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 114 times.
114 if (mount_point_->catalog_mgr()->volatile_flag())
278 label.flags |= CacheManager::kLabelVolatile;
279
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 114 times.
114 if (dirent.IsExternalFile())
280 label.flags |= CacheManager::kLabelExternal;
281
2/4
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 114 times.
✗ Branch 6 not taken.
342 const int fd = this_fetcher->Fetch(
282
1/2
✓ Branch 2 taken 114 times.
✗ Branch 3 not taken.
228 CacheManager::LabeledObject(dirent.checksum(), label));
283
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (fd >= 0)
284 mount_point_->file_system()->cache_mgr()->Close(fd);
285
2/2
✓ Branch 2 taken 114 times.
✓ Branch 3 taken 114 times.
228 }
286
287 1520 void *BundleMgr::MainBundleMgrFetcher(void *data) {
288 #ifndef __APPLE__
289 1520 pthread_setname_np(pthread_self(), "bm_fetcher");
290 #endif
291 1520 BundleMgr *mgr = static_cast<BundleMgr *>(data);
292 1520 const int rfd = mgr->pipe_bm_[0];
293
294 while (true) {
295 1748 Command cmd = Command::kTerminate;
296 1748 PathString path;
297 1748 bool got_path = false;
298 1748 bool eof = false;
299
300 // Atomically receive cmd + (optional) path payload. The whole receipt
301 // is under worker_read_mutex_ so messages aren't interleaved between
302 // workers reading from the shared pipe.
303 1748 pthread_mutex_lock(&mgr->worker_read_mutex_);
304
1/2
✓ Branch 1 taken 1748 times.
✗ Branch 2 not taken.
1748 const ssize_t n = read(rfd, &cmd, sizeof(Command));
305
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1748 times.
1748 if (n != static_cast<ssize_t>(sizeof(Command))) {
306 eof = true;
307
2/2
✓ Branch 0 taken 228 times.
✓ Branch 1 taken 1520 times.
1748 } else if (cmd == Command::kFetch) {
308
2/4
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 228 times.
✗ Branch 5 not taken.
228 path = mgr->ReceivePath(rfd);
309 228 got_path = true;
310 }
311 1748 pthread_mutex_unlock(&mgr->worker_read_mutex_);
312
313
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1748 times.
1748 if (eof)
314 break;
315
316 1748 bool terminate = false;
317
2/2
✓ Branch 0 taken 228 times.
✓ Branch 1 taken 1520 times.
1748 switch (cmd) {
318 228 case Command::kFetch: {
319
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 228 times.
228 if (!got_path) {
320 terminate = true;
321 break;
322 }
323
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 mgr->FetchPath(path);
324 228 } break;
325 1520 case Command::kTerminate:
326 default:
327 1520 terminate = true;
328 1520 break;
329 }
330
2/2
✓ Branch 0 taken 1520 times.
✓ Branch 1 taken 228 times.
1748 if (terminate) {
331 1520 break;
332 }
333
2/2
✓ Branch 1 taken 228 times.
✓ Branch 2 taken 1520 times.
1976 }
334
335 1520 pthread_exit(nullptr);
336 }
337
338 304 PathString BundleMgr::ReceivePath(int fd) const {
339
1/2
✓ Branch 1 taken 304 times.
✗ Branch 2 not taken.
304 const std::string buffer = BlockingReceive(fd);
340
1/2
✓ Branch 1 taken 304 times.
✗ Branch 2 not taken.
304 assert(buffer.size() > 0 && "A path can't be empty");
341
1/2
✓ Branch 1 taken 304 times.
✗ Branch 2 not taken.
608 return PathString(buffer);
342 304 }
343
344 228 bool BundleMgr::TrySendPath(int fd, const PathString &path) const {
345 228 Command cmd = Command::kFetch;
346
2/4
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 228 times.
228 if ((write(fd, &cmd, sizeof(Command))) != sizeof(Command)) {
347 if (not(errno == EAGAIN || errno == EWOULDBLOCK)) {
348 LogCvmfs(kLogBundleMgr,
349 kLogDebug,
350 "write() on back channel failed unexpectedly");
351 }
352 return false;
353 } else {
354
1/2
✓ Branch 1 taken 228 times.
✗ Branch 2 not taken.
228 BlockingSend(fd, path);
355 }
356 228 return true;
357 }
358
359