Skip to content

CP-312874 SMAPIv3 storage-layer engine for SXM v3 snapshot migration - #7223

Merged
LunfanZhang merged 5 commits into
xapi-project:feature/sxm-v3from
LunfanZhang:private/luzhan/sxm-v3-p2-engine
Sep 14, 2026
Merged

LunfanZhang merged 5 commits into
xapi-project:feature/sxm-v3from
LunfanZhang:private/luzhan/sxm-v3-p2-engine

Conversation

@LunfanZhang

@LunfanZhang LunfanZhang commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

SMAPIv1 destinations reproduce a VM's snapshot chains by: each copy lands on whichever destination VDI has the most similar content, which happens to be the copy of the snapshot
before it. SMAPIv3 backends report no similar content, so every VDI arrives as a full, unrelated copy and the structure is lost.

The fix is to let the caller name the base, and have the VM migration path work out what it should be.

  • DATA.copy2 — DATA.copy with an explicit dest_base. SMAPIv1 keeps its own similar_content search and ignores it; SMAPIv3 clones the base, mirrors the source VDI into the clone over the NBD proxy, and freezes the result into a snapshot.

  • snapshot_parent — each vdi_mirror records the VDI it directly follows, derived by lineage_parent_of from the VM snapshot tree. A branch the VM has reverted away from starts a chain of its own.

  • Copy order — same-sized VDIs are now ordered by a depth-first walk of those edges rather than by snapshot time, so a VDI is always copied after its base. with_many already hands each VDI the results of the ones before it, so that accumulator is the source→destination mapping; miss parent just means a full copy.

Two layouts a SMAPIv3 destination cannot represent:

  1. a VDI.snapshot that belongs to no VM snapshot,
  2. a snapshot VDI whose active disk was deleted

both are refused with operation_not_allowed before anything is copied.

Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml
module MIRROR : SMAPIv2_MIRROR = struct
type context = unit

let send_start _ctx ~dbg ~task_id:_ ~dp ~sr ~vdi ~image_format ~mirror_vm

@changlei-li changlei-li Aug 14, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

send_start now blocks synchronously for the whole tree. Have you estimated the time it takes is acceptable when the tree is deep and wide?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

QEMU mirror has better performance, so when the tree is deep and wide, the new SMAPIv3 will much faster then SMAPIv1.
fig1_snapshots

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Were confounding removed from the comparison? Like using different hardware disks for both tests

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good question - Both sources are now local SSD on hardware-identical hosts. The two hosts are the same model throughout: AMD EPYC 9124 16-Core (32 CPU, 3.0 GHz), 137 GB RAM, XenServer 9.0.0 same build, and — directly on your point — the local SR on both hosts is the same disk, sda = PERC H755N Front, 1.5 TB, non-rotational, behind an identical Broadcom MegaRAID SAS39xx controller. The xfs (V3) and ext4 (V1) SRs are filesystems on the same disk model and controller, not different hardware tiers.

@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch from f687782 to c91f92c Compare August 17, 2026 05:38
Comment thread ocaml/xapi/storage_migrate_helper.ml Outdated
Comment on lines +328 to +341
let set_snapshot_mappings mirror_id relations =
Xapi_stdext_threads.Threadext.Mutex.execute mutex (fun () ->
Hashtbl.replace snapshot_mappings mirror_id relations
)

let get_snapshot_mappings mirror_id =
Xapi_stdext_threads.Threadext.Mutex.execute mutex (fun () ->
Hashtbl.find_opt snapshot_mappings mirror_id |> Option.value ~default:[]
)

let remove_snapshot_mappings mirror_id =
Xapi_stdext_threads.Threadext.Mutex.execute mutex (fun () ->
Hashtbl.remove snapshot_mappings mirror_id
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we create a separate mutex for the snapshot_mappings table, rather than reuse the access_table mutex? It seems to me that they are independent, and this creates unnecessary contention.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. update to use a new snapshot_mappings_m rather than reuse the mutex .

@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch 2 times, most recently from 32bce32 to 9013ff6 Compare August 25, 2026 05:40
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
@minglumlu

Copy link
Copy Markdown
Member

Generally, I prefer changing the way how to iterate individual VDIs to introducing the whole VDI tree when handling an active VDI.

send_start and receive_start3 spell out between them how a mirror is
set up: the path the nbd proxy listens on, the nbd URI built from it,
the thread that serves the proxy, and the export read out of the attach
info. Copying a snapshot chain is about to need all four again, so give
each of them a name first.

Waiting for a mirror to finish becomes wait_for_mirror. It takes the
message to fail with, and the mirror id whose send state it marks as
failed is now optional, so a copy that has no entry there can wait the
same way.

Two changes in behaviour come with the move. The wait loop sleeps
between polls instead of asking the storage layer as fast as it can
answer, and a Storage_error raised under send_start is re-raised
unchanged rather than flattened into a mirror failure, so the reason a
mirror never started survives the trip back to the caller.

Signed-off-by: Lunfan Zhang[Lunfan.Zhang] <Lunfan.Zhang@cloud.com>
start_nbd_proxy_thread hands back a thread and nothing else, and the
first thing that thread does is bind and listen on the socket path the
caller is about to give qemu-dp. Whether the listener is up in time is
left to the scheduler.

Bind and listen on the caller's thread instead and pass the socket in,
leaving accept and proxy to the thread. Once listen has returned the
kernel queues the connection in the backlog, so it no longer matters
where the thread has got to, and a bind that fails now fails the caller
rather than killing a detached thread.

Signed-off-by: Lunfan Zhang[Lunfan.Zhang] <Lunfan.Zhang@cloud.com>
@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch 2 times, most recently from 0eeb342 to ff6cdb3 Compare September 1, 2026 02:27
@LunfanZhang

Copy link
Copy Markdown
Collaborator Author

Generally, I prefer changing the way how to iterate individual VDIs to introducing the whole VDI tree when handling an active VDI.

@minglumlu that is what the latest push does. There is no VDI tree any more: send_start handles a single
VDI, and the chain is reproduced by ordering the VDIs plus one parent pointer on each.

  1. One parent pointer per VDI, not a tree

vdi_mirror gains

  ; snapshot_parent: [`VDI] API.Ref.t option
        (** The VDI this one directly follows in its disk's snapshot chain *)

filled by lineage_parent_of ~__context ~vm ~vdi, which walks VM.parent upwards from the VDI's owning VM to the
nearest ancestor that still has a disk in the same snapshot_of lineage. A branch the VM has reverted away from
therefore starts a chain of its own.

  1. Ordering: depth-first, as the tiebreak of the existing size sort

position numbers all_vdis in DFS pre-order over those edges (roots = no parent, or a parent that is not in the
migration set); siblings go oldest-first, so the live disk comes last. Size stays the primary key:

  let compare_fun v1 v2 =
    match Int64.compare v1.size v2.size with
    | 0 -> compare (position v1) (position v2)
    | r -> r

For a VM with disks A and B, four snapshots each, A smaller than B:

  [S_A1  S_A2  S_A3  S_A4  A_leaf   S_B1  S_B2  S_B3  S_B4  B_leaf]

The invariant this buys: a VDI is always copied after the VDI it follows.

  1. with_many hands each VDI what came before it — a one-line change

with_many already accumulated results and passed them to the final continuation; it just never gave them to the
individual elements:

  -        withfn x (fun y -> inner xs (y :: acc))
  +        withfn acc x (fun y -> inner xs (y :: acc))

That accumulator is done_map, a mirror_record list carrying mr_local_vdi_reference / mr_remote_vdi / mr_remote_sr.
vdi_copy_fun still handles exactly one VDI, so no with_vdi_tree / with_vdi_forest was needed.

  1. Where the two meet
  (* The destination copy of the VDI this one follows, if it is already on
     the same destination SR. Copying onto it reproduces the chain. *)
  let dest_base =
    Option.bind vconf.snapshot_parent (fun parent ->
        List.find_opt
          (fun mr ->
            mr.mr_local_vdi_reference = parent && mr.mr_remote_sr = dest_sr
          )
          done_map
        |> Option.map (fun mr -> mr.mr_remote_vdi)
    )

Because of (2), the parent is already in done_map by the time a node is reached, so the copy lands on it and the
chain is rebuilt one node at a time. None just means a full copy — never wrong data.

  1. Storage layer

The base is passed down via the new DATA.copy2: SMAPIv1 finds its own base through similar_content/nearest and
ignores dest_base; SMAPIv3 reports no similar content and relies on it.

Storage_smapiv3_migrate.get_snapshot_tree and the State.snapshot_mappings side table are both gone — no
tree is discovered, built or persisted anywhere.

Comment thread ocaml/xapi/xapi_vm_migrate.ml
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
; snapshot_of: [`VDI] API.Ref.t (** API's snapshot_of reference *)
; snapshot_parent: [`VDI] API.Ref.t option
(** The VDI this one directly follows in its disk's snapshot chain *)
; do_mirror: bool (** Whether we should mirror or just copy the VDI *)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now should it be is_active_leaf for being more accurate?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think snapshot_parent is accurate expression here.

Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
mr.mr_local_vdi_reference = parent && mr.mr_remote_sr = dest_sr
)
done_map
|> Option.map (fun mr -> mr.mr_remote_vdi)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It should be a bug in sorting VDIs when can't finding mirrored/copied parent on destination side. It should fail.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed — Option.bind silently swallows the case where the parent cannot be found, which should abort the migration with an explicit error instead.
Changed Option.bind to Option.map, so None can now only mean "this VDI is the root of its chain"; a parent that has not been copied raises instead.

Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
Comment thread ocaml/xapi/xapi_vm_migrate.ml
Comment thread ocaml/xapi/xapi_vm_migrate.ml
Comment thread ocaml/xapi/xapi_vm_migrate.ml
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
@-> vm_p
@-> url_p
@-> dest_p
@-> dest_base_p

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How about dest_sr_p and dest_vdi_p?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

dest_p is pre-existing: it is declared for DATA.copy and shared by copy2. I would rather not rename.
For dest_base_p I would prefer to keep the name. dest_vdi_p is already taken by another place (e.g., https://github.com/xapi-project/xen-api/blob/master/ocaml/xapi-idl/storage/storage_interface.ml#L733) and means the opposite. dest_base_p means a base parent here which sounds reasonable.


(** [copy] with the base of the destination copy given by the caller
rather than found by the backend. *)
let copy2 =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not related with this change.
But I think it's worth adding a comment for the local parent so that the semantic of the copy would be to copy all delta changes between (parent_vdi, vdi]. It is not used so far because for SMAPIv1, the parent is retrieved (layer-violation) again in vhd-tool, and current SMAPIv3 mirror doesn't support it. When the parent is None, it means the underline storage implementation needs to retrieve all changes up to the hidden root VDI. Lacking of this actually introduces some limitations - we have to assume there are no hidden VDIs between the visible VDIs in storage implementation.
So adding it now can make it easier for future improvements.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, copy2 pins the destination base but leaves the source-side delta base to the backend, which derives it from
the source chain itself and therefore assumes no hidden VDI in between.

I have written that down on copy2.

but I would rather not add the parameter in this PR. One that no backend impl is a trap — Some p reads as a promise and would be silently ignored. I would prefer to add it together with the backend change that can actually use it.

@minglumlu

minglumlu commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

Hi @LunfanZhang
The overview looks good to me now. Thank you! One still standing major comment is on how to prepare the VDI list. I would suggest to present the VDI trees explicitly. It's general data structures for both SMAPIv1 and SMAPIv3. They are helpful for understanding and hence ensuring the correctness. The sorting algorithm can keep the same for v1 trees to ensure no regression. May I please have your thoughts?

@gthvn1 gthvn1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Within the limits of my skills, as this code goes beyond them, I don't spot any errors. Congrats for the great work!

@LunfanZhang

Copy link
Copy Markdown
Collaborator Author

Hi @LunfanZhang The overview looks good to me now. Thank you! One still standing major comment is on how to prepare the VDI list. I would suggest to present the VDI trees explicitly. It's general data structures for both SMAPIv1 and SMAPIv3. They are helpful for understanding and hence ensuring the correctness. The sorting algorithm can keep the same for v1 trees to ensure no regression. May I please have your thoughts?

Thanks @minglumlu for your time to review this. I believe for the both SMAPIv1 and SMAPIv3 should apply the algorithm to DFS within the tree from top to down, and sort each trees with leaf size. I can keep the same for v1 trees to ensure no regression, but as I mentioned at : #7223 (comment), there is bug in current SMAPIv1 algorithm, so although it takes some risk, I think it`d better to change the SMAPIv1.

@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch from ff6cdb3 to 34d3b59 Compare September 4, 2026 06:38
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
| None ->
true
| Some parent ->
not (List.exists (fun v -> v.vdi = parent) all_vdis)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This means there is a parent which is not in all_vdis. SMAPIv1 will support it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

lineage_parent_of walks the VM.parent chain, which only contains this VM's own snapshots, and their disks are all in all_vdis.
A invalid VM.parent is already turned into None by the is_valid_ref check at the top of that function. So neither backend (v3 or v1) can reach this branch — it is defensive only.

Even there is hidden VDI, it does matter, I think SMAPIv1 do not care too much about the order as it check the parent directly with similar_content with each Copy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This would go through the whole all_vdis again.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
in
let is_snapshot v = Db.is_valid_ref __context v.snapshot_of in
let children_of vconf =
List.filter (fun v -> v.snapshot_parent = Some vconf.vdi) all_vdis

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is of O(N^2) complexity. It can be avoided.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
compare t1 t2
else
r
match Int64.compare v1.size v2.size with

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is the original code. But I can't understand it. I would think the key for comparison is something like:

  1. vconf.snapshot_of - for different trees
  2. depth in the same tree
  3. is_snapshot
  4. age

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The original logic is to transfer the small size VDI first, which then mirror the leaf first, thats why the tree is separate in SMAPIv1. From my perspective, dividing different tree with vconf.snapshot_of ` and compare the age for snapshot , and put the leaf at the end of snapshot set is enough. which is similar to a round of DFS. but DFS is more strict.

@minglumlu minglumlu Sep 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Then it can be changed to smaller tree (the size of the active leaf) first?
The approach of comparison should be either splitting between SMAPIv1 and SMAPIv3, or common for both. Such comparison by size does no meaning at all for SMAPIv3. It would make the whole sorting awkward.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hi @minglumlu old order had an unstated precondition, that for all the snapshot VDI belong to a tree, it's virtual size is usually the same, we can assume the walk was consulted only between VDIs of identical virtual_size.

and I proposal new order, sort tree first but not VDIs, Which is your suggestion for more strict.
in general we get the root from all_vdis first, and walk each root then sort the tree list base on the tree_size:

List.filter is_root all_vdis
 |> List.map (fun root ->
        let tree = walk root in
        (tree_size tree, tree)
    )
 |> List.stable_sort (fun (s1, _) (s2, _) -> Int64.compare s1 s2)
 |> List.concat_map snd

is_root is "no snapshot_parent, or a parent is in all_vdi". and tree_size impl like tree_size is List.fold_left (fun acc v -> Int64.max acc v.size) 0L tree which filter the max chain node for comparing.

and for tree, first create a table to reflect the parent-children:

let children =
  List.fold_left
    (fun acc v ->
      match v.snapshot_parent with
      | Some parent when VdiSet.mem parent present ->
          VdiMap.update parent
            (fun siblings -> Some (v :: Option.value ~default:[] siblings))
            acc
      | _ ->
          acc
    )
    VdiMap.empty eldest_first
  |> VdiMap.map List.rev

and walk the tree:

let rec walk v =
  v
  :: (VdiMap.find_opt v.vdi children
     |> Option.value ~default:[]
     |> List.concat_map walk
     )
in

children is a persistent Map built by one fold and never touched again; walk returns its subtree rather than writing into an index, so no Hashtbl and the int ref.
Sibling order is settled once by eldest_first sorting on a key (not is_snapshot, snapshot_time)

  • snapshots before the active disk,
  • oldest first

For a example as we mentioned before, 4 snapshots 2 disk, now the final order is:

[S_A1  S_A2  S_A3  S_A4  A_leaf]  [S_B1  S_B2  S_B3  S_B4  B_leaf]
 \______ tree A, 10G __________/    \______ tree B, 20G __________/

and current time complexity is reduce from O(N²) to O(N log N).

Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
(fun active ->
Db.VDI.get_snapshots ~__context ~self:active
|> List.filter (fun s ->
(not (List.mem s vm_snapshot_disks)) && is_v3_vdi s

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When the active is on a SMAPIv3 SR, all its snapshots will be in SMAPIv3 SR?
I think it is true. If so, is_v3_vdi can be avoided on each snapshot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I make some change, so the is_v3_vdi is only asked at the roots only.

is_v3_vdi now appears exactly twice in the function, both times as a List.filter at the top of a pipeline, and never inside hidden_below — one query per migrated disk instead of one per VDI in the tree. A tree rooted on a SMAPIv1 SR is dropped at the root and never walked, so the assert stays SMAPIv3-only.

Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
)
|> List.map (fun s -> (s, active))
)
active_disks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only active disks can have the hidden VDI snapshots? I think the snapshots VDI can have as well.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

snapshot VDI can have hidden VDI as well. I confirmed it on a pool for both ext and gfs2. and now I turn the walk to the whole subtree:

let rec hidden_below origin =
      Db.VDI.get_snapshots ~__context ~self:origin
      |> List.concat_map (fun s ->
          if List.mem s snapshot_vdis then [] else (s, origin) :: hidden_below s
      )
    in
    let hidden =
      active_vdis @ snapshot_vdis
      |> List.sort_uniq compare
      |> List.filter is_v3_vdi
      |> List.concat_map hidden_below
    in

Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
)
in
let orphans =
List.filter (fun s -> is_v3_vdi s && orphaned s) snapshot_vdis

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same comment for the is_v3_vdi.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

for the orphans snapshots case, is_v3_vdi only sees what survives it — on a healthy VM, no node would run the check:

    let origin_of s = Db.VDI.get_snapshot_of ~__context ~self:s in
    let orphans =
      snapshot_vdis
      |> List.filter (fun s -> not (List.mem (origin_of s) active_vdis))
      |> List.filter is_v3_vdi
    in

Comment thread ocaml/xapi/storage_smapiv3_migrate.ml Outdated
List.filter (fun s -> is_v3_vdi s && orphaned s) snapshot_vdis
in
abort
"it has orphan snapshot VDIs on SMAPIv3 SRs whose active disk was deleted; \

@minglumlu minglumlu Sep 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the checking is that a snapshot has an active leaf but

  1. the active leaf is invalid; or
  2. the active leaf is not in active leafs to be migrated.

The error message should split these two cases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. That single filter finds both — a dangling ref can never equal a live VDI's ref — and List.partition only has to decide which message applies:

    let deleted, detached =
      List.partition
        (fun s -> not (Db.is_valid_ref __context (origin_of s)))
        orphans
    in
  • deleted — snapshot_of no longer resolves; the active disk was destroyed and there is nothing at the destination to anchor the chain on. "…whose active disk has been deleted; delete these snapshots before migrating".
  • detached — snapshot_of resolves, but that VDI is not part of this migration. "…whose active disk is not part of this migration", and the message names the disk.

A SMAPIv3 destination is rebuilt from the VM's snapshot tree, so a VDI
that the tree does not account for has nowhere to go. Two of them can
reach the migration path. A snapshot taken with VDI.snapshot belongs to
no VM snapshot and is invisible to the walk. A snapshot VDI whose
active disk has been deleted has no leaf left to anchor its chain on.

Check for both before anything is copied and refuse the migration with
operation_not_allowed, naming the VDIs at fault and what to do about
them, so the VM keeps running where it is rather than arriving with a
tree the destination cannot describe.

Signed-off-by: Lunfan Zhang[Lunfan.Zhang] <Lunfan.Zhang@cloud.com>
SXM reproduces a VM's snapshot chain on the destination one VDI at a
time, each copy sitting on top of the copy of the snapshot before it.
SMAPIv1 finds that base itself, by matching the source VDI's similar
content against the content_ids already present in the destination SR.
SMAPIv3 backends report no similar content, so the base has to come
from the caller, which knows the chain from the VM's snapshot lineage.

DATA.copy2 is DATA.copy with that base made explicit. Storage_migrate
dispatches on the SR's SMAPI version: SMAPIv1 keeps its own search and
ignores the parameter; SMAPIv3 clones the base (or creates a blank VDI
when there is none), mirrors the source VDI into the clone over the NBD
proxy, and freezes the result into a snapshot.

DATA.MIRROR.receive_start3 takes the same parameter, for the leaf VDI
it creates. Nothing supplies a base yet; the VM migration path does
that next.

Signed-off-by: Lunfan Zhang[Lunfan.Zhang] <Lunfan.Zhang@cloud.com>
@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch from 34d3b59 to f225618 Compare September 9, 2026 10:01
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated
Comment thread ocaml/xapi/xapi_vm_migrate.ml Outdated

@minglumlu minglumlu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a few comments for style.

@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch from f225618 to ccff5f6 Compare September 14, 2026 03:01
A SMAPIv1 destination gets a VM's snapshot chains for free: each copy
is based on whichever VDI already in the destination SR has the most
similar content, which is the copy of the snapshot before it. SMAPIv3
backends report no similar content, so every VDI arrives as a full,
unrelated copy and the snapshot structure is lost.

Migration already copies a VM's VDIs one at a time, in a with_many
fold, and DATA.copy2 now takes the destination VDI a copy should be
based on. with_many hands each element the results of the elements
before it, so naming the destination copy of the VDI a VDI follows in
its disk's snapshot chain is enough to reproduce the chain. That parent
comes from the VM snapshot tree: the disk of the nearest ancestor of
the VM that has one, so snapshots on a branch the VM has reverted away
from start chains of their own.

It only works if a VDI is copied after the VDI it is based on, so
same-sized VDIs are now ordered by a depth-first walk of the snapshot
trees rather than by snapshot time. A disk's tree stays in one piece
whatever else is attached to the VM, siblings go oldest first so that
the branches a revert abandoned are copied before the branch the VM is
still on, and the disk itself, the only child that is not a snapshot,
comes last.

Signed-off-by: Lunfan Zhang[Lunfan.Zhang] <Lunfan.Zhang@cloud.com>
@LunfanZhang
LunfanZhang force-pushed the private/luzhan/sxm-v3-p2-engine branch from ccff5f6 to 5b5b474 Compare September 14, 2026 07:03
@LunfanZhang
LunfanZhang merged commit dc151d3 into xapi-project:feature/sxm-v3 Sep 14, 2026
16 checks passed
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.

6 participants