Skip to content

<fix>[vm]: add MetadataImpact#3609

Closed
ZStack-Robot wants to merge 1 commit into
feature-zsv-5.0.0-vm-registrationfrom
sync/tao.gan/ZSV-11559@@4
Closed

<fix>[vm]: add MetadataImpact#3609
ZStack-Robot wants to merge 1 commit into
feature-zsv-5.0.0-vm-registrationfrom
sync/tao.gan/ZSV-11559@@4

Conversation

@ZStack-Robot

Copy link
Copy Markdown
Collaborator

APIImpact

Resolves: ZSV-11559

Change-Id: I6b6a6378627264646d6a76726762736e77787373

sync from gitlab !9469

@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

新增完整的 VM 元数据子系统:数据库表、GlobalConfig、运行时注解与解析器、API 消息/事件/文档、主存储(Local/NFS)后端与代理命令、SDK 与测试支持,以及元数据读写/扫描/清理/路径替换与相应的 GC/Flow 实现。

Changes

Cohort / File(s) Summary
数据库 与 VO
conf/db/upgrade/V5.0.0__schema.sql, header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java, .../VmMetadataDirtyVO_.java, .../VmMetadataFingerprintVO.java
新增两张元数据表及对应 JPA VO 与静态元模型(脏标记与指纹表)。
常量/枚举/错误码
header/.../VmInstanceMetadataConstants.java, .../VmMetadataCategory.java, .../VmMetadataErrors.java
新增元数据常量、分类枚举、错误码与路径提取工具方法。
DTO/模型/服务接口
header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java, .../VolumeResourceMetadata.java, .../VmInstanceMetadataDTO.java, .../VmMetadataDirtyService.java
添加资源/卷/VM 元数据 DTO 与脏标记服务接口。
GlobalConfig
compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
新增 VM 元数据相关全局配置项(启用、版本、并发、轮询、批次、GC 间隔)。
API 消息/事件/文档(VM 元数据)
header/src/main/java/org/zstack/header/vm/metadata/*, ...Doc_zh_cn.groovy
新增多个 API 消息/事件类(Get/Register/Update/Cleanup/Scan 等)及中文文档描述。
主存储 消息/模型
header/src/main/java/org/zstack/header/storage/primary/*Get*,Scan*,Cleanup*,VmMetadataScanEntry,Rebase*等)
新增主存储层用于读写/扫描/清理/前缀重写的消息、回复与扫描条目模型。
运行时注解 与 解析器
header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java, VmUuidFromApiResolver.java
新增注解 @MetadataImpact 与 API 字段→VM UUID 解析接口。
在多处 API 上应用注解
多处 header/src/main/java/.../API*Msg.java
为大量 VM/卷/快照/标签/配置 等 API 添加 @MetadataImpact 注解(声明影响类型与解析器)。
Compute 层 Flow/GC/拦截
compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java, CleanupVmInstanceMetadataOnPrimaryStorageGC.java, VmInstanceApiInterceptor.java, VmSystemTags.java, VmInstanceState.java
新增删除/回收相关 Flow、基于时间的 GC、注册消息校验、系统标签与新状态 Registering
PrimaryStorage 基础路由与 API 端点
storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java, header/.../APIScanVmInstanceMetadataFromPrimaryStorageMsg*
扩展路由以处理元数据本地消息并新增扫描 API 的转发与扩展点解析逻辑。
Local 存储 实现与 模拟器
plugin/localstorage/.../LocalStorageBase.java, LocalStorageHypervisorBackend.java, LocalStorageKvmBackend.java, LocalStorageVmMetadataExtension.java, LocalStorageSimulator*.java, LocalStorageSimulatorConfig.java
Local 存储新增/扩展元数据消息处理、后端抽象、KVM agent DTO、模拟器端点、路径构建/校验/持久化实现与回调。
NFS 存储 实现 与 KVM 代理
plugin/nfsPrimaryStorage/.../NfsPrimaryStorage.java, NfsPrimaryStorageBackend.java, NfsPrimaryStorageKVMBackend.java, NfsPrimaryStorageKVMBackendCommands.java, NfsVmMetadataExtension.java
NFS 后端新增元数据转发、KVM 代理命令/响应类型、路径构建/校验/替换实现与处理逻辑。
主存储 新操作:前缀重写
header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java, ...Reply.java
新增 backing-file 前缀重写的消息/回复定义与后端签名。
SDK 支持
sdk/src/main/java/org/zstack/sdk/*(Cleanup/Get/Register/Scan/Update 等)
新增对应 SDK Action/Result/Model,并更新 SourceClassMap 与扫描条目映射。
测试 与 ApiHelper
testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy, LocalStorageSpec.groovy, NfsPrimaryStorageSpec.groovy
新增测试 helper 封装方法、模拟器端点注册并更新部分 helper 映射与路径记录。

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant API as API
    participant Flow as VmExpungeMetadataFlow
    participant PS as PrimaryStorageService
    participant Host as Host Agent
    participant DB as Database

    Client->>API: DELETE VM
    API->>Flow: 触发 VmExpungeMetadataFlow
    Flow->>PS: CleanupVmInstanceMetadataOnPrimaryStorageMsg
    PS->>Host: 转发到主机代理清理接口 (hostUuid)
    Host-->>PS: CleanupVmMetadataRsp
    PS-->>Flow: CleanupVmInstanceMetadataOnPrimaryStorageReply
    alt 成功
        Flow->>API: trigger.next()
    else 失败
        Flow->>DB: 提交 CleanupVmInstanceMetadataOnPrimaryStorageGC(重试/GC)
    end
Loading
sequenceDiagram
    participant Client as Client
    participant API as API
    participant PS as PrimaryStorageService
    participant HostA as Host A
    participant HostB as Host B
    participant DB as Database

    Client->>API: GET /primary-storage/vm-instances/metadata/scan?uuid=psUuid
    API->>PS: ScanVmInstanceMetadataFromPrimaryStorageMsg(metadataDir)
    PS->>HostA: 发送扫描请求
    PS->>HostB: 发送扫描请求
    HostA-->>PS: 返回 VmMetadataScanEntry 列表 (hostUuid set)
    HostB-->>PS: 返回 VmMetadataScanEntry 列表 (hostUuid set)
    PS->>DB:(可选)更新 fingerprint/dirty VO
    PS-->>API: 合并并过滤(去除 TEMPLATE_CACHE)后返回扫描结果
    API-->>Client: 返回扫描事件
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 我是小兔来报喜,元数据花园添新枝;
表与接口整队列,主存后端忙搭桥。
扫描写入与清理奏,GC 守夜梦也妙。

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/tao.gan/ZSV-11559@@4

@zstack-robot-2

Copy link
Copy Markdown
Collaborator

Comment from yaohua.wu:

Cross-Repo Review: MR !9469 (zstack) + !13338 (premium) + !6820 (utility) — ZSV-11559

Jira: ZSV-11559 — 【注册虚拟机】(New Feature: VM Registration)
Target Branch: feature-zsv-5.0.0-vm-registration

本次 review 覆盖 3 个仓库的关联 MR,按发现严重度分层。
注意:GitLab API 对 zstack 仓库返回了 49 个文件的空 diff(包括 LocalStorageKvmBackend、NfsPrimaryStorageKVMBackend、PrimaryStorageBase 等关键存储后端文件),对 premium 仓库返回了 6 个文件的空 diff(包括 MevocoVmManagerImpl、VmMetadataDirtyMarker、VmMetadataUpdateInterceptor 等核心实现文件)。以下 review 基于可见的 diff 内容;空 diff 文件中的存储后端实现(handle 方法中的 Completion 传参、chain.next() 调用、异步回调模式)无法在此验证,建议人工重点检查

🔴 Critical

  1. [zstack] VmExpungeMetadataFlow.java — GC fallback 中 hostUuid 可能为 null,导致 GC 创建后发送消息失败

    第 99-106 行构造 CleanupVmInstanceMetadataOnPrimaryStorageGChostUuid 来自 spec.getVmInventory().getHostUuid(),但 VM 处于 Expunged 状态时 hostUuid 通常为 null。GC 对象的 triggerNow() 构造 CleanupVmInstanceMetadataOnPrimaryStorageMsg 设置了 hostUuid,如果存储后端(如 LocalStorage)依赖 hostUuid 选择 agent 发送目标,null 值会导致消息无法路由。

    建议:在 GC 创建前加 hostUuid 兜底逻辑——从 PrimaryStorageClusterRefVO + HostVO 查找可用 host,或者在 CleanupVmInstanceMetadataOnPrimaryStorageMsg 的 handler 中自行选择 host(对 NFS/SharedBlock 类共享存储尤其重要)。

  2. [premium] SharedBlockGroupPrimaryStorageBase.javahandle(GetVmInstanceMetadataFromPrimaryStorageMsg) / handle(ScanVmInstanceMetadataFromPrimaryStorageMsg) / handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg) 未使用 ChainTask 保护

    handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg) 正确使用了 thdf.chainSubmit(new ChainTask(...)) 保护并发,但 Get/Scan/Cleanup 三个 handler 直接在消息线程中执行,没有经过 ChainTask 串行化。虽然读操作并发风险较小,但 Cleanup 是写操作(删除 LV 上的 metadata),如果和 Update 并发执行可能导致数据不一致(Update 刚写入的 metadata 被 Cleanup 删除)。

    建议:至少 handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg) 应通过 ChainTask 与 Update 共享同一 syncSignature 串行化。

  3. [utility] shared_block_plugin.py:prefix_rebase_backing_files — 缺少 newline at end of file

    文件末尾 return jsonobject.dumps(rsp) 后没有换行符(diff 显示 \ No newline at end of file)。虽然不影响功能,但更重要的是:该方法中 rebase_pairs 里存储了 (current_path, expected_old_backing, new_backing) 三元组,在执行 rebase 时用 OperateLv(current_path, shared=False) 获取排他锁,但 如果 current_path 和同一个 chain 中另一个 pair 的 new_backing 是同一个 LV,可能导致死锁(线程 A 持有 LV1 排他锁等待 LV2 共享锁,线程 B 持有 LV2 排他锁等待 LV1 共享锁)。虽然当前是单线程执行所以不会死锁,但如果未来有并发调用风险,应在方法入口对所有涉及的 LV 按固定顺序加锁。

    降级说明:当前单线程调用场景下不会触发,标记为 Critical 是因为 prefix_rebase_backing_files 是新增 API,未来并发调用概率较高。如果能确认始终单线程调用,可降级为 Warning。

🟡 Warning

  1. [zstack] VmGlobalConfig.javaVM_METADATA_LAST_REFRESH_VERSION 缺少 validation 约束

    其他 GlobalConfig 都有明确的 numberGreaterThan/numberLessThanvalidValues 约束,但 VM_METADATA_LAST_REFRESH_VERSION@GlobalConfigValidation() 是空的。如果这是一个版本号(long 类型),应加 numberGreaterThan = 0 防止非法值。

  2. [premium] VmMetadataMaintenanceManager.javaCancelablePeriodicTask.run() 返回值语义反转

    3 个 periodic task 的 run() 方法 return !xxxStopped.get()。根据 CancelablePeriodicTask 的契约,run() 返回 true 表示"继续执行",false 表示"取消"。代码中 if (contentDriftStopped.get()) { return false; } 后面又 return !contentDriftStopped.get(),逻辑上等价于 return true(因为前面已经 early return false)。逻辑正确但冗余,建议简化为直接 return true 或在 detect 方法内部做 stopped 检查后统一 return !stopped

  3. [premium] VmMetadataMaintenanceManager.java — 硬编码时间间隔常量

    CONTENT_CHECK_INTERVAL_SEC = 21600(6小时)、ORPHAN_CHECK_INTERVAL_SEC = 3600(1小时)、STALE_RECOVERY_INTERVAL_SEC = 1800(30分钟)均为硬编码。这是后台维护任务的关键参数,建议通过 GlobalConfig 管理(zstack MR 中已有 VM_METADATA_FLUSH_POLL_INTERVAL 等配置项,维护任务的间隔也应可配置)。

  4. [premium] VolumeUuidsToVmUuidResolver.java — 类名暗示批量处理但 resolveVmUuids(String) 实际处理单个 UUID

    类名 VolumeUuids(复数)暗示处理多个 volume UUID,但 resolveVmUuids(String fieldValue) 只处理单个 UUID。batchResolveVmUuids 才是真正的批量方法。这不影响正确性,但名称容易误导。

  5. [premium] MetadataRegistrationUtils.javarestoreSystemTagsrestoreResourceConfigs 没有事务保护

    批量 dbf.persist(tag)dbf.persist(config) 在循环中逐条执行,没有 @Transactional 注解或显式事务。如果中途异常,部分 tag/config 已持久化,部分未持久化,导致注册的 VM 状态不一致。

    建议:在调用方(MetadataRegistrationPersistHelper,diff 为空无法确认)确保这些方法在同一事务中执行。

  6. [utility] file_metadata_handler.py_LOCK_MAP_HIGH_WATER = 2000 的锁 evict 策略可能误删正在排队的锁

    _evict_unlocked_entries() 通过 lock.locked() 判断锁是否空闲,但在高并发场景下,一个线程刚释放锁、另一个线程还没来得及 acquire 的间隙,锁会被误判为 idle 而被 evict。被 evict 后,新线程会创建新的锁对象,导致两个线程持有不同的锁对象,并发保护失效。

    建议:使用引用计数替代 lock.locked() 判断,或采用 LRU 策略基于最后使用时间 evict。

  7. [utility] linux.py:qcow2_prefix_rebase_backing_filesrebased_count 统计可能不准确

    第 1289 行 rebased_count += 1qcow2_rebase_no_check 之后,但如果 qcow2_rebase_no_check 抛异常(如权限不足),异常会向上传播导致整个方法失败,而已经 rebase 成功的文件不会 rollback。建议对每个 rebase 加 try-except,记录失败的文件并继续处理其余文件。

  8. [zstack] V5.0.0__schema.sqlVmMetadataDirtyVOVmMetadataFingerprintVOcreateDate/lastOpDate 缺少 DEFAULT 值

    lastOpDateon update CURRENT_TIMESTAMP 但没有 DEFAULT CURRENT_TIMESTAMPcreateDate 也没有 DEFAULT。如果 Java 代码忘记设置这些字段,INSERT 会得到 null 值。ZStack 其他 schema 通常都加 DEFAULT CURRENT_TIMESTAMP

🟢 Suggestion

  1. [zstack] API Message MetadataImpact 注解 — 大量重复模式可考虑精简

    约 60+ 个 API Message 类添加了 @MetadataImpact 注解,模式高度统一(CONFIG + resolver + field)。当前一对一注解方式清晰但维护成本高——新增 API 容易遗忘添加注解。建议补充编译期或测试期检查(类似 MetadataImpactCheckerCase.java 的思路),确保所有影响 VM 状态的 API 都标注了 @MetadataImpact

  2. [premium] Test cases — 良好的分层抽象但缺少负面测试

    AbstractVmMetadataCaseAbstractVmMetadataCrudCase / AbstractVmMetadataRegisterCase → 6 个存储类型具体 Case 的分层设计很好。但当前测试主要覆盖 happy path,建议补充:

    • Agent 返回错误时的处理(模拟 agent rsp.success = false)
    • 并发 update + cleanup 的竞态场景
    • metadata payload 超过 REJECT_THRESHOLD 时的降级行为
  3. [utility] lv_protocol.py — 设计精良的双 Slot A/B 写入协议,建议补充文档

    524 行的 LV 元数据协议实现(header + dual slot + checksum + brute force recovery)设计考虑周全。建议在文件头或 README 中补充协议格式图,方便后续维护。

  4. [premium] VmMetadataBuilderUtils.javaDETERMINISTIC_GSON 序列化不保证字段顺序

    Gson 默认按字段声明顺序序列化,但不同 JVM 版本可能有差异。如果 metadata fingerprint 比对依赖 JSON 字符串相等性(detectContentDrift 中),可能导致误报 drift。建议使用 GsonBuilder().enableComplexMapKeySerialization() + 字段排序,或改用结构化比较而非字符串比较。

  5. [zstack] VmExpungeMetadataFlow.java — GC interval 硬编码 TimeUnit.HOURS, 1

    第 110 行 GC 的 timeInterval 硬编码为 1 小时,但 VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL 已定义了可配置的 GC 间隔。应使用 GlobalConfig 的值。

Verdict: APPROVED with warnings

整体架构设计合理:MetadataImpact 注解驱动脏标记、dirty polling + flush 的异步写入机制、file-based / LV-based 两种存储后端抽象、以及 GC 兜底清理都是好的设计。主要关注点集中在并发安全(SharedBlock Cleanup 缺少 ChainTask 保护)和 GC hostUuid 路由问题。建议修复 Critical 1-2 后合并。


🤖 Robot Reviewer

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (17)
header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java (1)

18-22: code 改为 final,并使用 %d 替换 %s

code 字段仅在枚举构造器中赋值一次,应改为 final 以确保不可变性;id 是整型,用 %d 格式符更准确。

♻️ 建议修改
-    private String code;
+    private final String code;

     private VmMetadataErrors(int id) {
-        code = String.format("VM_METADATA.%s", id);
+        code = String.format("VM_METADATA.%d", id);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java`
around lines 18 - 22, Make the private field code in the VmMetadataErrors enum
final and update the constructor to format the integer id with %d instead of %s;
specifically change the declaration of code to "private final String code" and
update the VmMetadataErrors(int id) constructor to use
String.format("VM_METADATA.%d", id) so code is immutable and formatted
correctly.
header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java (1)

3-8: 接口方法缺少 Javadoc 注释

根据编码规范,接口方法必须配有有效的 Javadoc 注释。建议为每个方法添加说明文档,特别是 validateMetadataPath 方法的返回值语义不够清晰(返回验证后的路径?错误信息?成功时返回 null?)。

📝 建议添加 Javadoc 注释
 public interface VmMetadataPathBuildExtensionPoint {
+    /**
+     * 获取此扩展点支持的主存储类型
+     * `@return` 主存储类型标识符
+     */
     String getPrimaryStorageType();
+
+    /**
+     * 构建 VM 元数据文件路径
+     * `@param` primaryStorageUuid 主存储 UUID
+     * `@param` vmInstanceUuid VM 实例 UUID
+     * `@return` 元数据文件完整路径
+     */
     String buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid);
+
+    /**
+     * 构建元数据目录路径
+     * `@param` primaryStorageUuid 主存储 UUID
+     * `@return` 元数据目录路径
+     */
     String buildMetadataDir(String primaryStorageUuid);
+
+    /**
+     * 验证元数据路径的有效性
+     * `@param` primaryStorageUuid 主存储 UUID
+     * `@param` path 待验证的路径
+     * `@return` 验证通过返回规范化后的路径,验证失败返回 null 或抛出异常(请明确语义)
+     */
     String validateMetadataPath(String primaryStorageUuid, String path);
 }

As per coding guidelines: "接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java`
around lines 3 - 8, Add Javadoc comments for every method in the
VmMetadataPathBuildExtensionPoint interface: document getPrimaryStorageType(),
buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid),
buildMetadataDir(String primaryStorageUuid), and especially
validateMetadataPath(String primaryStorageUuid, String path) — explicitly state
the return semantics (e.g. returns normalized/validated path on success, or
null/throws/returns error message on failure) and describe parameters and
expected behavior; keep methods without explicit modifiers (interface methods
remain implicitly public) and place the Javadoc directly above each method
declaration.
header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java (1)

27-41: 批量解析结果建议去重,避免重复处理同一 VM

当前实现会原样累加,若多个输入映射到同一 VM UUID,会产生重复结果。建议用 LinkedHashSet 去重后再返回。

♻️ 建议修改
-    default List<String> batchResolveVmUuids(List<String> fieldValues) {
-        List<String> result = new ArrayList<>();
+    default List<String> batchResolveVmUuids(List<String> fieldValues) {
+        java.util.Set<String> result = new java.util.LinkedHashSet<>();
         if (fieldValues == null || fieldValues.isEmpty()) {
-            return result;
+            return new ArrayList<>();
         }
         for (String v : fieldValues) {
             if (v == null) {
                 continue;
             }
             List<String> resolved = resolveVmUuids(v);
             if (resolved != null) {
                 result.addAll(resolved);
             }
         }
-        return result;
+        return new ArrayList<>(result);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java`
around lines 27 - 41, The batchResolveVmUuids method currently accumulates
resolved UUIDs into a List allowing duplicates; change the aggregation to
preserve insertion order and remove duplicates by collecting results into a
LinkedHashSet (or equivalent) while iterating over fieldValues and
resolveVmUuids(v), then return a List built from that set; reference
batchResolveVmUuids and resolveVmUuids to locate the code and ensure null checks
and skipping null fieldValues remain unchanged.
sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java (1)

22-27: failedVmUuids 补充泛型,避免 SDK 调用端强制转型

使用 raw List 会丢失编译期类型检查,建议改为 List<String>(同类问题也出现在 ScanVmInstanceMetadataFromPrimaryStorageResult.vmInstanceMetadata,可在代码生成模板统一修复)。

♻️ 建议修改
-    public java.util.List failedVmUuids;
-    public void setFailedVmUuids(java.util.List failedVmUuids) {
+    public java.util.List<String> failedVmUuids;
+    public void setFailedVmUuids(java.util.List<String> failedVmUuids) {
         this.failedVmUuids = failedVmUuids;
     }
-    public java.util.List getFailedVmUuids() {
+    public java.util.List<String> getFailedVmUuids() {
         return this.failedVmUuids;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java` around
lines 22 - 27, The field and its accessor signatures in
CleanupVmInstanceMetadataResult should use a generic type to avoid raw List
usage: change the field failedVmUuids and its setter/getter signatures from
java.util.List to java.util.List<String> (update the field declaration,
setFailedVmUuids(List<String> failedVmUuids), and getFailedVmUuids():
List<String>) and regenerate/compile; also apply the same change for
ScanVmInstanceMetadataFromPrimaryStorageResult.vmInstanceMetadata to use a typed
List (e.g., List<String> or the appropriate element type) in the codegen
template so callers don't need unchecked casts.
conf/db/upgrade/V5.0.0__schema.sql (1)

16-24: VmMetadataFingerprintVO 表缺少 lastOpDatecreateDate 审计列。

VmMetadataDirtyVO 表包含 lastOpDatecreateDate 时间戳列,但 VmMetadataFingerprintVO 表没有这些审计列。建议保持一致性,添加相同的时间戳列以便于跟踪记录变更。

♻️ 建议添加审计列
 CREATE TABLE IF NOT EXISTS `zstack`.`VmMetadataFingerprintVO` (
     `vmInstanceUuid` VARCHAR(32) NOT NULL,
     `metadataSnapshot` LONGTEXT,
     `lastFlushTime` TIMESTAMP NULL DEFAULT NULL,
     `lastFlushFailed` TINYINT(1) NOT NULL DEFAULT 0,
     `staleRecoveryCount` INT NOT NULL DEFAULT 0,
+    `lastOpDate` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    `createDate` timestamp DEFAULT CURRENT_TIMESTAMP,
     PRIMARY KEY (`vmInstanceUuid`),
     CONSTRAINT `fkVmMetadataFingerprintVOVmInstanceEO` FOREIGN KEY (`vmInstanceUuid`)  REFERENCES `VmInstanceEO` (`uuid`) ON DELETE CASCADE
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@conf/db/upgrade/V5.0.0__schema.sql` around lines 16 - 24, The
VmMetadataFingerprintVO table is missing the audit timestamp columns present on
VmMetadataDirtyVO; update the CREATE TABLE for VmMetadataFingerprintVO to add
the same audit columns (createDate and lastOpDate) with appropriate TIMESTAMP
types and defaults (e.g., createDate TIMESTAMP NOT NULL DEFAULT
CURRENT_TIMESTAMP and lastOpDate TIMESTAMP NULL DEFAULT NULL or with ON UPDATE
CURRENT_TIMESTAMP if used in your schema) so the table matches
VmMetadataDirtyVO’s auditing schema while keeping the existing PRIMARY KEY and
FK constraint fkVmMetadataFingerprintVOVmInstanceEO intact.
compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java (1)

1336-1346: 建议先检查主存储是否存在。

当前代码在查询 PrimaryStorageVO 时,如果 psUuid 对应的主存储不存在,psType 会是 null。虽然后续逻辑会因为找不到扩展点而报错,但错误信息 "primary storage[uuid:xxx, type:null] does not support vm metadata" 会让用户困惑。

建议在类型查询前先验证主存储是否存在,或者在 psTypenull 时提供更清晰的错误信息:

♻️ 建议优化错误处理
         // Delegate path validation to the storage-type-specific extension
         String psUuid = msg.getPrimaryStorageUuid();
         String psType = Q.New(PrimaryStorageVO.class)
                 .select(PrimaryStorageVO_.type)
                 .eq(PrimaryStorageVO_.uuid, psUuid)
                 .findValue();
+        if (psType == null) {
+            throw new ApiMessageInterceptionException(argerr(
+                    "primary storage[uuid:%s] not found", psUuid));
+        }
         VmMetadataPathBuildExtensionPoint ext = (psType != null)
                 ? pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class) : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`
around lines 1336 - 1346, The code in VmInstanceApiInterceptor should explicitly
check that the PrimaryStorageVO exists before assuming a type; change the lookup
to fetch the whole PrimaryStorageVO (e.g.,
Q.New(PrimaryStorageVO.class).eq(PrimaryStorageVO_.uuid, psUuid).find()) and if
the returned VO is null throw an ApiMessageInterceptionException with a clear
message like "primary storage[uuid:%s] not found"; otherwise read vo.getType()
into psType and then proceed to get the VmMetadataPathBuildExtensionPoint from
pluginRgty and, if missing, throw the existing "does not support vm metadata"
error including the actual non-null type. Ensure you reference
msg.getPrimaryStorageUuid(), PrimaryStorageVO/PrimaryStorageVO_.type,
pluginRgty, VmMetadataPathBuildExtensionPoint, ApiMessageInterceptionException
and argerr when applying the change.
header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java (1)

30-33: 建议在 __example__() 中统一走 setter 赋值。

当前直接访问成员字段可工作,但和类内其余赋值风格不一致;建议改为 setVmUuids(...)

♻️ 建议修改
 public static APIUpdateVmInstanceMetadataMsg __example__() {
     APIUpdateVmInstanceMetadataMsg msg = new APIUpdateVmInstanceMetadataMsg();
-    msg.vmUuids = Collections.singletonList(uuid());
+    msg.setVmUuids(Collections.singletonList(uuid()));
     return msg;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java`
around lines 30 - 33, The __example__() method sets vmUuids by assigning the
field directly (msg.vmUuids = ...) which is inconsistent with the rest of the
class; update __example__() to use the setter instead: call
msg.setVmUuids(Collections.singletonList(uuid())) so the example follows the
class's accessor pattern and any validation/logic in setVmUuids is applied.
storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java (1)

1863-1864: 可直接使用 self.getType(),避免一次额外 DB 查询。

当前实例已绑定具体主存储,类型通常可直接从 self 读取,逻辑更直接也更轻量。

♻️ 建议修改
-String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type).eq(PrimaryStorageVO_.uuid, msg.getPrimaryStorageUuid()).findValue();
+String psType = self.getType();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java`
around lines 1863 - 1864, Replace the extra DB query that obtains psType with
the instance's stored type: use self.getType() (from PrimaryStorageBase) instead
of calling Q.New(PrimaryStorageVO.class)...findValue(); then pass that type into
pluginRgty.getExtensionFromMap(..., VmMetadataPathBuildExtensionPoint.class). In
other words, remove the Q.New(...) lookup and call self.getType() (or
this.getType()) when resolving the extension in PrimaryStorageBase so
pluginRgty.getExtensionFromMap uses the in-memory type.
compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java (1)

95-96: 冗余的 final 变量赋值

psUuid 在第 48-54 行之后已经是有效的 final 变量(effectively final),不需要再赋值给 finalPsUuid。可以直接在 lambda 中使用 psUuid

♻️ 建议简化
-        final String finalPsUuid = psUuid;
         final String finalHostUuid = hostUuid;

         bus.makeTargetServiceIdByResourceUuid(cmsg, PrimaryStorageConstant.SERVICE_ID, psUuid);
         bus.send(cmsg, new CloudBusCallBack(trigger) {
             `@Override`
             public void run(MessageReply reply) {
                 if (reply.isSuccess()) {
                     logger.info(String.format("[MetadataExpunge] successfully deleted metadata for vm[uuid:%s] on ps[uuid:%s]",
-                            vmUuid, finalPsUuid));
+                            vmUuid, psUuid));
                 } else {
                     logger.warn(String.format("[MetadataExpunge] failed to delete metadata for vm[uuid:%s] on ps[uuid:%s]: %s, " +
-                            "submitting GC job for retry", vmUuid, finalPsUuid, reply.getError()));
-                    submitGC(finalPsUuid, vmUuid, rootVolumeUuid, metadataPath, finalHostUuid);
+                            "submitting GC job for retry", vmUuid, psUuid, reply.getError()));
+                    submitGC(psUuid, vmUuid, rootVolumeUuid, metadataPath, finalHostUuid);
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 95 - 96, 在 VmExpungeMetadataFlow.java 中删除多余的 final 变量赋值(即移除 `final
String finalPsUuid = psUuid;` 和 `final String finalHostUuid = hostUuid;`),直接在后续的
lambda/匿名内部类中使用已是 effectively final 的 `psUuid` 和 `hostUuid`。也请检查所有引用
`finalPsUuid` 和 `finalHostUuid` 的位置并改为直接使用 `psUuid` 和
`hostUuid`,确保编译通过并且语义不变(相关符号:psUuid, hostUuid, finalPsUuid, finalHostUuid, 相关
lambda/匿名类)。
header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java (1)

16-17: 建议将字段命名为 vmInstanceUuid 以提高可读性

字段命名为 uuid 不够明确,考虑到 @APIParam 指定了 resourceType = VmInstanceVO.class,建议重命名为 vmInstanceUuid 以更清晰地表达其含义,并与 VmInstanceMessage 接口方法 getVmInstanceUuid() 保持一致。

♻️ 建议修改
     `@APIParam`(resourceType = VmInstanceVO.class)
-    private String uuid;
+    private String vmInstanceUuid;

-    public String getUuid() {
-        return uuid;
+    public String getVmInstanceUuid() {
+        return vmInstanceUuid;
     }

-    public void setUuid(String uuid) {
-        this.uuid = uuid;
+    public void setVmInstanceUuid(String vmInstanceUuid) {
+        this.vmInstanceUuid = vmInstanceUuid;
     }

     `@Override`
-    public String getVmInstanceUuid() {
-        return uuid;
+    public String getVmInstanceUuid() {
+        return vmInstanceUuid;
     }

As per coding guidelines: "命名应尽量用完整的单词组合表达意图"。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java`
around lines 16 - 17, Rename the ambiguous field uuid in
APIGetVmInstanceMetadataFromPrimaryStorageMsg to vmInstanceUuid: change the
private field name, its getter/setter, and any usages to vmInstanceUuid, keep
the `@APIParam`(resourceType = VmInstanceVO.class) annotation on the field, and
ensure the class implements/overrides VmInstanceMessage.getVmInstanceUuid() (and
update any callers or marshalling logic that referenced the old uuid symbol) so
names remain consistent across the codebase.
header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java (1)

10-12: 考虑使用原始类型 int 替代 Integer 包装类型

根据 ZStack 代码规范,数值字段优先使用原始类型(如 int)而非包装类型(Integer),以避免 NPE 风险和不必要的装箱/拆箱操作。如果字段可能未设置,应在设置端确保默认值。

♻️ 建议修改
-    private Integer totalCleaned;
-    private Integer totalFailed;
+    private int totalCleaned;
+    private int totalFailed;

Based on learnings: "在 ZStack 代码库中,当响应类包含数值字段时,优先使用原始类型而不是包装类型,以避免 NPE 风险和不必要的装箱/拆箱操作。"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java`
around lines 10 - 12, Change the numeric wrapper fields in
APICleanupVmInstanceMetadataEvent from Integer to primitive int to follow ZStack
conventions: replace declarations totalCleaned and totalFailed with primitive
int, adjust any corresponding getters/setters and usages to work with int
(initialize defaults if needed), and leave failedVmUuids as List<String>; ensure
no code expects null for these fields.
sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java (1)

79-85: 布尔类型 getter 方法命名建议

Java Bean 规范中,布尔类型的 getter 方法通常使用 is 前缀而非 get 前缀。建议将 getIncomplete() 重命名为 isIncomplete()

♻️ 建议修改
-    public boolean getIncomplete() {
+    public boolean isIncomplete() {
         return this.incomplete;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java` around lines 79 -
85, Rename the boolean getter in class VmMetadataScanEntry from getIncomplete()
to isIncomplete() to follow JavaBean conventions: keep the field incomplete and
the setter setIncomplete(boolean), add or replace the existing getIncomplete()
method with a public boolean isIncomplete() method, and update any
references/call sites to use isIncomplete() (or keep getIncomplete() as a
deprecated wrapper if backward compatibility is required).
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java (1)

96-104: 给新增接口方法补上 Javadoc,明确 hostUuid 的约束。

这 5 个方法都把主机选择外提给调用方,但接口本身没说明 hostUuid 是否必须、是否允许任意 connected host、以及失败时的约定,后续实现很容易各自理解一套。

As per coding guidelines: 接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java`
around lines 96 - 104, Add Javadoc to each of the five interface methods
(handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg...),
handle(GetVmInstanceMetadataFromPrimaryStorageMsg...),
handle(ScanVmInstanceMetadataFromPrimaryStorageMsg...),
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg...),
handle(RebaseVolumeBackingFileOnPrimaryStorageMsg...)) that documents: (1)
hostUuid is required (non-null) and must be the UUID of a host that is connected
to the primary storage and where the operation will be executed (caller is
responsible for selecting such a host); (2) what the method does at a high
level; and (3) the failure contract — implementations must report errors via the
provided ReturnValueCompletion (e.g., completion.fail on any local/remote
failure) and must not throw unchecked exceptions. Ensure the Javadoc follows the
project style and add no extra modifiers to the interface methods.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java (1)

944-952: 内部类中存在冗余的类名限定符。

PrefixRebaseBackingFilesCmdPrefixRebaseBackingFilesRsp 继承时使用了完整类名 LocalStorageKvmBackend.AgentCommandLocalStorageKvmBackend.AgentResponse,但由于它们已在 LocalStorageKvmBackend 类内部定义,可以直接使用 AgentCommandAgentResponse

♻️ 简化类继承声明
-    public static class PrefixRebaseBackingFilesCmd extends LocalStorageKvmBackend.AgentCommand {
+    public static class PrefixRebaseBackingFilesCmd extends AgentCommand {
         public List<String> filePaths;
         public String oldPrefix;
         public String newPrefix;
     }

-    public static class PrefixRebaseBackingFilesRsp extends LocalStorageKvmBackend.AgentResponse {
+    public static class PrefixRebaseBackingFilesRsp extends AgentResponse {
         public int rebasedCount;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`
around lines 944 - 952, Remove the redundant outer-class qualifiers when
declaring the inner classes: change the extends clauses of
PrefixRebaseBackingFilesCmd and PrefixRebaseBackingFilesRsp to extend
AgentCommand and AgentResponse (respectively) instead of
LocalStorageKvmBackend.AgentCommand and LocalStorageKvmBackend.AgentResponse so
the inner classes use the enclosing class's types directly; update only the
class declarations for PrefixRebaseBackingFilesCmd and
PrefixRebaseBackingFilesRsp.
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java (1)

80-89: calculatePathReplacements 中存在与 resolveBaseDir 重复的逻辑。

此处的 baseDir 解析逻辑(先查 mountPath,再查 url)与 resolveBaseDir 方法重复。建议复用 resolveBaseDir 方法并处理异常,以保持代码一致性和可维护性。

♻️ 建议的重构方案
     `@Override`
     public PathReplacementResult calculatePathReplacements(String targetPsUuid, List<String> allOldPaths) {
         if (allOldPaths == null || allOldPaths.isEmpty()) {
             PathReplacementResult result = new PathReplacementResult();
             result.setMetadataToCurrentPathMap(Collections.emptyMap());
             return result;
         }

-        String baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.mountPath).eq(PrimaryStorageVO_.uuid, targetPsUuid).findValue();
-        if (baseDir == null) {
-            baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, targetPsUuid).findValue();
-        }
-        if (baseDir == null) {
+        String baseDir;
+        try {
+            baseDir = resolveBaseDir(targetPsUuid);
+        } catch (CloudRuntimeException e) {
             logger.warn(String.format("NFS PS[uuid:%s] has no mountPath or url, path replacement disabled", targetPsUuid));
             PathReplacementResult result = new PathReplacementResult();
             result.setMetadataToCurrentPathMap(Collections.emptyMap());
             return result;
         }
-        String newPrefix = baseDir.endsWith("/") ? baseDir : baseDir + "/";
+        String newPrefix = baseDir + "/";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java`
around lines 80 - 89, The code in calculatePathReplacements duplicates baseDir
resolution (checking mountPath then url); replace that block by calling
resolveBaseDir(targetPsUuid) and handle any exception it throws; if
resolveBaseDir returns null behave as before (logger.warn(...) and return a
PathReplacementResult with empty metadata map), and if it throws, log the error
and return the same empty PathReplacementResult—update references inside
calculatePathReplacements to use the resolved value and preserve existing
variables (targetPsUuid, PathReplacementResult) and logging.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java (2)

24-146: LocalStorageVmMetadataExtensionNfsVmMetadataExtension 实现高度相似。

这两个类的 calculatePathReplacementsvalidateMetadataPath 等方法实现几乎相同,仅在获取基础路径的来源上有差异(local 用 url,NFS 用 mountPathurl)。如果后续有更多存储类型需要支持,建议考虑提取抽象基类以减少代码重复。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java`
around lines 24 - 146, Extract the duplicated logic in
calculatePathReplacements, validateMetadataPath,
buildMetadataDir/buildVmMetadataPath string construction, normalizeBaseDir and
createResourceRef into a new abstract base class (e.g.,
AbstractVmMetadataExtension) that implements VmMetadataPathBuildExtensionPoint,
VmMetadataPathReplacementExtensionPoint and
VmMetadataResourcePersistExtensionPoint; add an abstract helper method like
getBaseDir(String primaryStorageUuid) that subclasses implement
(LocalStorageVmMetadataExtension returns PrimaryStorageVO_.url while
NfsVmMetadataExtension returns mountPath or url as needed), move shared
constants use (VmInstanceMetadataConstants) and path normalization there, keep
DB persistence (dbf.persist) either in the base createResourceRef or callable
from base, and update LocalStorageVmMetadataExtension and NfsVmMetadataExtension
to extend the new base and only implement the storage-specific base-dir
retrieval and any small differences.

37-53: buildMetadataDirbuildVmMetadataPath 存在重复的数据库查询。

这两个方法都单独查询 PrimaryStorageVO.url,当连续调用时会产生重复查询。考虑提取公共方法或在调用方缓存结果。

♻️ 建议提取公共方法
+    private String resolveUrl(String primaryStorageUuid) {
+        String url = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue();
+        if (url == null) {
+            throw new CloudRuntimeException(String.format("cannot find url for primary storage[uuid:%s]", primaryStorageUuid));
+        }
+        return normalizeBaseDir(url);
+    }
+
     `@Override`
     public String buildMetadataDir(String primaryStorageUuid) {
-        String url = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue();
-        if (url == null) {
-            throw new CloudRuntimeException(String.format("cannot find url for primary storage[uuid:%s]", primaryStorageUuid));
-        }
-        return String.format("%s/%s", normalizeBaseDir(url), VmInstanceMetadataConstants.METADATA_DIR_NAME);
+        return String.format("%s/%s", resolveUrl(primaryStorageUuid), VmInstanceMetadataConstants.METADATA_DIR_NAME);
     }

     `@Override`
     public String buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid) {
-        String url = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, primaryStorageUuid).findValue();
-        if (url == null) {
-            throw new CloudRuntimeException(String.format("cannot find url for primary storage[uuid:%s]", primaryStorageUuid));
-        }
-        return String.format("%s/%s/%s%s", normalizeBaseDir(url), VmInstanceMetadataConstants.METADATA_DIR_NAME, vmInstanceUuid, VmInstanceMetadataConstants.FILE_METADATA_SUFFIX);
+        return String.format("%s/%s/%s%s", resolveUrl(primaryStorageUuid), VmInstanceMetadataConstants.METADATA_DIR_NAME, vmInstanceUuid, VmInstanceMetadataConstants.FILE_METADATA_SUFFIX);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java`
around lines 37 - 53, Both buildMetadataDir and buildVmMetadataPath duplicate
the PrimaryStorageVO.url query; extract a private helper (e.g.,
getNormalizedBaseDir or fetchNormalizedUrl) that runs the
Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid,
primaryStorageUuid).findValue(), throws the same CloudRuntimeException if null,
and returns normalizeBaseDir(url); then have buildMetadataDir call that helper
and append VmInstanceMetadataConstants.METADATA_DIR_NAME, and have
buildVmMetadataPath call the helper and build the full path with
VmInstanceMetadataConstants.METADATA_DIR_NAME and
VmInstanceMetadataConstants.FILE_METADATA_SUFFIX to remove the duplicated query
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java`:
- Around line 8-10: Add clear method-level Javadoc to the
VmMetadataPathReplacementExtensionPoint interface: document
getPrimaryStorageType() to specify the expected returned storage type string and
whether null/empty is allowed; document calculatePathReplacements(String
targetPsUuid, List<String> allOldPaths) to describe semantics of targetPsUuid
and allOldPaths (non-null, expected format), what the implementation must
compute, concurrency expectations, error handling and whether it may throw
exceptions or should return an empty PathReplacementResult on no changes; and
document the nested PathReplacementResult class to explain fields, meaning of
empty vs null collections, and how callers should interpret the result.
Reference the methods getPrimaryStorageType(), calculatePathReplacements(...),
and the PathReplacementResult class when adding the Javadoc so implementors have
unambiguous input/output contracts.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java`:
- Around line 6-10: The interface VmMetadataResourcePersistExtensionPoint and
its methods getPrimaryStorageType, afterVolumePersist, and afterSnapshotPersist
lack Javadoc; add concise Javadoc for each method at the interface level (no
extra modifiers) describing purpose, caller contract, exact parameter semantics
(primaryStorageUuid, resourceUuid, resourceType, hostUuid, size, now), expected
return/side-effects, and when the method is invoked (e.g., after a volume or
snapshot is persisted to primary storage), plus any threading or exception
expectations so implementers have a clear, uniform contract.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`:
- Around line 3413-3437: The current flow treats a supplied but no-longer-valid
hostUuid as an ordinary error; update LocalStorageBase to (1) for
GetVmInstanceMetadataFromPrimaryStorageMsg: if msg.getHostUuid() is non-null but
lookup of host or hypervisor backend indicates the host is
deleted/unbound/invalid, fall back to resolving host via
getHostUuidByResourceUuid(msg.getRootVolumeUuid()) and proceed with the backend
from getHypervisorBackendFactoryByHostUuid(resolvedHostUuid) before failing; (2)
for CleanupVmInstanceMetadataOnPrimaryStorageMsg: if the persisted hostUuid is
invalid/terminal (host deleted or detached), consider the cleanup a no-op
success (or cancel GC) instead of retrying—detect this via the same host
lookup/validation and return success to the bus; change error branches around
getHostUuidByResourceUuid, getHypervisorBackendFactoryByHostUuid, and bkd.handle
to implement these two behaviors.
- Around line 3358-3367: The ChainTask submitted via thdf.chainSubmit uses
getSyncSignature() == "update-metadata-on-ps-" + self.getUuid() but
getSyncLevel() returns 10, which allows up to 10 concurrent metadata writes for
the same local primary storage and can cause race/overwrite on file-based
metadata; change the synchronization to serialize by either returning a
per-VM/per-root-volume signature (e.g. include rootVolumeUuid or vmUuid in
getSyncSignature()) or reduce getSyncLevel() to 1 to force single concurrency;
update the anonymous ChainTask in LocalStorageBase (the thdf.chainSubmit block
implementing getSyncSignature and getSyncLevel) accordingly so same
VM/rootVolume operations are bucketed or fully serialized.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java`:
- Around line 346-363: 在 getVmInstanceMetadata 和 scanVmMetadata 两个 handler(方法名
getVmInstanceMetadata、scanVmMetadata,返回类型 GetVmInstanceMetadataRsp /
ScanVmMetadataRsp)里补齐返回体:不仅记录命令(config.getVmInstanceMetadataCmds /
config.scanVmMetadataCmds),还要把模拟的 metadata 填入响应对象(例如为 GetVmInstanceMetadataRsp
设置 metadata 字段,为 ScanVmMetadataRsp 设置 metadatas/list 字段)并设置必要的状态/错误码为成功,然后通过现有
reply(entity, rsp) 返回;如果有 config 中的预置模拟数据或基于 cmd 的匹配逻辑,优先使用那些数据来填充响应。

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`:
- Around line 1935-1944: 当前将 metadata 更新按 primary storage 分桶且允许并发 10,会导致同一
VM/根云盘的并发更新冲突;在 ChainTask 的 getSyncSignature() 中不要只用
self.getUuid(),改为包含具体资源标识(例如 msg.getRootVolumeUuid() 或 vmUuid)以把同步粒度收敛到
VM/根云盘,同时将 getSyncLevel() 调整为 1,确保针对同一根云盘的更新串行化(定位: ChainTask 的
getSyncSignature() 和 getSyncLevel() 实现处)。

---

Nitpick comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`:
- Around line 95-96: 在 VmExpungeMetadataFlow.java 中删除多余的 final 变量赋值(即移除 `final
String finalPsUuid = psUuid;` 和 `final String finalHostUuid = hostUuid;`),直接在后续的
lambda/匿名内部类中使用已是 effectively final 的 `psUuid` 和 `hostUuid`。也请检查所有引用
`finalPsUuid` 和 `finalHostUuid` 的位置并改为直接使用 `psUuid` 和
`hostUuid`,确保编译通过并且语义不变(相关符号:psUuid, hostUuid, finalPsUuid, finalHostUuid, 相关
lambda/匿名类)。

In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`:
- Around line 1336-1346: The code in VmInstanceApiInterceptor should explicitly
check that the PrimaryStorageVO exists before assuming a type; change the lookup
to fetch the whole PrimaryStorageVO (e.g.,
Q.New(PrimaryStorageVO.class).eq(PrimaryStorageVO_.uuid, psUuid).find()) and if
the returned VO is null throw an ApiMessageInterceptionException with a clear
message like "primary storage[uuid:%s] not found"; otherwise read vo.getType()
into psType and then proceed to get the VmMetadataPathBuildExtensionPoint from
pluginRgty and, if missing, throw the existing "does not support vm metadata"
error including the actual non-null type. Ensure you reference
msg.getPrimaryStorageUuid(), PrimaryStorageVO/PrimaryStorageVO_.type,
pluginRgty, VmMetadataPathBuildExtensionPoint, ApiMessageInterceptionException
and argerr when applying the change.

In `@conf/db/upgrade/V5.0.0__schema.sql`:
- Around line 16-24: The VmMetadataFingerprintVO table is missing the audit
timestamp columns present on VmMetadataDirtyVO; update the CREATE TABLE for
VmMetadataFingerprintVO to add the same audit columns (createDate and
lastOpDate) with appropriate TIMESTAMP types and defaults (e.g., createDate
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP and lastOpDate TIMESTAMP NULL
DEFAULT NULL or with ON UPDATE CURRENT_TIMESTAMP if used in your schema) so the
table matches VmMetadataDirtyVO’s auditing schema while keeping the existing
PRIMARY KEY and FK constraint fkVmMetadataFingerprintVOVmInstanceEO intact.

In
`@header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java`:
- Around line 10-12: Change the numeric wrapper fields in
APICleanupVmInstanceMetadataEvent from Integer to primitive int to follow ZStack
conventions: replace declarations totalCleaned and totalFailed with primitive
int, adjust any corresponding getters/setters and usages to work with int
(initialize defaults if needed), and leave failedVmUuids as List<String>; ensure
no code expects null for these fields.

In
`@header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java`:
- Around line 16-17: Rename the ambiguous field uuid in
APIGetVmInstanceMetadataFromPrimaryStorageMsg to vmInstanceUuid: change the
private field name, its getter/setter, and any usages to vmInstanceUuid, keep
the `@APIParam`(resourceType = VmInstanceVO.class) annotation on the field, and
ensure the class implements/overrides VmInstanceMessage.getVmInstanceUuid() (and
update any callers or marshalling logic that referenced the old uuid symbol) so
names remain consistent across the codebase.

In
`@header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java`:
- Around line 30-33: The __example__() method sets vmUuids by assigning the
field directly (msg.vmUuids = ...) which is inconsistent with the rest of the
class; update __example__() to use the setter instead: call
msg.setVmUuids(Collections.singletonList(uuid())) so the example follows the
class's accessor pattern and any validation/logic in setVmUuids is applied.

In `@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java`:
- Around line 18-22: Make the private field code in the VmMetadataErrors enum
final and update the constructor to format the integer id with %d instead of %s;
specifically change the declaration of code to "private final String code" and
update the VmMetadataErrors(int id) constructor to use
String.format("VM_METADATA.%d", id) so code is immutable and formatted
correctly.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java`:
- Around line 3-8: Add Javadoc comments for every method in the
VmMetadataPathBuildExtensionPoint interface: document getPrimaryStorageType(),
buildVmMetadataPath(String primaryStorageUuid, String vmInstanceUuid),
buildMetadataDir(String primaryStorageUuid), and especially
validateMetadataPath(String primaryStorageUuid, String path) — explicitly state
the return semantics (e.g. returns normalized/validated path on success, or
null/throws/returns error message on failure) and describe parameters and
expected behavior; keep methods without explicit modifiers (interface methods
remain implicitly public) and place the Javadoc directly above each method
declaration.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java`:
- Around line 27-41: The batchResolveVmUuids method currently accumulates
resolved UUIDs into a List allowing duplicates; change the aggregation to
preserve insertion order and remove duplicates by collecting results into a
LinkedHashSet (or equivalent) while iterating over fieldValues and
resolveVmUuids(v), then return a List built from that set; reference
batchResolveVmUuids and resolveVmUuids to locate the code and ensure null checks
and skipping null fieldValues remain unchanged.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`:
- Around line 944-952: Remove the redundant outer-class qualifiers when
declaring the inner classes: change the extends clauses of
PrefixRebaseBackingFilesCmd and PrefixRebaseBackingFilesRsp to extend
AgentCommand and AgentResponse (respectively) instead of
LocalStorageKvmBackend.AgentCommand and LocalStorageKvmBackend.AgentResponse so
the inner classes use the enclosing class's types directly; update only the
class declarations for PrefixRebaseBackingFilesCmd and
PrefixRebaseBackingFilesRsp.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java`:
- Around line 24-146: Extract the duplicated logic in calculatePathReplacements,
validateMetadataPath, buildMetadataDir/buildVmMetadataPath string construction,
normalizeBaseDir and createResourceRef into a new abstract base class (e.g.,
AbstractVmMetadataExtension) that implements VmMetadataPathBuildExtensionPoint,
VmMetadataPathReplacementExtensionPoint and
VmMetadataResourcePersistExtensionPoint; add an abstract helper method like
getBaseDir(String primaryStorageUuid) that subclasses implement
(LocalStorageVmMetadataExtension returns PrimaryStorageVO_.url while
NfsVmMetadataExtension returns mountPath or url as needed), move shared
constants use (VmInstanceMetadataConstants) and path normalization there, keep
DB persistence (dbf.persist) either in the base createResourceRef or callable
from base, and update LocalStorageVmMetadataExtension and NfsVmMetadataExtension
to extend the new base and only implement the storage-specific base-dir
retrieval and any small differences.
- Around line 37-53: Both buildMetadataDir and buildVmMetadataPath duplicate the
PrimaryStorageVO.url query; extract a private helper (e.g., getNormalizedBaseDir
or fetchNormalizedUrl) that runs the
Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid,
primaryStorageUuid).findValue(), throws the same CloudRuntimeException if null,
and returns normalizeBaseDir(url); then have buildMetadataDir call that helper
and append VmInstanceMetadataConstants.METADATA_DIR_NAME, and have
buildVmMetadataPath call the helper and build the full path with
VmInstanceMetadataConstants.METADATA_DIR_NAME and
VmInstanceMetadataConstants.FILE_METADATA_SUFFIX to remove the duplicated query
logic.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java`:
- Around line 96-104: Add Javadoc to each of the five interface methods
(handle(UpdateVmInstanceMetadataOnPrimaryStorageMsg...),
handle(GetVmInstanceMetadataFromPrimaryStorageMsg...),
handle(ScanVmInstanceMetadataFromPrimaryStorageMsg...),
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg...),
handle(RebaseVolumeBackingFileOnPrimaryStorageMsg...)) that documents: (1)
hostUuid is required (non-null) and must be the UUID of a host that is connected
to the primary storage and where the operation will be executed (caller is
responsible for selecting such a host); (2) what the method does at a high
level; and (3) the failure contract — implementations must report errors via the
provided ReturnValueCompletion (e.g., completion.fail on any local/remote
failure) and must not throw unchecked exceptions. Ensure the Javadoc follows the
project style and add no extra modifiers to the interface methods.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java`:
- Around line 80-89: The code in calculatePathReplacements duplicates baseDir
resolution (checking mountPath then url); replace that block by calling
resolveBaseDir(targetPsUuid) and handle any exception it throws; if
resolveBaseDir returns null behave as before (logger.warn(...) and return a
PathReplacementResult with empty metadata map), and if it throws, log the error
and return the same empty PathReplacementResult—update references inside
calculatePathReplacements to use the resolved value and preserve existing
variables (targetPsUuid, PathReplacementResult) and logging.

In `@sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java`:
- Around line 22-27: The field and its accessor signatures in
CleanupVmInstanceMetadataResult should use a generic type to avoid raw List
usage: change the field failedVmUuids and its setter/getter signatures from
java.util.List to java.util.List<String> (update the field declaration,
setFailedVmUuids(List<String> failedVmUuids), and getFailedVmUuids():
List<String>) and regenerate/compile; also apply the same change for
ScanVmInstanceMetadataFromPrimaryStorageResult.vmInstanceMetadata to use a typed
List (e.g., List<String> or the appropriate element type) in the codegen
template so callers don't need unchecked casts.

In `@sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java`:
- Around line 79-85: Rename the boolean getter in class VmMetadataScanEntry from
getIncomplete() to isIncomplete() to follow JavaBean conventions: keep the field
incomplete and the setter setIncomplete(boolean), add or replace the existing
getIncomplete() method with a public boolean isIncomplete() method, and update
any references/call sites to use isIncomplete() (or keep getIncomplete() as a
deprecated wrapper if backward compatibility is required).

In `@storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java`:
- Around line 1863-1864: Replace the extra DB query that obtains psType with the
instance's stored type: use self.getType() (from PrimaryStorageBase) instead of
calling Q.New(PrimaryStorageVO.class)...findValue(); then pass that type into
pluginRgty.getExtensionFromMap(..., VmMetadataPathBuildExtensionPoint.class). In
other words, remove the Q.New(...) lookup and call self.getType() (or
this.getType()) when resolving the extension in PrimaryStorageBase so
pluginRgty.getExtensionFromMap uses the in-memory type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8fd4ef01-f15c-4f0d-a75f-13eb2986a26d

📥 Commits

Reviewing files that changed from the base of the PR and between 5924b9d and da8ac3e.

⛔ Files ignored due to path filters (5)
  • conf/globalConfig/vm.xml is excluded by !**/*.xml
  • conf/persistence.xml is excluded by !**/*.xml
  • conf/serviceConfig/primaryStorage.xml is excluded by !**/*.xml
  • conf/serviceConfig/vmInstance.xml is excluded by !**/*.xml
  • conf/springConfigXml/VmInstanceManager.xml is excluded by !**/*.xml
📒 Files selected for processing (144)
  • compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java
  • compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java
  • testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy

Comment on lines +8 to +10
String getPrimaryStorageType();
PathReplacementResult calculatePathReplacements(String targetPsUuid, List<String> allOldPaths);
class PathReplacementResult {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

扩展点契约建议补充方法级 Javadoc。

这里是跨主存储实现的关键扩展接口,建议明确输入输出语义,避免实现行为不一致。

建议补丁
 public interface VmMetadataPathReplacementExtensionPoint {
+    /**
+     * `@return` 当前扩展支持的主存储类型
+     */
     String getPrimaryStorageType();
+
+    /**
+     * 计算历史元数据路径到当前路径的替换关系。
+     */
     PathReplacementResult calculatePathReplacements(String targetPsUuid, List<String> allOldPaths);

As per coding guidelines, “接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String getPrimaryStorageType();
PathReplacementResult calculatePathReplacements(String targetPsUuid, List<String> allOldPaths);
class PathReplacementResult {
/**
* `@return` 当前扩展支持的主存储类型
*/
String getPrimaryStorageType();
/**
* 计算历史元数据路径到当前路径的替换关系
*/
PathReplacementResult calculatePathReplacements(String targetPsUuid, List<String> allOldPaths);
class PathReplacementResult {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java`
around lines 8 - 10, Add clear method-level Javadoc to the
VmMetadataPathReplacementExtensionPoint interface: document
getPrimaryStorageType() to specify the expected returned storage type string and
whether null/empty is allowed; document calculatePathReplacements(String
targetPsUuid, List<String> allOldPaths) to describe semantics of targetPsUuid
and allOldPaths (non-null, expected format), what the implementation must
compute, concurrency expectations, error handling and whether it may throw
exceptions or should return an empty PathReplacementResult on no changes; and
document the nested PathReplacementResult class to explain fields, meaning of
empty vs null collections, and how callers should interpret the result.
Reference the methods getPrimaryStorageType(), calculatePathReplacements(...),
and the PathReplacementResult class when adding the Javadoc so implementors have
unambiguous input/output contracts.

Comment on lines +6 to +10
String getPrimaryStorageType();
void afterVolumePersist(String primaryStorageUuid, String resourceUuid,
String resourceType, String hostUuid, long size, Timestamp now);
void afterSnapshotPersist(String primaryStorageUuid, String resourceUuid,
String resourceType, String hostUuid, long size, Timestamp now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

请为扩展点接口方法补充 Javadoc。

当前接口方法缺少说明,后续实现方难以统一参数语义与调用时机。建议在接口层补齐简明契约注释。

建议补丁
 public interface VmMetadataResourcePersistExtensionPoint {
+    /**
+     * `@return` 当前扩展支持的主存储类型
+     */
     String getPrimaryStorageType();
+
+    /**
+     * 卷元数据落库后的回调。
+     */
     void afterVolumePersist(String primaryStorageUuid, String resourceUuid,
                             String resourceType, String hostUuid, long size, Timestamp now);
+
+    /**
+     * 快照元数据落库后的回调。
+     */
     void afterSnapshotPersist(String primaryStorageUuid, String resourceUuid,
                               String resourceType, String hostUuid, long size, Timestamp now);
 }

As per coding guidelines, “接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释。”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String getPrimaryStorageType();
void afterVolumePersist(String primaryStorageUuid, String resourceUuid,
String resourceType, String hostUuid, long size, Timestamp now);
void afterSnapshotPersist(String primaryStorageUuid, String resourceUuid,
String resourceType, String hostUuid, long size, Timestamp now);
public interface VmMetadataResourcePersistExtensionPoint {
/**
* `@return` 当前扩展支持的主存储类型
*/
String getPrimaryStorageType();
/**
* 卷元数据落库后的回调
*/
void afterVolumePersist(String primaryStorageUuid, String resourceUuid,
String resourceType, String hostUuid, long size, Timestamp now);
/**
* 快照元数据落库后的回调
*/
void afterSnapshotPersist(String primaryStorageUuid, String resourceUuid,
String resourceType, String hostUuid, long size, Timestamp now);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java`
around lines 6 - 10, The interface VmMetadataResourcePersistExtensionPoint and
its methods getPrimaryStorageType, afterVolumePersist, and afterSnapshotPersist
lack Javadoc; add concise Javadoc for each method at the interface level (no
extra modifiers) describing purpose, caller contract, exact parameter semantics
(primaryStorageUuid, resourceUuid, resourceType, hostUuid, size, now), expected
return/side-effects, and when the method is invoked (e.g., after a volume or
snapshot is persisted to primary storage), plus any threading or exception
expectations so implementers have a clear, uniform contract.

Comment on lines +3358 to +3367
thdf.chainSubmit(new ChainTask(msg) {
@Override
public String getSyncSignature() {
return "update-metadata-on-ps-" + self.getUuid();
}

@Override
public int getSyncLevel() {
return 10;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

不要在同一块 local primary storage 上并发 10 个 metadata 写入。

当前同步签名只包含 self.getUuid(),而 getSyncLevel() 放开到 10;同一 VM 的 metadata 更新会并发执行。对这种直接落文件的路径来说,这很容易把后一条更新覆盖前一条。建议按 rootVolumeUuid/vmUuid 分桶,或者保持单并发。

💡 建议修改
             `@Override`
             public String getSyncSignature() {
-                return "update-metadata-on-ps-" + self.getUuid();
+                return "update-metadata-on-ps-" + self.getUuid() + "-root-volume-" + msg.getRootVolumeUuid();
             }

             `@Override`
             public int getSyncLevel() {
-                return 10;
+                return 1;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`
around lines 3358 - 3367, The ChainTask submitted via thdf.chainSubmit uses
getSyncSignature() == "update-metadata-on-ps-" + self.getUuid() but
getSyncLevel() returns 10, which allows up to 10 concurrent metadata writes for
the same local primary storage and can cause race/overwrite on file-based
metadata; change the synchronization to serialize by either returning a
per-VM/per-root-volume signature (e.g. include rootVolumeUuid or vmUuid in
getSyncSignature()) or reduce getSyncLevel() to 1 to force single concurrency;
update the anonymous ChainTask in LocalStorageBase (the thdf.chainSubmit block
implementing getSyncSignature and getSyncLevel) accordingly so same
VM/rootVolume operations are bucketed or fully serialized.

Comment on lines +3413 to +3437
String hostUuid = null;
if (msg.getHostUuid() != null) {
hostUuid = msg.getHostUuid();
}

if (hostUuid == null) {
if (msg.getRootVolumeUuid() == null) {
reply.setError(operr("cannot determine host for vm metadata get on local primary storage[uuid:%s]," +
" rootVolumeUuid is null", self.getUuid()));
bus.reply(msg, reply);
return;
}
try {
hostUuid = getHostUuidByResourceUuid(msg.getRootVolumeUuid());
} catch (Exception e) {
reply.setError(operr("cannot determine host for vm metadata get on local primary storage[uuid:%s], rootVolumeUuid[%s]: %s",
self.getUuid(), msg.getRootVolumeUuid(), e.getMessage()));
bus.reply(msg, reply);
return;
}
}

LocalStorageHypervisorFactory f = getHypervisorBackendFactoryByHostUuid(hostUuid);
LocalStorageHypervisorBackend bkd = f.getHypervisorBackend(self);
bkd.handle(msg, hostUuid, new ReturnValueCompletion<GetVmInstanceMetadataFromPrimaryStorageReply>(msg) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

把“传入的 hostUuid 已失效”当成单独分支处理。

GetVmInstanceMetadataFromPrimaryStorageMsg 里只在 hostUuid == null 时才回退到 rootVolumeUuid;卷迁移后如果调用方还带着旧 hostUuid,这里会直接失败,明明还能从当前 root volume 重新解析到新主机。反过来,CleanupVmInstanceMetadataOnPrimaryStorageMsg 如果持久化的是旧 hostUuid,又不能简单回退到当前 root-volume host——那会清到错误的主机;对“主机已删除/已解绑”这种终态更合适的行为是直接视为清理完成或取消 GC。现在这两条路径都把失效 hostUuid 当普通错误处理,前者会误失败,后者会把 GC 变成持续重试。

Also applies to: 3509-3527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`
around lines 3413 - 3437, The current flow treats a supplied but no-longer-valid
hostUuid as an ordinary error; update LocalStorageBase to (1) for
GetVmInstanceMetadataFromPrimaryStorageMsg: if msg.getHostUuid() is non-null but
lookup of host or hypervisor backend indicates the host is
deleted/unbound/invalid, fall back to resolving host via
getHostUuidByResourceUuid(msg.getRootVolumeUuid()) and proceed with the backend
from getHypervisorBackendFactoryByHostUuid(resolvedHostUuid) before failing; (2)
for CleanupVmInstanceMetadataOnPrimaryStorageMsg: if the persisted hostUuid is
invalid/terminal (host deleted or detached), consider the cleanup a no-op
success (or cancel GC) instead of retrying—detect this via the same host
lookup/validation and return success to the bus; change error branches around
getHostUuidByResourceUuid, getHypervisorBackendFactoryByHostUuid, and bkd.handle
to implement these two behaviors.

Comment on lines +346 to +363
@RequestMapping(value=LocalStorageKvmBackend.GET_VM_INSTANCE_METADATA_PATH, method= RequestMethod.POST)
public @ResponseBody
String getVmInstanceMetadata(HttpEntity<String> entity) {
GetVmInstanceMetadataCmd cmd = JSONObjectUtil.toObject(entity.getBody(), GetVmInstanceMetadataCmd.class);
config.getVmInstanceMetadataCmds.add(cmd);
GetVmInstanceMetadataRsp rsp = new GetVmInstanceMetadataRsp();
reply(entity, rsp);
return null;
}

@RequestMapping(value=LocalStorageKvmBackend.SCAN_VM_METADATA_PATH, method= RequestMethod.POST)
public @ResponseBody
String scanVmMetadata(HttpEntity<String> entity) {
ScanVmMetadataCmd cmd = JSONObjectUtil.toObject(entity.getBody(), ScanVmMetadataCmd.class);
config.scanVmMetadataCmds.add(cmd);
ScanVmMetadataRsp rsp = new ScanVmMetadataRsp();
reply(entity, rsp);
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

补齐 simulator 的读取/扫描返回体。

这两个 handler 现在只记录 command,没有把任何 metadata 内容写回 GetVmInstanceMetadataRsp / ScanVmMetadataRsp。这样在 simulator 下,“成功”分支也永远只能拿到空结果,新增的读取/扫描路径无法验证真实返回值。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java`
around lines 346 - 363, 在 getVmInstanceMetadata 和 scanVmMetadata 两个 handler(方法名
getVmInstanceMetadata、scanVmMetadata,返回类型 GetVmInstanceMetadataRsp /
ScanVmMetadataRsp)里补齐返回体:不仅记录命令(config.getVmInstanceMetadataCmds /
config.scanVmMetadataCmds),还要把模拟的 metadata 填入响应对象(例如为 GetVmInstanceMetadataRsp
设置 metadata 字段,为 ScanVmMetadataRsp 设置 metadatas/list 字段)并设置必要的状态/错误码为成功,然后通过现有
reply(entity, rsp) 返回;如果有 config 中的预置模拟数据或基于 cmd 的匹配逻辑,优先使用那些数据来填充响应。

Comment on lines +1935 to +1944
thdf.chainSubmit(new ChainTask(msg) {
@Override
public String getSyncSignature() {
return "update-metadata-on-ps-" + self.getUuid();
}

@Override
public int getSyncLevel() {
return 10;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

把 metadata 更新的同步粒度收敛到 VM/根云盘。

这里按 primary storage 分桶、但允许 10 并发;同一台 VM 的两次 metadata 更新会并行落到同一个后端写路径上。只要底层是覆盖写同一份 metadata 文件,就会出现丢更新/写出半成品的竞争。建议把 getSyncSignature() 至少细化到 msg.getRootVolumeUuid()(或 vmUuid),并把 getSyncLevel() 收到 1。

💡 建议修改
             `@Override`
             public String getSyncSignature() {
-                return "update-metadata-on-ps-" + self.getUuid();
+                return "update-metadata-on-ps-" + self.getUuid() + "-root-volume-" + msg.getRootVolumeUuid();
             }

             `@Override`
             public int getSyncLevel() {
-                return 10;
+                return 1;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 1935 - 1944, 当前将 metadata 更新按 primary storage 分桶且允许并发 10,会导致同一
VM/根云盘的并发更新冲突;在 ChainTask 的 getSyncSignature() 中不要只用
self.getUuid(),改为包含具体资源标识(例如 msg.getRootVolumeUuid() 或 vmUuid)以把同步粒度收敛到
VM/根云盘,同时将 getSyncLevel() 调整为 1,确保针对同一根云盘的更新串行化(定位: ChainTask 的
getSyncSignature() 和 getSyncLevel() 实现处)。

@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/ZSV-11559@@4 branch from da8ac3e to 208c773 Compare March 26, 2026 08:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java (1)

1935-1944: ⚠️ Potential issue | 🔴 Critical

把 metadata update 的同步粒度收敛到 VM/根云盘。

这个点在前一个 patch set 已提过,当前实现仍然只按 primary storage 分桶且允许 10 并发。同一台 VM/同一根云盘的并发 update 仍会同时改写同一个 metadata 文件,结果是非确定的。getSyncSignature() 至少要带上 msg.getRootVolumeUuid()msg.getVmUuid()getSyncLevel() 也应该收敛到 1。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 1935 - 1944, The current ChainTask submitted via thdf.chainSubmit
uses getSyncSignature() = "update-metadata-on-ps-" + self.getUuid() and
getSyncLevel() = 10 which buckets only by primary storage; change
getSyncSignature() to include the target VM/root volume identifier (e.g., append
msg.getVmUuid() or msg.getRootVolumeUuid() to
"update-metadata-on-ps-"+self.getUuid()) and reduce getSyncLevel() to 1 so
updates serialize per VM/root volume; update the ChainTask implementation
accordingly (refer to thdf.chainSubmit, ChainTask, getSyncSignature(),
getSyncLevel(), self.getUuid(), msg.getVmUuid(), msg.getRootVolumeUuid()).
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java (2)

3358-3367: ⚠️ Potential issue | 🔴 Critical

不要把同一 local PS 上的 metadata 更新放到 10 并发。

现在的同步签名只按 self.getUuid() 分桶,但 getSyncLevel() 放开到了 10;同一 VM/root volume 的 metadata 写入仍然会并发落到同一存储路径上,后到请求可以覆盖先到结果。

可选修正
             `@Override`
             public String getSyncSignature() {
-                return "update-metadata-on-ps-" + self.getUuid();
+                return "update-metadata-on-ps-" + self.getUuid() + "-root-volume-" + msg.getRootVolumeUuid();
             }

             `@Override`
             public int getSyncLevel() {
-                return 10;
+                return 1;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`
around lines 3358 - 3367, The current ChainTask submitted via thdf.chainSubmit
uses getSyncSignature() returning "update-metadata-on-ps-"+self.getUuid() but
getSyncLevel()=10, allowing concurrent metadata writes to the same storage path;
fix by making the sync key unique per storage path or volume (e.g., include the
target storage path/volume identifier in getSyncSignature() instead of only
self.getUuid(), or alternatively change getSyncLevel() to 1) so metadata updates
for the same local PS/storage path are serialized; update the anonymous
ChainTask implementations of getSyncSignature() / getSyncLevel() accordingly.

3413-3437: ⚠️ Potential issue | 🟠 Major

把“传入的 hostUuid 已失效”当成单独分支处理。

Get... 这里仍然只有在 hostUuid == null 时才回退到 rootVolumeUuid;卷迁移后如果调用方带着旧 hostUuid,会直接在 backend factory 解析处失败,本来还能从当前 root volume 重新定位。Cleanup... 也一样:对已删除/已解绑的宿主机继续返回 error,会把 GC 拉成持续重试;这种终态更适合视为 no-op 成功或直接取消 GC。

Also applies to: 3509-3527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`
around lines 3413 - 3437, The code only falls back to rootVolumeUuid when
msg.getHostUuid() is null, so when callers pass a stale/invalid hostUuid the
call fails at getHypervisorBackendFactoryByHostUuid and misses an opportunity to
re-resolve from the root volume; change the flow in methods handling
GetVmInstanceMetadataFromPrimaryStorage* and Cleanup* so that: read hostUuid =
msg.getHostUuid(); if hostUuid != null attempt
getHypervisorBackendFactoryByHostUuid(hostUuid) inside a try; if that fails,
call getHostUuidByResourceUuid(msg.getRootVolumeUuid()) and retry resolving the
factory; if resolution still fails return the original error for non-cleanup
operations, but for cleanup/GC paths treat “host not found/unbound” as terminal
and succeed/no-op (or cancel GC) instead of forcing retries; use symbols
getHostUuidByResourceUuid, getHypervisorBackendFactoryByHostUuid, bkd.handle,
and the GetVmInstanceMetadataFromPrimaryStorage*/Cleanup* handlers to implement
this fallback and terminal-success behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`:
- Around line 89-123: The code currently forwards a potentially null hostUuid
from VmExpungeMetadataFlow into submitGC which makes GC retries unroutable;
change the logic so we don't hard-set or propagate a null host: update
VmExpungeMetadataFlow (where hostUuid is derived and submitGC(...) is called) to
pass a nullable host marker (leave the parameter null) only when no host is
available, and modify submitGC (and the
CleanupVmInstanceMetadataOnPrimaryStorageGC fields assignment) to avoid
assigning gc.hostUuid when the incoming hostUuid is null and instead defer host
resolution to the GC execution/handler (implement host selection there by
looking up an available host for the primary storage psUuid or using
vm.lastHostUuid as fallback). Ensure symbols involved are VmExpungeMetadataFlow,
submitGC, and CleanupVmInstanceMetadataOnPrimaryStorageGC so the host selection
is performed at GC runtime rather than propagating null now.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`:
- Around line 2039-2060: The cleanup handler for
CleanupVmInstanceMetadataOnPrimaryStorageMsg currently calls backend.handle(...)
directly, bypassing the existing ChainTask-based serialization used for
updates/registrations and risking races that delete or interrupt concurrent
writes; change handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg) to submit
the cleanup as a ChainTask (same serialization path as the update/register flow)
using the same sync signature key (derive it from rootVolumeUuid or
metadataPath) so cleanup runs serialized with other metadata operations, and
inside the task invoke backend.handle(...) with the existing
ReturnValueCompletion to reply via bus.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`:
- Around line 2157-2162: The reply construction in NfsPrimaryStorageKVMBackend
(the block creating ScanVmInstanceMetadataFromPrimaryStorageReply r) returns
null when rsp.metadataEntries is null; update the code so
r.setVmInstanceMetadata(...) always receives a non-null list (use
Collections.emptyList() when rsp.metadataEntries == null), preserve the existing
hostUuid-setting logic for non-null entries, and add the necessary import for
java.util.Collections if missing, so callers never receive a null metadata list.
- Around line 2168-2177: 在
NfsPrimaryStorageKVMBackend.handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg...)
中不要直接用传入的 hostUuid 路由 KVMHostAsyncHttpCallMsg(hmsg),因为在 expunge/GC 场景 hostUuid
可能为空或不可用;改为当 msg.getHostUuid() 为 null 或该主机不可达时,查找任一已连接并挂载该 NFS
的可用主机(例如通过现有的主机查询/挂载检查方法、主机状态过滤器或已有的 findHostsByPrimaryStorageUuid/QueryHost
APIs),将找到的主机 UUID 赋给 hmsg.setHostUuid(...),并用该 UUID 调用
bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID,
selectedHostUuid);若没有可用主机则按原有逻辑通过 completion 返回失败。确保引用
CleanupVmInstanceMetadataOnPrimaryStorageMsg、CleanupVmMetadataCmd、KVMHostAsyncHttpCallMsg、handle
和 bus.makeTargetServiceIdByResourceUuid 等标识以便定位修改点。

---

Duplicate comments:
In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`:
- Around line 3358-3367: The current ChainTask submitted via thdf.chainSubmit
uses getSyncSignature() returning "update-metadata-on-ps-"+self.getUuid() but
getSyncLevel()=10, allowing concurrent metadata writes to the same storage path;
fix by making the sync key unique per storage path or volume (e.g., include the
target storage path/volume identifier in getSyncSignature() instead of only
self.getUuid(), or alternatively change getSyncLevel() to 1) so metadata updates
for the same local PS/storage path are serialized; update the anonymous
ChainTask implementations of getSyncSignature() / getSyncLevel() accordingly.
- Around line 3413-3437: The code only falls back to rootVolumeUuid when
msg.getHostUuid() is null, so when callers pass a stale/invalid hostUuid the
call fails at getHypervisorBackendFactoryByHostUuid and misses an opportunity to
re-resolve from the root volume; change the flow in methods handling
GetVmInstanceMetadataFromPrimaryStorage* and Cleanup* so that: read hostUuid =
msg.getHostUuid(); if hostUuid != null attempt
getHypervisorBackendFactoryByHostUuid(hostUuid) inside a try; if that fails,
call getHostUuidByResourceUuid(msg.getRootVolumeUuid()) and retry resolving the
factory; if resolution still fails return the original error for non-cleanup
operations, but for cleanup/GC paths treat “host not found/unbound” as terminal
and succeed/no-op (or cancel GC) instead of forcing retries; use symbols
getHostUuidByResourceUuid, getHypervisorBackendFactoryByHostUuid, bkd.handle,
and the GetVmInstanceMetadataFromPrimaryStorage*/Cleanup* handlers to implement
this fallback and terminal-success behavior.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`:
- Around line 1935-1944: The current ChainTask submitted via thdf.chainSubmit
uses getSyncSignature() = "update-metadata-on-ps-" + self.getUuid() and
getSyncLevel() = 10 which buckets only by primary storage; change
getSyncSignature() to include the target VM/root volume identifier (e.g., append
msg.getVmUuid() or msg.getRootVolumeUuid() to
"update-metadata-on-ps-"+self.getUuid()) and reduce getSyncLevel() to 1 so
updates serialize per VM/root volume; update the ChainTask implementation
accordingly (refer to thdf.chainSubmit, ChainTask, getSyncSignature(),
getSyncLevel(), self.getUuid(), msg.getVmUuid(), msg.getRootVolumeUuid()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cf42664d-a552-4049-94db-8856fd19b240

📥 Commits

Reviewing files that changed from the base of the PR and between da8ac3e and 208c773.

⛔ Files ignored due to path filters (5)
  • conf/globalConfig/vm.xml is excluded by !**/*.xml
  • conf/persistence.xml is excluded by !**/*.xml
  • conf/serviceConfig/primaryStorage.xml is excluded by !**/*.xml
  • conf/serviceConfig/vmInstance.xml is excluded by !**/*.xml
  • conf/springConfigXml/VmInstanceManager.xml is excluded by !**/*.xml
📒 Files selected for processing (144)
  • compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java
  • compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java
  • testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
✅ Files skipped from review due to trivial changes (82)
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
🚧 Files skipped from review as they are similar to previous changes (45)
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java

Comment on lines +89 to +123
String hostUuid = spec.getVmInventory().getHostUuid();
if (hostUuid == null) {
hostUuid = spec.getVmInventory().getLastHostUuid();
}
cmsg.setHostUuid(hostUuid);

final String finalPsUuid = psUuid;
final String finalHostUuid = hostUuid;

bus.makeTargetServiceIdByResourceUuid(cmsg, PrimaryStorageConstant.SERVICE_ID, psUuid);
bus.send(cmsg, new CloudBusCallBack(trigger) {
@Override
public void run(MessageReply reply) {
if (reply.isSuccess()) {
logger.info(String.format("[MetadataExpunge] successfully deleted metadata for vm[uuid:%s] on ps[uuid:%s]",
vmUuid, finalPsUuid));
} else {
logger.warn(String.format("[MetadataExpunge] failed to delete metadata for vm[uuid:%s] on ps[uuid:%s]: %s, " +
"submitting GC job for retry", vmUuid, finalPsUuid, reply.getError()));
submitGC(finalPsUuid, vmUuid, rootVolumeUuid, metadataPath, finalHostUuid);
}
trigger.next();
}
});
}

private void submitGC(String psUuid, String vmUuid, String rootVolumeUuid, String metadataPath, String hostUuid) {
CleanupVmInstanceMetadataOnPrimaryStorageGC gc = new CleanupVmInstanceMetadataOnPrimaryStorageGC();
gc.NAME = CleanupVmInstanceMetadataOnPrimaryStorageGC.getGCName(vmUuid);
gc.primaryStorageUuid = psUuid;
gc.vmUuid = vmUuid;
gc.rootVolumeUuid = rootVolumeUuid;
gc.metadataPath = metadataPath;
gc.hostUuid = hostUuid;
long gcIntervalSec = TimeUnit.HOURS.toSeconds(VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

不要把 null 的 hostUuid 继续传给 GC。

expunge 场景下 hostUuid/lastHostUuid 可能都拿不到,这里仍然把该值透传给 submitGC()。这样初次 cleanup 失败后,GC 重试也没有可路由的宿主机,只会重复失败。这里至少要补一个可用 host 的兜底,或者把 host 选择延后到 handler/GC 执行时再决定。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 89 - 123, The code currently forwards a potentially null hostUuid
from VmExpungeMetadataFlow into submitGC which makes GC retries unroutable;
change the logic so we don't hard-set or propagate a null host: update
VmExpungeMetadataFlow (where hostUuid is derived and submitGC(...) is called) to
pass a nullable host marker (leave the parameter null) only when no host is
available, and modify submitGC (and the
CleanupVmInstanceMetadataOnPrimaryStorageGC fields assignment) to avoid
assigning gc.hostUuid when the incoming hostUuid is null and instead defer host
resolution to the GC execution/handler (implement host selection there by
looking up an available host for the primary storage psUuid or using
vm.lastHostUuid as fallback). Ensure symbols involved are VmExpungeMetadataFlow,
submitGC, and CleanupVmInstanceMetadataOnPrimaryStorageGC so the host selection
is performed at GC runtime rather than propagating null now.

Comment on lines +2039 to +2060
protected void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg) {
CleanupVmInstanceMetadataOnPrimaryStorageReply reply = new CleanupVmInstanceMetadataOnPrimaryStorageReply();
List<HostInventory> connectedHosts = factory.getConnectedHostForOperation(getSelfInventory());
if (connectedHosts.isEmpty()) {
reply.setError(operr("no connected host found for NFS primary storage[uuid:%s]", self.getUuid()));
bus.reply(msg, reply);
return;
}
String hostUuid = connectedHosts.get(0).getUuid();
final NfsPrimaryStorageBackend backend = getBackendByHostUuid(hostUuid);
backend.handle(msg, hostUuid, new ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>(msg) {
@Override
public void success(CleanupVmInstanceMetadataOnPrimaryStorageReply r) {
bus.reply(msg, r);
}

@Override
public void fail(ErrorCode errorCode) {
reply.setError(errorCode);
bus.reply(msg, reply);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

让 cleanup 和 update 走同一条串行化通道。

CleanupVmInstanceMetadataOnPrimaryStorageMsg 这里直接下发 backend,绕过了 Line 1935-1944 的 ChainTask。如果 expunge cleanup 与 update/register 并发命中同一个 metadataPath,cleanup 可能删掉刚写好的文件或打断写入。建议复用同一个 sync signature(至少按 rootVolumeUuidmetadataPath)串行化 cleanup。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 2039 - 2060, The cleanup handler for
CleanupVmInstanceMetadataOnPrimaryStorageMsg currently calls backend.handle(...)
directly, bypassing the existing ChainTask-based serialization used for
updates/registrations and risking races that delete or interrupt concurrent
writes; change handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg) to submit
the cleanup as a ChainTask (same serialization path as the update/register flow)
using the same sync signature key (derive it from rootVolumeUuid or
metadataPath) so cleanup runs serialized with other metadata operations, and
inside the task invoke backend.handle(...) with the existing
ReturnValueCompletion to reply via bus.

Comment on lines +2168 to +2177
public void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply> completion) {
CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
cmd.setUuid(msg.getPrimaryStorageUuid());
cmd.metadataPath = msg.getMetadataPath();

KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg();
hmsg.setCommand(cmd);
hmsg.setPath(CLEANUP_VM_METADATA_PATH);
hmsg.setHostUuid(hostUuid);
bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

为 NFS 清理路径补上可用主机回退。

Line 2176-2177 直接用入参 hostUuid 路由消息。expunge/GC 场景里这个值可能为空或已经不可用,但 NFS 上的元数据文件本身是共享存储内容,本来可以改由任一已连接且已挂载该 NFS 的主机执行清理。现在这里会把一次本可成功的清理直接变成消息路由失败。

💡 建议修改
     public void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply> completion) {
         CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
         cmd.setUuid(msg.getPrimaryStorageUuid());
         cmd.metadataPath = msg.getMetadataPath();
 
+        String targetHostUuid = hostUuid;
+        if (targetHostUuid == null ||
+                !Q.New(HostVO.class).eq(HostVO_.uuid, targetHostUuid).eq(HostVO_.status, HostStatus.Connected).isExists()) {
+            PrimaryStorageVO psvo = dbf.findByUuid(msg.getPrimaryStorageUuid(), PrimaryStorageVO.class);
+            if (psvo == null) {
+                completion.fail(operr("cannot find nfs primary storage[uuid:%s]", msg.getPrimaryStorageUuid()));
+                return;
+            }
+            targetHostUuid = nfsFactory.getConnectedHostForOperation(PrimaryStorageInventory.valueOf(psvo)).get(0).getUuid();
+        }
+
         KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg();
         hmsg.setCommand(cmd);
         hmsg.setPath(CLEANUP_VM_METADATA_PATH);
-        hmsg.setHostUuid(hostUuid);
-        bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid);
+        hmsg.setHostUuid(targetHostUuid);
+        bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, targetHostUuid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`
around lines 2168 - 2177, 在
NfsPrimaryStorageKVMBackend.handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg...)
中不要直接用传入的 hostUuid 路由 KVMHostAsyncHttpCallMsg(hmsg),因为在 expunge/GC 场景 hostUuid
可能为空或不可用;改为当 msg.getHostUuid() 为 null 或该主机不可达时,查找任一已连接并挂载该 NFS
的可用主机(例如通过现有的主机查询/挂载检查方法、主机状态过滤器或已有的 findHostsByPrimaryStorageUuid/QueryHost
APIs),将找到的主机 UUID 赋给 hmsg.setHostUuid(...),并用该 UUID 调用
bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID,
selectedHostUuid);若没有可用主机则按原有逻辑通过 completion 返回失败。确保引用
CleanupVmInstanceMetadataOnPrimaryStorageMsg、CleanupVmMetadataCmd、KVMHostAsyncHttpCallMsg、handle
和 bus.makeTargetServiceIdByResourceUuid 等标识以便定位修改点。

@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/ZSV-11559@@4 branch 2 times, most recently from 300aab9 to 5f516f9 Compare March 26, 2026 09:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java (1)

2173-2177: ⚠️ Potential issue | 🔴 Critical

清理路由不要直接依赖传入的 hostUuid

expunge/GC 场景里这个值可能为空,或者对应主机已经不可用;这里直接拿它做 setHostUuid(...)makeTargetServiceIdByResourceUuid(...),会把本来可以在任意已挂载 NFS 的可用主机上完成的清理,变成消息路由失败。建议在为空或不可用时回退到 nfsFactory.getConnectedHostForOperation(...) 选出的主机。

💡 建议修改
         CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
         cmd.setUuid(msg.getPrimaryStorageUuid());
         cmd.metadataPath = msg.getMetadataPath();
 
+        String targetHostUuid = hostUuid;
+        if (targetHostUuid == null || !Q.New(HostVO.class)
+                .eq(HostVO_.uuid, targetHostUuid)
+                .eq(HostVO_.status, HostStatus.Connected)
+                .isExists()) {
+            PrimaryStorageVO ps = dbf.findByUuid(msg.getPrimaryStorageUuid(), PrimaryStorageVO.class);
+            if (ps == null) {
+                completion.fail(operr("cannot find nfs primary storage[uuid:%s]", msg.getPrimaryStorageUuid()));
+                return;
+            }
+
+            List<HostInventory> hosts = nfsFactory.getConnectedHostForOperation(PrimaryStorageInventory.valueOf(ps));
+            if (hosts.isEmpty()) {
+                completion.fail(operr("no connected host found for nfs primary storage[uuid:%s]", msg.getPrimaryStorageUuid()));
+                return;
+            }
+
+            targetHostUuid = hosts.get(0).getUuid();
+        }
+
         KVMHostAsyncHttpCallMsg hmsg = new KVMHostAsyncHttpCallMsg();
         hmsg.setCommand(cmd);
         hmsg.setPath(CLEANUP_VM_METADATA_PATH);
-        hmsg.setHostUuid(hostUuid);
-        bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, hostUuid);
+        hmsg.setHostUuid(targetHostUuid);
+        bus.makeTargetServiceIdByResourceUuid(hmsg, HostConstant.SERVICE_ID, targetHostUuid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`
around lines 2173 - 2177, The code currently uses the incoming hostUuid directly
when building the KVMHostAsyncHttpCallMsg (KVMHostAsyncHttpCallMsg, setHostUuid,
makeTargetServiceIdByResourceUuid), which breaks GC/expunge flows if hostUuid is
null or the host is unavailable; change the logic to validate the incoming
hostUuid and if it is null or not reachable, call
nfsFactory.getConnectedHostForOperation(...) to select a usable connected host,
set that host's UUID on the message via setHostUuid(...), and then call
bus.makeTargetServiceIdByResourceUuid(...) with the selected host UUID so the
cleanup can be routed to any available NFS-mounted host.
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java (2)

2039-2060: ⚠️ Potential issue | 🔴 Critical

cleanup 需要和 update 走同一条串行化通道。

这里直接下发 backend,仍然绕过了上面的 ChainTask。如果 expunge cleanup 和 register/update 并发命中同一个 metadataPath,cleanup 可能删掉刚写好的文件或打断写入。建议复用 update 的 sync signature(至少按 rootVolumeUuid 或 metadataPath)把 cleanup 串起来。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 2039 - 2060, The cleanup handler currently calls
backend.handle(...) directly (in
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg)) and bypasses the ChainTask
serialization used by update, risking race with register/update; change this to
enqueue the cleanup as a ChainTask using the same sync signature logic as the
update path (serialize by rootVolumeUuid or metadataPath) so cleanup runs on the
same serialized channel; locate the handler that builds ChainTask for update
(the update/register path that composes the sync signature) and reuse that
signature and queuing pattern instead of calling backend.handle directly, then
inside the ChainTask invoke the backend cleanup and reply with the existing
CleanupVmInstanceMetadataOnPrimaryStorageReply on success/failure.

1935-1944: ⚠️ Potential issue | 🔴 Critical

把 metadata 更新的串行化粒度收敛到 VM/根云盘。

现在的 sync key 只有 primary storage,而且 getSyncLevel() 允许 10 并发。同一台 VM 的两次 metadata 更新会并行落到同一路径上,覆盖写时很容易丢更新或写出半成品。这里至少要把 key 带上 msg.getRootVolumeUuid()(或 metadataPath),并把并发收敛到 1。

💡 建议修改
             `@Override`
             public String getSyncSignature() {
-                return "update-metadata-on-ps-" + self.getUuid();
+                return "update-metadata-on-ps-" + self.getUuid() + "-root-volume-" + msg.getRootVolumeUuid();
             }

             `@Override`
             public int getSyncLevel() {
-                return 10;
+                return 1;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 1935 - 1944, 当前把 metadata 更新的同步签名仅用 primary storage UUID(在
thdf.chainSubmit 的 ChainTask.getSyncSignature 返回 "update-metadata-on-ps-" +
self.getUuid())且 getSyncLevel() 返回 10,导致同一 VM 的并发 metadata 更新可能并行执行并互相覆盖。请把同步
key 收敛到 VM/根云盘粒度,例如在 getSyncSignature() 中包含 msg.getRootVolumeUuid() 或
metadataPath(例如 "update-metadata-on-ps-" + self.getUuid() + "-root-" +
msg.getRootVolumeUuid()),并把 getSyncLevel() 改为 1,以确保同一卷的 metadata
更新按序执行;调整对应代码位置为 thdf.chainSubmit(...) 内的 ChainTask 匿名类中的 getSyncSignature() 和
getSyncLevel() 实现。
compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java (1)

89-123: ⚠️ Potential issue | 🔴 Critical

不要把可能为 nullhostUuid 固化到 cleanup/GC 里。

expunge 场景下 hostUuid/lastHostUuid 都可能拿不到。这里仍然把它写进 cmsg 和 GC 字段,第一次 cleanup 失败后,后续 GC 重试也拿不到可路由的宿主机,会一直重复失败。更稳妥的是:拿不到 host 时不要固化空值,改成在提交 GC 前补一个兜底 host,或把 host 选择延后到 GC/handler 运行时再做。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 89 - 123, In VmExpungeMetadataFlow, avoid persisting a null
hostUuid into the outgoing cmsg and into the GC record: if
spec.getVmInventory().getHostUuid()/getLastHostUuid() yields null, do not call
cmsg.setHostUuid(hostUuid) with a null value and do not set
CleanupVmInstanceMetadataOnPrimaryStorageGC.hostUuid to null; instead either (a)
resolve a safe fallback host for the primary storage (e.g. a helper that picks
any reachable host for psUuid) and populate cmsg and gc.hostUuid with that
fallback before sending/submitting, or (b) remove host selection here and leave
gc.hostUuid unset and move the host resolution logic into the GC handler so the
retry can pick a valid routable host at runtime. Ensure changes touch
VmExpungeMetadataFlow.run/send block (cmsg usage) and submitGC(...) assignment
of gc.hostUuid and keep other fields unchanged.
🧹 Nitpick comments (6)
header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java (2)

9-24: 可选:使用 Stream 简化批量解析逻辑。

当前 for-loop 实现正确,但可以使用 Java Stream 使代码更简洁。

♻️ Stream 风格的实现
     default List<String> batchResolveVmUuids(List<String> fieldValues) {
-        List<String> result = new ArrayList<>();
         if (fieldValues == null || fieldValues.isEmpty()) {
-            return result;
+            return new ArrayList<>();
         }
-        for (String v : fieldValues) {
-            if (v == null) {
-                continue;
-            }
-            String vmUuid = resolveVmUuid(v);
-            if (vmUuid != null) {
-                result.add(vmUuid);
-            }
-        }
-        return result;
+        return fieldValues.stream()
+                .filter(v -> v != null)
+                .map(this::resolveVmUuid)
+                .filter(uuid -> uuid != null)
+                .collect(java.util.stream.Collectors.toList());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java`
around lines 9 - 24, The batchResolveVmUuids method currently uses an explicit
for-loop to filter null inputs, call resolveVmUuid for each, and collect
non-null results; refactor it to a Stream-based implementation: use
Optional.ofNullable(fieldValues).orElseGet(Collections::emptyList).stream() (or
Stream.ofNullable depending on style) to stream the values, filter out null
entries, map each value via resolveVmUuid, filter out null results, and collect
to a List; keep the method signature and behavior unchanged (return empty list
when input is null/empty) and ensure references to batchResolveVmUuids and
resolveVmUuid remain correct.

6-7: 建议为接口方法添加 Javadoc 注释。

根据编码规范,接口方法必须配有有效的 Javadoc 注释,说明方法用途、参数含义及返回值。

📝 建议添加 Javadoc
 public interface VmUuidFromApiResolver {
+    /**
+     * Resolves a VM UUID from the given API field value.
+     *
+     * `@param` fieldValue the field value extracted from API message
+     * `@return` the resolved VM UUID, or null if resolution fails
+     */
     String resolveVmUuid(String fieldValue);

As per coding guidelines: "接口方法不应有多余的修饰符(例如 public),且必须配有有效的 Javadoc 注释"。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java`
around lines 6 - 7, Add proper Javadoc to the VmUuidFromApiResolver interface
and its method resolveVmUuid(String fieldValue): document the interface purpose,
explain the parameter fieldValue and what constitutes a valid input, and
describe the return value (what VM UUID it returns or null/exception semantics).
Ensure the method's Javadoc appears directly above the resolveVmUuid declaration
and that no unnecessary modifiers are added to the interface method declaration
(leave it with default interface visibility).
testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy (1)

604-618: 建议:考虑添加 VFS hooks 以支持更完整的测试场景

当前仅注册了返回空响应的基础模拟器,对于初始集成测试是足够的。但如果需要测试以下场景,建议后续添加 VFS hooks:

  • SCAN_VM_METADATA_PATH: 返回实际扫描到的元数据条目
  • WRITE_VM_METADATA_PATH: 在 VFS 中持久化元数据
  • GET_VM_INSTANCE_METADATA_PATH: 从 VFS 读取已写入的元数据

可参考 VFS.vfsHook(LocalStorageKvmBackend.CREATE_VOLUME_FROM_CACHE_PATH, ...) 等现有实现模式。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy` around
lines 604 - 618, The simulators for LocalStorageKvmBackend currently return
empty responses only; add VFS hooks using VFS.vfsHook similar to the existing
CREATE_VOLUME_FROM_CACHE_PATH pattern to persist and serve VM metadata so tests
can exercise full flows: implement hooks for
LocalStorageKvmBackend.WRITE_VM_METADATA_PATH to store metadata into an
in-memory VFS store and return LocalStorageKvmBackend.WriteVmMetadataRsp, for
GET_VM_INSTANCE_METADATA_PATH to read and return stored metadata via
LocalStorageKvmBackend.GetVmInstanceMetadataRsp, for SCAN_VM_METADATA_PATH to
enumerate and return found entries via LocalStorageKvmBackend.ScanVmMetadataRsp,
and ensure CLEANUP_VM_METADATA_PATH removes entries and returns
LocalStorageKvmBackend.CleanupVmMetadataRsp; keep the response object types from
the diff and use the same keying scheme the other VFS hooks use so tests can
read/write/scan/cleanup metadata.
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java (1)

29-38: 建议重用 resolveBaseDir 方法以保持一致性。

calculatePathReplacements 方法中的 mountPath/url 查询和路径规范化逻辑与 resolveBaseDir 重复。建议重用以减少代码冗余:

♻️ 建议重构
     `@Override`
     public PathReplacementResult calculatePathReplacements(String targetPsUuid, List<String> allOldPaths) {
         if (allOldPaths == null || allOldPaths.isEmpty()) {
             PathReplacementResult result = new PathReplacementResult();
             result.setMetadataToCurrentPathMap(Collections.emptyMap());
             return result;
         }
 
-        String baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.mountPath).eq(PrimaryStorageVO_.uuid, targetPsUuid).findValue();
-        if (baseDir == null) {
-            baseDir = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.url).eq(PrimaryStorageVO_.uuid, targetPsUuid).findValue();
-        }
-        if (baseDir == null) {
+        String baseDir;
+        try {
+            baseDir = resolveBaseDir(targetPsUuid);
+        } catch (CloudRuntimeException e) {
             logger.warn(String.format("NFS PS[uuid:%s] has no mountPath or url, path replacement disabled", targetPsUuid));
             PathReplacementResult result = new PathReplacementResult();
             result.setMetadataToCurrentPathMap(Collections.emptyMap());
             return result;
         }
-        String newPrefix = baseDir.endsWith("/") ? baseDir : baseDir + "/";
+        String newPrefix = baseDir + "/";

Also applies to: 80-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java`
around lines 29 - 38, The calculatePathReplacements method duplicates the
mountPath/url query and trailing-slash normalization logic found in
resolveBaseDir; remove that duplication by calling
resolveBaseDir(primaryStorageUuid) from calculatePathReplacements instead of
re-querying PrimaryStorageVO (PrimaryStorageVO_.mountPath /
PrimaryStorageVO_.url) and redoing the endsWith("/") trim, and propagate or
handle the CloudRuntimeException from resolveBaseDir so behavior remains
identical.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java (1)

98-120: 存在与 NfsVmMetadataExtension 的代码重复。

validateMetadataPathnormalizeBaseDir 的实现与 NfsVmMetadataExtension 几乎完全相同。考虑到后续可能新增更多存储类型的扩展,建议将通用逻辑提取到基类或工具类中。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java`
around lines 98 - 120, The LocalStorageVmMetadataExtension duplicates logic from
NfsVmMetadataExtension for validateMetadataPath and normalizeBaseDir; extract
the shared logic into a common base class or utility (e.g.,
VmMetadataExtensionBase or VmMetadataUtils) and have both
LocalStorageVmMetadataExtension and NfsVmMetadataExtension delegate to it. Move
the validation routine (using buildMetadataDir,
VmInstanceMetadataConstants.FILE_METADATA_SUFFIX, the prefix/suffix checks, and
the 32-char hex regex) and the normalizeBaseDir implementation into the shared
class/utility, update both classes to call the common methods, and ensure
buildMetadataDir remains usable (or also refactor it into the base) so behavior
is unchanged.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java (1)

3907-3912: 可简化 metadataEntries 的空值分支,减少重复分支处理。

Line 3907-3912 可以统一为“先归一化为非空列表,再设置 hostUuid 并回填 reply”,逻辑更直观。

♻️ 建议写法
             public void success(ScanVmMetadataRsp rsp) {
                 ScanVmInstanceMetadataFromPrimaryStorageReply reply = new ScanVmInstanceMetadataFromPrimaryStorageReply();
-                if (rsp.metadataEntries != null) {
-                    rsp.metadataEntries.forEach(entry -> entry.setHostUuid(hostUuid));
-                    reply.setVmInstanceMetadata(rsp.metadataEntries);
-                } else {
-                    reply.setVmInstanceMetadata(Collections.emptyList());
-                }
+                List<VmMetadataScanEntry> entries = rsp.metadataEntries == null
+                        ? Collections.emptyList()
+                        : rsp.metadataEntries;
+                entries.forEach(entry -> entry.setHostUuid(hostUuid));
+                reply.setVmInstanceMetadata(entries);
                 completion.success(reply);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`
around lines 3907 - 3912, The null-check for rsp.metadataEntries can be
simplified by normalizing it to a non-null list and then performing the hostUuid
assignment and reply population once; replace the conditional with code that
assigns List metadata = rsp.metadataEntries == null ? Collections.emptyList() :
rsp.metadataEntries, then iterate metadata to call entry.setHostUuid(hostUuid)
and finally call reply.setVmInstanceMetadata(metadata) (referencing
rsp.metadataEntries, entry.setHostUuid, and reply.setVmInstanceMetadata).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java`:
- Line 16: APIDeleteTagMsg 使用了仅适用于系统标签的 SystemTagUuidToVmUuidResolver
导致非系统标签删除时元数据影响解析错误;请在 APIDeleteTagMsg 上替换或移除该注解:要么实现并引用一个通用的
TagUuidToVmUuidResolver(支持系统标签、用户标签和标签模式)并将注解的 resolver 字段改为该类名,以保证 UUID
正确解析和元数据影响跟踪;要么直接移除 APIDeleteTagMsg 上的 `@MetadataImpact` 注解(若仅系统标签需要 VM
元数据跟踪),两种方案任选其一并在相应的单元/集成测试中验证删除系统与非系统标签时的元数据影响行为。

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`:
- Around line 3924-3929: In handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg
msg, String hostUuid,
ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>
completion) validate that hostUuid is non-null before calling
httpCall(CLEANUP_VM_METADATA_PATH,...); if hostUuid is null, either fail-fast by
completing the completion with an error reply or attempt a fallback resolution
(e.g., derive hostUuid from fields on
CleanupVmInstanceMetadataOnPrimaryStorageMsg such as a host reference or
vm/volume identifiers) and only proceed to construct CleanupVmMetadataCmd and
invoke httpCall when a valid hostUuid is obtained; ensure the failure path sets
a clear error on CleanupVmInstanceMetadataOnPrimaryStorageReply so callers can
handle the routing failure.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java`:
- Around line 63-66: The new metadata command lists
(LocalStorageSimulatorConfig.writeVmMetadataCmds, getVmInstanceMetadataCmds,
scanVmMetadataCmds, cleanupVmMetadataCmds) are being modified without
synchronization; wrap all modifications to these lists inside
synchronized(config) blocks in the handlers writeVmMetadata(),
getVmInstanceMetadata(), scanVmMetadata(), and cleanupVmMetadata() in
LocalStorageSimulator so additions use e.g. synchronized(config) {
config.writeVmMetadataCmds.add(cmd); } (do the same pattern for the other three
lists) to prevent concurrent modification races.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`:
- Around line 2089-2095: The success branches for handling WriteVmMetadataRsp
and CleanupVmMetadataRsp are missing the primary storage capacity refresh; after
confirming rsp.isSuccess() (in the blocks handling WriteVmMetadataRsp and
CleanupVmMetadataRsp) call updatePrimaryStorageCapacity(...) the same way
asyncHttpCall(...) does before calling completion.success(...), so capacity is
recalculated after metadata file create/delete; reference the response classes
WriteVmMetadataRsp and CleanupVmMetadataRsp, the
updatePrimaryStorageCapacity(...) helper, and ensure you still invoke
completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply()) after
the capacity update.

In `@storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java`:
- Around line 1889-1892: 当前代码在构建 VM 元数据扫描结果时仅排除了
VmMetadataCategory.TEMPLATE_CACHE,导致 CATEGORY 为 TEMPLATE 的条目也被返回给 VM(通过
evt.setVmInstanceMetadata),使结果集中包含非 VM 元数据;请修改过滤逻辑(在处理 metadata 的流中,对
VmMetadataScanEntry 使用白名单过滤),只保留 VmMetadataCategory.REGULAR 的条目(识别符例如
VmMetadataScanEntry#getVmCategory、VmMetadataCategory、evt.setVmInstanceMetadata、metadata),确保只有
REGULAR 类型被收集并传入 evt.setVmInstanceMetadata。

---

Duplicate comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`:
- Around line 89-123: In VmExpungeMetadataFlow, avoid persisting a null hostUuid
into the outgoing cmsg and into the GC record: if
spec.getVmInventory().getHostUuid()/getLastHostUuid() yields null, do not call
cmsg.setHostUuid(hostUuid) with a null value and do not set
CleanupVmInstanceMetadataOnPrimaryStorageGC.hostUuid to null; instead either (a)
resolve a safe fallback host for the primary storage (e.g. a helper that picks
any reachable host for psUuid) and populate cmsg and gc.hostUuid with that
fallback before sending/submitting, or (b) remove host selection here and leave
gc.hostUuid unset and move the host resolution logic into the GC handler so the
retry can pick a valid routable host at runtime. Ensure changes touch
VmExpungeMetadataFlow.run/send block (cmsg usage) and submitGC(...) assignment
of gc.hostUuid and keep other fields unchanged.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`:
- Around line 2039-2060: The cleanup handler currently calls backend.handle(...)
directly (in handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg)) and bypasses
the ChainTask serialization used by update, risking race with register/update;
change this to enqueue the cleanup as a ChainTask using the same sync signature
logic as the update path (serialize by rootVolumeUuid or metadataPath) so
cleanup runs on the same serialized channel; locate the handler that builds
ChainTask for update (the update/register path that composes the sync signature)
and reuse that signature and queuing pattern instead of calling backend.handle
directly, then inside the ChainTask invoke the backend cleanup and reply with
the existing CleanupVmInstanceMetadataOnPrimaryStorageReply on success/failure.
- Around line 1935-1944: 当前把 metadata 更新的同步签名仅用 primary storage UUID(在
thdf.chainSubmit 的 ChainTask.getSyncSignature 返回 "update-metadata-on-ps-" +
self.getUuid())且 getSyncLevel() 返回 10,导致同一 VM 的并发 metadata 更新可能并行执行并互相覆盖。请把同步
key 收敛到 VM/根云盘粒度,例如在 getSyncSignature() 中包含 msg.getRootVolumeUuid() 或
metadataPath(例如 "update-metadata-on-ps-" + self.getUuid() + "-root-" +
msg.getRootVolumeUuid()),并把 getSyncLevel() 改为 1,以确保同一卷的 metadata
更新按序执行;调整对应代码位置为 thdf.chainSubmit(...) 内的 ChainTask 匿名类中的 getSyncSignature() 和
getSyncLevel() 实现。

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`:
- Around line 2173-2177: The code currently uses the incoming hostUuid directly
when building the KVMHostAsyncHttpCallMsg (KVMHostAsyncHttpCallMsg, setHostUuid,
makeTargetServiceIdByResourceUuid), which breaks GC/expunge flows if hostUuid is
null or the host is unavailable; change the logic to validate the incoming
hostUuid and if it is null or not reachable, call
nfsFactory.getConnectedHostForOperation(...) to select a usable connected host,
set that host's UUID on the message via setHostUuid(...), and then call
bus.makeTargetServiceIdByResourceUuid(...) with the selected host UUID so the
cleanup can be routed to any available NFS-mounted host.

---

Nitpick comments:
In
`@header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java`:
- Around line 9-24: The batchResolveVmUuids method currently uses an explicit
for-loop to filter null inputs, call resolveVmUuid for each, and collect
non-null results; refactor it to a Stream-based implementation: use
Optional.ofNullable(fieldValues).orElseGet(Collections::emptyList).stream() (or
Stream.ofNullable depending on style) to stream the values, filter out null
entries, map each value via resolveVmUuid, filter out null results, and collect
to a List; keep the method signature and behavior unchanged (return empty list
when input is null/empty) and ensure references to batchResolveVmUuids and
resolveVmUuid remain correct.
- Around line 6-7: Add proper Javadoc to the VmUuidFromApiResolver interface and
its method resolveVmUuid(String fieldValue): document the interface purpose,
explain the parameter fieldValue and what constitutes a valid input, and
describe the return value (what VM UUID it returns or null/exception semantics).
Ensure the method's Javadoc appears directly above the resolveVmUuid declaration
and that no unnecessary modifiers are added to the interface method declaration
(leave it with default interface visibility).

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`:
- Around line 3907-3912: The null-check for rsp.metadataEntries can be
simplified by normalizing it to a non-null list and then performing the hostUuid
assignment and reply population once; replace the conditional with code that
assigns List metadata = rsp.metadataEntries == null ? Collections.emptyList() :
rsp.metadataEntries, then iterate metadata to call entry.setHostUuid(hostUuid)
and finally call reply.setVmInstanceMetadata(metadata) (referencing
rsp.metadataEntries, entry.setHostUuid, and reply.setVmInstanceMetadata).

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java`:
- Around line 98-120: The LocalStorageVmMetadataExtension duplicates logic from
NfsVmMetadataExtension for validateMetadataPath and normalizeBaseDir; extract
the shared logic into a common base class or utility (e.g.,
VmMetadataExtensionBase or VmMetadataUtils) and have both
LocalStorageVmMetadataExtension and NfsVmMetadataExtension delegate to it. Move
the validation routine (using buildMetadataDir,
VmInstanceMetadataConstants.FILE_METADATA_SUFFIX, the prefix/suffix checks, and
the 32-char hex regex) and the normalizeBaseDir implementation into the shared
class/utility, update both classes to call the common methods, and ensure
buildMetadataDir remains usable (or also refactor it into the base) so behavior
is unchanged.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java`:
- Around line 29-38: The calculatePathReplacements method duplicates the
mountPath/url query and trailing-slash normalization logic found in
resolveBaseDir; remove that duplication by calling
resolveBaseDir(primaryStorageUuid) from calculatePathReplacements instead of
re-querying PrimaryStorageVO (PrimaryStorageVO_.mountPath /
PrimaryStorageVO_.url) and redoing the endsWith("/") trim, and propagate or
handle the CloudRuntimeException from resolveBaseDir so behavior remains
identical.

In `@testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy`:
- Around line 604-618: The simulators for LocalStorageKvmBackend currently
return empty responses only; add VFS hooks using VFS.vfsHook similar to the
existing CREATE_VOLUME_FROM_CACHE_PATH pattern to persist and serve VM metadata
so tests can exercise full flows: implement hooks for
LocalStorageKvmBackend.WRITE_VM_METADATA_PATH to store metadata into an
in-memory VFS store and return LocalStorageKvmBackend.WriteVmMetadataRsp, for
GET_VM_INSTANCE_METADATA_PATH to read and return stored metadata via
LocalStorageKvmBackend.GetVmInstanceMetadataRsp, for SCAN_VM_METADATA_PATH to
enumerate and return found entries via LocalStorageKvmBackend.ScanVmMetadataRsp,
and ensure CLEANUP_VM_METADATA_PATH removes entries and returns
LocalStorageKvmBackend.CleanupVmMetadataRsp; keep the response object types from
the diff and use the same keying scheme the other VFS hooks use so tests can
read/write/scan/cleanup metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c29bac9a-3bc6-4dbb-b607-af8077f1658a

📥 Commits

Reviewing files that changed from the base of the PR and between 208c773 and 5f516f9.

⛔ Files ignored due to path filters (5)
  • conf/globalConfig/vm.xml is excluded by !**/*.xml
  • conf/persistence.xml is excluded by !**/*.xml
  • conf/serviceConfig/primaryStorage.xml is excluded by !**/*.xml
  • conf/serviceConfig/vmInstance.xml is excluded by !**/*.xml
  • conf/springConfigXml/VmInstanceManager.xml is excluded by !**/*.xml
📒 Files selected for processing (144)
  • compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java
  • compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java
  • testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
✅ Files skipped from review due to trivial changes (82)
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • sdk/src/main/java/SourceClassMap.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
🚧 Files skipped from review as they are similar to previous changes (33)
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java

method = HttpMethod.DELETE,
responseClass = APIDeleteTagEvent.class
)
@MetadataImpact(value = MetadataImpact.Impact.CONFIG, resolver = "SystemTagUuidToVmUuidResolver", field = "uuid")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 查找 SystemTagUuidToVmUuidResolver 的实现,确认其是否能处理非系统标签
ast-grep --pattern $'class SystemTagUuidToVmUuidResolver {
  $$$
}'

# 查找该 resolver 的 resolveVmUuids 方法实现
rg -n -A 20 'class SystemTagUuidToVmUuidResolver'

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Search for MetadataImpact annotation usage patterns
rg -n "MetadataImpact.*resolver" --type java -A 2

Repository: MatheMatrix/zstack

Length of output: 29381


🏁 Script executed:

# Find APIDeleteTagMsg definition
fd -e java -x grep -l "class APIDeleteTagMsg" {} \;

Repository: MatheMatrix/zstack

Length of output: 127


🏁 Script executed:

# Search for resolver implementations
rg -n "Resolver.*class|class.*Resolver" --type java | head -30

Repository: MatheMatrix/zstack

Length of output: 462


🏁 Script executed:

# Check for SystemTagUuidToVmUuidResolver references
rg -n "SystemTagUuidToVmUuidResolver" --type java

Repository: MatheMatrix/zstack

Length of output: 820


🏁 Script executed:

# Find system tag related APIs
rg -n "APICreateSystemTagMsg|APIDeleteSystemTagMsg" --type java | head -20

Repository: MatheMatrix/zstack

Length of output: 906


🏁 Script executed:

# Read APIDeleteTagMsg.java file
cat -n header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java

Repository: MatheMatrix/zstack

Length of output: 1214


🏁 Script executed:

# Check if there's a separate APIDeleteSystemTagMsg
rg -l "class APIDeleteSystemTagMsg" --type java

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Search for tag deletion patterns to understand the design
rg -n "deleteTag|delete.*tag" --type java -i | grep -i "api\|msg" | head -20

Repository: MatheMatrix/zstack

Length of output: 2504


🏁 Script executed:

# Check the resolver implementation
rg -n "class SystemTagUuidToVmUuidResolver" --type java -A 30

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Search for resolver implementations with different patterns
rg -n "SystemTagUuidToVmUuidResolver" --type java

Repository: MatheMatrix/zstack

Length of output: 820


🏁 Script executed:

# Look for MetadataImpact resolver implementations
rg -n "MetadataImpactResolver\|extends.*Resolver" --type java | grep -i "metadata\|tag" | head -20

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Check tag management for understanding how system tags vs user tags are handled
cat -n tag/src/main/java/org/zstack/tag/TagManagerImpl.java | head -100

Repository: MatheMatrix/zstack

Length of output: 5161


🏁 Script executed:

# Look for metadata impact related code
fd -e java -x grep -l "MetadataImpact" {} \; | head -10

Repository: MatheMatrix/zstack

Length of output: 888


🏁 Script executed:

# Search for resolver interfaces/base classes
rg -n "interface.*Resolver|abstract.*Resolver" --type java | head -20

Repository: MatheMatrix/zstack

Length of output: 182


🏁 Script executed:

# Find implementations of VmUuidFromApiResolver
rg -n "implements VmUuidFromApiResolver" --type java

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Search for any class that might be the SystemTagUuidToVmUuidResolver
rg -n "SystemTagUuidToVmUuidResolver" --type java -A 1 -B 1

Repository: MatheMatrix/zstack

Length of output: 1649


🏁 Script executed:

# Look at the VmUuidFromApiResolver interface definition
cat -n header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java

Repository: MatheMatrix/zstack

Length of output: 913


🏁 Script executed:

# Search for resolver registration or bean definitions
rg -n "@Bean.*Resolver|resolver.*bean" --type java -i | head -20

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Check TagApiInterceptor to understand how deletion works for both tag types
cat -n tag/src/main/java/org/zstack/tag/TagApiInterceptor.java | sed -n '98,160p'

Repository: MatheMatrix/zstack

Length of output: 3646


🏁 Script executed:

# Search for resolver implementation in tag module
find tag -name "*.java" -type f | xargs grep -l "SystemTagUuidToVmUuidResolver"

Repository: MatheMatrix/zstack

Length of output: 46


🏁 Script executed:

# Search more broadly for any resolver implementations
rg -n "class.*VmUuidFromApiResolver|implements VmUuidFromApiResolver" --type java -A 10

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Look for metadata impact framework to understand how resolvers are used
rg -n "class.*MetadataImpact|interface.*MetadataImpact" --type java

Repository: MatheMatrix/zstack

Length of output: 169


🏁 Script executed:

# Check if there's explicit handling in tag deletion for system vs user tags
cat -n tag/src/main/java/org/zstack/tag/TagManagerImpl.java | sed -n '726,760p'

Repository: MatheMatrix/zstack

Length of output: 1814


🏁 Script executed:

# Read the MetadataImpact annotation definition
cat -n header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java

Repository: MatheMatrix/zstack

Length of output: 1434


🏁 Script executed:

# Search for actual resolver implementations or bean definitions
find . -name "*.java" -type f | xargs grep -l "VmUuidFromApiResolver" 2>/dev/null | head -10

Repository: MatheMatrix/zstack

Length of output: 214


🏁 Script executed:

# Check for any resolver bean configurations
rg -n "@Component|@Bean|@Service.*Resolver" --type java | grep -i "resolver\|tag" | head -20

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Look for how the resolver string is resolved/used in metadata framework
rg -n "resolver.*=|getResolver" --type java | head -30

Repository: MatheMatrix/zstack

Length of output: 6014


🏁 Script executed:

# Search for metadata impact interceptor/framework implementation
rg -n "class.*MetadataImpactInterceptor|MetadataImpact.*interceptor" --type java -A 10 -i

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

# Look for how the resolver is invoked and what happens on failure/null
rg -n "resolveVmUuid|batchResolveVmUuids" --type java | head -20

Repository: MatheMatrix/zstack

Length of output: 605


🏁 Script executed:

# Check for null/empty handling in resolver usage
rg -n "resolveVmUuids.*null|== null.*resolveVmUuids" --type java

Repository: MatheMatrix/zstack

Length of output: 44


使用通用标签删除 API 应避免使用系统标签专用 resolver。

APIDeleteTagMsg 是通用标签删除 API,可删除系统标签、用户标签和标签模式,但注解使用的 SystemTagUuidToVmUuidResolver 仅适用于系统标签。当删除非系统标签时,该 resolver 无法正确解析 UUID,导致元数据影响跟踪失效。

建议:

  1. 为通用标签删除创建专用 resolver(如 TagUuidToVmUuidResolver),支持所有标签类型;或
  2. 移除 @MetadataImpact 注解,因为只有系统标签需要 VM 元数据跟踪。

参考现有设计:其他通用 DELETE API(如 APIDeleteResourceConfigMsg)使用专用 resolver(ConfigUuidToVmUuidResolver)处理资源特定的元数据映射。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java` at line 16,
APIDeleteTagMsg 使用了仅适用于系统标签的 SystemTagUuidToVmUuidResolver
导致非系统标签删除时元数据影响解析错误;请在 APIDeleteTagMsg 上替换或移除该注解:要么实现并引用一个通用的
TagUuidToVmUuidResolver(支持系统标签、用户标签和标签模式)并将注解的 resolver 字段改为该类名,以保证 UUID
正确解析和元数据影响跟踪;要么直接移除 APIDeleteTagMsg 上的 `@MetadataImpact` 注解(若仅系统标签需要 VM
元数据跟踪),两种方案任选其一并在相应的单元/集成测试中验证删除系统与非系统标签时的元数据影响行为。

Comment on lines +3924 to +3929
void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply> completion) {
CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
cmd.metadataPath = msg.getMetadataPath();

httpCall(CLEANUP_VM_METADATA_PATH, hostUuid, cmd, CleanupVmMetadataRsp.class, new ReturnValueCompletion<CleanupVmMetadataRsp>(completion) {
@Override

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

清理元数据前缺少 hostUuid 兜底/校验,存在路由失败风险。

Line 3928 直接使用 hostUuid 发起 httpCall。当 hostUuid 为空时,会在消息路由阶段失败,导致元数据清理无法执行。建议至少先 fail-fast,并优先补充 host 解析兜底逻辑后再下发。

🔧 最小修复(先避免隐式路由失败)
     `@Override`
     void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply> completion) {
+        if (StringUtils.isBlank(hostUuid)) {
+            completion.fail(operr("cleanup vm metadata failed: hostUuid is null, metadataPath=%s", msg.getMetadataPath()));
+            return;
+        }
         CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
         cmd.metadataPath = msg.getMetadataPath();

         httpCall(CLEANUP_VM_METADATA_PATH, hostUuid, cmd, CleanupVmMetadataRsp.class, new ReturnValueCompletion<CleanupVmMetadataRsp>(completion) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`
around lines 3924 - 3929, In handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg
msg, String hostUuid,
ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>
completion) validate that hostUuid is non-null before calling
httpCall(CLEANUP_VM_METADATA_PATH,...); if hostUuid is null, either fail-fast by
completing the completion with an error reply or attempt a fallback resolution
(e.g., derive hostUuid from fields on
CleanupVmInstanceMetadataOnPrimaryStorageMsg such as a host reference or
vm/volume identifiers) and only proceed to construct CleanupVmMetadataCmd and
invoke httpCall when a valid hostUuid is obtained; ensure the failure path sets
a clear error on CleanupVmInstanceMetadataOnPrimaryStorageReply so callers can
handle the routing failure.

Comment on lines +63 to +66
public List<WriteVmMetadataCmd> writeVmMetadataCmds = new ArrayList<>();
public List<GetVmInstanceMetadataCmd> getVmInstanceMetadataCmds = new ArrayList<>();
public List<ScanVmMetadataCmd> scanVmMetadataCmds = new ArrayList<>();
public List<CleanupVmMetadataCmd> cleanupVmMetadataCmds = new ArrayList<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 验证 LocalStorageSimulator.java 中新增的元数据命令处理器是否使用了 synchronized(config) 同步块

# 搜索 writeVmMetadata、getVmInstanceMetadata、scanVmMetadata、cleanupVmMetadata 相关的处理器
rg -n -A 10 'writeVmMetadataCmds|getVmInstanceMetadataCmds|scanVmMetadataCmds|cleanupVmMetadataCmds' --type java

Repository: MatheMatrix/zstack

Length of output: 6571


🏁 Script executed:

# Check the synchronization pattern in LocalStorageSimulator.java around lines 235-260
sed -n '230,270p' plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java

Repository: MatheMatrix/zstack

Length of output: 1693


新增的元数据命令处理器缺少线程同步保护

虽然新增的四个字段遵循了现有的命令列表模式,但是在 LocalStorageSimulator.java 中对这些列表的访问缺少必要的同步保护。经检查发现:

  • delete()deleteDir() 处理器(第 243-248、250-257 行)正确地使用 synchronized(config) 块包装 ArrayList 修改
  • 新增的 writeVmMetadata()getVmInstanceMetadata()scanVmMetadata()cleanupVmMetadata() 处理器(第 341、350、360、370 行)直接对列表调用 .add() 方法,未使用同步保护

这会导致多个并发 HTTP 请求在修改共享 ArrayList 时产生竞态条件。需要为所有新增的命令列表访问添加 synchronized(config) 块:

synchronized (config) {
    config.writeVmMetadataCmds.add(cmd);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java`
around lines 63 - 66, The new metadata command lists
(LocalStorageSimulatorConfig.writeVmMetadataCmds, getVmInstanceMetadataCmds,
scanVmMetadataCmds, cleanupVmMetadataCmds) are being modified without
synchronization; wrap all modifications to these lists inside
synchronized(config) blocks in the handlers writeVmMetadata(),
getVmInstanceMetadata(), scanVmMetadata(), and cleanupVmMetadata() in
LocalStorageSimulator so additions use e.g. synchronized(config) {
config.writeVmMetadataCmds.add(cmd); } (do the same pattern for the other three
lists) to prevent concurrent modification races.

Comment on lines +2089 to +2095
WriteVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(WriteVmMetadataRsp.class);
if (!rsp.isSuccess()) {
completion.fail(operr("failed to write vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError()));
return;
}

completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

写/删元数据成功后把容量刷新漏掉了。

WriteVmMetadataRspCleanupVmMetadataRsp 都继承了 NfsPrimaryStorageAgentResponse,但这里成功后没有像 asyncHttpCall(...) 那样调用 updatePrimaryStorageCapacity(...)。元数据文件的创建/删除会让主存容量在下一次 ping 前保持陈旧。

💡 建议修改
                 WriteVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(WriteVmMetadataRsp.class);
                 if (!rsp.isSuccess()) {
                     completion.fail(operr("failed to write vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError()));
                     return;
                 }
+                updatePrimaryStorageCapacity(msg.getPrimaryStorageUuid(), rsp);
 
                 completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply());
@@
                 CleanupVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(CleanupVmMetadataRsp.class);
                 if (!rsp.isSuccess()) {
                     completion.fail(operr("failed to cleanup vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError()));
                     return;
                 }
+                updatePrimaryStorageCapacity(msg.getPrimaryStorageUuid(), rsp);
 
                 CleanupVmInstanceMetadataOnPrimaryStorageReply r = new CleanupVmInstanceMetadataOnPrimaryStorageReply();
                 completion.success(r);

Also applies to: 2186-2193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`
around lines 2089 - 2095, The success branches for handling WriteVmMetadataRsp
and CleanupVmMetadataRsp are missing the primary storage capacity refresh; after
confirming rsp.isSuccess() (in the blocks handling WriteVmMetadataRsp and
CleanupVmMetadataRsp) call updatePrimaryStorageCapacity(...) the same way
asyncHttpCall(...) does before calling completion.success(...), so capacity is
recalculated after metadata file create/delete; reference the response classes
WriteVmMetadataRsp and CleanupVmMetadataRsp, the
updatePrimaryStorageCapacity(...) helper, and ensure you still invoke
completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply()) after
the capacity update.

Comment on lines +1889 to +1892
List<VmMetadataScanEntry> filtered = metadata.stream()
.filter(e -> !VmMetadataCategory.TEMPLATE_CACHE.toString().equals(e.getVmCategory()))
.collect(Collectors.toList());
evt.setVmInstanceMetadata(filtered);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

这里会把 TEMPLATE 条目也返回给 VM 扫描结果。

当前只排除了 TEMPLATE_CACHE,但 VmMetadataCategory 里还有 TEMPLATE。这些条目会直接进入 evt.setVmInstanceMetadata(...),结果集就不再是纯 VM metadata。这里更稳妥的是按 REGULAR 做白名单过滤。

💡 建议修改
-                List<VmMetadataScanEntry> filtered = metadata.stream()
-                        .filter(e -> !VmMetadataCategory.TEMPLATE_CACHE.toString().equals(e.getVmCategory()))
-                        .collect(Collectors.toList());
+                List<VmMetadataScanEntry> filtered = metadata.stream()
+                        .filter(e -> VmMetadataCategory.REGULAR.toString().equals(e.getVmCategory()))
+                        .collect(Collectors.toList());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
List<VmMetadataScanEntry> filtered = metadata.stream()
.filter(e -> !VmMetadataCategory.TEMPLATE_CACHE.toString().equals(e.getVmCategory()))
.collect(Collectors.toList());
evt.setVmInstanceMetadata(filtered);
List<VmMetadataScanEntry> filtered = metadata.stream()
.filter(e -> VmMetadataCategory.REGULAR.toString().equals(e.getVmCategory()))
.collect(Collectors.toList());
evt.setVmInstanceMetadata(filtered);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java`
around lines 1889 - 1892, 当前代码在构建 VM 元数据扫描结果时仅排除了
VmMetadataCategory.TEMPLATE_CACHE,导致 CATEGORY 为 TEMPLATE 的条目也被返回给 VM(通过
evt.setVmInstanceMetadata),使结果集中包含非 VM 元数据;请修改过滤逻辑(在处理 metadata 的流中,对
VmMetadataScanEntry 使用白名单过滤),只保留 VmMetadataCategory.REGULAR 的条目(识别符例如
VmMetadataScanEntry#getVmCategory、VmMetadataCategory、evt.setVmInstanceMetadata、metadata),确保只有
REGULAR 类型被收集并传入 evt.setVmInstanceMetadata。

@ZStack-Robot

Copy link
Copy Markdown
Collaborator Author

Comment from yaohua.wu:

Review: MR !9469 — ZSV-11559 (zstackio/zstack)

MR: <fix>[vm]: add MetadataImpactfeature-zsv-5.0.0-vm-registration
Jira: ZSV-11559 — 【注册虚拟机】(New Feature)
变更概览: 149 个文件变更。新增 VM Metadata 框架的核心层:@MetadataImpact 注解、VO/DTO 定义、SQL schema、metadata 过期清理 GC、Expunge Flow,以及 NFS/LocalStorage 的 metadata 扩展点。大量 API Message 添加 @MetadataImpact 注解声明元数据影响类型。

⚠️ 注意:GitLab API 对本次 MR 返回了 49 个文件的空 diff(含 LocalStorageBase、LocalStorageKvmBackend、NfsPrimaryStorage、PrimaryStorageBase 等关键文件),以下 review 基于可见的 100 个文件 diff。


🟡 Warning

1. [VmMetadataDirtyVO.java] lastOpDate 缺少 NOT NULL DEFAULT CURRENT_TIMESTAMP

VO 中 lastOpDate 字段使用 @PreUpdate 手动设为 null,依赖 MySQL 的 ON UPDATE CURRENT_TIMESTAMP 自动更新。但 SQL schema 定义为:

`lastOpDate` timestamp on update CURRENT_TIMESTAMP,

缺少 NOT NULL DEFAULT CURRENT_TIMESTAMP。首次 insert 时 lastOpDate 会是 NULL,直到首次 update 才有值。ZStack 其他 VO 通常用 @PrePersist 初始化 createDatelastOpDate,此处 VmMetadataDirtyVO 缺少 @PrePersist

建议:添加 @PrePersist 初始化 createDatelastOpDate,或在 SQL 中加 NOT NULL DEFAULT CURRENT_TIMESTAMP

2. [VmMetadataDirtyVO.java] createDate 同样缺少初始化

SQL 定义 createDate timestamp 无默认值,Java VO 无 @PrePersist。insert 后 createDate 为 NULL,失去审计价值。

3. [VmExpungeMetadataFlow.java:~57] 获取 psType 发起了额外 DB 查询,rootVolume 已持有 PS 信息

String psType = Q.New(PrimaryStorageVO.class).select(PrimaryStorageVO_.type)
    .eq(PrimaryStorageVO_.uuid, psUuid).findValue();

在 expunge flow 的热路径上额外查 DB 获取 PS type。如果 VmInstanceSpecrootVolume 上能携带 PS type 信息,可避免此查询。当前实现功能正确,但建议确认是否有现成字段可复用。

4. [VmExpungeMetadataFlow.java:~116-126] GC interval 使用 VmGlobalConfig 但类型转换缺防护

long gcIntervalSec = TimeUnit.HOURS.toSeconds(
    VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class));

GlobalConfig.value(Long.class) 如果配置值被人为设为非数字字符串,会抛 NumberFormatException。虽然有 @GlobalConfigValidation 约束,但 GC 提交路径上的异常会导致 metadata 永远清不掉。

建议:加 try-catch 或使用默认值兜底。

5. [V5.0.0__schema.sql] VmMetadataFingerprintVO 缺少 createDate/lastOpDate 字段

VmMetadataDirtyVOcreateDate/lastOpDate,但 VmMetadataFingerprintVO 只有业务字段(lastFlushTime, lastFlushFailed, staleRecoveryCount),缺少标准审计字段。不影响功能但不符合 ZStack 表设计规范。

6. [VmInstanceApiInterceptor.java] validate(APIRegisterVmInstanceFromMetadataMsg)getExtensionFromMap 返回 null 时错误信息含 psType=null

String psType = Q.New(PrimaryStorageVO.class)...findValue();
VmMetadataPathBuildExtensionPoint ext = (psType != null)
    ? pluginRgty.getExtensionFromMap(psType, ...) : null;
if (ext == null) {
    throw new ApiMessageInterceptionException(argerr(
        "primary storage[uuid:%s, type:%s] does not support vm metadata", psUuid, psType));
}

psType 为 null 时(PS 不存在),错误信息会显示 type:null,不够清晰。建议分开处理:PS 不存在 vs PS 类型不支持 metadata。


🟢 Suggestion

1. [MetadataImpact.java] 注解的 resolverfield 属性默认为空字符串

注解声明 resolver() default ""field() default ""。对于 Impact.CONFIGImpact.STORAGE,Javadoc 说明"must be specified"但没有编译时校验。依赖运行时拦截器检查,建议在 VmMetadataUpdateInterceptor(premium MR)中加 startup 阶段的校验,扫描所有带 @MetadataImpact 的 API Message 确保 resolver/field 非空。

2. [VmMetadataErrors.java] 错误码使用数字 ID 拼接

code = String.format("VM_METADATA.%s", id);

产生 VM_METADATA.1300 这样的错误码。ZStack 通常使用枚举名称作为错误码标识(如 SYS_ERROR),数字 ID 对调试不够直观。

3. [CleanupVmInstanceMetadataOnPrimaryStorageGC.java] GC 名称硬编码格式

getGCName 返回 gc-cleanup-vm-metadata-{vmUuid},建议提取为常量或使用 GC 框架的标准命名约定。

4. [conf/globalConfig/vm.xml] 多个新增 GlobalConfig 缺少中文描述

新增的 vm.metadata.* 系列配置项需要确认是否都有对应的 descriptionlongDescription 国际化资源。

Verdict: APPROVED (with minor suggestions)

整体架构设计合理:

  • @MetadataImpact 注解驱动的拦截器模式避免了在每个 API handler 中散布 metadata 标脏逻辑
  • VmMetadataDirtyService 接口 + @Autowired(required=false) 实现社区版/Premium 优雅降级
  • GC 机制处理 expunge 失败场景
  • metadata DTO 包含 schema version 支持未来格式演进

建议修复 Warning #1/#2(VO 缺少 @PrePersist)以确保审计字段完整。


🤖 Robot Reviewer

@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/ZSV-11559@@4 branch from 5f516f9 to 146ae94 Compare March 26, 2026 10:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (7)
storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java (1)

1889-1892: ⚠️ Potential issue | 🟠 Major

扫描结果这里应该对白名单过滤 REGULAR

现在只排除了 TEMPLATE_CACHETEMPLATE 仍然会进入 evt.setVmInstanceMetadata(...),返回结果不再是纯 VM metadata。

💡 建议修改
-                List<VmMetadataScanEntry> filtered = metadata.stream()
-                        .filter(e -> !VmMetadataCategory.TEMPLATE_CACHE.toString().equals(e.getVmCategory()))
-                        .collect(Collectors.toList());
+                List<VmMetadataScanEntry> filtered = metadata.stream()
+                        .filter(e -> VmMetadataCategory.REGULAR.toString().equals(e.getVmCategory()))
+                        .collect(Collectors.toList());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java`
around lines 1889 - 1892, The filtered metadata currently only excludes
VmMetadataCategory.TEMPLATE_CACHE, but TEMPLATE entries still pass through;
update the filtering in PrimaryStorageBase (the stream that produces 'filtered'
before evt.setVmInstanceMetadata) to whitelist only VmMetadataCategory.REGULAR
(or explicitly exclude both TEMPLATE and TEMPLATE_CACHE) so
evt.setVmInstanceMetadata receives pure VM metadata; adjust the lambda that
checks e.getVmCategory() accordingly.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java (1)

3924-3928: ⚠️ Potential issue | 🔴 Critical

这里需要在下发前兜底或拒绝空 hostUuid

httpCall(...) 会按 hostUuid 路由到宿主机服务;在 expunge/GC 场景传入空值时,这里会直接在路由阶段失败,元数据清理根本不会执行。

🔧 最小修复
     `@Override`
     void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply> completion) {
+        if (StringUtils.isBlank(hostUuid)) {
+            completion.fail(operr("cleanup vm metadata failed: hostUuid is null, metadataPath=%s", msg.getMetadataPath()));
+            return;
+        }
         CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
         cmd.metadataPath = msg.getMetadataPath();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`
around lines 3924 - 3928, The handle method should guard against a null/empty
hostUuid before calling httpCall: in
LocalStorageKvmBackend.handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg,
String hostUuid,
ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>) validate
msg.getMetadataPath() and hostUuid (the latter must not be null/empty) and if
hostUuid is missing immediately complete with an error via the provided
completion (e.g., completion.fail(...) or construct a failed
CleanupVmInstanceMetadataOnPrimaryStorageReply), otherwise proceed to create
CleanupVmMetadataCmd and call httpCall(CLEANUP_VM_METADATA_PATH, hostUuid, ...).
Ensure the error message clearly states hostUuid is empty so routing is not
attempted.
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java (1)

2089-2095: ⚠️ Potential issue | 🟡 Minor

写/删 metadata 成功后仍然漏掉容量刷新。

这两个分支绕过了 asyncHttpCall(...),成功后没有调用 updatePrimaryStorageCapacity(...)。metadata 文件在 NFS 上创建/删除后,主存容量会一直保留旧值到下一次 ping。

补上容量刷新的最小改动
                 WriteVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(WriteVmMetadataRsp.class);
                 if (!rsp.isSuccess()) {
                     completion.fail(operr("failed to write vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError()));
                     return;
                 }
+                updatePrimaryStorageCapacity(msg.getPrimaryStorageUuid(), rsp);

                 completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply());
@@
                 CleanupVmMetadataRsp rsp = ((KVMHostAsyncHttpCallReply) reply).toResponse(CleanupVmMetadataRsp.class);
                 if (!rsp.isSuccess()) {
                     completion.fail(operr("failed to cleanup vm metadata on nfs via host[uuid:%s]: %s", hostUuid, rsp.getError()));
                     return;
                 }
+                updatePrimaryStorageCapacity(msg.getPrimaryStorageUuid(), rsp);

                 CleanupVmInstanceMetadataOnPrimaryStorageReply r = new CleanupVmInstanceMetadataOnPrimaryStorageReply();

Also applies to: 2186-2193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`
around lines 2089 - 2095, The write/delete metadata success branches return
without refreshing primary storage capacity; after receiving a successful
WriteVmMetadataRsp (from KVMHostAsyncHttpCallReply) you must call
updatePrimaryStorageCapacity(...) to refresh NFS usage before calling
completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply()); locate
the success branches handling WriteVmMetadataRsp (and the analogous branch
around lines 2186-2193) and insert a call to updatePrimaryStorageCapacity with
the host/primary storage identifiers and a callback that then calls
completion.success(...) (or completion.fail(...) on error) so capacity is
updated immediately after metadata create/delete.
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java (2)

1935-1944: ⚠️ Potential issue | 🔴 Critical

同一份 metadata 仍然可能并发写入。

现在 getSyncSignature() 只按 primary storage 分桶,但 getSyncLevel() 允许 10 并发;同一个 metadataPath 的两个更新仍然会同时落到后端,覆盖写文件时会出现丢更新或半写状态。这里至少要把同步粒度收敛到单个 metadata 文件,并把 sync level 收到 1。

把同步键收敛到单个 metadata 文件
             `@Override`
             public String getSyncSignature() {
-                return "update-metadata-on-ps-" + self.getUuid();
+                String key = msg.getMetadataPath() != null ? msg.getMetadataPath() : msg.getRootVolumeUuid();
+                return "vm-metadata-on-ps-" + self.getUuid() + "-" + key;
             }

             `@Override`
             public int getSyncLevel() {
-                return 10;
+                return 1;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 1935 - 1944, 当前并发控制以 primary storage 为粒度(getSyncSignature() 返回
"update-metadata-on-ps-"+self.getUuid())但 getSyncLevel() 允许 10 个并发,导致同一
metadataPath 的并发写入会覆盖或半写;在提交 thdf.chainSubmit(new ChainTask(msg) { ... }) 时,将
getSyncSignature() 收敛到单个 metadata 文件(例如包含 metadataPath 或 metadata 文件名/UUID 而非仅
self.getUuid()),并把 getSyncLevel() 返回值改为 1,以确保对同一 metadataPath 的更新串行化(参考
ChainTask, thdf.chainSubmit, getSyncSignature(), getSyncLevel(), metadataPath)。

2038-2060: ⚠️ Potential issue | 🔴 Critical

cleanup 仍然绕过了 metadata 串行化通道。

UpdateVmInstanceMetadataOnPrimaryStorageMsg 已经走了 ChainTask,但 cleanup 这里还是直接下发 backend。只要 expunge/GC 和 update/register 并发命中同一个 metadataPath,cleanup 就可能删掉刚写好的文件,或者打断正在进行的写入。这里需要复用和 update 相同的 sync key,把 cleanup 也放进同一条串行化链路。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 2038 - 2060, handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg)
currently calls backend.handle(...) directly, bypassing the serialization used
by UpdateVmInstanceMetadataOnPrimaryStorageMsg and risking races; change it to
compute the same sync key (the metadataPath/key used by
UpdateVmInstanceMetadataOnPrimaryStorageMsg) and submit a ChainTask (using the
same chain/queue mechanism) that runs the backend.handle(...) inside its run()
so cleanup executes serialized with update/register operations; keep existing
Reply/ReturnValueCompletion semantics (use the same reply object and error
handling) and still obtain the hostUuid via
factory.getConnectedHostForOperation/getBackendByHostUuid before invoking the
chained task.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java (1)

337-372: ⚠️ Potential issue | 🟡 Minor

simulator 的 metadata 读写状态没有连起来。

这里 writeVmMetadata() / cleanupVmMetadata() 只记录 command,getVmInstanceMetadata() / scanVmMetadata() 又直接回空 rsp。这样在 simulator 下,写后读、删后扫都会得到“调用成功但结果为空”的假阳性,新增 metadata 路径的测试覆盖不到真实行为。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java`
around lines 337 - 372, The simulator's metadata lifecycle is broken:
writeVmMetadata() and cleanupVmMetadata() only record commands while
getVmInstanceMetadata() and scanVmMetadata() always return empty responses; fix
this in LocalStorageSimulator by maintaining an in-memory metadata store keyed
by VM/volume IDs (e.g., add a Map in the simulator or extend config to hold
metadata entries), update writeVmMetadata() to persist the metadata into that
store when handling WriteVmMetadataCmd (in addition to appending to
config.writeVmMetadataCmds), update getVmInstanceMetadata() to read from the
store and populate GetVmInstanceMetadataRsp, update scanVmMetadata() to return
matching entries from the store into ScanVmMetadataRsp, and update
cleanupVmMetadata() to remove entries from the store when handling
CleanupVmMetadataCmd (while still recording config.cleanupVmMetadataCmds);
ensure the response objects mirror the stored data so tests see realistic
read/scan/cleanup behavior.
compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java (1)

89-93: ⚠️ Potential issue | 🟠 Major

hostUuid 为 null 时 GC 重试将持续失败。

对于已 expunge 的 VM,hostUuidlastHostUuid 可能都为 null。将 null 的 hostUuid 传递给 CleanupVmInstanceMetadataOnPrimaryStorageMsg 和 GC 会导致消息路由失败,GC 会重复失败而无法完成清理。

建议在 GC 执行时动态查找可用主机,而非在提交时固定 hostUuid:

  1. 如果 hostUuid 为 null,可以通过 PrimaryStorageClusterRefVO + HostVO 查找该主存储关联集群下的任意可用主机
  2. 或者在 handler 端实现主机选择逻辑
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 89 - 93, The flow currently sets a possibly-null hostUuid on
CleanupVmInstanceMetadataOnPrimaryStorageMsg (in VmExpungeMetadataFlow by using
spec.getVmInventory().getHostUuid() / getLastHostUuid()), which causes GC
routing to fail when both are null; instead, remove assigning a null hostUuid
here and implement host selection either by (A) querying
PrimaryStorageClusterRefVO + HostVO to pick any available host in the primary
storage's clusters and set that hostUuid before sending the message, or (B)
leave hostUuid unset and add selection logic in the
CleanupVmInstanceMetadataOnPrimaryStorageMsg handler to lookup
PrimaryStorageClusterRefVO and HostVO and choose a live host at runtime; update
VmExpungeMetadataFlow (the code that constructs cmsg) and/or the handler to
perform the DB query and set a non-null hostUuid only when an actual host is
found.
🧹 Nitpick comments (6)
sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java (2)

1-6: 冗余的同包导入

import org.zstack.sdk.*; 是不必要的,因为当前类已经位于 org.zstack.sdk 包中,同包内的类无需导入即可直接访问。

♻️ 建议移除冗余导入
 package org.zstack.sdk;
 
 import java.util.HashMap;
 import java.util.Map;
-import org.zstack.sdk.*;
 
 public class GetVmInstanceMetadataFromPrimaryStorageAction extends AbstractAction {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java`
around lines 1 - 6, Remove the redundant same-package wildcard import in
GetVmInstanceMetadataFromPrimaryStorageAction: the class is already declared in
package org.zstack.sdk so delete the line "import org.zstack.sdk.*;" and keep
only the necessary imports (e.g., java.util.HashMap, java.util.Map) so classes
in org.zstack.sdk are accessed directly without that import.

31-35: 原始 List 类型缺少泛型参数

systemTagsuserTags 字段使用原始 List 类型,建议添加泛型参数以提高类型安全性。

♻️ 建议添加泛型参数
     `@Param`(required = false)
-    public java.util.List systemTags;
+    public java.util.List<String> systemTags;
 
     `@Param`(required = false)
-    public java.util.List userTags;
+    public java.util.List<String> userTags;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java`
around lines 31 - 35, 在 GetVmInstanceMetadataFromPrimaryStorageAction
类中,systemTags 和 userTags 字段目前使用原始类型 java.util.List;请改为使用带泛型的类型(例如
java.util.List<String> 或其他合适的元素类型)以提升类型安全性,并更新相应的声明(字段名
systemTags、userTags)及必要的导入/序列化注解匹配。
header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java (1)

5-5: 未使用的导入 VmInstanceVO

VmInstanceVO 被导入但未在代码中使用,实际引用的是 VmInstanceEO。建议移除此冗余导入。

♻️ 建议的修复
 import org.zstack.header.managementnode.ManagementNodeVO;
 import org.zstack.header.vm.VmInstanceEO;
-import org.zstack.header.vm.VmInstanceVO;
 import org.zstack.header.vo.ForeignKey;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java` at
line 5, The file VmMetadataDirtyVO imports VmInstanceVO but actually uses
VmInstanceEO; remove the unused import statement for VmInstanceVO in
VmMetadataDirtyVO to eliminate the redundant import and rely on the existing
reference to VmInstanceEO (no other code changes required).
sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java (1)

28-35: 为参数 List 补全泛型,避免弱类型输入。

Line [29]/[32]/[35] 当前是原始 java.util.List,会丢失编译期约束。这里建议改为 List<String>,与 VM 元数据 API 契约保持一致。

建议修改
-    public java.util.List vmUuids;
+    public java.util.List<String> vmUuids;

-    public java.util.List systemTags;
+    public java.util.List<String> systemTags;

-    public java.util.List userTags;
+    public java.util.List<String> userTags;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java` around
lines 28 - 35, Update the three raw List fields in
UpdateVmInstanceMetadataAction to use generics (List<String>) to enforce
compile-time type safety and match the VM metadata API contract: change vmUuids,
systemTags, and userTags from java.util.List to java.util.List<String>, and
update any related imports or usages that rely on raw types to the parameterized
type.
compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java (1)

123-124: GC 间隔配置值缺少防御性处理。

VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class) 如果配置值无效或为 null,可能导致异常。建议添加防御性处理:

♻️ 建议添加默认值保护
-        long gcIntervalSec = TimeUnit.HOURS.toSeconds(VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class));
+        Long intervalHours = VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class);
+        long gcIntervalSec = TimeUnit.HOURS.toSeconds(intervalHours != null ? intervalHours : 1L);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`
around lines 123 - 124, VmExpungeMetadataFlow currently reads
VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class) directly into
gcIntervalSec and passes it to gc.deduplicateSubmit, which can throw if the
config is null/invalid or non-positive; change the flow to defensively obtain
the config: call
VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class) into a local
Long, check for null or <=0 and fall back to a sensible default (e.g.,
TimeUnit.HOURS.toSeconds(1L) or a defined constant), optionally clamp extremely
large values, then compute gcIntervalSec from that safe value and pass the
sanitized gcIntervalSec to gc.deduplicateSubmit to avoid NPEs or invalid
scheduling.
compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java (1)

1329-1352: 验证逻辑整体良好,有一个小的健壮性建议。

新增的 validate(APIRegisterVmInstanceFromMetadataMsg) 方法逻辑清晰,符合 ZStack API 拦截器的设计模式:

  • 正确使用了 ApiMessageInterceptionException 而非 OperationFailureException
  • 采用了早返回模式,避免深层嵌套

但有一个潜在问题:当 psTypenull 时(第 1341 行),表示主存储记录可能不存在或类型字段为空。此时抛出的异常消息 "primary storage[uuid:%s, type:%s] does not support vm metadata" 可能会误导用户,因为实际原因是主存储不存在。

♻️ 建议区分主存储不存在与不支持元数据的场景
         String psType = Q.New(PrimaryStorageVO.class)
                 .select(PrimaryStorageVO_.type)
                 .eq(PrimaryStorageVO_.uuid, psUuid)
                 .findValue();
+        if (psType == null) {
+            throw new ApiMessageInterceptionException(argerr(
+                    "primary storage[uuid:%s] not found", psUuid));
+        }
+
         VmMetadataPathBuildExtensionPoint ext = (psType != null)
                 ? pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class) : null;
         if (ext == null) {
             throw new ApiMessageInterceptionException(argerr(
                     "primary storage[uuid:%s, type:%s] does not support vm metadata", psUuid, psType));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`
around lines 1329 - 1352, The current
validate(APIRegisterVmInstanceFromMetadataMsg) treats a null psType the same as
"storage does not support vm metadata" which is misleading when the primary
storage record is missing; change the logic to first check existence of the
PrimaryStorageVO for psUuid (e.g. query for uuid or use an existence check on
PrimaryStorageVO), and if the record is missing throw an
ApiMessageInterceptionException with argerr("primary storage[uuid:%s] not
found", psUuid); only when the record exists but psType is present and no
extension is found should you throw the existing "does not support vm metadata"
message; update the branches around psType, ext and the error handling in
validate(APIRegisterVmInstanceFromMetadataMsg) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java`:
- Around line 41-46: When building the
CleanupVmInstanceMetadataOnPrimaryStorageMsg in triggerNow(), ensure hostUuid
has a fallback: if hostUuid is null (inherited from VmExpungeMetadataFlow) query
for an available host on the given primaryStorageUuid that has an active
agent/connection and can reach the primary (using existing host query utilities
or QueryHostMsg), set that host's UUID into
CleanupVmInstanceMetadataOnPrimaryStorageMsg.setHostUuid(...) before sending,
and if none found fail fast with a clear error so GC retry won't repeatedly fail
due to message routing.

In `@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java`:
- Around line 24-25: The field dirtyVersion in class VmMetadataDirtyVO currently
uses Java's primitive long which defaults to 0, conflicting with the DB schema
default of 1; update the entity so new instances get 1 by either initializing
the field (private long dirtyVersion = 1) or setting it in a `@PrePersist` method
(e.g., ensure dirtyVersion is set to 1 if <= 0) to match the SQL DEFAULT and
preserve correct dirty-version semantics.

In
`@sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java`:
- Around line 28-29: The uuid field in
ScanVmInstanceMetadataFromPrimaryStorageAction is marked required=true but
currently allows empty strings; update the `@Param` annotation on the public
java.lang.String uuid field so emptyString=false and nonempty=true (i.e.,
require a non-empty, non-blank uuid) to enable fail-fast validation; keep other
flags (required, nullElements, noTrim) as appropriate.

---

Duplicate comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`:
- Around line 89-93: The flow currently sets a possibly-null hostUuid on
CleanupVmInstanceMetadataOnPrimaryStorageMsg (in VmExpungeMetadataFlow by using
spec.getVmInventory().getHostUuid() / getLastHostUuid()), which causes GC
routing to fail when both are null; instead, remove assigning a null hostUuid
here and implement host selection either by (A) querying
PrimaryStorageClusterRefVO + HostVO to pick any available host in the primary
storage's clusters and set that hostUuid before sending the message, or (B)
leave hostUuid unset and add selection logic in the
CleanupVmInstanceMetadataOnPrimaryStorageMsg handler to lookup
PrimaryStorageClusterRefVO and HostVO and choose a live host at runtime; update
VmExpungeMetadataFlow (the code that constructs cmsg) and/or the handler to
perform the DB query and set a non-null hostUuid only when an actual host is
found.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`:
- Around line 3924-3928: The handle method should guard against a null/empty
hostUuid before calling httpCall: in
LocalStorageKvmBackend.handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg,
String hostUuid,
ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>) validate
msg.getMetadataPath() and hostUuid (the latter must not be null/empty) and if
hostUuid is missing immediately complete with an error via the provided
completion (e.g., completion.fail(...) or construct a failed
CleanupVmInstanceMetadataOnPrimaryStorageReply), otherwise proceed to create
CleanupVmMetadataCmd and call httpCall(CLEANUP_VM_METADATA_PATH, hostUuid, ...).
Ensure the error message clearly states hostUuid is empty so routing is not
attempted.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java`:
- Around line 337-372: The simulator's metadata lifecycle is broken:
writeVmMetadata() and cleanupVmMetadata() only record commands while
getVmInstanceMetadata() and scanVmMetadata() always return empty responses; fix
this in LocalStorageSimulator by maintaining an in-memory metadata store keyed
by VM/volume IDs (e.g., add a Map in the simulator or extend config to hold
metadata entries), update writeVmMetadata() to persist the metadata into that
store when handling WriteVmMetadataCmd (in addition to appending to
config.writeVmMetadataCmds), update getVmInstanceMetadata() to read from the
store and populate GetVmInstanceMetadataRsp, update scanVmMetadata() to return
matching entries from the store into ScanVmMetadataRsp, and update
cleanupVmMetadata() to remove entries from the store when handling
CleanupVmMetadataCmd (while still recording config.cleanupVmMetadataCmds);
ensure the response objects mirror the stored data so tests see realistic
read/scan/cleanup behavior.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`:
- Around line 1935-1944: 当前并发控制以 primary storage 为粒度(getSyncSignature() 返回
"update-metadata-on-ps-"+self.getUuid())但 getSyncLevel() 允许 10 个并发,导致同一
metadataPath 的并发写入会覆盖或半写;在提交 thdf.chainSubmit(new ChainTask(msg) { ... }) 时,将
getSyncSignature() 收敛到单个 metadata 文件(例如包含 metadataPath 或 metadata 文件名/UUID 而非仅
self.getUuid()),并把 getSyncLevel() 返回值改为 1,以确保对同一 metadataPath 的更新串行化(参考
ChainTask, thdf.chainSubmit, getSyncSignature(), getSyncLevel(), metadataPath)。
- Around line 2038-2060: handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg)
currently calls backend.handle(...) directly, bypassing the serialization used
by UpdateVmInstanceMetadataOnPrimaryStorageMsg and risking races; change it to
compute the same sync key (the metadataPath/key used by
UpdateVmInstanceMetadataOnPrimaryStorageMsg) and submit a ChainTask (using the
same chain/queue mechanism) that runs the backend.handle(...) inside its run()
so cleanup executes serialized with update/register operations; keep existing
Reply/ReturnValueCompletion semantics (use the same reply object and error
handling) and still obtain the hostUuid via
factory.getConnectedHostForOperation/getBackendByHostUuid before invoking the
chained task.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`:
- Around line 2089-2095: The write/delete metadata success branches return
without refreshing primary storage capacity; after receiving a successful
WriteVmMetadataRsp (from KVMHostAsyncHttpCallReply) you must call
updatePrimaryStorageCapacity(...) to refresh NFS usage before calling
completion.success(new UpdateVmInstanceMetadataOnPrimaryStorageReply()); locate
the success branches handling WriteVmMetadataRsp (and the analogous branch
around lines 2186-2193) and insert a call to updatePrimaryStorageCapacity with
the host/primary storage identifiers and a callback that then calls
completion.success(...) (or completion.fail(...) on error) so capacity is
updated immediately after metadata create/delete.

In `@storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java`:
- Around line 1889-1892: The filtered metadata currently only excludes
VmMetadataCategory.TEMPLATE_CACHE, but TEMPLATE entries still pass through;
update the filtering in PrimaryStorageBase (the stream that produces 'filtered'
before evt.setVmInstanceMetadata) to whitelist only VmMetadataCategory.REGULAR
(or explicitly exclude both TEMPLATE and TEMPLATE_CACHE) so
evt.setVmInstanceMetadata receives pure VM metadata; adjust the lambda that
checks e.getVmCategory() accordingly.

---

Nitpick comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java`:
- Around line 123-124: VmExpungeMetadataFlow currently reads
VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class) directly into
gcIntervalSec and passes it to gc.deduplicateSubmit, which can throw if the
config is null/invalid or non-positive; change the flow to defensively obtain
the config: call
VmGlobalConfig.VM_METADATA_CLEANUP_GC_INTERVAL.value(Long.class) into a local
Long, check for null or <=0 and fall back to a sensible default (e.g.,
TimeUnit.HOURS.toSeconds(1L) or a defined constant), optionally clamp extremely
large values, then compute gcIntervalSec from that safe value and pass the
sanitized gcIntervalSec to gc.deduplicateSubmit to avoid NPEs or invalid
scheduling.

In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`:
- Around line 1329-1352: The current
validate(APIRegisterVmInstanceFromMetadataMsg) treats a null psType the same as
"storage does not support vm metadata" which is misleading when the primary
storage record is missing; change the logic to first check existence of the
PrimaryStorageVO for psUuid (e.g. query for uuid or use an existence check on
PrimaryStorageVO), and if the record is missing throw an
ApiMessageInterceptionException with argerr("primary storage[uuid:%s] not
found", psUuid); only when the record exists but psType is present and no
extension is found should you throw the existing "does not support vm metadata"
message; update the branches around psType, ext and the error handling in
validate(APIRegisterVmInstanceFromMetadataMsg) accordingly.

In `@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java`:
- Line 5: The file VmMetadataDirtyVO imports VmInstanceVO but actually uses
VmInstanceEO; remove the unused import statement for VmInstanceVO in
VmMetadataDirtyVO to eliminate the redundant import and rely on the existing
reference to VmInstanceEO (no other code changes required).

In
`@sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java`:
- Around line 1-6: Remove the redundant same-package wildcard import in
GetVmInstanceMetadataFromPrimaryStorageAction: the class is already declared in
package org.zstack.sdk so delete the line "import org.zstack.sdk.*;" and keep
only the necessary imports (e.g., java.util.HashMap, java.util.Map) so classes
in org.zstack.sdk are accessed directly without that import.
- Around line 31-35: 在 GetVmInstanceMetadataFromPrimaryStorageAction
类中,systemTags 和 userTags 字段目前使用原始类型 java.util.List;请改为使用带泛型的类型(例如
java.util.List<String> 或其他合适的元素类型)以提升类型安全性,并更新相应的声明(字段名
systemTags、userTags)及必要的导入/序列化注解匹配。

In `@sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java`:
- Around line 28-35: Update the three raw List fields in
UpdateVmInstanceMetadataAction to use generics (List<String>) to enforce
compile-time type safety and match the VM metadata API contract: change vmUuids,
systemTags, and userTags from java.util.List to java.util.List<String>, and
update any related imports or usages that rely on raw types to the parameterized
type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5d62f588-e824-4a5b-a724-840e0e038a01

📥 Commits

Reviewing files that changed from the base of the PR and between 5f516f9 and 146ae94.

⛔ Files ignored due to path filters (5)
  • conf/globalConfig/vm.xml is excluded by !**/*.xml
  • conf/persistence.xml is excluded by !**/*.xml
  • conf/serviceConfig/primaryStorage.xml is excluded by !**/*.xml
  • conf/serviceConfig/vmInstance.xml is excluded by !**/*.xml
  • conf/springConfigXml/VmInstanceManager.xml is excluded by !**/*.xml
📒 Files selected for processing (144)
  • compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java
  • compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java
  • testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
✅ Files skipped from review due to trivial changes (95)
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java
🚧 Files skipped from review as they are similar to previous changes (27)
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java
  • testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy

Comment on lines +41 to +46
CleanupVmInstanceMetadataOnPrimaryStorageMsg msg = new CleanupVmInstanceMetadataOnPrimaryStorageMsg();
msg.setPrimaryStorageUuid(primaryStorageUuid);
msg.setVmUuid(vmUuid);
msg.setRootVolumeUuid(rootVolumeUuid);
msg.setMetadataPath(metadataPath);
msg.setHostUuid(hostUuid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

GC 执行时应实现 hostUuid 兜底逻辑。

hostUuid 为 null(从 VmExpungeMetadataFlow 继承而来)时,GC 重试也会因消息路由失败而无法完成。建议在 triggerNow() 中实现 host 动态选择:

♻️ 建议在 GC 执行时查找可用主机
+        String effectiveHostUuid = hostUuid;
+        if (effectiveHostUuid == null) {
+            // 通过 PrimaryStorageClusterRefVO 查找该主存储关联的集群,再选择一个可用主机
+            List<String> clusterUuids = Q.New(PrimaryStorageClusterRefVO.class)
+                    .select(PrimaryStorageClusterRefVO_.clusterUuid)
+                    .eq(PrimaryStorageClusterRefVO_.primaryStorageUuid, primaryStorageUuid)
+                    .listValues();
+            if (!clusterUuids.isEmpty()) {
+                effectiveHostUuid = Q.New(HostVO.class)
+                        .select(HostVO_.uuid)
+                        .in(HostVO_.clusterUuid, clusterUuids)
+                        .eq(HostVO_.state, HostState.Enabled)
+                        .eq(HostVO_.status, HostStatus.Connected)
+                        .limit(1)
+                        .findValue();
+            }
+            if (effectiveHostUuid == null) {
+                logger.warn(String.format("[MetadataCleanupGC] no available host found for ps[uuid:%s], " +
+                        "will retry later", primaryStorageUuid));
+                completion.fail(Platform.operr("no available host for metadata cleanup"));
+                return;
+            }
+        }
+
         CleanupVmInstanceMetadataOnPrimaryStorageMsg msg = new CleanupVmInstanceMetadataOnPrimaryStorageMsg();
         msg.setPrimaryStorageUuid(primaryStorageUuid);
         msg.setVmUuid(vmUuid);
         msg.setRootVolumeUuid(rootVolumeUuid);
         msg.setMetadataPath(metadataPath);
-        msg.setHostUuid(hostUuid);
+        msg.setHostUuid(effectiveHostUuid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java`
around lines 41 - 46, When building the
CleanupVmInstanceMetadataOnPrimaryStorageMsg in triggerNow(), ensure hostUuid
has a fallback: if hostUuid is null (inherited from VmExpungeMetadataFlow) query
for an available host on the given primaryStorageUuid that has an active
agent/connection and can reach the primary (using existing host query utilities
or QueryHostMsg), set that host's UUID into
CleanupVmInstanceMetadataOnPrimaryStorageMsg.setHostUuid(...) before sending,
and if none found fail fast with a clear error so GC retry won't repeatedly fail
due to message routing.

Comment on lines +24 to +25
@Column
private long dirtyVersion;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

dirtyVersion 默认值与数据库 schema 不一致。

Java 原始类型 long 默认值为 0,但 SQL schema 定义 dirtyVersion BIGINT NOT NULL DEFAULT 1。当 JPA 持久化新实体时,会使用 Java 字段值 0 而非数据库默认值 1,可能导致脏版本追踪逻辑不一致。

建议在字段声明时显式初始化为 1,或在 @PrePersist 中设置。

🛠️ 建议的修复
     `@Column`
-    private long dirtyVersion;
+    private long dirtyVersion = 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column
private long dirtyVersion;
`@Column`
private long dirtyVersion = 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java`
around lines 24 - 25, The field dirtyVersion in class VmMetadataDirtyVO
currently uses Java's primitive long which defaults to 0, conflicting with the
DB schema default of 1; update the entity so new instances get 1 by either
initializing the field (private long dirtyVersion = 1) or setting it in a
`@PrePersist` method (e.g., ensure dirtyVersion is set to 1 if <= 0) to match the
SQL DEFAULT and preserve correct dirty-version semantics.

Comment on lines +28 to +29
@Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String uuid;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

uuid 必填参数不应允许空字符串。

Line 28 的 @Param 当前配置为 required = true 但同时 emptyString = truenonempty = false,这会放过 "",导致请求在更后层才失败,错误定位不友好。建议这里直接 fail-fast。

建议修改
-    `@Param`(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
+    `@Param`(required = true, nonempty = true, nullElements = false, emptyString = false, noTrim = false)
     public java.lang.String uuid;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Param(required = true, nonempty = false, nullElements = false, emptyString = true, noTrim = false)
public java.lang.String uuid;
`@Param`(required = true, nonempty = false, nullElements = false, emptyString = false, noTrim = false)
public java.lang.String uuid;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java`
around lines 28 - 29, The uuid field in
ScanVmInstanceMetadataFromPrimaryStorageAction is marked required=true but
currently allows empty strings; update the `@Param` annotation on the public
java.lang.String uuid field so emptyString=false and nonempty=true (i.e.,
require a non-empty, non-blank uuid) to enable fail-fast validation; keep other
flags (required, nullElements, noTrim) as appropriate.

@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/ZSV-11559@@4 branch from 146ae94 to a86fd7a Compare March 26, 2026 12:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (9)
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java (2)

2157-2162: ⚠️ Potential issue | 🟡 Minor

扫描结果请始终返回空列表。

这里只有 rsp.metadataEntries != null 才给 reply 赋值,结果会在“扫描成功但无结果”时暴露 null。调用方一旦直接遍历 getVmInstanceMetadata(),这里就会变成不必要的 NPE 风险。建议明确回 Collections.emptyList()

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`
around lines 2157 - 2162, ScanVmInstanceMetadataFromPrimaryStorageReply may be
returned with a null vmInstanceMetadata when rsp.metadataEntries is null,
causing NPEs for callers; modify the block around
ScanVmInstanceMetadataFromPrimaryStorageReply r so that you always call
r.setVmInstanceMetadata(...) with an empty list when rsp.metadataEntries is null
(e.g. Collections.emptyList()), and still set hostUuid on entries when present
before calling completion.success(r); ensure use of
ScanVmInstanceMetadataFromPrimaryStorageReply, rsp.metadataEntries,
entry.setHostUuid(...), r.setVmInstanceMetadata(...), and
completion.success(...) as the target symbols.

2089-2095: ⚠️ Potential issue | 🟡 Minor

写/删 metadata 成功后别漏掉容量刷新。

这两个 handler 没走下面通用的 asyncHttpCall(...) helper,所以成功分支也绕过了 updatePrimaryStorageCapacity(...)。元数据文件的创建/删除后,主存容量会一直保留旧值,直到后续 ping/reconnect 才被动修正。

Also applies to: 2186-2193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`
around lines 2089 - 2095, The success branches in NfsPrimaryStorageKVMBackend
handling WriteVmMetadataRsp (method handling reply using WriteVmMetadataRsp)
currently call completion.success(...) but skip the capacity refresh; after a
successful write/delete response invoke updatePrimaryStorageCapacity(...) (the
same routine used by asyncHttpCall-based handlers) to refresh the primary
storage capacity for the affected primary storage/host before completing the
operation so metadata file create/delete updates capacity immediately; apply the
same change to the analogous handler around the WriteVmMetadataRsp logic at the
2186-2193 region.
plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java (2)

1935-1944: ⚠️ Potential issue | 🔴 Critical

把 metadata 更新串行化收敛到 VM/metadataPath。

现在同步键只按 primary storage 分桶,且 getSyncLevel() 允许 10 并发。同一台 VM / 同一个 metadata 文件的并发更新仍会同时落到同一个写路径上,最后一次写入会覆盖前一个结果。建议把 sync signature 至少细化到 msg.getRootVolumeUuid()msg.getMetadataPath(),并把 sync level 收到 1。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 1935 - 1944, The current ChainTask submitted via thdf.chainSubmit
uses a sync signature only keyed by self.getUuid() and a parallelism of 10
(getSyncLevel returns 10), which allows concurrent updates to the same
VM/metadata file and causes race overwrites; change ChainTask.getSyncSignature()
to include a finer-grained identifier such as msg.getRootVolumeUuid() or
msg.getMetadataPath() (e.g., "update-metadata-on-ps-" + self.getUuid() + "-" +
msg.getRootVolumeUuid()) and reduce ChainTask.getSyncLevel() to 1 so updates for
the same VM/metadataPath are serialized. Ensure you update the ChainTask
implementation where thdf.chainSubmit(...) is invoked to reference these message
fields.

2039-2060: ⚠️ Potential issue | 🔴 Critical

cleanup 也要走和 update 相同的串行化通道。

这里直接选 host 并下发 backend,完全绕过了上面的 ChainTask。一旦 cleanup 和 update/register 并发命中同一个 metadata 文件,cleanup 可能删掉刚写好的内容,或者把正在进行的写操作打断。建议复用和 update 完全相同的 sync signature / sync level,把 cleanup 包进同一条串行化路径。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`
around lines 2039 - 2060, The cleanup handler currently picks a host and calls
backend.handle(...) directly, bypassing the ChainTask serialization used by
update/register and risking races; change
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg) to submit the cleanup work
as a ChainTask using the exact same sync signature and sync level used by the
update/register path (so cleanup is enqueued on the same serialized queue), and
move the logic that picks connectedHosts/getBackendByHostUuid and invokes
backend.handle(...) into the ChainTask body; ensure the ChainTask reports
success/failure back using the same ReturnValueCompletion/Reply plumbing as the
update flow rather than replying directly outside the task.
compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java (1)

41-46: ⚠️ Potential issue | 🔴 Critical

GC 执行时不要盲用历史 hostUuid

compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java:89-123 里这个值最多只从 hostUuid/lastHostUuid 兜一层,仍可能为空;而且 GC 延迟执行时原 host 也可能已经不可用。这里直接透传给 cleanup 消息,会让依赖 host 执行清理的后端把一次可恢复的重试变成持续失败。建议在 triggerNow() 重新选择当前可用 host,或明确要求下游在 msg.getHostUuid() == null 时自行选 host。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java`
around lines 41 - 46, The GC flow is passing a potentially stale or absent
hostUuid into CleanupVmInstanceMetadataOnPrimaryStorageMsg; update triggerNow()
in CleanupVmInstanceMetadataOnPrimaryStorageGC so it does not blindly forward
the historical hostUuid from VmExpungeMetadataFlow: either (A) re-resolve a
currently available host before calling msg.setHostUuid(...) (e.g., query
available HostVOs for the primaryStorageUuid and pick a reachable host) or (B)
explicitly clear the hostUuid (msg.setHostUuid(null)) so downstream cleanup
handlers know to choose a host themselves when msg.getHostUuid() == null; adjust
triggerNow() to implement one of these behaviors and add a short comment
referencing CleanupVmInstanceMetadataOnPrimaryStorageMsg to document the choice.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java (1)

346-363: ⚠️ Potential issue | 🟡 Minor

simulator 需要回填读取/扫描结果。

现在 getVmInstanceMetadata() / scanVmMetadata() 只记录了 command,成功分支却返回空 rsp。这样新增用例最多只能断言“接口被调用”,无法覆盖上层对 metadata 内容的解析和聚合逻辑。至少需要把预置的 metadata 结果填回这两个响应对象。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java`
around lines 346 - 363, The simulator handlers getVmInstanceMetadata and
scanVmMetadata currently record the incoming commands but return empty
GetVmInstanceMetadataRsp/ScanVmMetadataRsp objects; change both to populate
their response objects with preconfigured metadata from the simulator config
(e.g., a map/list of preloaded metadata entries) that matches the incoming
GetVmInstanceMetadataCmd/ScanVmMetadataCmd (lookup by VM UUID or scan path),
assign the matching metadata into the appropriate fields on
GetVmInstanceMetadataRsp and ScanVmMetadataRsp, then call reply(entity, rsp);
ensure you reference the existing symbols GetVmInstanceMetadataCmd,
ScanVmMetadataCmd, GetVmInstanceMetadataRsp, ScanVmMetadataRsp,
config.getVmInstanceMetadataCmds and config.scanVmMetadataCmds when implementing
the lookup and response population.
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java (2)

3413-3437: ⚠️ Potential issue | 🔴 Critical

失效 hostUuid 的回退/终态处理前一轮提过,这里还是原样。

get 路径仍然只有在 hostUuid == null 时才回退到 rootVolumeUuidcleanup 路径也没有把“host 已删除/解绑”当成终态。卷迁移后会读错 host,VM 已 expunge 的清理则会继续失败并把 GC 拖成持续重试。

Also applies to: 3509-3527

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`
around lines 3413 - 3437, The code only falls back to rootVolumeUuid when
msg.getHostUuid() is null but still treats failures from
getHostUuidByResourceUuid(...) as transient errors; change this to treat "host
not found / host unbound / resource deleted" as a terminal/no-op for both the
get path (around hostUuid/getHostUuidByResourceUuid and bkd.handle(...)) and the
cleanup path (the similar block at lines 3509-3527): detect when
getHostUuidByResourceUuid returns null or throws an exception that indicates the
resource/host is deleted/unbound, and in that case short-circuit by returning
the appropriate terminal reply (no-op success or explicit terminal error as your
codebase expects) instead of leaving it to retry/requeue; ensure both the get
handler and the cleanup handler use the same terminal-handling logic.

3358-3367: ⚠️ Potential issue | 🔴 Critical

这个 metadata 写入并发问题前一轮已经提过,这里还没收住。

getSyncSignature() 还是只按 PS 维度分桶,而 getSyncLevel() 仍然放到 10;同一 VM/root volume 的 metadata 更新会并发落到同一块 local PS,上一次写很容易被后一次覆盖或乱序。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`
around lines 3358 - 3367, 当前的 ChainTask 在 thdf.chainSubmit(...) 中的
getSyncSignature() 仅按主存储(self.getUuid())分桶且 getSyncLevel() 返回 10,会导致同一
VM/根卷(root volume)在同一 PS 上的 metadata 更新并发并发生覆盖或乱序;请把 getSyncSignature()
扩展为包含更细粒度的标识(例如在 "update-metadata-on-ps-" + self.getUuid() 后追加 rootVolumeUuid 或
vmUuid/volumeUuid)以按 VM/卷级别分桶,并将 getSyncLevel() 从 10 调整为更严格的级别(例如 1)以强制序列化这些更新;在
ChainTask 的 getSyncSignature() 与 getSyncLevel() 实现处做相应修改以避免并发写覆盖。
plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java (1)

3924-3929: ⚠️ Potential issue | 🔴 Critical

清理元数据前仍缺少 hostUuid 兜底或前置校验。

Line 3928 仍然直接把 hostUuid 传给 httpCall(...)。在 VM expunge 的 GC fallback 场景里,这个值可能为空;一旦为空,主机路由会直接失败,导致元数据清理根本不会下发。这里至少要先 fail-fast,更稳妥的是在发送前补一个 host 解析兜底。

🔧 最小修复
 `@Override`
 void handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid, ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply> completion) {
+    if (StringUtils.isBlank(hostUuid)) {
+        completion.fail(operr("cleanup vm metadata failed: hostUuid is null, metadataPath=%s", msg.getMetadataPath()));
+        return;
+    }
+
     CleanupVmMetadataCmd cmd = new CleanupVmMetadataCmd();
     cmd.metadataPath = msg.getMetadataPath();
 
     httpCall(CLEANUP_VM_METADATA_PATH, hostUuid, cmd, CleanupVmMetadataRsp.class, new ReturnValueCompletion<CleanupVmMetadataRsp>(completion) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`
around lines 3924 - 3929, The handler method
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid,
ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>
completion) must guard against a null hostUuid before calling
httpCall(CLEANUP_VM_METADATA_PATH, hostUuid,...); either fail-fast by calling
completion.fail(...) with a clear error when hostUuid is null, or resolve a
fallback host (e.g. look up a connected host for the primary storage using
msg.getPrimaryStorageUuid() or a HostFinder/DAO) and set hostUuid to that value
before invoking httpCall; ensure the error path uses the same completion to
return failure and the resolved-path passes the non-null hostUuid into httpCall.
🧹 Nitpick comments (3)
sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java (1)

28-35: 原始类型 List 缺少泛型参数。

vmUuidssystemTagsuserTags 字段使用了原始的 java.util.List 类型而不是 List<String>。虽然这在 ZStack SDK 自动生成的代码中是常见模式,但会产生编译器警告。

♻️ 如果手动维护此文件,建议添加泛型参数
-    public java.util.List vmUuids;
+    public java.util.List<String> vmUuids;

-    public java.util.List systemTags;
+    public java.util.List<String> systemTags;

-    public java.util.List userTags;
+    public java.util.List<String> userTags;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java` around
lines 28 - 35, The fields vmUuids, systemTags, and userTags in class
UpdateVmInstanceMetadataAction are declared as raw java.util.List which causes
compiler warnings; change their types to java.util.List<String> (i.e., replace
the raw types for vmUuids, systemTags, and userTags with generic List<String>)
while keeping the existing `@Param` annotations and other code unchanged so the
signatures are type-safe and warnings are eliminated.
header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java (1)

15-26: extractOldPrefix 方法逻辑正确,建议添加单元测试。

该方法从路径中提取存储基础目录前缀(如从 /mnt/ps/rootVolumes/... 中提取 /mnt/ps/)。逻辑正确,但由于涉及字符串索引计算,建议为该方法添加单元测试以覆盖各种边界情况,例如:

  • 路径不包含任何 marker
  • 路径包含多个 marker
  • marker 位于路径开头
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java`
around lines 15 - 26, Add unit tests for
VmInstanceMetadataConstants.extractOldPrefix to cover null and non-leading-slash
inputs, paths that do not contain any STORAGE_PATH_MARKERS, paths containing
multiple markers (ensure the first marker is used and returned prefix ends with
'/'), and a path where a marker is at the very beginning; use the actual
STORAGE_PATH_MARKERS constant in tests and assert expected substring results
(including null returns) to validate the index/substr logic and boundary
conditions.
compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java (1)

1329-1352: 验证逻辑实现正确,但错误消息可以更清晰。

primaryStorageUuid 在数据库中不存在时,psType 为 null,错误消息会显示 type:null,这对用户排查问题不够友好。建议在 extension point 检查失败时区分"主存储不存在"和"主存储类型不支持元数据"两种情况。

♻️ 建议改进错误消息
         String psType = Q.New(PrimaryStorageVO.class)
                 .select(PrimaryStorageVO_.type)
                 .eq(PrimaryStorageVO_.uuid, psUuid)
                 .findValue();
+        if (psType == null) {
+            throw new ApiMessageInterceptionException(argerr(
+                    "primary storage[uuid:%s] not found", psUuid));
+        }
         VmMetadataPathBuildExtensionPoint ext = (psType != null)
                 ? pluginRgty.getExtensionFromMap(psType, VmMetadataPathBuildExtensionPoint.class) : null;
         if (ext == null) {
             throw new ApiMessageInterceptionException(argerr(
-                    "primary storage[uuid:%s, type:%s] does not support vm metadata", psUuid, psType));
+                    "primary storage[uuid:%s, type:%s] does not support vm metadata registration", psUuid, psType));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`
around lines 1329 - 1352, In validate(APIRegisterVmInstanceFromMetadataMsg msg)
improve error clarity by first checking whether a PrimaryStorageVO exists for
psUuid (query for existence or select uuid) and throw
ApiMessageInterceptionException(argerr("primary storage[uuid:%s] not found",
psUuid)) if missing; only then obtain psType and call
pluginRgty.getExtensionFromMap(...) for VmMetadataPathBuildExtensionPoint, and
if ext is null throw ApiMessageInterceptionException(argerr("primary
storage[uuid:%s, type:%s] does not support vm metadata", psUuid, psType)); keep
the existing ext.validateMetadataPath(psUuid, path) behavior for path-specific
errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@conf/db/upgrade/V5.0.0__schema.sql`:
- Around line 9-10: The timestamp columns lastOpDate and createDate are
missing/using a sentinel default; update the schema so lastOpDate includes a
DEFAULT (e.g., DEFAULT CURRENT_TIMESTAMP) in addition to ON UPDATE
CURRENT_TIMESTAMP and change createDate to DEFAULT CURRENT_TIMESTAMP if you
intend to record actual creation time; if the sentinel '1999-12-31 23:59:59' is
intentional, add a comment near the column definitions clarifying that
createDate uses a sentinel value and leave it as-is.

---

Duplicate comments:
In
`@compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java`:
- Around line 41-46: The GC flow is passing a potentially stale or absent
hostUuid into CleanupVmInstanceMetadataOnPrimaryStorageMsg; update triggerNow()
in CleanupVmInstanceMetadataOnPrimaryStorageGC so it does not blindly forward
the historical hostUuid from VmExpungeMetadataFlow: either (A) re-resolve a
currently available host before calling msg.setHostUuid(...) (e.g., query
available HostVOs for the primaryStorageUuid and pick a reachable host) or (B)
explicitly clear the hostUuid (msg.setHostUuid(null)) so downstream cleanup
handlers know to choose a host themselves when msg.getHostUuid() == null; adjust
triggerNow() to implement one of these behaviors and add a short comment
referencing CleanupVmInstanceMetadataOnPrimaryStorageMsg to document the choice.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java`:
- Around line 3413-3437: The code only falls back to rootVolumeUuid when
msg.getHostUuid() is null but still treats failures from
getHostUuidByResourceUuid(...) as transient errors; change this to treat "host
not found / host unbound / resource deleted" as a terminal/no-op for both the
get path (around hostUuid/getHostUuidByResourceUuid and bkd.handle(...)) and the
cleanup path (the similar block at lines 3509-3527): detect when
getHostUuidByResourceUuid returns null or throws an exception that indicates the
resource/host is deleted/unbound, and in that case short-circuit by returning
the appropriate terminal reply (no-op success or explicit terminal error as your
codebase expects) instead of leaving it to retry/requeue; ensure both the get
handler and the cleanup handler use the same terminal-handling logic.
- Around line 3358-3367: 当前的 ChainTask 在 thdf.chainSubmit(...) 中的
getSyncSignature() 仅按主存储(self.getUuid())分桶且 getSyncLevel() 返回 10,会导致同一
VM/根卷(root volume)在同一 PS 上的 metadata 更新并发并发生覆盖或乱序;请把 getSyncSignature()
扩展为包含更细粒度的标识(例如在 "update-metadata-on-ps-" + self.getUuid() 后追加 rootVolumeUuid 或
vmUuid/volumeUuid)以按 VM/卷级别分桶,并将 getSyncLevel() 从 10 调整为更严格的级别(例如 1)以强制序列化这些更新;在
ChainTask 的 getSyncSignature() 与 getSyncLevel() 实现处做相应修改以避免并发写覆盖。

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java`:
- Around line 3924-3929: The handler method
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg msg, String hostUuid,
ReturnValueCompletion<CleanupVmInstanceMetadataOnPrimaryStorageReply>
completion) must guard against a null hostUuid before calling
httpCall(CLEANUP_VM_METADATA_PATH, hostUuid,...); either fail-fast by calling
completion.fail(...) with a clear error when hostUuid is null, or resolve a
fallback host (e.g. look up a connected host for the primary storage using
msg.getPrimaryStorageUuid() or a HostFinder/DAO) and set hostUuid to that value
before invoking httpCall; ensure the error path uses the same completion to
return failure and the resolved-path passes the non-null hostUuid into httpCall.

In
`@plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java`:
- Around line 346-363: The simulator handlers getVmInstanceMetadata and
scanVmMetadata currently record the incoming commands but return empty
GetVmInstanceMetadataRsp/ScanVmMetadataRsp objects; change both to populate
their response objects with preconfigured metadata from the simulator config
(e.g., a map/list of preloaded metadata entries) that matches the incoming
GetVmInstanceMetadataCmd/ScanVmMetadataCmd (lookup by VM UUID or scan path),
assign the matching metadata into the appropriate fields on
GetVmInstanceMetadataRsp and ScanVmMetadataRsp, then call reply(entity, rsp);
ensure you reference the existing symbols GetVmInstanceMetadataCmd,
ScanVmMetadataCmd, GetVmInstanceMetadataRsp, ScanVmMetadataRsp,
config.getVmInstanceMetadataCmds and config.scanVmMetadataCmds when implementing
the lookup and response population.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java`:
- Around line 1935-1944: The current ChainTask submitted via thdf.chainSubmit
uses a sync signature only keyed by self.getUuid() and a parallelism of 10
(getSyncLevel returns 10), which allows concurrent updates to the same
VM/metadata file and causes race overwrites; change ChainTask.getSyncSignature()
to include a finer-grained identifier such as msg.getRootVolumeUuid() or
msg.getMetadataPath() (e.g., "update-metadata-on-ps-" + self.getUuid() + "-" +
msg.getRootVolumeUuid()) and reduce ChainTask.getSyncLevel() to 1 so updates for
the same VM/metadataPath are serialized. Ensure you update the ChainTask
implementation where thdf.chainSubmit(...) is invoked to reference these message
fields.
- Around line 2039-2060: The cleanup handler currently picks a host and calls
backend.handle(...) directly, bypassing the ChainTask serialization used by
update/register and risking races; change
handle(CleanupVmInstanceMetadataOnPrimaryStorageMsg) to submit the cleanup work
as a ChainTask using the exact same sync signature and sync level used by the
update/register path (so cleanup is enqueued on the same serialized queue), and
move the logic that picks connectedHosts/getBackendByHostUuid and invokes
backend.handle(...) into the ChainTask body; ensure the ChainTask reports
success/failure back using the same ReturnValueCompletion/Reply plumbing as the
update flow rather than replying directly outside the task.

In
`@plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java`:
- Around line 2157-2162: ScanVmInstanceMetadataFromPrimaryStorageReply may be
returned with a null vmInstanceMetadata when rsp.metadataEntries is null,
causing NPEs for callers; modify the block around
ScanVmInstanceMetadataFromPrimaryStorageReply r so that you always call
r.setVmInstanceMetadata(...) with an empty list when rsp.metadataEntries is null
(e.g. Collections.emptyList()), and still set hostUuid on entries when present
before calling completion.success(r); ensure use of
ScanVmInstanceMetadataFromPrimaryStorageReply, rsp.metadataEntries,
entry.setHostUuid(...), r.setVmInstanceMetadata(...), and
completion.success(...) as the target symbols.
- Around line 2089-2095: The success branches in NfsPrimaryStorageKVMBackend
handling WriteVmMetadataRsp (method handling reply using WriteVmMetadataRsp)
currently call completion.success(...) but skip the capacity refresh; after a
successful write/delete response invoke updatePrimaryStorageCapacity(...) (the
same routine used by asyncHttpCall-based handlers) to refresh the primary
storage capacity for the affected primary storage/host before completing the
operation so metadata file create/delete updates capacity immediately; apply the
same change to the analogous handler around the WriteVmMetadataRsp logic at the
2186-2193 region.

---

Nitpick comments:
In `@compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java`:
- Around line 1329-1352: In validate(APIRegisterVmInstanceFromMetadataMsg msg)
improve error clarity by first checking whether a PrimaryStorageVO exists for
psUuid (query for existence or select uuid) and throw
ApiMessageInterceptionException(argerr("primary storage[uuid:%s] not found",
psUuid)) if missing; only then obtain psType and call
pluginRgty.getExtensionFromMap(...) for VmMetadataPathBuildExtensionPoint, and
if ext is null throw ApiMessageInterceptionException(argerr("primary
storage[uuid:%s, type:%s] does not support vm metadata", psUuid, psType)); keep
the existing ext.validateMetadataPath(psUuid, path) behavior for path-specific
errors.

In
`@header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java`:
- Around line 15-26: Add unit tests for
VmInstanceMetadataConstants.extractOldPrefix to cover null and non-leading-slash
inputs, paths that do not contain any STORAGE_PATH_MARKERS, paths containing
multiple markers (ensure the first marker is used and returned prefix ends with
'/'), and a path where a marker is at the very beginning; use the actual
STORAGE_PATH_MARKERS constant in tests and assert expected substring results
(including null returns) to validate the index/substr logic and boundary
conditions.

In `@sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java`:
- Around line 28-35: The fields vmUuids, systemTags, and userTags in class
UpdateVmInstanceMetadataAction are declared as raw java.util.List which causes
compiler warnings; change their types to java.util.List<String> (i.e., replace
the raw types for vmUuids, systemTags, and userTags with generic List<String>)
while keeping the existing `@Param` annotations and other code unchanged so the
signatures are type-safe and warnings are eliminated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0f591b0c-697a-4b75-90dc-ffb19e0ca16c

📥 Commits

Reviewing files that changed from the base of the PR and between 146ae94 and a86fd7a.

⛔ Files ignored due to path filters (5)
  • conf/globalConfig/vm.xml is excluded by !**/*.xml
  • conf/persistence.xml is excluded by !**/*.xml
  • conf/serviceConfig/primaryStorage.xml is excluded by !**/*.xml
  • conf/serviceConfig/vmInstance.xml is excluded by !**/*.xml
  • conf/springConfigXml/VmInstanceManager.xml is excluded by !**/*.xml
📒 Files selected for processing (144)
  • compute/src/main/java/org/zstack/compute/vm/CleanupVmInstanceMetadataOnPrimaryStorageGC.java
  • compute/src/main/java/org/zstack/compute/vm/VmExpungeMetadataFlow.java
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
  • compute/src/main/java/org/zstack/compute/vm/VmInstanceApiInterceptor.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
  • conf/db/upgrade/V5.0.0__schema.sql
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagsMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachIsoFromVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIMigrateVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataConstants.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageBase.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageHypervisorBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageKvmBackend.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulator.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageVmMetadataExtension.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorage.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackend.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageKVMBackendCommands.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsVmMetadataExtension.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • sdk/src/main/java/SourceClassMap.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataAction.java
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • storage/src/main/java/org/zstack/storage/primary/PrimaryStorageBase.java
  • testlib/src/main/java/org/zstack/testlib/ApiHelper.groovy
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy
✅ Files skipped from review due to trivial changes (93)
  • sdk/src/main/java/org/zstack/sdk/UpdateVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataReply.java
  • sdk/src/main/java/org/zstack/sdk/RegisterVmInstanceFromMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataCategory.java
  • header/src/main/java/org/zstack/header/vm/metadata/VolumeResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APISetVmQxlMemoryMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmPriorityMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUngroupVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertTemplatedVmInstanceToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicNetworkMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmUuidFromApiResolver.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/APIDestroyVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataReply.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIRecoverDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APIConvertVmInstanceToTemplatedVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/tag/APIUpdateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/vm/APIReimageVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDetachDataVolumeFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmHostnameMsg.java
  • testlib/src/main/java/org/zstack/testlib/LocalStorageSpec.groovy
  • header/src/main/java/org/zstack/header/vm/APISetVmSoundTypeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/ResourceMetadata.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/volume/APISyncVolumeSizeMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeVmNicStateMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/RebaseVolumeBackingFileOnPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIUpdateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmStaticIpMsg.java
  • header/src/main/java/org/zstack/header/tag/APIDeleteTagMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachVmNicToVmMsg.java
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/LocalStorageSimulatorConfig.java
  • sdk/src/main/java/org/zstack/sdk/CleanupVmInstanceMetadataResult.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmBootModeMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmClockTrackMsg.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/ScanVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APISetVmBootOrderMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/CleanupVmInstanceMetadataOnPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/volume/APIAttachDataVolumeToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachL3NetworkToVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIChangeInstanceOfferingMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/ReadVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageEvent.java
  • header/src/main/java/org/zstack/header/tag/APICreateSystemTagMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIUpdateVolumeSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathBuildExtensionPoint.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/volume/APIUpdateVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIShrinkVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIDeleteVolumeSnapshotGroupMsg.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIUpdateResourceConfigMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIRevertVolumeFromSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmNicDriverMsg.java
  • header/src/main/java/org/zstack/header/volume/APICreateVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APIAttachIsoToVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEventDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/primary/VmMetadataScanEntry.java
  • compute/src/main/java/org/zstack/compute/vm/VmSystemTags.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyService.java
  • header/src/main/java/org/zstack/header/storage/primary/APIScanVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • plugin/localstorage/src/main/java/org/zstack/storage/primary/local/APILocalStorageMigrateVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageReply.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIGetVmInstanceMetadataFromPrimaryStorageMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO_.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsgDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/vm/APIRecoverVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/volume/APIExpungeDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/volume/APIFlattenVolumeMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/APICleanupVmInstanceMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/cdrom/APISetVmInstanceDefaultCdRomMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmInstanceMetadataDTO.java
  • sdk/src/main/java/org/zstack/sdk/VmMetadataScanEntry.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataFingerprintVO.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataResourcePersistExtensionPoint.java
  • sdk/src/main/java/SourceClassMap.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataMsgDoc_zh_cn.groovy
  • compute/src/main/java/org/zstack/compute/vm/VmGlobalConfig.java
🚧 Files skipped from review as they are similar to previous changes (24)
  • header/src/main/java/org/zstack/header/vm/APIDetachL3NetworkFromVmMsg.java
  • header/src/main/java/org/zstack/header/vm/APIDeleteVmHostnameMsg.java
  • header/src/main/java/org/zstack/header/volume/APIDeleteDataVolumeMsg.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIDeleteVolumeSnapshotMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmConsolePasswordMsg.java
  • header/src/main/java/org/zstack/header/vm/VmInstanceState.java
  • resourceconfig/src/main/java/org/zstack/resourceconfig/APIDeleteResourceConfigMsg.java
  • sdk/src/main/java/org/zstack/sdk/GetVmInstanceMetadataFromPrimaryStorageResult.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataDirtyVO.java
  • header/src/main/java/org/zstack/header/vm/APIUpdateVmInstanceMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/MetadataImpact.java
  • plugin/nfsPrimaryStorage/src/main/java/org/zstack/storage/primary/nfs/NfsPrimaryStorageBackend.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIUpdateVmInstanceMetadataMsg.java
  • header/src/main/java/org/zstack/header/volume/APIUndoSnapshotCreationMsg.java
  • header/src/main/java/org/zstack/header/volume/APIChangeVolumeStateMsg.java
  • header/src/main/java/org/zstack/header/vm/APISetVmSshKeyMsg.java
  • header/src/main/java/org/zstack/header/storage/primary/GetVmInstanceMetadataFromPrimaryStorageMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataPathReplacementExtensionPoint.java
  • sdk/src/main/java/org/zstack/sdk/ScanVmInstanceMetadataFromPrimaryStorageAction.java
  • header/src/main/java/org/zstack/header/vm/metadata/APIRegisterVmInstanceFromMetadataEvent.java
  • header/src/main/java/org/zstack/header/vm/metadata/VmMetadataErrors.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/APIRevertVmFromSnapshotGroupMsg.java
  • header/src/main/java/org/zstack/header/vm/metadata/UpdateVmInstanceMetadataOnPrimaryStorageMsg.java
  • testlib/src/main/java/org/zstack/testlib/NfsPrimaryStorageSpec.groovy

Comment on lines +9 to +10
`lastOpDate` timestamp on update CURRENT_TIMESTAMP,
`createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

lastOpDatecreateDate 的默认值需要确认。

  1. lastOpDate 仅有 on update CURRENT_TIMESTAMP,缺少 DEFAULT 子句。首次 INSERT 时该列将为 NULL,直到发生 UPDATE 才会有值。
  2. createDate 使用 '1999-12-31 23:59:59' 作为默认值而非 CURRENT_TIMESTAMP,这是一个异常的占位符日期。

建议确认这是否为预期的 sentinel 值模式。如果需要记录实际创建时间,应使用 DEFAULT CURRENT_TIMESTAMP

💡 如果需要修复时间戳默认值
-    `lastOpDate` timestamp on update CURRENT_TIMESTAMP,
-    `createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59',
+    `lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    `createDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`lastOpDate` timestamp on update CURRENT_TIMESTAMP,
`createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59',
`lastOpDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`createDate` timestamp NOT NULL DEFAULT '1999-12-31 23:59:59',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@conf/db/upgrade/V5.0.0__schema.sql` around lines 9 - 10, The timestamp
columns lastOpDate and createDate are missing/using a sentinel default; update
the schema so lastOpDate includes a DEFAULT (e.g., DEFAULT CURRENT_TIMESTAMP) in
addition to ON UPDATE CURRENT_TIMESTAMP and change createDate to DEFAULT
CURRENT_TIMESTAMP if you intend to record actual creation time; if the sentinel
'1999-12-31 23:59:59' is intentional, add a comment near the column definitions
clarifying that createDate uses a sentinel value and leave it as-is.

@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/ZSV-11559@@4 branch from a86fd7a to 9dce3f2 Compare March 26, 2026 13:03
APIImpact

Resolves: ZSV-11559

Change-Id: I6b6a6378627264646d6a76726762736e77787373
@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/ZSV-11559@@4 branch from 9dce3f2 to ec40b4b Compare March 26, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants