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