Skip to content

Commit 0939176

Browse files
committed
[manager] harden EventReport GC lifecycle and accounting
1 parent eed745c commit 0939176

18 files changed

Lines changed: 492 additions & 145 deletions

docs/design/event_report_background_gc.md

Lines changed: 29 additions & 25 deletions
Large diffs are not rendered by default.

kv_cache_manager/common/cache/lru_cache.cc

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,16 +507,38 @@ bool LRUCacheShard::ApplyToEntryNoTouch(
507507
return false;
508508
}
509509
assert(e->InCache());
510+
const bool in_lru = !e->HasRefs();
510511
const ssize_t delta = callback(e->value, e->total_charge, e->helper);
511512
if (delta > 0) {
512-
e->total_charge += static_cast<size_t>(delta);
513-
usage_ += static_cast<size_t>(delta);
513+
const size_t increase = static_cast<size_t>(delta);
514+
e->total_charge += increase;
515+
usage_ += increase;
516+
if (in_lru) {
517+
lru_usage_ += increase;
518+
if (e->InHighPriPool()) {
519+
high_pri_pool_usage_ += increase;
520+
} else if (e->InLowPriPool()) {
521+
low_pri_pool_usage_ += increase;
522+
}
523+
MaintainPoolSize();
524+
}
514525
} else if (delta < 0) {
515526
size_t decrease = static_cast<size_t>(-delta);
516527
decrease = std::min(decrease, e->total_charge);
517528
decrease = std::min(decrease, usage_);
518529
e->total_charge -= decrease;
519530
usage_ -= decrease;
531+
if (in_lru) {
532+
assert(lru_usage_ >= decrease);
533+
lru_usage_ -= decrease;
534+
if (e->InHighPriPool()) {
535+
assert(high_pri_pool_usage_ >= decrease);
536+
high_pri_pool_usage_ -= decrease;
537+
} else if (e->InLowPriPool()) {
538+
assert(low_pri_pool_usage_ >= decrease);
539+
low_pri_pool_usage_ -= decrease;
540+
}
541+
}
520542
}
521543
return true;
522544
}

kv_cache_manager/common/test/lru_cache_test.cc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,33 @@ TEST_F(LRUCacheTest, BatchReleaseFreesEntryErasedWhilePinned) {
204204
ValidateLRUList({"b"}, 0, 1);
205205
}
206206

207+
TEST_F(LRUCacheTest, ApplyToEntryNoTouchKeepsUnpinnedPoolChargeConsistent) {
208+
NewCache(100, /* high_pri_pool_ratio */ 0.50, /* low_pri_pool_ratio */ 0.50);
209+
Insert("a", Cache::Priority::HIGH, 20);
210+
ASSERT_EQ(20u, cache_->GetUsage());
211+
ASSERT_EQ(0u, cache_->GetPinnedUsage());
212+
ValidateLRUList({"a"}, 1, 0, 0);
213+
214+
ASSERT_TRUE(cache_->ApplyToEntryNoTouch(
215+
"a", 0, [](Cache::ObjectPtr, size_t, const Cache::CacheItemHelper *) { return -10; }));
216+
EXPECT_EQ(10u, cache_->GetUsage());
217+
EXPECT_EQ(0u, cache_->GetPinnedUsage());
218+
ValidateLRUList({"a"}, 1, 0, 0);
219+
220+
ASSERT_TRUE(cache_->ApplyToEntryNoTouch(
221+
"a", 0, [](Cache::ObjectPtr, size_t, const Cache::CacheItemHelper *) { return 20; }));
222+
EXPECT_EQ(30u, cache_->GetUsage());
223+
EXPECT_EQ(0u, cache_->GetPinnedUsage());
224+
ValidateLRUList({"a"}, 1, 0, 0);
225+
226+
// A later insertion exercises MaintainPoolSize and LRU_Remove using the
227+
// adjusted charge; stale pool counters would corrupt these transitions.
228+
Insert("b", Cache::Priority::HIGH, 30);
229+
EXPECT_EQ(60u, cache_->GetUsage());
230+
EXPECT_EQ(0u, cache_->GetPinnedUsage());
231+
ValidateLRUList({"a", "b"}, 1, 1, 0);
232+
}
233+
207234
TEST_F(LRUCacheTest, LowPriorityMidpointInsertion) {
208235
// Allocate 2 cache entries to high-pri pool and 3 to low-pri pool.
209236
NewCache(5, /* high_pri_pool_ratio */ 0.40, /* low_pri_pool_ratio */ 0.60);

kv_cache_manager/data_storage/event_report_backend.cc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,10 +1250,9 @@ EventReportBackend::CleanupLeaseAcquireResult EventReportBackend::AcquireDownLif
12501250
if (lifecycle_fence->generation != expected_generation) {
12511251
return CleanupLeaseAcquireResult::kStale;
12521252
}
1253-
// The liveness loop publishes a cleanup intent before performing its
1254-
// generation-checked unregister so a concurrent heartbeat can still win.
1255-
// An active reporter in that same generation is therefore transient, not
1256-
// stale; retry after the liveness callback finishes the unregister.
1253+
// Producers publish DownHost only after generation-checked unregister.
1254+
// Treat an unexpected same-generation active state fail-closed as busy:
1255+
// retain the intent and never authorize deletion until down is observed.
12571256
if (lifecycle_fence->registered) {
12581257
return CleanupLeaseAcquireResult::kBusy;
12591258
}

kv_cache_manager/manager/cache_garbage_collector.cc

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,8 @@ bool CacheGarbageCollector::RegisterHostCleanupIntent(const std::string &instanc
399399

400400
void CacheGarbageCollector::CancelHostCleanupIntent(const std::string &instance_id,
401401
DataStorageType storage_type,
402-
const std::string &host_ip_port) noexcept {
402+
const std::string &host_ip_port,
403+
uint64_t active_lifecycle_generation) noexcept {
403404
try {
404405
bool canceled = false;
405406
EventReportIntentType canceled_type = EventReportIntentType::kDownHost;
@@ -408,7 +409,11 @@ void CacheGarbageCollector::CancelHostCleanupIntent(const std::string &instance_
408409
const EventReportIntentKey key{instance_id, storage_type, host_ip_port};
409410
recovery_observations_.erase(key);
410411
const auto it = event_report_intents_.find(key);
411-
if (it != event_report_intents_.end() && it->second.type != EventReportIntentType::kStaleSnapshot) {
412+
// Cancellation is authorized only by a strictly newer active
413+
// lifecycle. A same-generation notification does not prove that
414+
// the captured down lifecycle has been superseded.
415+
if (it != event_report_intents_.end() && it->second.type != EventReportIntentType::kStaleSnapshot &&
416+
it->second.lifecycle_generation < active_lifecycle_generation) {
412417
canceled_type = it->second.type;
413418
event_report_intents_.erase(it);
414419
canceled = true;
@@ -824,6 +829,14 @@ bool CacheGarbageCollector::RunEventReportTick() {
824829
for (const auto &[instance_id, _] : event_report_retry_batches_) {
825830
active_instances.insert(instance_id);
826831
}
832+
// A pass owns its frozen intent snapshot until its cursor reaches base.
833+
// Keep scheduling it even if every live intent is canceled mid-pass;
834+
// otherwise a later intent would inherit the stale cursor and barrier.
835+
for (const auto &[instance_id, state] : event_report_scan_states_) {
836+
if (state.context) {
837+
active_instances.insert(instance_id);
838+
}
839+
}
827840
if (active_instances.empty()) {
828841
UpdateEventReportMetrics();
829842
return false;
@@ -936,9 +949,8 @@ bool CacheGarbageCollector::BeginEventReportPass(const std::string &instance_id,
936949
return false;
937950
}
938951
if (intent.type == EventReportIntentType::kDownHost) {
939-
// Liveness publishes the intent before its generation-checked unregister. Do not put the SyncAll
940-
// barrier ahead of an old-lifecycle mutation that is still holding a shared lease: first prove that
941-
// unregister has completed, release this short preflight lease, and only then flush the mutation queues.
952+
// Host intents are published after generation-checked unregister. Revalidate the captured down
953+
// lifecycle before SyncAll so a concurrent REGISTER cannot let an old intent cross into a new lifecycle.
942954
EventReportBackend::LifecycleMutationLease preflight_lease;
943955
const auto lease_result = backend->AcquireDownLifecycleCleanupLease(
944956
{key.instance_id, key.host_ip_port}, intent.lifecycle_generation, preflight_lease);
@@ -965,6 +977,11 @@ bool CacheGarbageCollector::BeginEventReportPass(const std::string &instance_id,
965977
return false;
966978
}
967979

980+
auto indexer = meta_indexer_manager_->GetMetaIndexer(instance_id);
981+
if (!indexer || !indexer->IsMaintenanceDeleteReady()) {
982+
FailEventReportPass(instance_id, state, indexer ? "meta_recovering" : "indexer_missing");
983+
return false;
984+
}
968985
MetaSearcher *meta_searcher = meta_searcher_manager_->GetMetaSearcher(instance_id);
969986
if (!meta_searcher || !meta_searcher->SyncAllForMaintenance()) {
970987
FailEventReportPass(instance_id, state, meta_searcher ? "sync_all" : "searcher_missing");
@@ -1094,7 +1111,8 @@ std::vector<CacheGarbageCollector::EventReportDeleteTarget> CacheGarbageCollecto
10941111
if (backend->ParseLocationId(location_id, medium, host)) {
10951112
const EventReportIntentKey key{instance_id, location->type(), host};
10961113
if (backend->IsNodeRegistered(instance_id, host)) {
1097-
CancelHostCleanupIntent(instance_id, location->type(), host);
1114+
CancelHostCleanupIntent(
1115+
instance_id, location->type(), host, backend->GetNodeGeneration(instance_id, host));
10981116
} else {
10991117
std::lock_guard<std::mutex> lock(event_report_intent_mutex_);
11001118
recovery_observations_.try_emplace(
@@ -1297,6 +1315,8 @@ bool CacheGarbageCollector::ExecuteEventReportDeleteBatch(const std::string &ins
12971315
static_cast<double>(std::max<int64_t>(0, action_duration_ms));
12981316
metrics_registry_->GetGauge("cache_gc.event_report_last_action_shard_lock_wait_us") =
12991317
static_cast<double>(std::max<int64_t>(0, result.shard_lock_wait_time_us));
1318+
metrics_registry_->GetGauge("cache_gc.event_report_last_action_shard_lock_hold_us") =
1319+
static_cast<double>(std::max<int64_t>(0, result.shard_lock_hold_time_us));
13001320
} catch (...) { KVCM_LOG_ERROR("cache gc failed to record event report action metrics"); }
13011321

13021322
if (!result.sync_succeeded || (result.ec != EC_OK && result.ec != EC_MISMATCH)) {

kv_cache_manager/manager/cache_garbage_collector.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ class CacheGarbageCollector {
109109
const std::shared_ptr<EventReportBackend> &backend) noexcept;
110110
void CancelHostCleanupIntent(const std::string &instance_id,
111111
DataStorageType storage_type,
112-
const std::string &host_ip_port) noexcept;
112+
const std::string &host_ip_port,
113+
uint64_t active_lifecycle_generation) noexcept;
113114

114115
private:
115116
using Clock = std::chrono::steady_clock;

kv_cache_manager/manager/cache_manager.cc

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -412,10 +412,17 @@ CacheManager::CacheManager(std::shared_ptr<MetricsRegistry> metrics_registry,
412412

413413
CacheManager::~CacheManager() {
414414
if (cache_garbage_collector_) {
415-
cache_garbage_collector_->Stop();
416-
cache_garbage_collector_.reset();
415+
// Close intent admission before detaching callbacks. A liveness thread
416+
// may already have copied a callback, so the callback itself also uses
417+
// a weak GC reference and will either observe stopped admission or an
418+
// expired collector.
419+
cache_garbage_collector_->RequestStop();
417420
}
418421
ClearEventCleanupCallbacks();
422+
if (cache_garbage_collector_) {
423+
cache_garbage_collector_->Join();
424+
cache_garbage_collector_.reset();
425+
}
419426
StopRecoverRetryLoop();
420427
DeactivateEventCleanupCallbacks();
421428
if (write_location_manager_) {
@@ -2584,54 +2591,6 @@ class ValidatedEventLocationSpecs {
25842591
std::vector<ValidatedEventLocationSpec> many_;
25852592
};
25862593

2587-
bool IsSnapshotLocationStale(const EventReportBackend *event_backend,
2588-
const std::string &instance_id,
2589-
const CacheLocation &location,
2590-
bool preserve_in_flight = false) {
2591-
if (!event_backend) {
2592-
return false;
2593-
}
2594-
2595-
std::string medium;
2596-
std::string reporter_host;
2597-
if (!event_backend->ParseLocationId(location.id(), medium, reporter_host)) {
2598-
return false;
2599-
}
2600-
2601-
const ReporterSnapshotKey reporter_key{instance_id, reporter_host};
2602-
std::string committed_version;
2603-
std::string in_flight_version;
2604-
event_backend->GetSnapshotVersionTokens(reporter_key, committed_version, in_flight_version);
2605-
if (location.location_specs().empty()) {
2606-
return true;
2607-
}
2608-
bool contains_committed = false;
2609-
bool contains_in_flight = false;
2610-
for (const auto &spec : location.location_specs()) {
2611-
const size_t version_param_count =
2612-
SnapshotUriUtils::CountUriParam(spec.uri(), SnapshotUriUtils::kSnapshotVersionParam);
2613-
if (version_param_count == 0) {
2614-
// Legacy metadata is a stale reconciliation component, but a
2615-
// current delta spec in the same stable location still protects
2616-
// the location from coarse-grained cleanup.
2617-
continue;
2618-
}
2619-
SnapshotUriInfo info;
2620-
if (version_param_count != 1 || !SnapshotUriUtils::ParseSnapshotUriInfo(spec.uri(), info)) {
2621-
return true;
2622-
}
2623-
contains_committed = contains_committed || (!committed_version.empty() && info.version == committed_version);
2624-
contains_in_flight = contains_in_flight ||
2625-
(preserve_in_flight && !in_flight_version.empty() && info.version == in_flight_version);
2626-
}
2627-
// Delta merge is spec-granular and can temporarily leave multiple
2628-
// generations in one stable location. Cleanup is location-granular, so it
2629-
// must preserve the whole location when any current/in-flight spec is
2630-
// present; deleting stale sibling specs is deferred to a later complete
2631-
// snapshot rather than risking a false negative for a successful delta.
2632-
return !contains_committed && !contains_in_flight;
2633-
}
2634-
26352594
bool IsEventReportLocationReadable(const CacheLocation &location,
26362595
bool strict_query_visibility,
26372596
const std::string &committed_version) {
@@ -2777,7 +2736,7 @@ ErrorCode CacheManager::ReportEvent(RequestContext *request_context,
27772736
const std::string &cleanup_instance,
27782737
const std::string &down_host,
27792738
uint64_t generation) {
2780-
if (auto backend = weak_backend.lock()) {
2739+
if (auto backend = weak_backend.lock(); backend) {
27812740
auto gc = weak_gc.lock();
27822741
if (gc && !gc->RegisterHostCleanupIntent(
27832742
cleanup_instance, requested_type, down_host, generation, backend)) {
@@ -3671,7 +3630,8 @@ ErrorCode CacheManager::ReportEvent(RequestContext *request_context,
36713630
}
36723631
if (!has_host_down && event_backend->IsNodeRegistered(instance_id, host_ip_port) && cache_garbage_collector_ &&
36733632
cache_garbage_collector_->IsEventReportCleanupEnabled()) {
3674-
cache_garbage_collector_->CancelHostCleanupIntent(instance_id, requested_type, host_ip_port);
3633+
cache_garbage_collector_->CancelHostCleanupIntent(
3634+
instance_id, requested_type, host_ip_port, event_backend->GetNodeGeneration(instance_id, host_ip_port));
36753635
}
36763636

36773637
// MetaSearcher calls this once after the fused target-location read and
@@ -3906,6 +3866,14 @@ ErrorCode CacheManager::ReportEvent(RequestContext *request_context,
39063866
if (cache_garbage_collector_ && cache_garbage_collector_->IsEventReportCleanupEnabled()) {
39073867
cleanup_dispatched = cache_garbage_collector_->RegisterHostCleanupIntent(
39083868
instance_id, requested_type, host_ip_port, gen_at_trigger, event_backend);
3869+
if (!cleanup_dispatched) {
3870+
KVCM_LOG_WARN("trace_id [%s] | HOST_DOWN: failed to register GC cleanup intent for host [%s], "
3871+
"instance [%s], gen=%" PRIu64,
3872+
trace_id.c_str(),
3873+
host_ip_port.c_str(),
3874+
instance_id.c_str(),
3875+
gen_at_trigger);
3876+
}
39093877
} else {
39103878
const auto cleanup_state = event_cleanup_callback_state_;
39113879
uint64_t cleanup_epoch = 0;

kv_cache_manager/manager/meta_searcher.cc

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3529,6 +3529,7 @@ MetaSearcher::BatchDeleteLocationsForMaintenance(RequestContext *request_context
35293529
request_context, keys, location_ids_per_key, expected_location_values);
35303530
out.ec = result.ec;
35313531
out.shard_lock_wait_time_us = result.shard_lock_wait_time_us;
3532+
out.shard_lock_hold_time_us = result.shard_lock_hold_time_us;
35323533
out.per_location_results = std::move(result.per_location_results);
35333534
if (out.per_location_results.size() != keys.size()) {
35343535
out.ec = EC_ERROR;
@@ -3553,18 +3554,15 @@ MetaSearcher::BatchDeleteLocationsForMaintenance(RequestContext *request_context
35533554
for (size_t i = 0; i < out.per_location_results.size(); ++i) {
35543555
for (size_t j = 0; j < out.per_location_results[i].size(); ++j) {
35553556
auto &current = out.per_location_results[i][j];
3556-
if (current.ec == EC_OK || current.ec == EC_NOENT) {
3557-
if (prior_shape_valid) {
3558-
const auto &prior = (*prior_results)[i][j];
3559-
current.removed_from_hot = current.removed_from_hot || prior.removed_from_hot;
3560-
current.removed_from_persistent = current.removed_from_persistent || prior.removed_from_persistent;
3561-
current.reclaimed_hot_key = current.reclaimed_hot_key || prior.reclaimed_hot_key;
3562-
current.reclaimed_persistent_key =
3563-
current.reclaimed_persistent_key || prior.reclaimed_persistent_key;
3564-
}
3565-
needs_sync = needs_sync || current.removed_from_hot || current.removed_from_persistent ||
3566-
current.reclaimed_hot_key || current.reclaimed_persistent_key;
3567-
}
3557+
if (prior_shape_valid) {
3558+
const auto &prior = (*prior_results)[i][j];
3559+
current.removed_from_hot = current.removed_from_hot || prior.removed_from_hot;
3560+
current.removed_from_persistent = current.removed_from_persistent || prior.removed_from_persistent;
3561+
current.reclaimed_hot_key = current.reclaimed_hot_key || prior.reclaimed_hot_key;
3562+
current.reclaimed_persistent_key = current.reclaimed_persistent_key || prior.reclaimed_persistent_key;
3563+
}
3564+
needs_sync = needs_sync || current.removed_from_hot || current.removed_from_persistent ||
3565+
current.reclaimed_hot_key || current.reclaimed_persistent_key;
35683566
}
35693567
}
35703568

kv_cache_manager/manager/meta_searcher.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ class MetaSearcher {
347347
MaintenanceLocationDeleteResults per_location_results;
348348
bool sync_succeeded = false;
349349
int64_t shard_lock_wait_time_us = 0;
350+
int64_t shard_lock_hold_time_us = 0;
350351
};
351352
// Exact-value metadata-only maintenance deletion. The optional prior
352353
// result carries mutations from a previous Sync failure so accounting is

0 commit comments

Comments
 (0)