Incremental nas backup fixes#13571
Conversation
…- Backup deletes are serialized by using a per-VM GlobalLock Backup delete checks if it has a live dependent backup not just a live immediate child.
|
HI @jmsperu |
There was a problem hiding this comment.
Pull request overview
This PR builds on the incremental NAS backup work by hardening incremental chain deletion: it serializes chain-mutating deletes with a per-VM lock, fixes the “can I delete this backup?” decision to consider all downstream dependents (not just the immediate child), removes dead chain-navigation helpers, and adds a null-check around answer.getNeedsCleanup().
Changes:
- Serialize backup-chain deletions per VM using a
GlobalLockand re-read the target backup row under the lock to avoid race-induced “stuck Hidden” backups. - Update chain delete semantics to detect any live descendants (via
CHAIN_POSITION) before physically deleting a backup; enhance sweep behavior and add multiple new unit tests for edge cases. - Remove unused chain helper methods and add a null-check before calling
answer.getNeedsCleanup().
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java |
Adds per-VM locking for chain deletes and updates descendant detection + sweeping logic for incremental backup chains. |
plugins/backup/nas/src/test/java/org/apache/cloudstack/backup/NASBackupProviderTest.java |
Updates and extends unit tests to cover new lock behavior and additional chain-delete edge cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
@blueorangutan package |
|
@abh1sar a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:1136
chainPositionreturnsInteger.MAX_VALUEfor missing/invalidCHAIN_POSITION, butgetChainOrderedLeafToRootsorts in descending order. That means missing metadata sorts to the front (treated as the newest/leaf), contradicting the in-code comment and potentially causing incorrect sweep ordering and dependent detection when metadata is missing or malformed.
private int chainPosition(Backup b) {
String s = readDetail(b, NASBackupChainKeys.CHAIN_POSITION);
if (s == null) {
return Integer.MAX_VALUE; // no metadata => sort to end
}
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return Integer.MAX_VALUE;
}
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #13571 +/- ##
============================================
+ Coverage 3.46% 19.45% +15.99%
- Complexity 0 19199 +19199
============================================
Files 479 6288 +5809
Lines 41162 565359 +524197
Branches 7793 69006 +61213
============================================
+ Hits 1426 110017 +108591
- Misses 39543 443395 +403852
- Partials 193 11947 +11754
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✖️ debian ✔️ suse15. SL-JID 18501 |
|
@blueorangutan test |
|
@weizhouapache a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests |
|
There was integration test failure in test_backup_recovery_nas.py. Fixed and verified by running the test manually multiple times. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
plugins/backup/nas/src/main/java/org/apache/cloudstack/backup/NASBackupProvider.java:1131
chainPositionreturnsInteger.MAX_VALUEwhen CHAIN_POSITION is missing or unparsable, but the comment says this should "sort to end". With the current leaf-first (descending) sort,MAX_VALUEactually sorts first and can also causehasLiveChildrento miss real descendants when the current backup’s CHAIN_POSITION is absent, leading to unsafe deletions.
private int chainPosition(Backup b) {
String s = readDetail(b, NASBackupChainKeys.CHAIN_POSITION);
if (s == null) {
return Integer.MAX_VALUE; // no metadata => sort to end
}
great! |
jmsperu
left a comment
There was a problem hiding this comment.
Thanks @abh1sar, reviewed. The core fixes look correct:
- The null guard (
answer != null && answer.getNeedsCleanup()) is right — the log line above already treatsansweras nullable, so the previous code could NPE. - Switching
hasLiveChildrento compareCHAIN_POSITIONcorrectly fixes the "only the immediate child was checked" bug — any non-tombstoned member deeper in the chain now correctly blocks deletion. - The per-VM
GlobalLockplus the re-read viafindByIdunder the lock (returning early if the row is already gone) is the right way to close the concurrent-delete race. - Dead-code removal and the added tests for the deeper-orphan and live-descendant cases look good.
One thing I'd like changed before merge:
In deleteLeafBackupAndSweepPendingAncestors, the sweep now ignores the result of deleteBackupFileAndRow:
for (Backup member : chain) {
if (member.getId() == backup.getId()) continue;
if (!isDeletePending(member)) break;
deleteBackupFileAndRow(member, repo, host); // return value dropped
}The previous version stopped the sweep on failure. Because the sweep walks leaf→root, if a child's on-NAS delete fails and the loop continues, we can delete that child's parent while the failed child still exists on the repository, which orphans the child (its base data is gone and it becomes unrestorable). Can we restore the stop-on-failure behaviour, i.e. break/return when deleteBackupFileAndRow returns false, so a partial NAS failure leaves a consistent chain for the next sweep to retry?
Two smaller points to confirm:
- On the
forcedpath,deleteBackupnow holds the per-VM lock and then callscascadeDeleteSubtree. Could you confirm nothing under it re-entersdeleteBackupfor the same VM in a way that unbalances the lock ref-count? (GlobalLockis reentrant per thread, so it should be fine, just want to be sure since that method isn't in this diff.) hasLiveChildrenrelies onchainPosition. IfCHAIN_POSITIONis ever absent, the>comparison could misjudge dependents. Probably a non-issue since position is always written, but a defensive default may be worth it.
|
@blueorangutan package |
|
@abh1sar a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress. |
We are deleting Hidden backups that don't have any dependents. Hidden backups without any live children are by design not restorable. Let's say we have 4 Hidden backups in a chain and one live leaf backup. In case one of the Hidden backup delete fails, and we stop walking the chain to delete rest of the backups. These Hidden backups will never get deleted.
That's right. deleteBackup is only called from BackupManagerImpl.
Yes, chain position is always written. Not doing anything about it now. Maybe we can add some defensive code later |
jmsperu
left a comment
There was a problem hiding this comment.
Thanks @abh1sar, that all makes sense.
On the sweep: agreed, you're right and I'll withdraw that. Since every member the sweep touches is already a Hidden tombstone with no live dependents, none of them are restorable regardless of order, so there's no live backup to orphan. Best-effort is the better behaviour here — stopping on the first failure would strand the remaining tombstones permanently (nothing would ever re-trigger their sweep), whereas continuing frees everything it can and leaves only the single failed file to be reclaimed later. Good call.
Confirmed on the other two: deleteBackup only entering from BackupManagerImpl settles the lock ref-count question, and the CHAIN_POSITION default is fine to leave for a later defensive pass since position is always written.
LGTM
|
Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✖️ debian ✔️ suse15. SL-JID 18512 |
|
@blueorangutan test |
|
@abh1sar a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests |
|
[SF] Trillian test result (tid-16508)
|
Description
This PR contains some fixes that are required on top of #13074
Concurrent deletes can modify the same backup chain in parallel. There is no synchronisation mechanism, so in a race condition a backup can be left in the Hidden state without ever being deleted. Using a per-VM global lock to synchronise deletes.
If a backup can be deleted only checks if its immediate child is present or not (not hidden and not removed). But it should check all its dependents. Otherwise it will be wrongly deleted.
Removed dead code: findChainTail and findChainParent
Added null check before accessing answer.getNeedsCleanup()
Types of changes
Feature/Enhancement Scale or Bug Severity
Feature/Enhancement Scale
Bug Severity
Screenshots (if appropriate):
How Has This Been Tested?
Reproduced the failing test cases and verified with the fixes.
How did you try to break this feature and the system with this change?