From b8085172a7f8a15ab6b8df3184ae2e6758c84557 Mon Sep 17 00:00:00 2001 From: J M Date: Mon, 13 Jul 2026 06:00:34 +0800 Subject: [PATCH] [storage]: support typed storage resource stats Generalize stats for volume and snapshot resources and rely on AsyncSafeAspect for completion errors. Resolves: ZSTAC-86038 Change-Id: I8660e9884c76cac94aedc8736f99b488779b7897 --- .../header/storage/addon/StorageResource.java | 44 ++++++++++ .../primary/PrimaryStorageControllerSvc.java | 2 +- .../storage/snapshot/VolumeSnapshotStats.java | 19 +--- .../org/zstack/header/volume/VolumeStats.java | 39 +------- .../zstack/expon/ExponStorageController.java | 20 ++++- .../org/zstack/xinfini/XInfiniApiHelper.java | 7 ++ .../xinfini/XInfiniStorageController.java | 20 ++++- .../storage/zbs/ZbsStorageController.java | 8 +- .../addon/primary/ExternalPrimaryStorage.java | 88 ++++++++++++++----- .../xinfini/XinfiniPrimaryStorageCase.groovy | 46 ++++++++++ .../StorageResourceContractTest.groovy | 81 +++++++++++++++++ 11 files changed, 290 insertions(+), 84 deletions(-) create mode 100644 header/src/main/java/org/zstack/header/storage/addon/StorageResource.java create mode 100644 test/src/test/groovy/org/zstack/test/unittest/storage/StorageResourceContractTest.groovy diff --git a/header/src/main/java/org/zstack/header/storage/addon/StorageResource.java b/header/src/main/java/org/zstack/header/storage/addon/StorageResource.java new file mode 100644 index 00000000000..d2a3f26b1e0 --- /dev/null +++ b/header/src/main/java/org/zstack/header/storage/addon/StorageResource.java @@ -0,0 +1,44 @@ +package org.zstack.header.storage.addon; + +public abstract class StorageResource { + protected String installPath; + protected Long size; + protected Long actualSize; + protected String parentUri; + + public String getInstallPath() { + return installPath; + } + + public void setInstallPath(String installPath) { + this.installPath = installPath; + } + + public Long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public Long getResourceActualSize() { + return actualSize; + } + + public void setActualSize(Long actualSize) { + this.actualSize = actualSize; + } + + public void setActualSize(long actualSize) { + this.actualSize = actualSize; + } + + public String getParentUri() { + return parentUri; + } + + public void setParentUri(String parentUri) { + this.parentUri = parentUri; + } +} diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java b/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java index feb4c5c11c7..92fb46ebb08 100644 --- a/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java +++ b/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java @@ -62,7 +62,7 @@ public interface PrimaryStorageControllerSvc { void flattenVolume(String installPath, ReturnValueCompletioncomp); // support uri or path - void stats(String installPath, ReturnValueCompletion comp); + void stats(String installPath, ReturnValueCompletion comp); void batchStats(Collection installPath, ReturnValueCompletion> comp); diff --git a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotStats.java b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotStats.java index 8b4e3778ef3..a3e1496ba7f 100644 --- a/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotStats.java +++ b/header/src/main/java/org/zstack/header/storage/snapshot/VolumeSnapshotStats.java @@ -1,22 +1,9 @@ package org.zstack.header.storage.snapshot; -public class VolumeSnapshotStats { - private String installPath; - private long actualSize; - - public String getInstallPath() { - return installPath; - } - - public void setInstallPath(String installPath) { - this.installPath = installPath; - } +import org.zstack.header.storage.addon.StorageResource; +public class VolumeSnapshotStats extends StorageResource { public long getActualSize() { - return actualSize; - } - - public void setActualSize(long actualSize) { - this.actualSize = actualSize; + return actualSize == null ? 0L : actualSize; } } diff --git a/header/src/main/java/org/zstack/header/volume/VolumeStats.java b/header/src/main/java/org/zstack/header/volume/VolumeStats.java index 2931945e7ed..a123c135105 100644 --- a/header/src/main/java/org/zstack/header/volume/VolumeStats.java +++ b/header/src/main/java/org/zstack/header/volume/VolumeStats.java @@ -1,14 +1,9 @@ package org.zstack.header.volume; -public class VolumeStats { - protected String installPath; +import org.zstack.header.storage.addon.StorageResource; + +public class VolumeStats extends StorageResource { protected String format; - protected Long actualSize; - protected Long size; - /** - * The parent uri of the volume, vendor://pool/path@snapshot or snapshot://uuid - */ - protected String parentUri; // TODO(shenjin): remove it @Deprecated @@ -29,30 +24,10 @@ public VolumeStats(String installPath, Long actualSize, Long size) { public VolumeStats() { } - public String getInstallPath() { - return installPath; - } - - public void setInstallPath(String installPath) { - this.installPath = installPath; - } - public Long getActualSize() { return actualSize; } - public void setActualSize(Long actualSize) { - this.actualSize = actualSize; - } - - public Long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - public void setFormat(String format) { this.format = format; } @@ -61,14 +36,6 @@ public String getFormat() { return format; } - public void setParentUri(String parentUri) { - this.parentUri = parentUri; - } - - public String getParentUri() { - return parentUri; - } - @Deprecated public String getRunStatus() { return runStatus; diff --git a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java index ae0ca47de4c..d3d6fca016e 100644 --- a/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java +++ b/plugin/expon/src/main/java/org/zstack/expon/ExponStorageController.java @@ -1143,18 +1143,32 @@ public void fail(ErrorCode errorCode) { @Override public void flattenVolume(String installPath, ReturnValueCompletion comp) { // TODO flatten snapshot - stats(installPath, comp); + comp.success(getVolumeStats(installPath)); } @Override - public void stats(String installPath, ReturnValueCompletion comp) { + public void stats(String installPath, ReturnValueCompletion comp) { + if (installPath.contains("@")) { + VolumeSnapshotModule snap = apiHelper.getVolumeSnapshot(getSnapIdFromPath(installPath)); + VolumeSnapshotStats stats = new VolumeSnapshotStats(); + stats.setInstallPath(installPath); + stats.setSize(snap.getSnapSize()); + stats.setActualSize(snap.getDataSize()); + comp.success(stats); + return; + } + + comp.success(getVolumeStats(installPath)); + } + + private VolumeStats getVolumeStats(String installPath) { VolumeModule vol = apiHelper.getVolume(getVolIdFromPath(installPath)); VolumeStats stats = new VolumeStats(); stats.setInstallPath(installPath); stats.setSize(vol.getVolumeSize()); stats.setActualSize(vol.getDataSize()); stats.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); - comp.success(stats); + return stats; } @Override diff --git a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniApiHelper.java b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniApiHelper.java index b67bfa0da7e..813e6cee8c4 100644 --- a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniApiHelper.java +++ b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniApiHelper.java @@ -9,6 +9,7 @@ import org.zstack.core.retry.RetryCondition; import org.zstack.header.core.Completion; import org.zstack.header.errorcode.OperationFailureException; +import org.zstack.header.errorcode.SysErrors; import org.zstack.header.volume.VolumeConfigs; import org.zstack.header.volume.VolumeVO; import org.zstack.header.volume.VolumeVO_; @@ -36,6 +37,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.zstack.core.Platform.err; import static org.zstack.core.Platform.operr; import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.*; @@ -102,6 +104,11 @@ public void call(XInfiniRequest req, Completion comp } public void errorOut(XInfiniResponse rsp) { + if (rsp.resourceIsDeleted()) { + throw new OperationFailureException(err(ORG_ZSTACK_XINFINI_10002, SysErrors.RESOURCE_NOT_FOUND, + "xinfini request failed, message: %s.", rsp.getMessage())); + } + if (!rsp.isSuccess()) { throw new OperationFailureException(operr(ORG_ZSTACK_XINFINI_10002, "xinfini request failed, message: %s.", rsp.getMessage())); } diff --git a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageController.java b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageController.java index 187ee73ff22..96a38ee0433 100644 --- a/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageController.java +++ b/plugin/xinfini/src/main/java/org/zstack/xinfini/XInfiniStorageController.java @@ -851,7 +851,23 @@ public void flattenVolume(String installPath, ReturnValueCompletion } @Override - public void stats(String installPath, ReturnValueCompletion comp) { + public void stats(String installPath, ReturnValueCompletion comp) { + if (installPath.contains("@")) { + VolumeSnapshotModule snapshot = apiHelper.getVolumeSnapshot(getSnapIdFromPath(installPath)); + VolumeSnapshotStats stats = new VolumeSnapshotStats(); + long size = SizeUnit.MEGABYTE.toByte(snapshot.getSpec().getSizeMb()); + stats.setInstallPath(installPath); + stats.setSize(size); + stats.setActualSize(size); + stats.setParentUri(getParentUri(apiHelper.getVolume(snapshot.getSpec().getBsVolumeId()))); + comp.success(stats); + return; + } + + comp.success(getVolumeStats(installPath)); + } + + private VolumeStats getVolumeStats(String installPath) { VolumeModule vol = apiHelper.getVolume(getVolIdFromPath(installPath)); VolumeStats stats = new VolumeStats(); stats.setInstallPath(installPath); @@ -859,7 +875,7 @@ public void stats(String installPath, ReturnValueCompletion comp) { stats.setActualSize(vol.getStatus().getAllocatedSizeByte()); stats.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); stats.setParentUri(getParentUri(vol)); - comp.success(stats); + return stats; } private String getParentUri(VolumeModule vol) { diff --git a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java index a723f79c12e..933c328dc19 100644 --- a/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java +++ b/plugin/zbs/src/main/java/org/zstack/storage/zbs/ZbsStorageController.java @@ -1315,19 +1315,21 @@ public void fail(ErrorCode errorCode) { } @Override - public void stats(String installPath, ReturnValueCompletion comp) { + public void stats(String installPath, ReturnValueCompletion comp) { QueryVolumeCmd cmd = new QueryVolumeCmd(); cmd.setPath(installPath); httpCall(QUERY_VOLUME_PATH, cmd, QueryVolumeRsp.class, new ReturnValueCompletion(comp) { @Override public void success(QueryVolumeRsp returnValue) { - VolumeStats stats = new VolumeStats(); + StorageResource stats = installPath.contains("@") ? new VolumeSnapshotStats() : new VolumeStats(); stats.setInstallPath(installPath); stats.setSize(returnValue.getSize()); stats.setActualSize(returnValue.getActualSize()); - stats.setFormat(VolumeConstant.VOLUME_FORMAT_RAW); stats.setParentUri(ZbsHelper.normalizeToZbsPath(returnValue.getParentUri())); + if (stats instanceof VolumeStats) { + ((VolumeStats) stats).setFormat(VolumeConstant.VOLUME_FORMAT_RAW); + } comp.success(stats); } diff --git a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java index 6e222dd4959..bd487ab8a6c 100644 --- a/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java +++ b/storage/src/main/java/org/zstack/storage/addon/primary/ExternalPrimaryStorage.java @@ -28,6 +28,7 @@ import org.zstack.header.errorcode.ErrorCode; import org.zstack.header.errorcode.ErrorCodeList; import org.zstack.header.errorcode.OperationFailureException; +import org.zstack.header.errorcode.SysErrors; import org.zstack.header.host.HostInventory; import org.zstack.header.host.HostState; import org.zstack.header.host.HostStatus; @@ -42,6 +43,7 @@ import org.zstack.header.storage.addon.RemoteTarget; import org.zstack.header.storage.addon.StorageCapacity; import org.zstack.header.storage.addon.StorageHealthy; +import org.zstack.header.storage.addon.StorageResource; import org.zstack.header.storage.addon.primary.*; import org.zstack.header.storage.backup.*; import org.zstack.header.storage.primary.*; @@ -1397,19 +1399,59 @@ private void doDownloadImageCache(ImageInventory image, String allocatedUrl, Ret .eq(ImageCacheVO_.imageUuid, image.getUuid()) .list(); + ImageCacheVO candidate = null; if (!controller.reportCapabilities().isSupportMultiSpace() && !caches.isEmpty()) { - // TODO check exists in ps - completion.success(caches.get(0).toInventory()); - return; + candidate = caches.get(0); + } else { + for (ImageCacheVO cache : caches) { + ImageCacheInventory inv = cache.toInventory(); + // TODO: suppose that path always starts with allocatedUrl + if (allocatedUrl != null && ImageCacheUtil.getImageCachePath(inv).startsWith(allocatedUrl)) { + candidate = cache; + break; + } + } } - for (ImageCacheVO cache : caches) { - ImageCacheInventory inv = cache.toInventory(); - // TODO: suppose that path always starts with allocatedUrl - if (allocatedUrl != null && ImageCacheUtil.getImageCachePath(inv).startsWith(allocatedUrl)) { - completion.success(inv); - return; - } + if (candidate != null) { + ImageCacheVO cache = candidate; + String cachePath = ImageCacheUtil.getImageCachePath(cache.getInstallUrl()); + ReturnValueCompletion statsCompletion = new ReturnValueCompletion(completion) { + @Override + public void success(StorageResource ignored) { + completion.success(cache.toInventory()); + } + + @Override + public void fail(ErrorCode errorCode) { + if (!errorCode.isError(SysErrors.RESOURCE_NOT_FOUND)) { + completion.fail(errorCode); + return; + } + + if (cache.getInstallUrl().startsWith(ImageConstant.SNAPSHOT_REUSE_IMAGE_SCHEMA)) { + completion.fail(operr("image cache[imageUuid:%s, primaryStorageUuid:%s] refers to a missing reused volume snapshot", + cache.getImageUuid(), cache.getPrimaryStorageUuid())); + return; + } + + controller.deleteVolumeAndSnapshot(cachePath, new Completion(completion) { + @Override + public void success() { + dbf.removeByPrimaryKey(cache.getId(), ImageCacheVO.class); + doDownloadImageCache(image, allocatedUrl, completion); + } + + @Override + public void fail(ErrorCode errorCode) { + completion.fail(errorCode); + } + }); + } + }; + + controller.stats(cachePath, statsCompletion); + return; } downloadImageTo(image, spec, ImageCacheVO.class.getSimpleName(), new ReturnValueCompletion(completion) { @@ -1649,13 +1691,13 @@ protected void handle(GetInstallPathForDataVolumeDownloadMsg msg) { protected void handle(GetVolumeBackingChainFromPrimaryStorageMsg msg) { GetVolumeBackingChainFromPrimaryStorageReply r = new GetVolumeBackingChainFromPrimaryStorageReply(); new While<>(msg.getRootInstallPaths()).each((installPath, compl) -> { - getParentChain(installPath, new ArrayList<>(), new ReturnValueCompletion>(compl) { + getParentChain(installPath, new ArrayList<>(), new ReturnValueCompletion>(compl) { @Override - public void success(List chains) { + public void success(List chains) { long tsize = 0; List chainPaths = new ArrayList<>(); - for (VolumeStats stats : chains) { - tsize += stats.getActualSize(); + for (StorageResource stats : chains) { + tsize += stats.getResourceActualSize(); chainPaths.add(stats.getInstallPath()); } @@ -1681,10 +1723,10 @@ public void done(ErrorCodeList errorCodeList) { }); } - private void getParentChain(String installPath, List chains, ReturnValueCompletion> completion) { - controller.stats(installPath, new ReturnValueCompletion(completion) { + private void getParentChain(String installPath, List chains, ReturnValueCompletion> completion) { + controller.stats(installPath, new ReturnValueCompletion(completion) { @Override - public void success(VolumeStats stats) { + public void success(StorageResource stats) { chains.add(stats); if (stats.getParentUri() == null) { chains.remove(0); // do not contain self @@ -1886,10 +1928,10 @@ protected void handle(AskVolumeSnapshotCapabilityMsg msg) { @Override protected void handle(SyncVolumeSizeOnPrimaryStorageMsg msg) { SyncVolumeSizeOnPrimaryStorageReply reply = new SyncVolumeSizeOnPrimaryStorageReply(); - controller.stats(msg.getInstallPath(), new ReturnValueCompletion(msg) { + controller.stats(msg.getInstallPath(), new ReturnValueCompletion(msg) { @Override - public void success(VolumeStats stats) { - reply.setActualSize(stats.getActualSize()); + public void success(StorageResource stats) { + reply.setActualSize(stats.getResourceActualSize()); reply.setSize(stats.getSize()); bus.reply(msg, reply); } @@ -1905,10 +1947,10 @@ public void fail(ErrorCode errorCode) { @Override protected void handle(EstimateVolumeTemplateSizeOnPrimaryStorageMsg msg) { EstimateVolumeTemplateSizeOnPrimaryStorageReply reply = new EstimateVolumeTemplateSizeOnPrimaryStorageReply(); - controller.stats(msg.getInstallPath(), new ReturnValueCompletion(msg) { + controller.stats(msg.getInstallPath(), new ReturnValueCompletion(msg) { @Override - public void success(VolumeStats stats) { - reply.setActualSize(stats.getActualSize()); + public void success(StorageResource stats) { + reply.setActualSize(stats.getResourceActualSize()); reply.setSize(stats.getSize()); bus.reply(msg, reply); } diff --git a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy index 2b31ba85be7..5ff90e33442 100644 --- a/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy +++ b/test/src/test/groovy/org/zstack/test/integration/storage/primary/addon/xinfini/XinfiniPrimaryStorageCase.groovy @@ -13,6 +13,7 @@ import org.zstack.xinfini.XInfiniPathHelper import org.zstack.xinfini.sdk.vhost.BdcModule import org.zstack.xinfini.sdk.vhost.BdcBdevModule import org.zstack.header.core.Completion +import org.zstack.header.core.ReturnValueCompletion import org.zstack.header.errorcode.ErrorCode import org.zstack.header.errorcode.OperationFailureException import org.zstack.header.host.HostConstant @@ -20,6 +21,7 @@ import org.zstack.header.host.HostVO import org.zstack.header.host.HostVO_ import org.zstack.header.host.PingHostMsg import org.zstack.header.message.MessageReply +import org.zstack.header.storage.addon.StorageResource import org.zstack.header.storage.backup.DownloadImageFromRemoteTargetMsg import org.zstack.header.storage.backup.DownloadImageFromRemoteTargetReply import org.zstack.header.storage.backup.UploadImageToRemoteTargetReply @@ -30,6 +32,7 @@ import org.zstack.header.storage.primary.ImageCacheVO import org.zstack.header.storage.primary.ImageCacheVO_ import org.zstack.header.storage.primary.PrimaryStorageHostRefVO import org.zstack.header.storage.primary.PrimaryStorageHostRefVO_ +import org.zstack.header.storage.snapshot.VolumeSnapshotStats import org.zstack.header.vm.VmBootDevice import org.zstack.header.vm.VmInstanceState import org.zstack.header.vm.VmInstanceVO @@ -37,6 +40,7 @@ import org.zstack.header.vm.VmInstanceVO_ import org.zstack.header.vm.VmStateChangedOnHostMsg import org.zstack.header.vm.devices.DeviceAddress import org.zstack.header.vm.devices.VirtualDeviceInfo +import org.zstack.header.volume.VolumeStats import org.zstack.header.volume.VolumeVO import org.zstack.header.volume.VolumeVO_ import org.zstack.kvm.KVMAgentCommands @@ -613,6 +617,26 @@ class XinfiniPrimaryStorageCase extends SubCase { volumeUuid = vol.uuid } as VolumeSnapshotInventory + StorageResource volumeStats = queryStorageResourceStats(vol.installPath) + assert volumeStats instanceof VolumeStats : + "volume path must return VolumeStats: expected=${VolumeStats.name} actual=${volumeStats.class.name} installPath=${vol.installPath}" + assert volumeStats.installPath == vol.installPath : + "volume stats installPath mismatch: expected=${vol.installPath} actual=${volumeStats.installPath}" + assert volumeStats.size != null && volumeStats.size > 0 : + "volume stats size must be positive: expected>0 actual=${volumeStats.size} installPath=${vol.installPath}" + assert volumeStats.actualSize != null && volumeStats.actualSize >= 0 : + "volume stats actualSize must be non-negative: expected>=0 actual=${volumeStats.actualSize} installPath=${vol.installPath}" + + StorageResource snapshotStats = queryStorageResourceStats(snapshot.primaryStorageInstallPath) + assert snapshotStats instanceof VolumeSnapshotStats : + "snapshot path must return VolumeSnapshotStats: expected=${VolumeSnapshotStats.name} actual=${snapshotStats.class.name} installPath=${snapshot.primaryStorageInstallPath}" + assert snapshotStats.installPath == snapshot.primaryStorageInstallPath : + "snapshot stats installPath mismatch: expected=${snapshot.primaryStorageInstallPath} actual=${snapshotStats.installPath}" + assert snapshotStats.size != null && snapshotStats.size > 0 : + "snapshot stats size must be positive: expected>0 actual=${snapshotStats.size} installPath=${snapshot.primaryStorageInstallPath}" + assert snapshotStats.actualSize != null && snapshotStats.actualSize > 0 : + "snapshot stats actualSize must be positive: expected>0 actual=${snapshotStats.actualSize} installPath=${snapshot.primaryStorageInstallPath}" + stopVmInstance { uuid = vm.uuid } @@ -630,6 +654,28 @@ class XinfiniPrimaryStorageCase extends SubCase { } } + StorageResource queryStorageResourceStats(String installPath) { + StorageResource[] stats = new StorageResource[1] + ErrorCode[] errors = new ErrorCode[1] + controller.stats(installPath, new ReturnValueCompletion(null) { + @Override + void success(StorageResource returnValue) { + stats[0] = returnValue + } + + @Override + void fail(ErrorCode errorCode) { + errors[0] = errorCode + } + }) + + assert errors[0] == null : + "storage resource stats query must succeed: expectedError=null actualError=${errors[0]} installPath=${installPath}" + assert stats[0] != null : + "storage resource stats query must return a result: expected=non-null actual=${stats[0]} installPath=${installPath}" + return stats[0] + } + void testCreateTemplate() { env.message(DownloadImageFromRemoteTargetMsg.class) { DownloadImageFromRemoteTargetMsg msg, CloudBus bus -> DownloadImageFromRemoteTargetReply r = new DownloadImageFromRemoteTargetReply() diff --git a/test/src/test/groovy/org/zstack/test/unittest/storage/StorageResourceContractTest.groovy b/test/src/test/groovy/org/zstack/test/unittest/storage/StorageResourceContractTest.groovy new file mode 100644 index 00000000000..fcd6386f3ab --- /dev/null +++ b/test/src/test/groovy/org/zstack/test/unittest/storage/StorageResourceContractTest.groovy @@ -0,0 +1,81 @@ +package org.zstack.test.unittest.storage + +import org.junit.Test +import org.zstack.header.storage.addon.StorageResource +import org.zstack.header.storage.snapshot.VolumeSnapshotStats +import org.zstack.header.volume.VolumeStats + +class StorageResourceContractTest { + @Test + void storageResourceKeepsNullableFieldsAndPublicDescriptors() { + Class sizeType = StorageResource.getDeclaredField("size").type + Class actualSizeType = StorageResource.getDeclaredField("actualSize").type + + assert sizeType == Long.class && actualSizeType == Long.class : + "StorageResource size fields must preserve nullable Long values: " + + "expectedSize=${Long.class.name} actualSize=${sizeType.name}, " + + "expectedActualSize=${Long.class.name} actualActualSize=${actualSizeType.name}" + + Map actualGetterTypes = [ + "StorageResource.getSize" : StorageResource.getDeclaredMethod("getSize").returnType, + "StorageResource.getResourceActualSize" : StorageResource.getDeclaredMethod("getResourceActualSize").returnType, + "VolumeStats.getActualSize" : VolumeStats.getDeclaredMethod("getActualSize").returnType, + "VolumeSnapshotStats.getActualSize" : VolumeSnapshotStats.getDeclaredMethod("getActualSize").returnType, + ] + Map expectedGetterTypes = [ + "StorageResource.getSize" : Long.class, + "StorageResource.getResourceActualSize" : Long.class, + "VolumeStats.getActualSize" : Long.class, + "VolumeSnapshotStats.getActualSize" : Long.TYPE, + ] + + assert actualGetterTypes == expectedGetterTypes : + "Storage stats public getter descriptors changed: expected=${expectedGetterTypes} actual=${actualGetterTypes}" + + assert StorageResource.getDeclaredMethod("setSize", Long.TYPE).returnType == Void.TYPE : + "StorageResource.setSize(long) descriptor must return void" + assert StorageResource.getDeclaredMethod("setActualSize", Long.class).returnType == Void.TYPE : + "StorageResource.setActualSize(Long) descriptor must return void" + assert StorageResource.getDeclaredMethod("setActualSize", Long.TYPE).returnType == Void.TYPE : + "StorageResource.setActualSize(long) descriptor must return void" + assert VolumeStats.getDeclaredConstructor(String.class, Long.class).parameterTypes.toList() == + [String.class, Long.class] : "VolumeStats(String, Long) constructor descriptor changed" + assert VolumeStats.getDeclaredConstructor(String.class, Long.class, Long.class).parameterTypes.toList() == + [String.class, Long.class, Long.class] : "VolumeStats(String, Long, Long) constructor descriptor changed" + } + + @Test + void volumeStatsPreservesMissingSizesAsNull() { + VolumeStats defaultStats = new VolumeStats() + VolumeStats constructorStats = new VolumeStats("path", null, null) + + assert defaultStats.getSize() == null : + "unset volume size must remain nullable: expected=null actual=${defaultStats.getSize()}" + assert defaultStats.getActualSize() == null : + "unset volume actualSize must remain nullable: expected=null actual=${defaultStats.getActualSize()}" + assert constructorStats.getSize() == null : + "boxed constructor size must preserve null: expected=null actual=${constructorStats.getSize()}" + assert constructorStats.getActualSize() == null : + "boxed constructor actualSize must preserve null: expected=null actual=${constructorStats.getActualSize()}" + + constructorStats.setActualSize((Long) null) + assert constructorStats.getActualSize() == null : + "boxed actualSize setter must preserve null: expected=null actual=${constructorStats.getActualSize()}" + } + + @Test + void snapshotStatsKeepsPrimitiveGetterNullFallback() { + VolumeSnapshotStats snapshotStats = new VolumeSnapshotStats() + + assert snapshotStats.getResourceActualSize() == null : + "unset snapshot resource actualSize must remain nullable: expected=null actual=${snapshotStats.getResourceActualSize()}" + assert snapshotStats.getActualSize() == 0L : + "snapshot primitive getter must map missing actualSize to zero: expected=0 actual=${snapshotStats.getActualSize()}" + + snapshotStats.setActualSize((Long) null) + assert snapshotStats.getResourceActualSize() == null : + "boxed snapshot setter must preserve null: expected=null actual=${snapshotStats.getResourceActualSize()}" + assert snapshotStats.getActualSize() == 0L : + "snapshot primitive getter must keep its null fallback: expected=0 actual=${snapshotStats.getActualSize()}" + } +}