Skip to content

Commit 212595e

Browse files
photonlibosxiaoyang-hhhclaude
authored
feat(cache): make FileCachePool thread-safe for multi-vCPU access (#1555) (#1571)
* feat(cache): make FileCachePool thread-safe for multi-vCPU access Guard all FileCachePool metadata (fileIndex_, lru_, cold tiers, totalUsed_, tuning state) with a coarse photon::mutex (m_lock_) so one pool can be shared across multiple photon vCPUs (OS threads). Invariants: - m_lock_ is held only across in-memory ops; never across open()/do_open() or forceRecycle()/eviction(), keeping the lock order rw_lock -> m_lock_ one-way (avoids ABBA with ObjectCache's per-item mutex and non-reentrant self-deadlock). - eviction/evict snapshot a victim under m_lock_, release it, do the I/O (open()+WLOCK truncate), then re-lock to finalize. - the write path accounts size under the store rw_lock (updateSpace fstats under m_lock_) so it can't drift against eviction's WLOCK+truncate; forceRecycle() is deferred to do_pwritev2 after rw_lock is released. - running_/exit_/isFull_ and LruEntry::truncate_done become std::atomic. Add a multi-vCPU concurrency stress test (CachePool.concurrent_stress). QuotaFilePool is left unchanged and documented as not-yet-thread-safe (it is currently unwired: the factory always builds a plain FileCachePool). * check total used in eviction loop --------- Co-authored-by: Xiaoyang Lu <luxiaoyang.lxy@alibaba-inc.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b851294 commit 212595e

6 files changed

Lines changed: 240 additions & 122 deletions

File tree

fs/cache/full_file_cache/cache_pool.cpp

Lines changed: 118 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
#include "cache_pool.h"
18+
#include <cassert>
1819
#include <dirent.h>
1920
#include <fcntl.h>
2021
#include <sys/stat.h>
@@ -117,6 +118,7 @@ void FileCachePool::probeFiemap() {
117118
}
118119

119120
ICacheStore* FileCachePool::do_open(std::string_view pathname, int flags, mode_t mode) {
121+
SCOPED_LOCK(m_lock_);
120122
auto localFile = openMedia(pathname, flags, mode);
121123
if (!localFile) {
122124
return nullptr;
@@ -169,43 +171,56 @@ int FileCachePool::stat(CacheStat* stat, std::string_view pathname) {
169171
return -1;
170172
}
171173

172-
int FileCachePool::evict(std::string_view filename) {
173-
// Check cold tiers first
174-
for (auto* tier : coldTiers_) {
175-
if (tier->contains(filename)) {
176-
auto freed = truncateAndUnlink(filename);
177-
tier->remove(filename);
178-
return freed >= 0 ? 0 : -1;
179-
}
180-
}
181-
182-
auto fileIter = fileIndex_.find(filename);
183-
if (fileIter == fileIndex_.end()) {
184-
LOG_ERROR("Evict no such file , name: `", filename);
185-
return 0;
186-
}
187-
188-
const auto& filePath = fileIter->first;
189-
auto lruEntry = fileIter->second.get();
190-
if (lruEntry->openCount == 0) {
191-
lru_.mark_key_cleared(lruEntry->lruIter);
192-
}
174+
// Opens `name`, truncates it, then finalizes eviction.
175+
// MUST be called WITHOUT m_lock_ held.
176+
bool FileCachePool::evictOpenedFile(const std::string& name) {
193177
int err = 0;
194-
auto cacheStore = static_cast<FileCacheStore*>(open(filePath, O_RDWR, 0644));
178+
auto cacheStore = static_cast<FileCacheStore*>(open(name, O_RDWR, 0644));
195179
if (cacheStore) {
196180
DEFER(cacheStore->release());
197181
photon::scoped_rwlock rl(cacheStore->rw_lock(), photon::WLOCK);
198182
err = cacheStore->evict(0);
199-
lruEntry->truncate_done = false;
200183
}
201184
if (err) {
202185
ERRNO e;
203-
LOG_ERROR("truncate(0) failed, name: `, ret: `, error code: `", filePath,
204-
err, e);
205-
// If truncate fails, we can attempt to remove the file directly
206-
// in the afterFtrucate function.
186+
LOG_ERROR("truncate(0) failed, name: `, ret: `, error code: `", name, err, e);
187+
// Fall through: afterFtrucate can still remove the file directly.
188+
}
189+
return finalizeEvicted(name);
190+
}
191+
192+
// Acquires m_lock_ and finalizes eviction bookkeeping for `name`.
193+
bool FileCachePool::finalizeEvicted(const std::string& name) {
194+
SCOPED_LOCK(m_lock_);
195+
auto it = fileIndex_.find(name);
196+
if (it == fileIndex_.end()) return true; // already evicted concurrently
197+
it->second->truncate_done = false;
198+
return afterFtrucate(it);
199+
}
200+
201+
int FileCachePool::evict(std::string_view filename) {
202+
std::string name(filename);
203+
{
204+
SCOPED_LOCK(m_lock_);
205+
// Check cold tiers first
206+
for (auto* tier : coldTiers_) {
207+
if (tier->contains(filename)) {
208+
auto freed = evictColdVictim(tier, filename);
209+
return freed >= 0 ? 0 : -1;
210+
}
211+
}
212+
213+
auto fileIter = fileIndex_.find(name);
214+
if (fileIter == fileIndex_.end()) {
215+
LOG_ERROR("Evict no such file , name: `", filename);
216+
return 0;
217+
}
218+
auto lruEntry = fileIter->second.get();
219+
if (lruEntry->openCount == 0) {
220+
lru_.mark_key_cleared(lruEntry->lruIter);
221+
}
207222
}
208-
return afterFtrucate(fileIter) ? 0 : -1;
223+
return evictOpenedFile(name) ? 0 : -1;
209224
}
210225

211226
int FileCachePool::evict(size_t size) {
@@ -223,6 +238,7 @@ bool FileCachePool::isFull() {
223238
}
224239

225240
void FileCachePool::removeOpenFile(FileNameMap::iterator iter) {
241+
SCOPED_LOCK(m_lock_);
226242
iter->second->openCount--;
227243
}
228244

@@ -231,11 +247,18 @@ void FileCachePool::forceRecycle() {
231247
}
232248

233249
void FileCachePool::updateLru(FileNameMap::iterator iter) {
250+
SCOPED_LOCK(m_lock_);
234251
lru_.access(iter->second->lruIter);
235252
}
236253

237254
// currently, we exist duplicate pwrite
238-
int64_t FileCachePool::updateSpace(FileNameMap::iterator iter, uint64_t size) {
255+
int64_t FileCachePool::updateSpace(FileNameMap::iterator iter, IFile* localFile) {
256+
SCOPED_LOCK(m_lock_);
257+
struct stat st = {};
258+
if (localFile->fstat(&st) != 0) {
259+
LOG_ERRNO_RETURN(0, 0, "fstat failed");
260+
}
261+
uint64_t size = kDiskBlockSize * st.st_blocks;
239262
auto lruEntry = iter->second.get();
240263
auto diff = static_cast<int64_t>(size) - static_cast<int64_t>(lruEntry->size);
241264
totalUsed_ += diff;
@@ -257,13 +280,7 @@ int64_t FileCachePool::updateSpace(FileNameMap::iterator iter, uint64_t size) {
257280
LOG_WARN("disk free space below floor `, force recycle", diskAvailInBytes_);
258281
}
259282
}
260-
261-
if (full) {
262-
isFull_ = true;
263-
forceRecycle();
264-
if (lruEntry->size==0) diff = 0;//in some extream condition ,
265-
//forceRecycle maybe truncate current file to 0
266-
}
283+
if (full) isFull_ = true;
267284
return diff;
268285
}
269286

@@ -279,10 +296,10 @@ bool FileCachePool::diskSpaceLow() {
279296

280297
uint64_t FileCachePool::timerHandler(void* data) {
281298
auto cur = static_cast<FileCachePool*>(data);
282-
if (cur->running_) {
299+
// Atomic test-and-set: only one vCPU runs eviction at a time.
300+
if (cur->running_.exchange(true)) {
283301
return 0;
284302
}
285-
cur->running_ = true;
286303
DEFER(cur->running_ = false;);
287304
cur->eviction();
288305
return 0;
@@ -309,79 +326,71 @@ void FileCachePool::eviction() {
309326
}
310327
}
311328

312-
if (totalUsed_ >= static_cast<int64_t>(waterMark_)) {
313-
evictByCache = totalUsed_ - waterMark_;
314-
}
329+
int64_t actualEvict;
330+
{
331+
SCOPED_LOCK(m_lock_);
332+
if (totalUsed_ >= static_cast<int64_t>(waterMark_)) {
333+
evictByCache = totalUsed_ - waterMark_;
334+
}
315335

316-
auto actualEvict = std::min(
317-
static_cast<int64_t>(std::max(evictByCache, evictByDisk)),
318-
totalUsed_
319-
);
336+
actualEvict = std::min(
337+
static_cast<int64_t>(std::max(evictByCache, evictByDisk)),
338+
totalUsed_
339+
);
320340

321-
if (actualEvict <= 0) {
322-
return;
323-
}
341+
if (actualEvict <= 0) {
342+
return;
343+
}
324344

325-
isFull_ = true;
345+
isFull_ = true;
326346

327-
// Evict from cold tiers first in reverse order
328-
for (int i = coldTiers_.size() - 1; i >= 0; i--) {
329-
auto* tier = coldTiers_[i];
330-
while (actualEvict > 0 && !tier->empty() && !exit_) {
331-
auto name = tier->victim();
332-
auto freed = truncateAndUnlink(name);
333-
tier->remove(name);
334-
if (freed >= 0) actualEvict -= freed;
335-
photon::thread_yield();
347+
for (auto i = coldTiers_.size(); i > 0 && actualEvict > 0 && !exit_; --i) {
348+
auto* tier = coldTiers_[i - 1];
349+
while (actualEvict > 0 && !tier->empty() && !exit_) {
350+
auto freed = evictColdVictim(tier, tier->victim());
351+
if (freed >= 0) actualEvict -= freed;
352+
}
336353
}
337-
}
338354

339-
if (!lru_.empty() && !exit_) {
340-
LOG_AUDIT("eviction", VALUE(actualEvict), VALUE(evictByCache), VALUE(evictByDisk), VALUE(totalUsed_));
355+
if (!lru_.empty() && !exit_) {
356+
LOG_AUDIT("eviction", VALUE(actualEvict), VALUE(evictByCache), VALUE(evictByDisk), VALUE(totalUsed_));
357+
}
341358
}
342359

343360
uint64_t empty_files_sequence = 0;
344-
while (actualEvict > 0 && !lru_.empty() && !exit_) {
345-
auto fileIter = lru_.back();
346-
const auto& fileName = fileIter->first;
347-
auto lruEntry = fileIter->second.get();
348-
auto fileSize = lruEntry->size;
349-
if (lruEntry->openCount == 0){
350-
lru_.mark_key_cleared(fileIter->second->lruIter);
351-
} else {
352-
lru_.access(fileIter->second->lruIter);
353-
}
354-
//as soon as possible truncate and unlink
355-
if (0 == fileSize) {
356-
if (0 == fileIter->second->openCount) {
357-
afterFtrucate(fileIter);
361+
while (actualEvict > 0 && !exit_) {
362+
std::string fileName;
363+
uint64_t fileSize;
364+
{
365+
SCOPED_LOCK(m_lock_);
366+
if (lru_.empty() || totalUsed_ <= 0) break;
367+
auto fileIter = lru_.back();
368+
fileName = std::string(fileIter->first);
369+
auto lruEntry = fileIter->second.get();
370+
fileSize = lruEntry->size;
371+
if (lruEntry->openCount == 0) {
372+
lru_.mark_key_cleared(lruEntry->lruIter);
373+
} else {
374+
lru_.access(lruEntry->lruIter);
375+
}
376+
if (0 == fileSize) {
377+
if (0 == lruEntry->openCount) {
378+
afterFtrucate(fileIter);
358379
} else {
359380
empty_files_sequence++;
360-
if (empty_files_sequence == lru_.size()) {
381+
if (empty_files_sequence >= lru_.size()) {
361382
LOG_ERROR("eviction: all ` LRU entries have size=0 with openCount>0, "
362383
"cannot make progress. actualEvict=`, totalUsed=`",
363384
lru_.size(), actualEvict, totalUsed_);
364385
break;
365386
}
366387
}
367-
continue;
368-
}
369-
370-
empty_files_sequence = 0;
371-
int err = 0;
372-
auto cacheStore = static_cast<FileCacheStore*>(open(fileName, O_RDWR, 0644));
373-
if (cacheStore) {
374-
DEFER(cacheStore->release());
375-
photon::scoped_rwlock rl(cacheStore->rw_lock(), photon::WLOCK);
376-
err = cacheStore->evict(0);
377-
lruEntry->truncate_done = false;
378-
}
379-
380-
if (err) {
381-
ERRNO e;
382-
LOG_ERROR("truncate(0) failed, name : `, ret : `, error code : `", fileName, err, e);
388+
continue; // releases m_lock_ via SCOPED_LOCK scope
389+
}
390+
empty_files_sequence = 0;
383391
}
384-
afterFtrucate(fileIter);
392+
// open()+WLOCK+finalize with m_lock_ released.
393+
evictOpenedFile(fileName);
385394
actualEvict -= fileSize;
386395
photon::thread_yield();
387396
}
@@ -392,6 +401,7 @@ uint64_t FileCachePool::calcWaterMark(uint64_t capacity, uint64_t maxFreeSpace)
392401
capacity > maxFreeSpace ? capacity - maxFreeSpace : 0);
393402
}
394403

404+
// With m_lock_ held.
395405
bool FileCachePool::afterFtrucate(FileNameMap::iterator iter) {
396406
auto lruEntry = iter->second.get();
397407
totalUsed_ -= static_cast<int64_t>(lruEntry->size);
@@ -432,6 +442,7 @@ int FileCachePool::insertFile(std::string_view file) {
432442
}
433443
auto fileSize = st.st_blocks * kDiskBlockSize;
434444

445+
SCOPED_LOCK(m_lock_);
435446
auto lruIter = lru_.push_front(fileIndex_.end());
436447
auto entry = std::unique_ptr<LruEntry>(new LruEntry{lruIter, 0, fileSize});
437448
auto iter = fileIndex_.emplace(file, std::move(entry)).first;
@@ -459,6 +470,7 @@ void FileCachePool::adaptThresholds() {
459470
tierHits_.fill(0);
460471
}
461472

473+
// With m_lock_ held.
462474
void FileCachePool::demoteToCold() {
463475
while (lru_.size() > thresholds_[0].value) {
464476
auto tailIt = lru_.back();
@@ -478,6 +490,7 @@ void FileCachePool::demoteToCold() {
478490
}
479491
}
480492

493+
// With m_lock_ held.
481494
void FileCachePool::promoteToHot(std::string_view filename) {
482495
auto find = fileIndex_.find(filename);
483496
if (find != fileIndex_.end()) {
@@ -511,6 +524,18 @@ void FileCachePool::promoteToHot(std::string_view filename) {
511524
lru_.front() = iter;
512525
}
513526

527+
// With m_lock_ held.
528+
ssize_t FileCachePool::evictColdVictim(ColdCacheTier* tier, std::string_view name) {
529+
auto freed = truncateAndUnlink(name);
530+
tier->remove(name);
531+
if (freed >= 0) {
532+
totalUsed_ -= freed;
533+
if (totalUsed_ < 0) totalUsed_ = 0;
534+
}
535+
return freed;
536+
}
537+
538+
// I/O only: does NOT touch totalUsed_ and does NOT require m_lock_.
514539
ssize_t FileCachePool::truncateAndUnlink(std::string_view filename) {
515540
struct stat st = {};
516541
uint64_t fileSize = 0;
@@ -523,8 +548,6 @@ ssize_t FileCachePool::truncateAndUnlink(std::string_view filename) {
523548
if (err) {
524549
LOG_ERRNO_RETURN(0, -1, "truncate(0) failed, name : `", filename);
525550
}
526-
totalUsed_ -= static_cast<int64_t>(fileSize);
527-
if (totalUsed_ < 0) totalUsed_ = 0;
528551
}
529552
int err = mediaFs_->unlink(filename.data());
530553
if (err) {

0 commit comments

Comments
 (0)