| Directory: | cvmfs/ | 
|---|---|
| File: | cvmfs/fuse_evict.cc | 
| Date: | 2025-10-26 02:35:25 | 
| Exec | Total | Coverage | |
|---|---|---|---|
| Lines: | 112 | 164 | 68.3% | 
| Branches: | 62 | 127 | 48.8% | 
| Line | Branch | Exec | Source | 
|---|---|---|---|
| 1 | /** | ||
| 2 | * This file is part of the CernVM File System. | ||
| 3 | */ | ||
| 4 | |||
| 5 | #define __STDC_FORMAT_MACROS | ||
| 6 | |||
| 7 | |||
| 8 | #include "fuse_evict.h" | ||
| 9 | |||
| 10 | #include <inttypes.h> | ||
| 11 | #include <stdint.h> | ||
| 12 | |||
| 13 | #include <cassert> | ||
| 14 | #include <cstdlib> | ||
| 15 | #include <cstring> | ||
| 16 | #include <new> | ||
| 17 | #include <vector> | ||
| 18 | |||
| 19 | #include "glue_buffer.h" | ||
| 20 | #include "mountpoint.h" | ||
| 21 | #include "shortstring.h" | ||
| 22 | #include "util/logging.h" | ||
| 23 | #include "util/platform.h" | ||
| 24 | #include "util/posix.h" | ||
| 25 | #include "util/smalloc.h" | ||
| 26 | |||
| 27 | using namespace std; // NOLINT | ||
| 28 | |||
| 29 | 196 | FuseInvalidator::Handle::Handle(unsigned timeout_s) | |
| 30 | 2/2✓ Branch 1 taken 98 times. ✓ Branch 2 taken 98 times. | 196 | : timeout_s_((timeout_s == 0) ? 0 : (timeout_s + kTimeoutSafetyMarginSec)) { | 
| 31 | 196 | status_ = reinterpret_cast<atomic_int32 *>(smalloc(sizeof(atomic_int32))); | |
| 32 | 196 | atomic_init32(status_); | |
| 33 | 196 | } | |
| 34 | |||
| 35 | |||
| 36 | 196 | FuseInvalidator::Handle::~Handle() { free(status_); } | |
| 37 | |||
| 38 | |||
| 39 | 245 | void FuseInvalidator::Handle::WaitFor() { | |
| 40 | 2/2✓ Branch 1 taken 294 times. ✓ Branch 2 taken 245 times. | 539 | while (!IsDone()) | 
| 41 | 294 | SafeSleepMs(FuseInvalidator::kCheckTimeoutFreqMs); | |
| 42 | 245 | } | |
| 43 | |||
| 44 | |||
| 45 | //------------------------------------------------------------------------------ | ||
| 46 | |||
| 47 | |||
| 48 | const unsigned FuseInvalidator::kTimeoutSafetyMarginSec = 1; | ||
| 49 | const unsigned FuseInvalidator::kCheckTimeoutFreqMs = 100; | ||
| 50 | const unsigned FuseInvalidator::kCheckTimeoutFreqOps = 256; | ||
| 51 | |||
| 52 | bool FuseInvalidator::g_fuse_notify_invalidation_ = true; | ||
| 53 | |||
| 54 | 147 | bool FuseInvalidator::HasFuseNotifyInval() { | |
| 55 | /** | ||
| 56 | * Technically, also libfuse 2.8 has support. Libfuse 2.8 comes with EL6, | ||
| 57 | * which had bugs reported related to the fuse_notify_inval_...() functions. | ||
| 58 | * Since just waiting for the timeout works perfectly fine, there is no reason | ||
| 59 | * to optimize for forced cache eviction too aggressively. | ||
| 60 | * | ||
| 61 | * TODO(jblomer): could we have libfuse 2.9 or higher with a very old kernel | ||
| 62 | * that doesn't support active invalidation? How old does the kernel need | ||
| 63 | * to be? Probably that situation is never triggered in practice. | ||
| 64 | */ | ||
| 65 | 147 | return FuseInvalidator::g_fuse_notify_invalidation_ && (FUSE_VERSION >= 29); | |
| 66 | } | ||
| 67 | |||
| 68 | |||
| 69 | ✗ | FuseInvalidator::FuseInvalidator(MountPoint *mount_point, | |
| 70 | void **fuse_channel_or_session, | ||
| 71 | ✗ | bool fuse_notify_invalidation) | |
| 72 | ✗ | : mount_point_(mount_point) | |
| 73 | ✗ | , inode_tracker_(mount_point->inode_tracker()) | |
| 74 | ✗ | , dentry_tracker_(mount_point->dentry_tracker()) | |
| 75 | ✗ | , fuse_channel_or_session_(fuse_channel_or_session) | |
| 76 | ✗ | , spawned_(false) { | |
| 77 | ✗ | g_fuse_notify_invalidation_ = fuse_notify_invalidation; | |
| 78 | ✗ | memset(&thread_invalidator_, 0, sizeof(thread_invalidator_)); | |
| 79 | ✗ | atomic_init32(&terminated_); | |
| 80 | } | ||
| 81 | |||
| 82 | 245 | FuseInvalidator::FuseInvalidator(glue::InodeTracker *inode_tracker, | |
| 83 | glue::DentryTracker *dentry_tracker, | ||
| 84 | void **fuse_channel_or_session, | ||
| 85 | 245 | bool fuse_notify_invalidation) | |
| 86 | 245 | : mount_point_(NULL) | |
| 87 | 245 | , inode_tracker_(inode_tracker) | |
| 88 | 245 | , dentry_tracker_(dentry_tracker) | |
| 89 | 245 | , fuse_channel_or_session_(fuse_channel_or_session) | |
| 90 | 1/2✓ Branch 3 taken 245 times. ✗ Branch 4 not taken. | 245 | , spawned_(false) { | 
| 91 | 245 | g_fuse_notify_invalidation_ = fuse_notify_invalidation; | |
| 92 | 245 | memset(&thread_invalidator_, 0, sizeof(thread_invalidator_)); | |
| 93 | 245 | atomic_init32(&terminated_); | |
| 94 | 245 | } | |
| 95 | |||
| 96 | |||
| 97 | 245 | FuseInvalidator::~FuseInvalidator() { | |
| 98 | 245 | atomic_cas32(&terminated_, 0, 1); | |
| 99 | 2/2✓ Branch 0 taken 196 times. ✓ Branch 1 taken 49 times. | 245 | if (spawned_) { | 
| 100 | 196 | QuitCommand *cmd = new (smalloc(sizeof(QuitCommand))) QuitCommand(); | |
| 101 | 196 | channel_.PushBack(cmd); | |
| 102 | 196 | pthread_join(thread_invalidator_, NULL); | |
| 103 | } | ||
| 104 | 245 | } | |
| 105 | |||
| 106 | |||
| 107 | 245 | void FuseInvalidator::InvalidateInodes(Handle *handle) { | |
| 108 | 1/2✗ Branch 0 not taken. ✓ Branch 1 taken 245 times. | 245 | assert(handle != NULL); | 
| 109 | InvalInodesCommand *inval_inodes_command = new ( | ||
| 110 | 245 | smalloc(sizeof(InvalInodesCommand))) InvalInodesCommand(); | |
| 111 | 245 | inval_inodes_command->handle = handle; | |
| 112 | 245 | channel_.PushBack(inval_inodes_command); | |
| 113 | 245 | } | |
| 114 | |||
| 115 | ✗ | void FuseInvalidator::InvalidateDentry(uint64_t parent_ino, | |
| 116 | const NameString &name) { | ||
| 117 | InvalDentryCommand *inval_dentry_command; | ||
| 118 | ✗ | vector<Command *> *items = channel_.StartEnqueueing(); | |
| 119 | ✗ | for (size_t i = 0; i < items->size(); ++i) { | |
| 120 | ✗ | inval_dentry_command = dynamic_cast<InvalDentryCommand *>(items->at(i)); | |
| 121 | ✗ | if (!inval_dentry_command) | |
| 122 | ✗ | continue; | |
| 123 | ✗ | if (inval_dentry_command->parent_ino != parent_ino) | |
| 124 | ✗ | continue; | |
| 125 | ✗ | if (inval_dentry_command->name != name) | |
| 126 | ✗ | continue; | |
| 127 | ✗ | channel_.AbortEnqueueing(); | |
| 128 | ✗ | return; | |
| 129 | } | ||
| 130 | |||
| 131 | ✗ | inval_dentry_command = new (smalloc(sizeof(InvalDentryCommand))) | |
| 132 | ✗ | InvalDentryCommand(); | |
| 133 | ✗ | inval_dentry_command->parent_ino = parent_ino; | |
| 134 | ✗ | inval_dentry_command->name = name; | |
| 135 | ✗ | items->push_back(inval_dentry_command); | |
| 136 | ✗ | channel_.CommitEnqueueing(); | |
| 137 | } | ||
| 138 | |||
| 139 | 196 | void *FuseInvalidator::MainInvalidator(void *data) { | |
| 140 | 196 | FuseInvalidator *invalidator = reinterpret_cast<FuseInvalidator *>(data); | |
| 141 | 196 | LogCvmfs(kLogCvmfs, kLogDebug, "starting dentry invalidator thread"); | |
| 142 | |||
| 143 | 196 | bool reported_missing_inval_support = false; | |
| 144 | while (true) { | ||
| 145 | 1/2✓ Branch 1 taken 441 times. ✗ Branch 2 not taken. | 441 | Command *command = invalidator->channel_.PopFront(); | 
| 146 | |||
| 147 | 3/4✓ Branch 0 taken 441 times. ✗ Branch 1 not taken. ✓ Branch 2 taken 196 times. ✓ Branch 3 taken 245 times. | 441 | if (dynamic_cast<QuitCommand *>(command)) { | 
| 148 | 196 | command->~Command(); | |
| 149 | 196 | free(command); | |
| 150 | 196 | break; | |
| 151 | } | ||
| 152 | |||
| 153 | InvalDentryCommand | ||
| 154 | 1/2✓ Branch 0 taken 245 times. ✗ Branch 1 not taken. | 245 | *inval_dentry_command = dynamic_cast<InvalDentryCommand *>(command); | 
| 155 | 1/2✗ Branch 0 not taken. ✓ Branch 1 taken 245 times. | 245 | if (inval_dentry_command) { | 
| 156 | ✗ | if (invalidator->fuse_channel_or_session_ == NULL) { | |
| 157 | ✗ | if (!reported_missing_inval_support) { | |
| 158 | ✗ | LogCvmfs(kLogCvmfs, kLogSyslogWarn, | |
| 159 | "missing fuse support for dentry invalidation " | ||
| 160 | "(%" PRIu64 "/%s)", | ||
| 161 | inval_dentry_command->parent_ino, | ||
| 162 | ✗ | inval_dentry_command->name.ToString().c_str()); | |
| 163 | ✗ | reported_missing_inval_support = true; | |
| 164 | } | ||
| 165 | ✗ | inval_dentry_command->~InvalDentryCommand(); | |
| 166 | ✗ | free(inval_dentry_command); | |
| 167 | 98 | continue; | |
| 168 | } | ||
| 169 | ✗ | LogCvmfs(kLogCvmfs, kLogDebug, "evicting single dentry %" PRIu64 "/%s", | |
| 170 | inval_dentry_command->parent_ino, | ||
| 171 | ✗ | inval_dentry_command->name.ToString().c_str()); | |
| 172 | #if CVMFS_USE_LIBFUSE == 2 | ||
| 173 | ✗ | fuse_lowlevel_notify_inval_entry( | |
| 174 | *reinterpret_cast<struct fuse_chan **>( | ||
| 175 | ✗ | invalidator->fuse_channel_or_session_), | |
| 176 | inval_dentry_command->parent_ino, | ||
| 177 | inval_dentry_command->name.GetChars(), | ||
| 178 | ✗ | inval_dentry_command->name.GetLength()); | |
| 179 | #else | ||
| 180 | ✗ | fuse_lowlevel_notify_inval_entry( | |
| 181 | *reinterpret_cast<struct fuse_session **>( | ||
| 182 | ✗ | invalidator->fuse_channel_or_session_), | |
| 183 | inval_dentry_command->parent_ino, | ||
| 184 | inval_dentry_command->name.GetChars(), | ||
| 185 | ✗ | inval_dentry_command->name.GetLength()); | |
| 186 | #endif | ||
| 187 | ✗ | inval_dentry_command->~InvalDentryCommand(); | |
| 188 | ✗ | free(inval_dentry_command); | |
| 189 | ✗ | continue; | |
| 190 | } | ||
| 191 | |||
| 192 | InvalInodesCommand | ||
| 193 | 1/2✓ Branch 0 taken 245 times. ✗ Branch 1 not taken. | 245 | *inval_inodes_command = dynamic_cast<InvalInodesCommand *>(command); | 
| 194 | 1/2✗ Branch 0 not taken. ✓ Branch 1 taken 245 times. | 245 | assert(inval_inodes_command); | 
| 195 | |||
| 196 | 245 | Handle *handle = inval_inodes_command->handle; | |
| 197 | 1/2✓ Branch 1 taken 245 times. ✗ Branch 2 not taken. | 245 | LogCvmfs(kLogCvmfs, kLogDebug, "invalidating kernel caches, timeout %u", | 
| 198 | handle->timeout_s_); | ||
| 199 | 245 | inval_inodes_command->~InvalInodesCommand(); | |
| 200 | 245 | free(inval_inodes_command); | |
| 201 | |||
| 202 | 245 | const uint64_t deadline = platform_monotonic_time() + handle->timeout_s_; | |
| 203 | |||
| 204 | // Fallback: drainout by timeout | ||
| 205 | 588 | if ((invalidator->fuse_channel_or_session_ == NULL) | |
| 206 | 5/6✓ Branch 0 taken 147 times. ✓ Branch 1 taken 98 times. ✗ Branch 3 not taken. ✓ Branch 4 taken 147 times. ✓ Branch 5 taken 98 times. ✓ Branch 6 taken 147 times. | 245 | || !HasFuseNotifyInval()) { | 
| 207 | 2/2✓ Branch 1 taken 49 times. ✓ Branch 2 taken 49 times. | 98 | while (platform_monotonic_time() < deadline) { | 
| 208 | 1/2✓ Branch 1 taken 49 times. ✗ Branch 2 not taken. | 49 | SafeSleepMs(kCheckTimeoutFreqMs); | 
| 209 | 1/2✓ Branch 1 taken 49 times. ✗ Branch 2 not taken. | 49 | if (atomic_read32(&invalidator->terminated_) == 1) { | 
| 210 | 1/2✓ Branch 1 taken 49 times. ✗ Branch 2 not taken. | 49 | LogCvmfs(kLogCvmfs, kLogDebug, | 
| 211 | "cancel cache eviction due to termination"); | ||
| 212 | 49 | break; | |
| 213 | } | ||
| 214 | } | ||
| 215 | 98 | handle->SetDone(); | |
| 216 | 98 | continue; | |
| 217 | } | ||
| 218 | |||
| 219 | // We must not hold a lock when calling fuse_lowlevel_notify_inval_entry. | ||
| 220 | // Therefore, we first copy all the inodes into a temporary data structure. | ||
| 221 | glue::InodeTracker::Cursor inode_cursor( | ||
| 222 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | invalidator->inode_tracker_->BeginEnumerate()); | 
| 223 | uint64_t inode; | ||
| 224 | 3/4✓ Branch 1 taken 150675 times. ✗ Branch 2 not taken. ✓ Branch 3 taken 150528 times. ✓ Branch 4 taken 147 times. | 150675 | while (invalidator->inode_tracker_->NextInode(&inode_cursor, &inode)) { | 
| 225 | 1/2✓ Branch 1 taken 150528 times. ✗ Branch 2 not taken. | 150528 | invalidator->evict_list_.PushBack(inode); | 
| 226 | } | ||
| 227 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | invalidator->inode_tracker_->EndEnumerate(&inode_cursor); | 
| 228 | |||
| 229 | 147 | unsigned i = 0; | |
| 230 | 147 | const unsigned N = invalidator->evict_list_.size(); | |
| 231 | 2/2✓ Branch 0 taken 75264 times. ✓ Branch 1 taken 49 times. | 75313 | while (i < N) { | 
| 232 | 75264 | uint64_t inode = invalidator->evict_list_.At(i); | |
| 233 | 1/2✗ Branch 0 not taken. ✓ Branch 1 taken 75264 times. | 75264 | if (inode == 0) | 
| 234 | ✗ | inode = FUSE_ROOT_ID; | |
| 235 | // Can fail, e.g. the inode might be already evicted | ||
| 236 | |||
| 237 | int dbg_retval; | ||
| 238 | |||
| 239 | #if CVMFS_USE_LIBFUSE == 2 | ||
| 240 | 75264 | dbg_retval = fuse_lowlevel_notify_inval_inode( | |
| 241 | *reinterpret_cast<struct fuse_chan **>( | ||
| 242 | 75264 | invalidator->fuse_channel_or_session_), | |
| 243 | inode, 0, 0); | ||
| 244 | #else | ||
| 245 | ✗ | dbg_retval = fuse_lowlevel_notify_inval_inode( | |
| 246 | *reinterpret_cast<struct fuse_session **>( | ||
| 247 | ✗ | invalidator->fuse_channel_or_session_), | |
| 248 | inode, 0, 0); | ||
| 249 | #endif | ||
| 250 | 1/2✓ Branch 1 taken 75264 times. ✗ Branch 2 not taken. | 75264 | LogCvmfs(kLogCvmfs, kLogDebug, | 
| 251 | "evicting inode %" PRIu64 " with retval: %d", inode, dbg_retval); | ||
| 252 | |||
| 253 | (void)dbg_retval; // prevent compiler complaining | ||
| 254 | |||
| 255 | 2/2✓ Branch 0 taken 294 times. ✓ Branch 1 taken 74970 times. | 75264 | if ((++i % kCheckTimeoutFreqOps) == 0) { | 
| 256 | 2/2✓ Branch 1 taken 49 times. ✓ Branch 2 taken 245 times. | 294 | if (platform_monotonic_time() >= deadline) { | 
| 257 | 1/2✓ Branch 1 taken 49 times. ✗ Branch 2 not taken. | 49 | LogCvmfs(kLogCvmfs, kLogDebug, | 
| 258 | "cancel cache eviction after %u entries due to timeout", i); | ||
| 259 | 49 | break; | |
| 260 | } | ||
| 261 | 2/2✓ Branch 1 taken 49 times. ✓ Branch 2 taken 196 times. | 245 | if (atomic_read32(&invalidator->terminated_) == 1) { | 
| 262 | 1/2✓ Branch 1 taken 49 times. ✗ Branch 2 not taken. | 49 | LogCvmfs(kLogCvmfs, kLogDebug, | 
| 263 | "cancel cache eviction due to termination"); | ||
| 264 | 49 | break; | |
| 265 | } | ||
| 266 | } | ||
| 267 | } | ||
| 268 | |||
| 269 | // Do the dentry tracker last to increase the effectiveness of pruning | ||
| 270 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | invalidator->dentry_tracker_->Prune(); | 
| 271 | // Copy and empty the dentry tracker in a single atomic operation | ||
| 272 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | glue::DentryTracker *dentries_copy = invalidator->dentry_tracker_->Move(); | 
| 273 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | glue::DentryTracker::Cursor dentry_cursor = dentries_copy->BeginEnumerate(); | 
| 274 | uint64_t entry_parent; | ||
| 275 | 147 | NameString entry_name; | |
| 276 | 147 | i = 0; | |
| 277 | |||
| 278 | #if CVMFS_USE_LIBFUSE == 2 | ||
| 279 | int (*notify_func)(struct fuse_chan *, fuse_ino_t, const char *, size_t); | ||
| 280 | 147 | notify_func = &fuse_lowlevel_notify_inval_entry; | |
| 281 | #else | ||
| 282 | int (*notify_func)(struct fuse_session *, fuse_ino_t, const char *, size_t); | ||
| 283 | ✗ | notify_func = &fuse_lowlevel_notify_inval_entry; | |
| 284 | #if FUSE_VERSION >= FUSE_MAKE_VERSION(3, 16) | ||
| 285 | // must be libfuse >= 3.16, otherwise the signature is wrong and it | ||
| 286 | // will fail building | ||
| 287 | // mount_point can only be NULL for unittests | ||
| 288 | if (invalidator->mount_point_ != NULL | ||
| 289 | && invalidator->mount_point_->fuse_expire_entry()) { | ||
| 290 | notify_func = &fuse_lowlevel_notify_expire_entry; | ||
| 291 | } | ||
| 292 | #endif | ||
| 293 | #endif | ||
| 294 | |||
| 295 | 147 | while ( | |
| 296 | 3/4✓ Branch 1 taken 62818 times. ✗ Branch 2 not taken. ✓ Branch 3 taken 62720 times. ✓ Branch 4 taken 98 times. | 62818 | dentries_copy->NextEntry(&dentry_cursor, &entry_parent, &entry_name)) { | 
| 297 | 1/2✓ Branch 2 taken 62720 times. ✗ Branch 3 not taken. | 62720 | LogCvmfs(kLogCvmfs, kLogDebug, "evicting dentry %lu --> %s", entry_parent, | 
| 298 | entry_name.c_str()); | ||
| 299 | // Can fail, e.g. the entry might be already evicted | ||
| 300 | #if CVMFS_USE_LIBFUSE == 2 | ||
| 301 | struct fuse_chan | ||
| 302 | 62720 | *channel_or_session = *reinterpret_cast<struct fuse_chan **>( | |
| 303 | 62720 | invalidator->fuse_channel_or_session_); | |
| 304 | #else | ||
| 305 | struct fuse_session | ||
| 306 | ✗ | *channel_or_session = *reinterpret_cast<struct fuse_session **>( | |
| 307 | ✗ | invalidator->fuse_channel_or_session_); | |
| 308 | #endif | ||
| 309 | |||
| 310 | 1/2✓ Branch 2 taken 62720 times. ✗ Branch 3 not taken. | 62720 | notify_func(channel_or_session, entry_parent, entry_name.GetChars(), | 
| 311 | 62720 | entry_name.GetLength()); | |
| 312 | |||
| 313 | 2/2✓ Branch 0 taken 245 times. ✓ Branch 1 taken 62475 times. | 62720 | if ((++i % kCheckTimeoutFreqOps) == 0) { | 
| 314 | 2/2✓ Branch 1 taken 49 times. ✓ Branch 2 taken 196 times. | 245 | if (atomic_read32(&invalidator->terminated_) == 1) { | 
| 315 | 1/2✓ Branch 1 taken 49 times. ✗ Branch 2 not taken. | 49 | LogCvmfs(kLogCvmfs, kLogDebug, | 
| 316 | "cancel cache eviction due to termination"); | ||
| 317 | 49 | break; | |
| 318 | } | ||
| 319 | } | ||
| 320 | } | ||
| 321 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | dentries_copy->EndEnumerate(&dentry_cursor); | 
| 322 | 1/2✓ Branch 0 taken 147 times. ✗ Branch 1 not taken. | 147 | delete dentries_copy; | 
| 323 | |||
| 324 | 147 | handle->SetDone(); | |
| 325 | 1/2✓ Branch 1 taken 147 times. ✗ Branch 2 not taken. | 147 | invalidator->evict_list_.Clear(); | 
| 326 | 392 | } | |
| 327 | |||
| 328 | 196 | LogCvmfs(kLogCvmfs, kLogDebug, "stopping dentry invalidator thread"); | |
| 329 | 196 | return NULL; | |
| 330 | } | ||
| 331 | |||
| 332 | |||
| 333 | 196 | void FuseInvalidator::Spawn() { | |
| 334 | int retval; | ||
| 335 | 196 | retval = pthread_create(&thread_invalidator_, NULL, MainInvalidator, this); | |
| 336 | 1/2✗ Branch 0 not taken. ✓ Branch 1 taken 196 times. | 196 | assert(retval == 0); | 
| 337 | 196 | spawned_ = true; | |
| 338 | 196 | } | |
| 339 |