Skip to content

Commit d8153d4

Browse files
pbhandar2meta-codesync[bot]
authored andcommitted
Fix expand ceiling bug in ObjectSizeController
Summary: ## Background Object cache capacity is configured by setting `l1EntriesLimit` via `ObjectCacheConfig::setCacheCapacity()`. During initialization, the cache estimates how many slabs are needed to hold that many entries: allocsPerSlab = Slab::kSize / l1AllocSize (items that fit in one slab) slabsPerShard = ceil(l1EntriesLimit / numShards / allocsPerSlab) Because of the ceiling division, the total number of allocation slots (slabsPerShard × allocsPerSlab × numShards) is always >= l1EntriesLimit. The surplus slots are filled with "structural placeholders" — dummy items that prevent user data from exceeding the configured limit. ## Bug & Fix In `FreeMemoryOnly` mode, `work()` calls `expandCacheByEntriesNum(expandCacheBy)` directly — it never flows through `adjustCacheEntriesLimit()` where the `std::min(newEntriesLimit, l1EntriesLimit)` cap lives. Without a ceiling check inside `expandCacheByEntriesNum()` itself, repeated expand calls could pop structural placeholders, pushing `currentEntriesLimit_` above `l1EntriesLimit`. Example (l1EntriesLimit=100, 5 structural placeholders from slab rounding): After init: currentEntriesLimit_=100, placeholders=5 Shrink by 10: currentEntriesLimit_=90, placeholders=15 Expand by 15: pops all 15 → currentEntriesLimit_=105 > 100! Fix: pre-calculate `maxExpand = l1EntriesLimit - currentEntriesLimit_` as the loop bound in `expandCacheByEntriesNum()`, so it caps itself regardless of the call path. Also snapshot the atomic `currentEntriesLimit_` in `adjustCacheEntriesLimit()` and fix `totalEntriesExpanded_` counter. Reviewed By: rlyerly Differential Revision: D97062845 fbshipit-source-id: e736a9325dcea88bc2ff4e4b44d9e0e985989145
1 parent fe39d81 commit d8153d4

2 files changed

Lines changed: 95 additions & 14 deletions

File tree

cachelib/object_cache/ObjectCacheSizeController.h

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class ObjectCacheSizeController : public PeriodicWorker {
5757

5858
private:
5959
void work() override final;
60-
void expandCacheByEntriesNum(size_t entries);
60+
void expandCacheByEntriesNum(size_t entries, size_t currentLimit);
6161
int64_t getNewNumEntries();
6262

6363
std::pair<size_t, size_t> trackJemallocMemStats() const {
@@ -123,7 +123,8 @@ void ObjectCacheSizeController<AllocatorT>::work() {
123123
ensureFreeMemoryAboveLowerLimit();
124124
} else if (freeMemBytes > objCache_.config_.upperLimitBytes) {
125125
// Only expand cache when free memory is above the upper limit
126-
expandCacheByEntriesNum(objCache_.config_.expandCacheBy);
126+
expandCacheByEntriesNum(objCache_.config_.expandCacheBy,
127+
getCurrentEntriesLimit());
127128
}
128129
// When free memory is between lower and upper limits, do nothing (stable
129130
// zone)
@@ -241,15 +242,17 @@ void ObjectCacheSizeController<AllocatorT>::work() {
241242

242243
// entriesLimit should never exceed the configured entries limit
243244
newEntriesLimit = std::min(newEntriesLimit, objCache_.config_.l1EntriesLimit);
244-
if (newEntriesLimit < currentEntriesLimit_ &&
245+
auto currentEntriesLimit = getCurrentEntriesLimit();
246+
if (newEntriesLimit < currentEntriesLimit &&
245247
currentNumEntries >= newEntriesLimit) {
246248
// shrink the cache
247-
shrinkCacheByEntriesNum(currentEntriesLimit_ - newEntriesLimit);
248-
} else if (newEntriesLimit > currentEntriesLimit_ &&
249-
currentNumEntries == currentEntriesLimit_) {
249+
shrinkCacheByEntriesNum(currentEntriesLimit - newEntriesLimit);
250+
} else if (newEntriesLimit > currentEntriesLimit &&
251+
currentNumEntries == currentEntriesLimit) {
250252
// expand cache when getting a higher new limit and current entries num
251253
// reaches the old limit
252-
expandCacheByEntriesNum(newEntriesLimit - currentEntriesLimit_);
254+
expandCacheByEntriesNum(newEntriesLimit - currentEntriesLimit,
255+
currentEntriesLimit);
253256
}
254257
}
255258

@@ -275,31 +278,43 @@ void ObjectCacheSizeController<AllocatorT>::shrinkCacheByEntriesNum(
275278

276279
XLOGF_EVERY_MS(
277280
INFO, 60'000,
278-
"CacheLib size-controller: request to shrink cache by {} entries. "
281+
"CacheLib size-controller: request to shrink cache by {} entries "
282+
"(actual: {}). "
279283
"Placeholders num before: {}, after: {}. currentEntriesLimit: {}",
280-
entries, before, objCache_.getNumPlaceholders(), currentEntriesLimit_);
284+
entries, actualShrunk, before, objCache_.getNumPlaceholders(),
285+
currentEntriesLimit_);
281286
}
282287

283288
template <typename AllocatorT>
284289
void ObjectCacheSizeController<AllocatorT>::expandCacheByEntriesNum(
285-
size_t entries) {
290+
size_t entries, size_t currentLimit) {
286291
expandCacheCalls_.inc();
287292
util::Throttler t(throttlerConfig_);
288293
auto before = objCache_.getNumPlaceholders();
289294
entries = std::min<size_t>(entries, before);
295+
// Not all callers cap entries against l1EntriesLimit (e.g. FreeMemoryOnly
296+
// mode calls this directly), so enforce the ceiling here.
297+
size_t maxExpand = (currentLimit < objCache_.config_.l1EntriesLimit)
298+
? objCache_.config_.l1EntriesLimit - currentLimit
299+
: 0;
300+
entries = std::min(entries, maxExpand);
301+
size_t actualExpanded = 0;
290302
for (size_t i = 0; i < entries; i++) {
291303
objCache_.placeholders_.pop_back();
292304
currentEntriesLimit_++;
305+
actualExpanded++;
293306
// throttle to slow down the release speed
294307
t.throttle();
295308
}
296-
totalEntriesExpanded_.add(entries);
309+
totalEntriesExpanded_.add(actualExpanded);
297310

298311
XLOGF_EVERY_MS(
299312
INFO, 60'000,
300-
"CacheLib size-controller: request to expand cache by {} entries. "
313+
"CacheLib size-controller: request to expand cache by {} entries "
314+
"(actual: {}). "
301315
"Placeholders num before: {}, after: {}. currentEntriesLimit: {}",
302-
entries, before, objCache_.getNumPlaceholders(), currentEntriesLimit_);
316+
entries, actualExpanded, before, objCache_.getNumPlaceholders(),
317+
currentEntriesLimit_);
303318
}
304319

305320
template <typename AllocatorT>

cachelib/object_cache/tests/ObjectCacheTest.cpp

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2343,7 +2343,7 @@ TEST(ObjectCacheTest, DynamicFreeMemorySizeControlTest) {
23432343
auto lowerLimitBytes = 9 * kMB;
23442344
auto itemSize = 1024;
23452345
uint64_t maxNumEntries = 1024 * 20;
2346-
uint64_t sizeControlIntervalMs = 50;
2346+
int sizeControlIntervalMs = 50;
23472347

23482348
ObjectCache::Config config;
23492349
config.setCacheName("dynamic_free_mem_test");
@@ -2448,5 +2448,71 @@ TEST(ObjectCacheTest, DynamicFreeMemorySizeControlTest) {
24482448
auto entriesAfterExpand = objcache->getCurrentEntriesLimit();
24492449

24502450
EXPECT_GT(entriesAfterExpand, entriesAfterShrink);
2451+
EXPECT_LE(entriesAfterExpand, maxNumEntries);
2452+
}
2453+
2454+
TEST(ObjectCacheTest, ExpandNeverExceedsL1EntriesLimit) {
2455+
const uint64_t kMB = 1024 * 1024;
2456+
auto upperLimitBytes = 15 * kMB;
2457+
auto lowerLimitBytes = 9 * kMB;
2458+
auto itemSize = 1024;
2459+
uint64_t maxNumEntries = 1024 * 20;
2460+
int sizeControlIntervalMs = 50;
2461+
2462+
ObjectCache::Config config;
2463+
config.setCacheName("expand_ceiling_test");
2464+
config.setItemDestructor([&](ObjectCacheDestructorData data) {
2465+
data.deleteObject<MemoryConsumer>();
2466+
});
2467+
config.setCacheCapacity(maxNumEntries, itemSize * maxNumEntries,
2468+
sizeControlIntervalMs);
2469+
config.setObjectSizeControllerMode(ObjCacheSizeControlMode::FreeMemoryOnly,
2470+
upperLimitBytes, lowerLimitBytes);
2471+
config.memoryMode = FreeMemoryOnly;
2472+
// Large expand/shrink to trigger structural placeholder popping
2473+
config.expandCacheBy = 500;
2474+
config.shrinkCacheBy = 500;
2475+
2476+
static std::atomic<uint64_t> sExpandTestFreeMem{20 * kMB};
2477+
config.setFreeMemCb([]() { return sExpandTestFreeMem.load(); });
2478+
2479+
auto objcache = ObjectCache::create(config);
2480+
2481+
// Fill cache
2482+
for (size_t i = 0; i < maxNumEntries; i++) {
2483+
auto key = folly::sformat("key_{}", i);
2484+
objcache->insertOrReplace(key, std::make_unique<MemoryConsumer>(itemSize),
2485+
itemSize);
2486+
}
2487+
EXPECT_EQ(objcache->getCurrentEntriesLimit(), maxNumEntries);
2488+
2489+
// Run 3 shrink->expand cycles
2490+
for (int cycle = 0; cycle < 3; cycle++) {
2491+
// Shrink: set free memory below lower limit
2492+
sExpandTestFreeMem.store(5 * kMB);
2493+
auto shrunk = test_util::eventuallyTrue(
2494+
[&]() { return objcache->getCurrentEntriesLimit() < maxNumEntries; },
2495+
3 /* timeoutSecs */);
2496+
EXPECT_TRUE(shrunk) << "Shrink failed on cycle " << cycle;
2497+
2498+
// Expand: set free memory above upper limit
2499+
sExpandTestFreeMem.store(20 * kMB);
2500+
// Wait for expand to settle
2501+
size_t lastLimit = objcache->getCurrentEntriesLimit();
2502+
auto settled = test_util::eventuallyTrue(
2503+
[&]() {
2504+
auto cur = objcache->getCurrentEntriesLimit();
2505+
if (cur != lastLimit) {
2506+
lastLimit = cur;
2507+
return false;
2508+
}
2509+
return true;
2510+
},
2511+
3 /* timeoutSecs */);
2512+
EXPECT_TRUE(settled) << "Expand didn't settle on cycle " << cycle;
2513+
2514+
EXPECT_LE(objcache->getCurrentEntriesLimit(), maxNumEntries)
2515+
<< "currentEntriesLimit exceeded l1EntriesLimit on cycle " << cycle;
2516+
}
24512517
}
24522518
} // namespace facebook::cachelib::objcache2::test

0 commit comments

Comments
 (0)