Conversation
… load (apache#3162) - Throw HgStoreException in onSnapshotSave when RocksDB compaction is in progress so JRaft retries rather than committing an empty snapshot dir. - In onSnapshotLoad, fall through to the real load path when should_not_load is present but data/ is missing (JVM-killed mid-checkpoint), so JRaft can signal the error and request a fresh snapshot from the leader. - Add unit tests covering both fix paths in HgSnapshotHandlerTest. - Add docker/test/test-snapshot-corruption.sh, a deterministic Docker reproducer that confirms the bug and validates the fix (--fixed mode). Fixes apache#3162 Co-Authored-By: Claude <noreply@anthropic.com>
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: Snapshot loading still silently accepts a non-directory data path, and the added reproducer cannot resolve its compose/root paths on a clean checkout; fixed mode also references a missing Dockerfile. Evidence: exact head 7ee5d42; all 17 exact-head check runs completed successfully.
… load (apache#3162) - Addressed Review comments
|
Thanks @imbajin for review. I have addressed the review comments. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3164 +/- ##
============================================
+ Coverage 37.78% 40.68% +2.89%
- Complexity 6556 7119 +563
============================================
Files 800 800
Lines 68929 69085 +156
Branches 9157 9193 +36
============================================
+ Hits 26046 28108 +2062
+ Misses 39824 37737 -2087
- Partials 3059 3240 +181 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
imbajin
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The snapshot handling change is covered by green exact-head checks, but the new reproducer still cannot validate the compaction-busy save fix. Evidence: exact-head CI and Codecov checks completed successfully; see the inline finding.
… load (apache#3162) - Added comment in test-snapshot-corruption.sh to make clear that its just load-path reproducer for the HStore snapshot corruption bug
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The load-side validation is correct (isDirectory() rather than exists(), and deliberately not requiring a non-empty data/, which keeps empty partitions working) and the fix lands in the handler that PartitionEngine actually wires up; the save-side change is broader than the defect needs, and the new tests and reproducer have a few rough edges. Evidence: read of SnapshotHandler.java, HgSnapshotHandlerTest.java and docker/test/test-snapshot-corruption.sh at 8e121d4; PartitionEngine.java:176-177, PartitionStateMachine.java:192-206 and BusinessHandlerImpl.dbCompaction read for the surrounding lifecycle; gh -R apache/hugegraph pr checks 3164 (all 17 pass).
… load (apache#3162) - Addressed review comments. Added few Unit test cases, -Removed test-snapshot-corruption.sh because scenario is already covered by UTs.
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The save-side throw is the right shape and is handled by PartitionStateMachine as a raft EIO status, but none of the new tests runs in any build, so the change merges with 0% patch coverage. The load-side data/ guard sits inside the should_not_load branch, so it covers only the flag-present variant of the signature #3162 records. Evidence: exact-head diff against merge-base 98477f0f (4 files, +216/-3); surefire include lists at hugegraph-store/hg-store-test/pom.xml:225-302; CoreSuiteTest.java:22-44 with the suite annotations commented out; .github/workflows/pd-store-ci.yml:281-296 running common/client/rocksdb/raftcore only; codecov on this head reporting 0% patch coverage, 8 lines missing, all in SnapshotHandler.java; RocksDBSession.java:740-745 already throwing for a missing snapshot path; and git ls-tree -r --name-only 0e1c319 showing docker/test/test-snapshot-corruption.sh absent from this head, though the description still names it as the verification path.
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: The snapshot guard still has a check-then-act race and does not guarantee that snapshots cannot overlap compaction. Evidence: exact-head static review of SnapshotHandler.java:97-105, BusinessHandlerImpl.java:1413-1421, and PartitionStateMachine.java:192-204; all non-Codecov exact-head checks are completed.
…es (apache#3162) Addresses review comments: - onSnapshotSave/dbCompaction shared a non-atomic state check, letting saves race with compaction; coordinate both through a dedicated per-partition lock, checked non-blockingly on both sides - add EC_RKDB_SNAPSHOT_SAVE_BUSY_FAIL so the busy-save case has its own grep-able error code, and fix the exception text (compaction, not "skipped") and a stray non-ASCII em dash - onSnapshotLoad checks data/ before should_not_load, so a snapshot missing its flag is reported as corrupt instead of failing later with an unrelated RocksDB path error - drop the duplicate jraft/protobuf imports in HgSnapshotHandlerTest - register SnapshotHandlerTest in RaftSuiteTest and HgSnapshotHandlerTest in CoreSuiteTest, and run store-core-test in CI, so both actually execute instead of being skipped by every bound surefire profile
The pd-store-ci.yml store job gained a store-core-test profile and hg-store-core module in a prior commit, but test-check-jacoco-report.sh's hardcoded aggregation contract still asserted the old 4-profile set, breaking CI with an AssertionError on the set-equality checks.
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: On compaction-busy the new throw reaches jraft as RaftError.EIO, the one code SnapshotExecutorImpl escalates to reportError, which ends in PartitionEngine.restartRaftNode(); and the Run core test step this PR adds is red on this head. Two blocking comments (SnapshotHandler.java, CoreSuiteTest.java) and three nits (SnapshotHandler.java load check, RaftRocksdbOptions.java, HgStoreException.java). Evidence: failing check store at 98fdaac, https://github.com/apache/hugegraph/actions/runs/33957642990/job/101289530213 ; jraft 1.3.13 sources for the snapshot error path; line references in each comment.
…he#3164) - report EBUSY instead of EIO when a snapshot save is skipped due to an in-progress compaction, so jRaft retries later instead of escalating to a full raft node restart (only EIO triggers that in SnapshotExecutorImpl#onSnapshotSaveDone) - check should_not_load before validating the data/ directory in onSnapshotLoad, so a locally-saved snapshot (which has no data/ by design) is skipped instead of reported as corrupt - keep the raftRocksdbConfigRegistered guard flag unset until registration actually completes, so a failure partway through can be retried instead of being silently swallowed forever - restore EC_RKDB_TRUNCATE_FAIL, EC_RKDB_TRANSFER_SNAPSHOT_FAIL, and EC_METRIC_FAIL, which were unintentionally dropped and would have broken binary compatibility for downstream consumers - stop CoreSuiteTest and BatchGraphIsolationTest from sharing a surefire fork: HgStoreEngine's `closing` flag is set by the former's teardown and never reset, so the latter failed with "store is closing" whenever both ran in the same JVM Updates HgSnapshotHandlerTest's should_not_load/data-missing case to expect a skip rather than a throw, matching the corrected check order.
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The range lock closes the check-then-act race and the EBUSY mapping keeps a busy save from restarting the raft node; one regression remains: a full compaction that collides with a snapshot save is now silently dropped with no retry. Evidence: exact head 468e234 static review of BusinessHandlerImpl.java:1413-1481, SnapshotHandler.java:92-132, PartitionStateMachine.java:192-212 and every dbCompaction caller (PartitionEngine.java:1017,1263; HgStoreEngine.java:505; PartitionAPI.java:207; TTLCleaner.java:207,264); gh -R apache/hugegraph pr checks 3164 all 24 checks pass, store job runs CoreSuiteTest (7 tests) and RaftSuiteTest (6 tests).
… load (apache#3162)- apache#3164 - Implemented logic to wait for compactionRangeLock instead of failing dbCompaction fast. - Added UT to cover this scenario.
The race reproduces deterministicallyOn a 3-VM lab (1 PD + 3 Store + 1 Server, replication 1, 25 M edges, partitions of 60–275 MB) the window is not narrow: a full compaction of a partition holding ~250 MB of unflushed data takes 7 s, and a snapshot requested 300 ms after it lands in The only case where the window really disappears is a freshly compacted partition: What master does, step by step
Two things this says about the PR
Numbers for the 10 s discussion
If it helps, I can repeat the same runs on the next PR head or at replication 3 (where the interesting part is what a follower installing a snapshot in that window receives). |
Thanks, @SebastianGruza. Yes, this will really help! |
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The previous rounds' findings are resolved at this head. Snapshot save and compactRange() now share a per-partition ReentrantLock, a busy save reports RaftError.EBUSY instead of restarting the raft node, a compaction that meets a running save waits up to 10s before skipping with a WARN, and the data/ check sits after the should_not_load return. Evidence: exact-head diff and the incremental diff 468e2343..1c6bc23a (BusinessHandlerImpl.java:1440-1463, SnapshotHandler.java, PartitionStateMachine.java); all 24 checks on 1c6bc23a pass, and the store job log shows CoreSuiteTest (8 tests, including the skip path: Partition 4 skip dbCompaction ... after 200ms wait), BatchGraphIsolationTest (6) and RaftSuiteTest (6) passing. Optional nit: the comment at BusinessHandlerImpl.java:1446-1449 still says the next compaction pass will succeed, but the event-driven triggers are one-shot.
Address review comment to improve message.
Thanks @bitflicker64. I have fixed the comment at |
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: At cf0e3e1 the shared range lock closes the save/compaction race, a busy save reports EBUSY (jraft 1.3.13 onSnapshotSaveDone only escalates EIO), the data/ check sits after the should_not_load return, and the new tests run in CI. One leftover documentation nit. Evidence: exact-head diff and git diff 1c6bc23 cf0e3e1; SnapshotHandler.java:96-130,180-191, BusinessHandlerImpl.java:1436-1462, PartitionEngine.java:1085-1125; all 24 check runs at head pass.
imbajin
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: The snapshot/compaction range lock closes the normal overlap race, but one interrupted wait can leave a partition stuck. Evidence: exact-head diff inspection found the cleanup gap below; the exact-head store-core/store-raftcore Maven verification passed, and all visible remote checks are green. Requested change: release the path lock when the timed range-lock wait is interrupted, while preserving the existing handoff after successful compaction.
… load (apache#3162)- apache#3164 - InterruptedException from rangeLock.tryLock() previously escaped to the outer catch without releasing pathLock, Fixed this issue and added UT to cover this
|
Follow-up to the measurement of Sep 12, same lab, now with PD
Conclusion: with three replicas the master bug costs one replica and a manual wipe of one partition on one node, not data; with one replica (the run of Sep 12) it costs the partition and, after the only possible cleanup, the unflushed writes too. The save-side fix in this PR removes the cause in both cases, so after this measurement I see nothing open from my side. The "wipe one partition on one node → InstallSnapshot from the leader" procedure may deserve a sentence in the operations docs, because nothing in the log suggests that this is the way out. One observation outside the scope of this PR, recorded on our side as a separate finding: when the lab's disk filled up during the run, a single-node PD stayed leaderless for good after one failed 5-minute snapshot of its own raft ( |
imbajin
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: Table-specific compaction remains outside the snapshot exclusion lock, so the reported snapshot race can still occur for a PD table-compaction request. Evidence: BusinessHandlerImpl.dbCompaction acquires compactionRangeLock only when tableName is empty, while the non-empty branch still calls op.compactRange(tableName); the current head's PartitionEngine and PartitionInstructionProcessor pass PD tableName values into this method.
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: no. Summary: Most of the save/load fixes from earlier rounds hold at this head, but the range lock still does not cover every compaction path: besides the table-specific branch already raised, cleanPartition runs a full compactRange() without it. Evidence: exact-head read of BusinessHandlerImpl.java (dbCompaction, cleanPartition at line 1290) and every non-test cleanPartition( caller; gh -R apache/hugegraph pr checks 3164 shows all 24 checks passing.
… load (apache#3162)- apache#3164 -Implemented review comments. - dbCompaction: move the tableName-specific compactRange(tableName) call inside the rangeLock acquire/release, instead of leaving it outside the lock entirely - a table-specific compaction could otherwise race SnapshotHandler.onSnapshotSave's checkpoint (imbajin) - cleanPartition: wrap the async cleanup work (inside Utils.runInThread) with the same bounded compactionRangeLock.tryLock()/unlock() used by dbCompaction, instead of leaving its compactRange() call unguarded (bitflicker64)
|
@imbajin @bitflicker64 |
bitflicker64
left a comment
There was a problem hiding this comment.
Blocking: yes. Summary: the latest commit moved the compaction range lock in front of lock(path), so a queued compaction now holds that lock while it waits on the previous compaction's path lock, and the interrupt handler releases a path lock the task never took. cleanPartition also holds the range lock for the whole scan-and-delete rather than just the compactRange() at the end. Evidence: static read of BusinessHandlerImpl.dbCompaction/cleanPartition and PartitionEngine.doSnapshotSync at 6393444, compared with the ordering in 54d4fe5.
| // the range lock. The path lock was already acquired and | ||
| // must be released here, otherwise later compactions for this | ||
| // partition would block until the path lock timeout. | ||
| unlock(path); |
There was a problem hiding this comment.
rangeLock.tryLock(...) in front of lock(path) (in 54d4fe5 it came after). That causes two problems.
-
This
unlock(path)now runs when the task never held the path lock: an interrupt intryLockat 1464, or inlock(path)itself, or any interrupt on thetableNamebranch, which never takes it at all.unlocksets the sharedAtomicIntegerback tocompactionCanStart. If an earlier compaction on this partition still holds it (it stays held untilPartitionEngine.doSnapshotSyncfinishes), that lock gets cleared and a third compaction can start before the earlier post-compaction snapshot is done. The comment above says "the path lock was already acquired", which is no longer true.testDbCompactionReleasesPathLockWhenInterruptedWaitingForRangeLockstill passes only because the path lock was never set todoingin that test. -
The range lock is now held while
lock(path)waits, which can be up totimeoutMillis(6h), and while the blank-task RPC runs. When a seconddbCompactionfor the same partition arrives while the first one's post-compaction snapshot is pending (the callers at PartitionEngine.java:1018/1266, HgStoreEngine.java:505 and the TTL submitter don't go through the semaphore), it takes the range lock and blocks inlock(path). The snapshot that would releasepaththen callstryLockCompactionRange, gets EBUSY, and every periodic snapshot on the partition fails the same way until the wait ends.
Requested change: go back to taking lock(path) first, hold the range lock only around op.compactRange() / op.compactRange(tableName), and call unlock(path) only when lock(path) actually succeeded (for example with a pathLocked flag). Please also update the test so the path lock is really in doing before the interrupt.
| compactionRangeLock.computeIfAbsent(partId, k -> new ReentrantLock()); | ||
| boolean rangeLocked = false; | ||
| try { | ||
| rangeLocked = rangeLock.tryLock(compactionRangeLockWaitMillis, |
There was a problem hiding this comment.
cleanPartition(partition, ...) call. That call scans every key in the partition and deletes the ones out of range before it reaches op.compactRange() at line 1310. On a large partition that can take minutes. For all of that time every onSnapshotSave for the partition fails with EBUSY, so the raft log isn't truncated, and any dbCompaction on the partition waits 10s and is then dropped. The field comment at 168-178 assumes the lock is held for under a second.
The skip branch also has a cost: if a snapshot save holds the lock past 10s, the whole cleanup is dropped, not just the compaction. This is the one-shot cleanup after a split or move (DataManagerImpl.java:246, DefaultDataMover.java:245), so the data that no longer belongs to this partition stays, and the CleanTask is left in START.
Requested change: take the range lock only around the final op.compactRange() inside the private cleanPartition(Partition, Function), with the same bounded wait, and let the delete pass run unlocked. If the compaction step times out, log it but keep the cleanup.
Purpose of the PR
__raft_snapshot_metabut nodata/#3162This PR addresses a race condition that occurs during snapshot saves when compaction is busy, which could previously lead to corrupted snapshots or stuck partitions.
Main Changes
data/on load: Added validation during the snapshot load process to verify the presence of thedata/directory, preventing the system from loading incomplete snapshots.test-snapshot-corruption.shto deterministically reproduce the bug and validate the fix across different storage states.Verifying these changes
test-snapshot-corruption.shscript to verify the corrupted snapshot detection and prevention.Does this PR potentially affect the following parts?
Documentation Status
Doc - TODODoc - DoneDoc - No Need