fix(replication): prevent full-sync checkpoint race with cron purge (…#3559
fix(replication): prevent full-sync checkpoint race with cron purge (…#3559advisedy wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a replication full-sync race where the server cron could purge a live RocksDB checkpoint directory while replication metadata collection was building the checkpoint file list, leading to undefined behavior (std::string::pop_back() on empty) and oversized allocations reported by TSan.
Changes:
- Add an explicit empty-checkpoint guard in
ReplDataManager::GetFullReplDataInfoto avoidpop_back()on an empty file list. - Introduce
Storage::TryPurgeCheckpoint()to purge checkpoints via an under-lock rename to*.trashfollowed byDestroyDB()outside the lock. - Switch
Server::cron()checkpoint cleanup to useTryPurgeCheckpoint, and add new C++ unit tests covering empty-checkpoint rejection and purge behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/cppunit/storage_test.cc | Adds new gtests for empty checkpoint handling and checkpoint purge behavior. |
| src/storage/storage.h | Declares the new TryPurgeCheckpoint API and documents its rename-then-destroy behavior. |
| src/storage/storage.cc | Implements TryPurgeCheckpoint and adds a safety check for empty checkpoint listings in full-sync metadata generation. |
| src/server/server.cc | Replaces direct DestroyDB(checkpoint_dir) purge logic with Storage::TryPurgeCheckpoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hello @advisedy. Thanks for your contribution. If you used an LLM, please follow our guidelines for AI-assisted contributions: https://kvrocks.apache.org/community/contributing#guidelines-for-ai-assisted-contributions |
Applied suggestion from Copilot code review. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
Hi @jihuayu, thanks for the reminder. Here's my AI usage: The main fix logic and code were written by myself. |
|
|
Hi, @jihuayu . just friendly ping. CI has passed. Could you please take a look when you have time? |


Fixes #3100
Problem
TSan reported an oversized allocation:
Root cause:
Before the fix,
GetFullReplDataInforeleased the lock before callingGetChildren, whileServer::croncalledrocksdb::DestroyDBon the live checkpoint directory without holding the lock at all. This allowed the following interleaving:GetFullReplDataInfobuilds the file list by appending entries and then callsfiles->pop_back(). If the directory is empty,filesis an empty string, andpop_backon an empty string is undefined behavior. (https://en.cppreference.com/cpp/string/basic_string/pop_back)On libstdc++, the common manifestation is a size underflow that produces a very large number. The subsequent
files + CRLFthen allocates memory based on this corrupted size, triggering the TSan error.eaxmple:
Running with GCC 11 + ASan on Linux:
The corrupted size (
0x100000000000000) and the threshold (0x10000000000) match the TSan report in the issue.While investigating, we also found that the same race condition, besides the "empty list" extreme case, could cause the replica to receive an incomplete file list (
GetChildrenreturned only a subset of files).Fix
No
pop_backon empty list:In
GetFullReplDataInfo, iffilesis empty, returnNotOKimmediately instead of callingpop_back.Introduce
TryPurgeCheckpoint— rename then destroy:Added
Storage::TryPurgeCheckpoint, which changes the purge logic to:checkpoint_mu_, atomically renamecheckpoint_dirtocheckpoint_dir.trashviaRenameFile;checkpoint_info_;DestroyDB(trash).This way, the meta listing either sees a complete live directory or no directory at all.
Tests
Two new C++ gtests added (
storage_test.cc):GetFullReplDataInfoRejectsEmptyCheckpoint: Creates a checkpoint → clears the directory → assertsNotOK.TryPurgeCheckpointPolicyAndAtomicRemove: Verifies idle purge, rename atomicity, andcheckpoint_info_reset.