From 001ad5108bb581ca995388975834d403e7806610 Mon Sep 17 00:00:00 2001 From: Jerry Zhao Date: Fri, 10 Jul 2026 04:45:03 +0000 Subject: [PATCH] fix: don't disable TTL cleanup when a scan error interrupts CleanExpiredKeys The expired-key collection loop was `do {...} while (iter.Next() == NoError)`, so a real IO error while paging the TTL tree (EIO, cloud fetch failure) was treated the same as reaching the end: the loop stopped having seen only a prefix of the expired keys, then committed the partial deletion with next_expire_ts_ = 0. TriggerTTL early-returns on next_expire_ts_ == 0 and is the only producer of TTL work, so the partition's TTL cleanup stayed disabled until a later TTL upsert / RootMeta reload / reopen / restart re-armed it -- the remaining expired keys lingered on disk and stayed readable, silently. Distinguish EndOfFile (genuine end -> next_expire_ts_ = 0 is correct) from a real error. On a real error, abort without committing so next_expire_ts_ stays armed and the next TriggerTTL retries. The Seek() error above already made this distinction; the Next() loop did not. Happy-path TTL behavior is unchanged (delete.cpp [TTL] tests pass); the error path has no clean deterministic hook (async cleanup + scan-path fault injection), so it is not separately tested. --- src/tasks/batch_write_task.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/tasks/batch_write_task.cpp b/src/tasks/batch_write_task.cpp index d7093b51..a1b075fd 100644 --- a/src/tasks/batch_write_task.cpp +++ b/src/tasks/batch_write_task.cpp @@ -2027,6 +2027,7 @@ KvError BatchWriteTask::CleanExpiredKeys() const uint64_t now_ts_ms = utils::UnixTs(); const uint64_t now_ts_us = utils::UnixTs(); uint64_t next_expire_ts = 0; + KvError scan_err = KvError::NoError; do { std::string_view ttl_key = iter.Key(); @@ -2040,7 +2041,18 @@ KvError BatchWriteTask::CleanExpiredKeys() std::string key(ttl_key.substr(8)); data_batch.emplace_back( std::move(key), "", now_ts_us, WriteOp::Delete, expire_ts); - } while (iter.Next() == KvError::NoError); + scan_err = iter.Next(); + } while (scan_err == KvError::NoError); + + // Real IO error (not EndOfFile): only a prefix of the expired keys was + // scanned. Aborting leaves next_expire_ts_ armed so the next TriggerTTL + // retries; committing would set it to 0 below and silently disable TTL. + if (scan_err != KvError::NoError && scan_err != KvError::EndOfFile) + { + LOG(ERROR) << "clean expired keys interrupted by scan error: " + << ErrorString(scan_err); + return scan_err; + } if (ttl_batch.empty()) {