fix(store): throw on compaction-busy snapshot save; validate data/ on load (#3162) - #3164
fix(store): throw on compaction-busy snapshot save; validate data/ on load (#3162)#3164vaijosh wants to merge 3 commits into
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✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3164 +/- ##
=============================================
+ Coverage 41.06% 93.25% +52.19%
+ Complexity 519 65 -454
=============================================
Files 771 9 -762
Lines 65962 267 -65695
Branches 8766 22 -8744
=============================================
- Hits 27088 249 -26839
+ Misses 36008 8 -36000
+ Partials 2866 10 -2856 ☔ 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.
| # Sub-case A: remove both data/ and should_not_load — exactly what the race produces. | ||
| log " Target: partition $PARTITION_ID / $SNAP_NAME" | ||
| log " Removing data/ and should_not_load — leaving only __raft_snapshot_meta (sub-case A)" | ||
| docker run --rm \ |
There was a problem hiding this comment.
data/ and should_not_load at lines 211-214 and labels the resulting load error as BUG REPRODUCED. The fixed implementation would hit the same injected corruption, while --fixed only checks the load-side warning. Please add a run that triggers snapshot while compaction is actually active and asserts the save failure/retry, or narrow the script's claim. Evidence: lines 168-178 and 211-214; /test/snapshot only invokes PartitionEngine.snapshot() and the script never invokes /test/compact.
There was a problem hiding this comment.
Hi @imbajin
You're correct. The save-side throw (SnapshotHandler.java:98–101) only fires if compaction is actually in flight when snapshot() is called, and we have no reliable way to hold that state long enough. Because /test/compact submits a background task and returns immediately, the race window is too narrow to hit deterministically. This would make the test flaky.
Currently, the script simulates the outcome of that race (a missing data/ dir) by removing it manually, which lets us at least verify the load-side behavior. I have added a comment in the script to make this clear.
If there is a way to deterministically reproduce the save-side throw, I would be happy to incorporate it into the script.
There was a problem hiding this comment.
@imbajin
I could not found deterministic steps to repro this issue ( because its timing issue). I had encountered this while testing changes related to Cloud storage support feature.
But this PR addresses critical edge case. For users running distributed HugeGraph clusters, this enhancement prevents silent failures or data inconsistency during high-throughput operations when snapshots and compactions overlap.
… 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).
| AtomicInteger state = businessHandler.getState(groupId); | ||
| if (state != null && state.get() == BusinessHandler.doing) { | ||
| return; | ||
| throw new HgStoreException( |
There was a problem hiding this comment.
BusinessHandlerImpl.dbCompaction sets doing immediately before op.compactRange() and clears it to compactionDone immediately after (BusinessHandlerImpl.java:1418-1421) — a full RocksDB range compaction, which on a large partition runs for a long time. PartitionStateMachine.onSnapshotSave turns this exception into done.run(new Status(RaftError.EIO, e.toString())) (PartitionStateMachine.java:199-201), so with this change every jraft periodic snapshot that happens to overlap a compaction is reported as an IO failure and the raft log is not truncated for the duration of that compaction.
The same compaction path already arranges for a snapshot as soon as it finishes — it logs "dbCompaction end and start to do snapshot" and submits SYNC_BLANK_TASK (BusinessHandlerImpl.java:1422-1437) — which suggests the previous early return was a deliberate skip rather than an oversight. The defect reported in #3162 is that the skip let jraft commit the snapshot as valid, not that the skip existed.
Requested change: bound the impact instead of failing outright — wait a configurable interval for the state to leave doing (the setAndNotifyState path already exists) and only throw if it is still busy when that budget expires, so an ordinary compaction/snapshot overlap resolves itself rather than surfacing as a raft-level IO error.
| // No exception should propagate from the state guard. | ||
| // (saveSnapshot will throw because the mock returns null for it — that's fine, | ||
| // we only care the doing-check is not hit.) | ||
| try { |
There was a problem hiding this comment.
🧹 This test asserts nothing on its success path, and the new tests sit in the wrong test class.
handler.onSnapshotSave(stubWriter) is wrapped in try { ... } catch (HgStoreException e) with the only assertion inside the catch, so if no exception is thrown the test passes without checking anything — and it would still pass if the compaction guard at SnapshotHandler.java:98-102 were deleted outright, since the guard is not the thing being exercised on this path.
Separately, all four new tests construct SnapshotHandler, but the class is HgSnapshotHandlerTest and its setUp builds new HgSnapshotHandler(createPartitionEngine(0)) (line 65) for the pre-existing tests. With two near-identical handler classes in the same package — only SnapshotHandler is wired into production, at PartitionEngine.java:176 — mixing them in one file makes it hard to tell which one any given test covers.
Requested change: assert positively that the non-busy path proceeds (for example verify(mockBusinessHandler).saveSnapshot(anyString(), eq(""), eq(0))) rather than swallowing the exception, and move the four SnapshotHandler tests into their own SnapshotHandlerTest.
| # Requires: Docker Desktop >= 20.10, >= 12 GB allocated to Docker, Docker Compose v2 | ||
| # Run from the repo root: | ||
| # bash docker/hbase/test/test-snapshot-corruption.sh # confirm load-path bug is present (buggy image) | ||
| # bash docker/hbase/test/test-snapshot-corruption.sh --fixed # confirm load-path fix is active (fixed image) |
There was a problem hiding this comment.
🧹 The documented invocation points at a path that does not exist.
Both usage lines say bash docker/hbase/test/test-snapshot-corruption.sh, but the script is committed at docker/test/test-snapshot-corruption.sh (confirmed against the head tree at 8e121d4d). The code below resolves COMPOSE_FILE correctly from $SCRIPT_DIR, so only the documentation is wrong — but it is the first thing a reader copies, and the previous review round already flagged the stale docker/hbase/test prefix.
Requested change: update both lines to bash docker/test/test-snapshot-corruption.sh (and ... --fixed).
|
|
||
| [ -n "$TARGET" ] || fail "No suitable snapshot found (need data/ + should_not_load + __raft_snapshot_meta)" | ||
|
|
||
| PARTITION_ID=$(echo "$TARGET" | grep -oE '/[0-9]{5}/' | head -1 | tr -d '/') |
There was a problem hiding this comment.
🧹 This line can abort the whole run with no diagnostic.
The script sets set -euo pipefail (line 35). If $TARGET has no five-digit path segment, grep -oE '/[0-9]{5}/' exits 1, pipefail surfaces that as the pipeline status even though head and tr succeed, and the failing command substitution in a simple assignment trips set -e — the script exits silently right after reporting Target: partition ... , with the cluster left running and one snapshot already about to be corrupted.
Requested change: tolerate the miss and fail loudly instead, e.g.
PARTITION_ID=$(echo "$TARGET" | grep -oE '/[0-9]{5}/' | head -1 | tr -d '/' || true)
[ -n "$PARTITION_ID" ] || fail "Could not derive partition id from snapshot path: $TARGET"
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