From 094d2c3e283d5172081b423c7fb90e925d6de4be Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Mon, 25 May 2026 17:38:17 +0800 Subject: [PATCH 1/2] [storage]: return group tree in APIQueryVolumeSnapshotTree Add groupTrees field to APIQueryVolumeSnapshotTreeReply, built from VolumeSnapshotGroupVO topology with vote-majority parent resolution. Extract build logic into VolumeSnapshotGroupTreeBuilder. Resolves: ZSV-9792 Change-Id: I71616f6f6f6c637a6167637863727575746f616c --- .../APIQueryVolumeSnapshotTreeReply.java | 10 + ...eryVolumeSnapshotTreeReplyDoc_zh_cn.groovy | 9 + .../VolumeSnapshotGroupTreeInventory.java | 107 +++++++ ...SnapshotGroupTreeInventoryDoc_zh_cn.groovy | 79 +++++ .../VolumeSnapshotGroupTreeRefInventory.java | 60 ++++ ...pshotGroupTreeRefInventoryDoc_zh_cn.groovy | 47 +++ .../sdk/QueryVolumeSnapshotTreeResult.java | 8 + .../sdk/VolumeSnapshotGroupTreeInventory.java | 95 ++++++ .../VolumeSnapshotGroupTreeRefInventory.java | 55 ++++ .../snapshot/VolumeSnapshotManagerImpl.java | 30 +- .../snapshot/VolumeSnapshotTreeBase.java | 53 ++-- .../group/VolumeSnapshotGroupTreeBuilder.java | 270 ++++++++++++++++++ 12 files changed, 803 insertions(+), 20 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java create mode 100644 header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy create mode 100644 sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java create mode 100644 sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java create mode 100644 storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java index 522ec92d29b..c759485391b 100755 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java @@ -2,6 +2,7 @@ import org.zstack.header.query.APIQueryReply; import org.zstack.header.rest.RestResponse; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory; import org.zstack.header.volume.VolumeType; import java.util.List; @@ -11,6 +12,7 @@ @RestResponse(allTo = "inventories") public class APIQueryVolumeSnapshotTreeReply extends APIQueryReply { private List inventories; + private List groupTrees; public List getInventories() { return inventories; @@ -19,6 +21,14 @@ public List getInventories() { public void setInventories(List inventories) { this.inventories = inventories; } + + public List getGroupTrees() { + return groupTrees; + } + + public void setGroupTrees(List groupTrees) { + this.groupTrees = groupTrees; + } public static APIQueryVolumeSnapshotTreeReply __example__() { APIQueryVolumeSnapshotTreeReply reply = new APIQueryVolumeSnapshotTreeReply(); diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy index e0ea093436e..7b070b9a77b 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy +++ b/header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy @@ -1,6 +1,7 @@ package org.zstack.header.storage.snapshot import org.zstack.header.errorcode.ErrorCode +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory doc { @@ -28,4 +29,12 @@ doc { since "0.6" clz VolumeSnapshotTreeInventory.class } + ref { + name "groupTrees" + path "org.zstack.header.storage.snapshot.APIQueryVolumeSnapshotTreeReply.groupTrees" + desc "虚拟机快照组树清单(仅当查询条件包含 volumeUuid eq <根云盘UUID> 时返回)" + type "List" + since "5.0" + clz VolumeSnapshotGroupTreeInventory.class + } } diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java new file mode 100644 index 00000000000..ca23b47de0c --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java @@ -0,0 +1,107 @@ +package org.zstack.header.storage.snapshot.group; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; + +public class VolumeSnapshotGroupTreeInventory { + private String uuid; + private String name; + private String description; + private String vmInstanceUuid; + private Timestamp createDate; + private Timestamp lastOpDate; + private boolean current; + private boolean incomplete; + private String parentGroupUuid; + private List children = new ArrayList<>(); + private List refs = new ArrayList<>(); + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getVmInstanceUuid() { + return vmInstanceUuid; + } + + public void setVmInstanceUuid(String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + + public Timestamp getCreateDate() { + return createDate; + } + + public void setCreateDate(Timestamp createDate) { + this.createDate = createDate; + } + + public Timestamp getLastOpDate() { + return lastOpDate; + } + + public void setLastOpDate(Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + + public boolean isCurrent() { + return current; + } + + public void setCurrent(boolean current) { + this.current = current; + } + + public boolean isIncomplete() { + return incomplete; + } + + public void setIncomplete(boolean incomplete) { + this.incomplete = incomplete; + } + + public String getParentGroupUuid() { + return parentGroupUuid; + } + + public void setParentGroupUuid(String parentGroupUuid) { + this.parentGroupUuid = parentGroupUuid; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public List getRefs() { + return refs; + } + + public void setRefs(List refs) { + this.refs = refs; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..bd2ab05c69d --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy @@ -0,0 +1,79 @@ +package org.zstack.header.storage.snapshot.group + +import java.sql.Timestamp + +doc { + + title "虚拟机快照组树清单" + + field { + name "uuid" + desc "快照组UUID" + type "String" + since "5.0" + } + field { + name "name" + desc "快照组名称" + type "String" + since "5.0" + } + field { + name "description" + desc "快照组描述" + type "String" + since "5.0" + } + field { + name "vmInstanceUuid" + desc "虚拟机UUID" + type "String" + since "5.0" + } + field { + name "createDate" + desc "创建时间" + type "Timestamp" + since "5.0" + } + field { + name "lastOpDate" + desc "最后一次修改时间" + type "Timestamp" + since "5.0" + } + field { + name "current" + desc "是否是当前快照组(虚拟机维度)" + type "boolean" + since "5.0" + } + field { + name "incomplete" + desc "是否为残缺快照组(部分盘的快照已被删除)" + type "boolean" + since "5.0" + } + field { + name "parentGroupUuid" + desc "父快照组UUID" + type "String" + since "5.0" + } + ref { + name "children" + path "org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory.children" + desc "子快照组列表" + type "List" + since "5.0" + clz VolumeSnapshotGroupTreeInventory.class + } + ref { + name "refs" + path "org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory.refs" + desc "快照组成员盘列表" + type "List" + since "5.0" + clz VolumeSnapshotGroupTreeRefInventory.class + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java new file mode 100644 index 00000000000..47ccdbf7c43 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java @@ -0,0 +1,60 @@ +package org.zstack.header.storage.snapshot.group; + +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; + +public class VolumeSnapshotGroupTreeRefInventory { + private String volumeUuid; + private String volumeName; + private String volumeType; + private String volumeSnapshotUuid; + private boolean snapshotDeleted; + private VolumeSnapshotInventory snapshot; + + public String getVolumeUuid() { + return volumeUuid; + } + + public void setVolumeUuid(String volumeUuid) { + this.volumeUuid = volumeUuid; + } + + public String getVolumeName() { + return volumeName; + } + + public void setVolumeName(String volumeName) { + this.volumeName = volumeName; + } + + public String getVolumeType() { + return volumeType; + } + + public void setVolumeType(String volumeType) { + this.volumeType = volumeType; + } + + public String getVolumeSnapshotUuid() { + return volumeSnapshotUuid; + } + + public void setVolumeSnapshotUuid(String volumeSnapshotUuid) { + this.volumeSnapshotUuid = volumeSnapshotUuid; + } + + public boolean isSnapshotDeleted() { + return snapshotDeleted; + } + + public void setSnapshotDeleted(boolean snapshotDeleted) { + this.snapshotDeleted = snapshotDeleted; + } + + public VolumeSnapshotInventory getSnapshot() { + return snapshot; + } + + public void setSnapshot(VolumeSnapshotInventory snapshot) { + this.snapshot = snapshot; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy new file mode 100644 index 00000000000..43ea8bc700f --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy @@ -0,0 +1,47 @@ +package org.zstack.header.storage.snapshot.group + +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory + +doc { + + title "虚拟机快照组成员盘清单" + + field { + name "volumeUuid" + desc "云盘UUID" + type "String" + since "5.0" + } + field { + name "volumeName" + desc "云盘名称" + type "String" + since "5.0" + } + field { + name "volumeType" + desc "云盘类型(Root/Data)" + type "String" + since "5.0" + } + field { + name "volumeSnapshotUuid" + desc "对应的云盘快照UUID" + type "String" + since "5.0" + } + field { + name "snapshotDeleted" + desc "对应的云盘快照是否已被删除" + type "boolean" + since "5.0" + } + ref { + name "snapshot" + path "org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeRefInventory.snapshot" + desc "对应的云盘快照清单(被删除时为null,无访问权限时仅返回uuid)" + type "VolumeSnapshotInventory" + since "5.0" + clz VolumeSnapshotInventory.class + } +} diff --git a/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java b/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java index 27378c71079..29fd8e6e3b9 100644 --- a/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java +++ b/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java @@ -19,4 +19,12 @@ public java.lang.Long getTotal() { return this.total; } + public java.util.List groupTrees; + public void setGroupTrees(java.util.List groupTrees) { + this.groupTrees = groupTrees; + } + public java.util.List getGroupTrees() { + return this.groupTrees; + } + } diff --git a/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java b/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java new file mode 100644 index 00000000000..9d0f07a40b8 --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java @@ -0,0 +1,95 @@ +package org.zstack.sdk; + + + +public class VolumeSnapshotGroupTreeInventory { + + public java.lang.String uuid; + public void setUuid(java.lang.String uuid) { + this.uuid = uuid; + } + public java.lang.String getUuid() { + return this.uuid; + } + + public java.lang.String name; + public void setName(java.lang.String name) { + this.name = name; + } + public java.lang.String getName() { + return this.name; + } + + public java.lang.String description; + public void setDescription(java.lang.String description) { + this.description = description; + } + public java.lang.String getDescription() { + return this.description; + } + + public java.lang.String vmInstanceUuid; + public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { + this.vmInstanceUuid = vmInstanceUuid; + } + public java.lang.String getVmInstanceUuid() { + return this.vmInstanceUuid; + } + + public java.sql.Timestamp createDate; + public void setCreateDate(java.sql.Timestamp createDate) { + this.createDate = createDate; + } + public java.sql.Timestamp getCreateDate() { + return this.createDate; + } + + public java.sql.Timestamp lastOpDate; + public void setLastOpDate(java.sql.Timestamp lastOpDate) { + this.lastOpDate = lastOpDate; + } + public java.sql.Timestamp getLastOpDate() { + return this.lastOpDate; + } + + public boolean current; + public void setCurrent(boolean current) { + this.current = current; + } + public boolean getCurrent() { + return this.current; + } + + public boolean incomplete; + public void setIncomplete(boolean incomplete) { + this.incomplete = incomplete; + } + public boolean getIncomplete() { + return this.incomplete; + } + + public java.lang.String parentGroupUuid; + public void setParentGroupUuid(java.lang.String parentGroupUuid) { + this.parentGroupUuid = parentGroupUuid; + } + public java.lang.String getParentGroupUuid() { + return this.parentGroupUuid; + } + + public java.util.List children; + public void setChildren(java.util.List children) { + this.children = children; + } + public java.util.List getChildren() { + return this.children; + } + + public java.util.List refs; + public void setRefs(java.util.List refs) { + this.refs = refs; + } + public java.util.List getRefs() { + return this.refs; + } + +} diff --git a/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java b/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java new file mode 100644 index 00000000000..ebe60f70a1e --- /dev/null +++ b/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java @@ -0,0 +1,55 @@ +package org.zstack.sdk; + + + +public class VolumeSnapshotGroupTreeRefInventory { + + public java.lang.String volumeUuid; + public void setVolumeUuid(java.lang.String volumeUuid) { + this.volumeUuid = volumeUuid; + } + public java.lang.String getVolumeUuid() { + return this.volumeUuid; + } + + public java.lang.String volumeName; + public void setVolumeName(java.lang.String volumeName) { + this.volumeName = volumeName; + } + public java.lang.String getVolumeName() { + return this.volumeName; + } + + public java.lang.String volumeType; + public void setVolumeType(java.lang.String volumeType) { + this.volumeType = volumeType; + } + public java.lang.String getVolumeType() { + return this.volumeType; + } + + public java.lang.String volumeSnapshotUuid; + public void setVolumeSnapshotUuid(java.lang.String volumeSnapshotUuid) { + this.volumeSnapshotUuid = volumeSnapshotUuid; + } + public java.lang.String getVolumeSnapshotUuid() { + return this.volumeSnapshotUuid; + } + + public boolean snapshotDeleted; + public void setSnapshotDeleted(boolean snapshotDeleted) { + this.snapshotDeleted = snapshotDeleted; + } + public boolean getSnapshotDeleted() { + return this.snapshotDeleted; + } + + public org.zstack.sdk.VolumeSnapshotInventory snapshot; + public void setSnapshot(org.zstack.sdk.VolumeSnapshotInventory snapshot) { + this.snapshot = snapshot; + } + public org.zstack.sdk.VolumeSnapshotInventory getSnapshot() { + return this.snapshot; + } + +} diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java index 1f936ca6141..890e14eca68 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java @@ -27,6 +27,8 @@ import org.zstack.header.identity.*; import org.zstack.header.identity.quota.QuotaMessageHandler; import org.zstack.header.message.*; +import org.zstack.header.query.QueryCondition; +import org.zstack.header.query.QueryOp; import org.zstack.header.storage.backup.CleanUpVmBackupExtensionPoint; import org.zstack.header.storage.primary.*; import org.zstack.header.storage.primary.VolumeSnapshotCapability.VolumeSnapshotArrangementType; @@ -41,6 +43,7 @@ import org.zstack.storage.snapshot.group.MemorySnapshotGroupReferenceFactory; import org.zstack.storage.snapshot.group.VolumeSnapshotGroupBase; import org.zstack.storage.snapshot.group.VolumeSnapshotGroupChecker; +import org.zstack.storage.snapshot.group.VolumeSnapshotGroupTreeBuilder; import org.zstack.storage.snapshot.reference.VolumeSnapshotReferenceTreeBase; import org.zstack.storage.snapshot.reference.VolumeSnapshotReferenceUtils; import org.zstack.storage.volume.FireSnapShotCanonicalEvent; @@ -1338,11 +1341,14 @@ public List getReplyMessageClassForMarshalExtensionPoint() { @Override public void marshalReplyMessageBeforeSending(Message replyOrEvent, NeedReplyMessage msg) { if (replyOrEvent instanceof APIQueryVolumeSnapshotTreeReply) { - marshal(((APIMessage) msg).getSession(), (APIQueryVolumeSnapshotTreeReply) replyOrEvent); + APIQueryVolumeSnapshotTreeReply reply = (APIQueryVolumeSnapshotTreeReply) replyOrEvent; + APIQueryVolumeSnapshotTreeMsg apiMsg = (APIQueryVolumeSnapshotTreeMsg) msg; + marshalVolumeTrees(apiMsg.getSession(), reply); + marshalGroupTrees(apiMsg, reply); } } - private void marshal(SessionInventory session, APIQueryVolumeSnapshotTreeReply reply) { + private void marshalVolumeTrees(SessionInventory session, APIQueryVolumeSnapshotTreeReply reply) { if (reply.getInventories() == null) { // this is for count return; @@ -1357,6 +1363,26 @@ private void marshal(SessionInventory session, APIQueryVolumeSnapshotTreeReply r } } + private final VolumeSnapshotGroupTreeBuilder groupTreeBuilder = new VolumeSnapshotGroupTreeBuilder(); + + private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolumeSnapshotTreeReply reply) { + if (reply.getInventories() == null) { + return; + } + + String volumeUuid = groupTreeBuilder.extractVolumeUuidCondition(msg); + if (volumeUuid == null) { + return; + } + + String vmInstanceUuid = groupTreeBuilder.findRootVmUuid(volumeUuid); + if (vmInstanceUuid == null) { + return; + } + + reply.setGroupTrees(groupTreeBuilder.buildForVm(vmInstanceUuid)); + } + @SuppressWarnings("unchecked") private Set querySnapshotUuids(String treeUuid, SessionInventory session) { String zql = String.format("query volumesnapshot.uuid where treeUuid = '%s'", treeUuid); diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java index beb8b044d1a..86cf38ea9fa 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java @@ -1433,9 +1433,10 @@ private void ungroupAfterDeleteSingleSnapshot(VolumeSnapshotInventory volumeSnap .eq(VolumeSnapshotGroupRefVO_.snapshotDeleted, false).count(); if (count == 0) { cleanVmHostBackupFilesForGroup(list(volumeSnapshotInv.getGroupUuid())); + logger.debug(String.format("snapshot group[uuid:%s] all refs deleted, disbanding", + volumeSnapshotInv.getGroupUuid())); + vidm.deleteArchiveVmInstanceResourceMetadataGroup(volumeSnapshotInv.getGroupUuid()); dbf.removeByPrimaryKey(volumeSnapshotInv.getGroupUuid(), VolumeSnapshotGroupVO.class); - logger.debug(String.format("snapshot group[uuid:%s] all volume snapshot has been deleted, " + - "delete snapshot group", volumeSnapshotInv.getGroupUuid())); } } @@ -2142,27 +2143,43 @@ protected Boolean scripts() { return cleanup; } - // The logic for cleaning up snapshot groups when deleting a snapshot chain + // Mark deleted refs and disband a group only after all its refs are deleted. private void ungroupAfterDeleted(List snapshots) { - List uuids = snapshots.stream().map(VolumeSnapshotInventory::getUuid).collect(Collectors.toList()); - SQL.New(VolumeSnapshotGroupRefVO.class).in(VolumeSnapshotGroupRefVO_.volumeSnapshotUuid, uuids) - .set(VolumeSnapshotGroupRefVO_.snapshotDeleted, true).update(); - if (currentRoot.getVolumeType().equals(VolumeType.Root.toString())) { - List groupUuids = new ArrayList<>(); - for (VolumeSnapshotInventory snapshot : snapshots) { - String groupUuid = snapshot.getGroupUuid(); - if (groupUuid != null) { - logger.debug(String.format("root volume snapshot[uuid:%s, name:%s] has been deleted, " + - "ungroup snapshot group[uuid:%s]", snapshot.getUuid(), snapshot.getName(), groupUuid)); - groupUuids.add(groupUuid); - } + List uuids = snapshots.stream() + .map(VolumeSnapshotInventory::getUuid) + .collect(Collectors.toList()); + SQL.New(VolumeSnapshotGroupRefVO.class) + .in(VolumeSnapshotGroupRefVO_.volumeSnapshotUuid, uuids) + .set(VolumeSnapshotGroupRefVO_.snapshotDeleted, true) + .update(); + + Set touchedGroupUuids = snapshots.stream() + .map(VolumeSnapshotInventory::getGroupUuid) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (touchedGroupUuids.isEmpty()) { + return; + } + List groupsToDelete = new ArrayList<>(); + for (String groupUuid : touchedGroupUuids) { + long remaining = Q.New(VolumeSnapshotGroupRefVO.class) + .eq(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuid) + .eq(VolumeSnapshotGroupRefVO_.snapshotDeleted, false) + .count(); + if (remaining == 0) { + logger.debug(String.format("snapshot group[uuid:%s] all refs deleted, disbanding", groupUuid)); + groupsToDelete.add(groupUuid); } + } - groupUuids.forEach(groupUuid -> vidm.deleteArchiveVmInstanceResourceMetadataGroup(groupUuid)); - cleanVmHostBackupFilesForGroup(groupUuids); - dbf.removeByPrimaryKeys(groupUuids, VolumeSnapshotGroupVO.class); + if (groupsToDelete.isEmpty()) { + return; } + + groupsToDelete.forEach(vidm::deleteArchiveVmInstanceResourceMetadataGroup); + cleanVmHostBackupFilesForGroup(groupsToDelete); + dbf.removeByPrimaryKeys(groupsToDelete, VolumeSnapshotGroupVO.class); } private void cleanVmHostBackupFilesForGroup(List groupUuids) { diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java new file mode 100644 index 00000000000..9f880c5a7cd --- /dev/null +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java @@ -0,0 +1,270 @@ +package org.zstack.storage.snapshot.group; + +import org.zstack.core.db.Q; +import org.zstack.header.query.QueryCondition; +import org.zstack.header.query.QueryOp; +import org.zstack.header.storage.snapshot.APIQueryVolumeSnapshotTreeMsg; +import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO; +import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupRefVO_; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeInventory; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupTreeRefInventory; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO; +import org.zstack.header.storage.snapshot.group.VolumeSnapshotGroupVO_; +import org.zstack.header.volume.VolumeType; +import org.zstack.header.volume.VolumeVO; +import org.zstack.header.volume.VolumeVO_; +import org.zstack.utils.Utils; +import org.zstack.utils.logging.CLogger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class VolumeSnapshotGroupTreeBuilder { + private static final CLogger logger = Utils.getLogger(VolumeSnapshotGroupTreeBuilder.class); + + public List buildForVm(String vmInstanceUuid) { + List groupVOs = Q.New(VolumeSnapshotGroupVO.class) + .eq(VolumeSnapshotGroupVO_.vmInstanceUuid, vmInstanceUuid) + .list(); + if (groupVOs.isEmpty()) { + return Collections.emptyList(); + } + + List groupUuids = groupVOs.stream() + .map(VolumeSnapshotGroupVO::getUuid) + .collect(Collectors.toList()); + + List refs = Q.New(VolumeSnapshotGroupRefVO.class) + .in(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuids) + .list(); + + Map snapVOs = loadLiveSnapshotVOs(refs); + Map parentMap = buildParentMap(snapVOs); + Map snapToGroup = buildSnapToGroupMap(refs); + + Map> refsByGroup = refs.stream() + .collect(Collectors.groupingBy(VolumeSnapshotGroupRefVO::getVolumeSnapshotGroupUuid)); + + Map groupNodeMap = + buildGroupNodes(groupVOs, refsByGroup, snapVOs); + + linkParents(groupNodeMap, refsByGroup, parentMap, snapToGroup); + + return assembleForest(groupNodeMap); + } + + public String extractVolumeUuidCondition(APIQueryVolumeSnapshotTreeMsg msg) { + List conditions = msg.getConditions(); + if (conditions == null) { + return null; + } + String found = null; + for (QueryCondition c : conditions) { + if (!"volumeUuid".equals(c.getName())) { + continue; + } + if (!QueryOp.EQ.toString().equals(c.getOp())) { + continue; + } + if (found != null && !found.equals(c.getValue())) { + logger.warn(String.format("multiple conflicting volumeUuid eq conditions in APIQueryVolumeSnapshotTreeMsg: %s vs %s; taking the first", + found, c.getValue())); + return found; + } + found = c.getValue(); + } + return found; + } + + public String findRootVmUuid(String volumeUuid) { + return Q.New(VolumeVO.class) + .select(VolumeVO_.vmInstanceUuid) + .eq(VolumeVO_.uuid, volumeUuid) + .eq(VolumeVO_.type, VolumeType.Root) + .findValue(); + } + + private Map loadLiveSnapshotVOs(List refs) { + List liveSnapUuids = refs.stream() + .filter(r -> !r.isSnapshotDeleted()) + .map(VolumeSnapshotGroupRefVO::getVolumeSnapshotUuid) + .collect(Collectors.toList()); + if (liveSnapUuids.isEmpty()) { + return Collections.emptyMap(); + } + List svos = Q.New(VolumeSnapshotVO.class) + .in(VolumeSnapshotVO_.uuid, liveSnapUuids) + .list(); + return svos.stream().collect(Collectors.toMap(VolumeSnapshotVO::getUuid, v -> v)); + } + + private Map buildParentMap(Map snapVOs) { + Map m = new HashMap<>(); + for (VolumeSnapshotVO v : snapVOs.values()) { + m.put(v.getUuid(), v.getParentUuid()); + } + return m; + } + + private Map buildSnapToGroupMap(List refs) { + Map m = new HashMap<>(); + for (VolumeSnapshotGroupRefVO r : refs) { + m.put(r.getVolumeSnapshotUuid(), r.getVolumeSnapshotGroupUuid()); + } + return m; + } + + private Map buildGroupNodes( + List groupVOs, + Map> refsByGroup, + Map snapVOs) { + Map groupNodeMap = new HashMap<>(); + for (VolumeSnapshotGroupVO g : groupVOs) { + VolumeSnapshotGroupTreeInventory node = new VolumeSnapshotGroupTreeInventory(); + node.setUuid(g.getUuid()); + node.setName(g.getName()); + node.setDescription(g.getDescription()); + node.setVmInstanceUuid(g.getVmInstanceUuid()); + node.setCreateDate(g.getCreateDate()); + node.setLastOpDate(g.getLastOpDate()); + + List groupRefs = refsByGroup.getOrDefault(g.getUuid(), Collections.emptyList()); + long deletedCount = groupRefs.stream().filter(VolumeSnapshotGroupRefVO::isSnapshotDeleted).count(); + int total = groupRefs.size(); + node.setIncomplete(deletedCount > 0 && deletedCount < total); + + node.setRefs(buildRefInventories(groupRefs, snapVOs)); + groupNodeMap.put(g.getUuid(), node); + } + return groupNodeMap; + } + + private List buildRefInventories( + List groupRefs, + Map snapVOs) { + List refInvs = new ArrayList<>(); + for (VolumeSnapshotGroupRefVO r : groupRefs) { + VolumeSnapshotGroupTreeRefInventory refInv = new VolumeSnapshotGroupTreeRefInventory(); + refInv.setVolumeUuid(r.getVolumeUuid()); + refInv.setVolumeName(r.getVolumeName()); + refInv.setVolumeType(r.getVolumeType()); + refInv.setVolumeSnapshotUuid(r.getVolumeSnapshotUuid()); + refInv.setSnapshotDeleted(r.isSnapshotDeleted()); + if (r.isSnapshotDeleted()) { + refInv.setSnapshot(null); + } else { + VolumeSnapshotVO svo = snapVOs.get(r.getVolumeSnapshotUuid()); + refInv.setSnapshot(svo == null ? null : VolumeSnapshotInventory.valueOf(svo)); + } + refInvs.add(refInv); + } + return refInvs; + } + + private void linkParents(Map groupNodeMap, + Map> refsByGroup, + Map parentMap, + Map snapToGroup) { + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + String parentGroupUuid = resolveParentGroupUuid(node.getUuid(), + refsByGroup.getOrDefault(node.getUuid(), Collections.emptyList()), + parentMap, snapToGroup); + if (parentGroupUuid != null && groupNodeMap.containsKey(parentGroupUuid)) { + node.setParentGroupUuid(parentGroupUuid); + } + } + } + + private String resolveParentGroupUuid(String selfGroupUuid, + List selfRefs, + Map parentMap, + Map snapToGroup) { + Map votes = new HashMap<>(); + for (VolumeSnapshotGroupRefVO r : selfRefs) { + if (r.isSnapshotDeleted()) { + continue; + } + String cur = parentMap.get(r.getVolumeSnapshotUuid()); + Set visited = new HashSet<>(); + visited.add(r.getVolumeSnapshotUuid()); + while (cur != null && !visited.contains(cur)) { + visited.add(cur); + String g = snapToGroup.get(cur); + if (g != null && !g.equals(selfGroupUuid)) { + votes.merge(g, 1, Integer::sum); + break; + } + cur = parentMap.get(cur); + } + } + if (votes.isEmpty()) { + return null; + } + String winner = null; + int top = -1; + boolean tie = false; + for (Map.Entry e : votes.entrySet()) { + if (e.getValue() > top) { + winner = e.getKey(); + top = e.getValue(); + tie = false; + } else if (e.getValue() == top) { + tie = true; + } + } + if (tie) { + logger.warn(String.format("group[uuid:%s] has tied parentGroup votes: %s; picking %s", + selfGroupUuid, votes, winner)); + } + return winner; + } + + private List assembleForest(Map groupNodeMap) { + List forest = new ArrayList<>(); + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + if (node.getParentGroupUuid() == null) { + forest.add(node); + } else { + groupNodeMap.get(node.getParentGroupUuid()).getChildren().add(node); + } + } + + Comparator byCreateDateAsc = + Comparator.comparing(VolumeSnapshotGroupTreeInventory::getCreateDate, + Comparator.nullsFirst(Comparator.naturalOrder())); + forest.sort(byCreateDateAsc); + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + node.getChildren().sort(byCreateDateAsc); + } + + markCurrent(groupNodeMap); + return forest; + } + + private void markCurrent(Map groupNodeMap) { + VolumeSnapshotGroupTreeInventory newest = null; + for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { + if (newest == null) { + newest = node; + continue; + } + if (node.getCreateDate() != null && (newest.getCreateDate() == null + || node.getCreateDate().after(newest.getCreateDate()))) { + newest = node; + } + } + if (newest != null) { + newest.setCurrent(true); + } + } +} From bf0620bb0058f143e369b4c55cc2ce4567217ef8 Mon Sep 17 00:00:00 2001 From: "tao.gan" Date: Thu, 28 May 2026 17:13:48 +0800 Subject: [PATCH 2/2] [sdk]: drop stale SDK files for groupTrees Resolves: ZSV-9792 Change-Id: I84669169a926bf3fb6db8accaafb7d6d0751c0de --- .../sdk/QueryVolumeSnapshotTreeResult.java | 8 -- .../sdk/VolumeSnapshotGroupTreeInventory.java | 95 ------------------- .../VolumeSnapshotGroupTreeRefInventory.java | 55 ----------- .../snapshot/VolumeSnapshotManagerImpl.java | 2 +- .../group/VolumeSnapshotGroupTreeBuilder.java | 47 +++++---- 5 files changed, 31 insertions(+), 176 deletions(-) delete mode 100644 sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java delete mode 100644 sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java diff --git a/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java b/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java index 29fd8e6e3b9..27378c71079 100644 --- a/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java +++ b/sdk/src/main/java/org/zstack/sdk/QueryVolumeSnapshotTreeResult.java @@ -19,12 +19,4 @@ public java.lang.Long getTotal() { return this.total; } - public java.util.List groupTrees; - public void setGroupTrees(java.util.List groupTrees) { - this.groupTrees = groupTrees; - } - public java.util.List getGroupTrees() { - return this.groupTrees; - } - } diff --git a/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java b/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java deleted file mode 100644 index 9d0f07a40b8..00000000000 --- a/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeInventory.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.zstack.sdk; - - - -public class VolumeSnapshotGroupTreeInventory { - - public java.lang.String uuid; - public void setUuid(java.lang.String uuid) { - this.uuid = uuid; - } - public java.lang.String getUuid() { - return this.uuid; - } - - public java.lang.String name; - public void setName(java.lang.String name) { - this.name = name; - } - public java.lang.String getName() { - return this.name; - } - - public java.lang.String description; - public void setDescription(java.lang.String description) { - this.description = description; - } - public java.lang.String getDescription() { - return this.description; - } - - public java.lang.String vmInstanceUuid; - public void setVmInstanceUuid(java.lang.String vmInstanceUuid) { - this.vmInstanceUuid = vmInstanceUuid; - } - public java.lang.String getVmInstanceUuid() { - return this.vmInstanceUuid; - } - - public java.sql.Timestamp createDate; - public void setCreateDate(java.sql.Timestamp createDate) { - this.createDate = createDate; - } - public java.sql.Timestamp getCreateDate() { - return this.createDate; - } - - public java.sql.Timestamp lastOpDate; - public void setLastOpDate(java.sql.Timestamp lastOpDate) { - this.lastOpDate = lastOpDate; - } - public java.sql.Timestamp getLastOpDate() { - return this.lastOpDate; - } - - public boolean current; - public void setCurrent(boolean current) { - this.current = current; - } - public boolean getCurrent() { - return this.current; - } - - public boolean incomplete; - public void setIncomplete(boolean incomplete) { - this.incomplete = incomplete; - } - public boolean getIncomplete() { - return this.incomplete; - } - - public java.lang.String parentGroupUuid; - public void setParentGroupUuid(java.lang.String parentGroupUuid) { - this.parentGroupUuid = parentGroupUuid; - } - public java.lang.String getParentGroupUuid() { - return this.parentGroupUuid; - } - - public java.util.List children; - public void setChildren(java.util.List children) { - this.children = children; - } - public java.util.List getChildren() { - return this.children; - } - - public java.util.List refs; - public void setRefs(java.util.List refs) { - this.refs = refs; - } - public java.util.List getRefs() { - return this.refs; - } - -} diff --git a/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java b/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java deleted file mode 100644 index ebe60f70a1e..00000000000 --- a/sdk/src/main/java/org/zstack/sdk/VolumeSnapshotGroupTreeRefInventory.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.zstack.sdk; - - - -public class VolumeSnapshotGroupTreeRefInventory { - - public java.lang.String volumeUuid; - public void setVolumeUuid(java.lang.String volumeUuid) { - this.volumeUuid = volumeUuid; - } - public java.lang.String getVolumeUuid() { - return this.volumeUuid; - } - - public java.lang.String volumeName; - public void setVolumeName(java.lang.String volumeName) { - this.volumeName = volumeName; - } - public java.lang.String getVolumeName() { - return this.volumeName; - } - - public java.lang.String volumeType; - public void setVolumeType(java.lang.String volumeType) { - this.volumeType = volumeType; - } - public java.lang.String getVolumeType() { - return this.volumeType; - } - - public java.lang.String volumeSnapshotUuid; - public void setVolumeSnapshotUuid(java.lang.String volumeSnapshotUuid) { - this.volumeSnapshotUuid = volumeSnapshotUuid; - } - public java.lang.String getVolumeSnapshotUuid() { - return this.volumeSnapshotUuid; - } - - public boolean snapshotDeleted; - public void setSnapshotDeleted(boolean snapshotDeleted) { - this.snapshotDeleted = snapshotDeleted; - } - public boolean getSnapshotDeleted() { - return this.snapshotDeleted; - } - - public org.zstack.sdk.VolumeSnapshotInventory snapshot; - public void setSnapshot(org.zstack.sdk.VolumeSnapshotInventory snapshot) { - this.snapshot = snapshot; - } - public org.zstack.sdk.VolumeSnapshotInventory getSnapshot() { - return this.snapshot; - } - -} diff --git a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java index 890e14eca68..e00c46086cf 100755 --- a/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java @@ -1366,7 +1366,7 @@ private void marshalVolumeTrees(SessionInventory session, APIQueryVolumeSnapshot private final VolumeSnapshotGroupTreeBuilder groupTreeBuilder = new VolumeSnapshotGroupTreeBuilder(); private void marshalGroupTrees(APIQueryVolumeSnapshotTreeMsg msg, APIQueryVolumeSnapshotTreeReply reply) { - if (reply.getInventories() == null) { + if (reply.getInventories() == null || reply.getInventories().isEmpty()) { return; } diff --git a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java index 9f880c5a7cd..fccd0d78234 100644 --- a/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java +++ b/storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -58,7 +59,12 @@ public List buildForVm(String vmInstanceUuid) Map groupNodeMap = buildGroupNodes(groupVOs, refsByGroup, snapVOs); - linkParents(groupNodeMap, refsByGroup, parentMap, snapToGroup); + Map groupCreateDate = groupVOs.stream() + .collect(HashMap::new, + (m, g) -> m.put(g.getUuid(), g.getCreateDate()), + HashMap::putAll); + + linkParents(groupNodeMap, refsByGroup, parentMap, snapToGroup, groupCreateDate); return assembleForest(groupNodeMap); } @@ -174,11 +180,12 @@ private List buildRefInventories( private void linkParents(Map groupNodeMap, Map> refsByGroup, Map parentMap, - Map snapToGroup) { + Map snapToGroup, + Map groupCreateDate) { for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) { String parentGroupUuid = resolveParentGroupUuid(node.getUuid(), refsByGroup.getOrDefault(node.getUuid(), Collections.emptyList()), - parentMap, snapToGroup); + parentMap, snapToGroup, groupCreateDate); if (parentGroupUuid != null && groupNodeMap.containsKey(parentGroupUuid)) { node.setParentGroupUuid(parentGroupUuid); } @@ -188,7 +195,8 @@ private void linkParents(Map groupNode private String resolveParentGroupUuid(String selfGroupUuid, List selfRefs, Map parentMap, - Map snapToGroup) { + Map snapToGroup, + Map groupCreateDate) { Map votes = new HashMap<>(); for (VolumeSnapshotGroupRefVO r : selfRefs) { if (r.isSnapshotDeleted()) { @@ -210,20 +218,25 @@ private String resolveParentGroupUuid(String selfGroupUuid, if (votes.isEmpty()) { return null; } - String winner = null; - int top = -1; - boolean tie = false; - for (Map.Entry e : votes.entrySet()) { - if (e.getValue() > top) { - winner = e.getKey(); - top = e.getValue(); - tie = false; - } else if (e.getValue() == top) { - tie = true; + + List> ranked = new ArrayList<>(votes.entrySet()); + ranked.sort((a, b) -> { + int cmp = Integer.compare(b.getValue(), a.getValue()); + if (cmp != 0) { + return cmp; } - } - if (tie) { - logger.warn(String.format("group[uuid:%s] has tied parentGroup votes: %s; picking %s", + Date da = groupCreateDate.get(a.getKey()); + Date db = groupCreateDate.get(b.getKey()); + cmp = Comparator.nullsLast(Date::compareTo).compare(da, db); + if (cmp != 0) { + return cmp; + } + return a.getKey().compareTo(b.getKey()); + }); + + String winner = ranked.get(0).getKey(); + if (ranked.size() > 1 && ranked.get(1).getValue().equals(ranked.get(0).getValue())) { + logger.warn(String.format("group[uuid:%s] has tied parentGroup votes: %s; picked %s by (createDate asc, uuid asc)", selfGroupUuid, votes, winner)); } return winner;