Skip to content

feat: support v2 cloning (full-copy and linked-clone mode)#391

Merged
derekbit merged 1 commit into
longhorn:mainfrom
PhanLe1010:7794-cloning
Sep 10, 2025
Merged

feat: support v2 cloning (full-copy and linked-clone mode)#391
derekbit merged 1 commit into
longhorn:mainfrom
PhanLe1010:7794-cloning

Conversation

@PhanLe1010
Copy link
Copy Markdown
Contributor

@PhanLe1010 PhanLe1010 commented Aug 13, 2025

@mergify
Copy link
Copy Markdown

mergify Bot commented Aug 17, 2025

This pull request is now in conflict. Could you fix it @PhanLe1010? 🙏

@PhanLe1010 PhanLe1010 force-pushed the 7794-cloning branch 2 times, most recently from fcd5cd2 to f0de4ed Compare August 22, 2025 05:20
@PhanLe1010 PhanLe1010 marked this pull request as ready for review August 22, 2025 05:21
@PhanLe1010 PhanLe1010 changed the title feat: support v2 cloning feat: support v2 cloning (full-copy and linked-clone mode) Aug 22, 2025
@PhanLe1010 PhanLe1010 force-pushed the 7794-cloning branch 2 times, most recently from 09520b9 to aa1f7e8 Compare August 26, 2025 05:24
Copy link
Copy Markdown
Member

@derekbit derekbit left a comment

Choose a reason for hiding this comment

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

I will continue reviewing the PR tomorrow.

Comment thread vendor/github.com/longhorn/types/pkg/generated/spdkrpc/spdk.pb.go Outdated
Comment thread pkg/spdk/engine.go Outdated
Comment thread pkg/spdk/engine.go Outdated
Comment thread pkg/client/client.go Outdated
Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go
@derekbit
Copy link
Copy Markdown
Member

@derekbit
Copy link
Copy Markdown
Member

derekbit commented Aug 26, 2025

flowchart TD
  Start(["Start: Engine.SnapshotClone"])
  A["Engine: select dst replica (must be 1 RW)"]
  B["Engine: find src replica candidates (RW replicas)"]
  C{"cloneMode == linked-clone?"}
  D1["Check if there is a src candidate with the same IP/LvsUUID as dst"]
  D2["If not found and cloneMode==linked -> return error"]
  E_link_req["Engine -> dst: ReplicaSnapshotCloneDstStart(... , linked)"]
  F_link_dst_calls_src["dst -> src: ReplicaSnapshotCloneSrcStart(..., linked)"]
  G_link_set_parent["src: set dst parent -> point to src snapshot"]
  H_link_finish["dst: SnapshotCloneDstFinish (linked) => Done"]

  E_full_req["Engine -> dst: ReplicaSnapshotCloneDstStart(... , full-copy)"]
  I_create_cloning_lvol["dst: create cloning lvol & expose (allocate port)"]
  J_dst_call_src["dst -> src: ReplicaSnapshotCloneSrcStart(..., full-copy)"]
  K_src_start_deepcopy["src: start deep-copy, return op/status"]
  L_dst_set_inprogress["dst: set status InProgress, start monitor goroutine"]
  M_monitor["monitor goroutine: periodically call src.ReplicaSnapshotCloneSrcStatusCheck"]
  N{"status == COMPLETE ?"}
  O_finish_src_try["monitor: best-effort call ReplicaSnapshotCloneSrcFinish"]
  P_dst_finalize["dst: SnapshotCloneDstFinish -> create tmp snapshot, set parent, cleanup resources"]
  Q_Done(["Done"])
  R_Error(["Error branch -> set status Error -> cleanup & return"])

  Start --> A --> B --> C
  C -- yes --> D1
  D1 -- found --> E_link_req --> F_link_dst_calls_src --> G_link_set_parent --> H_link_finish --> Q_Done
  D1 -- not found --> D2 --> R_Error

  C -- no (full-copy) --> E_full_req --> I_create_cloning_lvol --> J_dst_call_src --> K_src_start_deepcopy --> L_dst_set_inprogress --> M_monitor
  M_monitor --> N
  N -- no --> M_monitor
  N -- yes --> O_finish_src_try --> P_dst_finalize --> Q_Done

  %% error flows
  K_src_start_deepcopy -- fail --> R_Error
  J_dst_call_src -- fail --> R_Error
  I_create_cloning_lvol -- fail --> R_Error
  M_monitor -- timeout/error --> R_Error
  O_finish_src_try -- fail (best-effort) --> P_dst_finalize
Loading
  • Engine:

    • Engine.SnapshotClone(snapshotName, srcEngineName, srcEngineAddress, cloneMode):
      • Lock engine; require destination engine to have exactly 1 RW replica (current implementation).
      • Choose dst RW replica; fetch src engine replica list and make candidate map (IP, LvsUUID, address).
      • Prefer src candidate that matches dst IP & LvsUUID for linked-clone.
      • If linked requested but no same-pool candidate found, error.
      • Invoke dstReplica.ReplicaSnapshotCloneDstStart(..., cloneMode).
  • Destination replica flow (dst)

    1. Replica.SnapshotCloneDstStart(...) called (via Engine -> dst RPC).
    2. Validate params and clear any previous cloning state.
    3. linked-clone:
    • Validate src/dst same IP/LvsUUID constraints.
    • Call src.ReplicaSnapshotCloneSrcStart(..., linked).
    • Immediately mark dst clone complete and run SnapshotCloneDstFinish for parent setup.
    1. full-copy:
    • Allocate a port and create a cloning LVOL on dst, expose it (NVMe target).
    • Call src.ReplicaSnapshotCloneSrcStart(dstCloningLvolAddress, full-copy).
    • Set dst state to in progress; start a monitor goroutine to poll src status.
  1. monitorSnapshotClone:
    • Periodically call src.ReplicaSnapshotCloneSrcStatusCheck.
    • Update processed/total clusters and progress.
    • On COMPLETE or ERROR (or timeout), best-effort call src.ReplicaSnapshotCloneSrcFinish, then call dst.SnapshotCloneDstFinish to finalize (create tmp snapshot, set head parent, cleanup resources).
  • Source replica flow (src)
    • Replica.SnapshotCloneSrcStart(...):
      • linked-clone: call BdevLvolSetParent for the dst lvol to point to the src snapshot (fast).
      • full-copy: start deep-copy operation on src (SPDK deep-copy op), store op id/status in snapshotCloningSrcCache.
    • Replica.SnapshotCloneSrcStatusCheck: return deep-copy progress (processed/total clusters, state, error).
    • `Replica.SnapshotCloneSrcFinish : finish/cleanup deep-copy on src.

Copy link
Copy Markdown
Member

@derekbit derekbit left a comment

Choose a reason for hiding this comment

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

One question
If a snapshot volume is cloning, can user delete the snapshot or the replica?

Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go Outdated
@PhanLe1010
Copy link
Copy Markdown
Contributor Author

One question
If a snapshot volume is cloning, can user delete the snapshot or the replica?

For linked-clone, If the src snapshot disappear before calling bdev_lvol_set_parent, SPDK set parent will fail and the clone is marked as failed

For full-clone, it uses bdev_lvol_start_deep_copy API. I think it is not possible delete the src lvol while the SPDK deep copy API is opening it. WDYT?

Comment thread pkg/spdk/engine.go
Comment thread pkg/spdk/engine.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/engine.go
Comment thread pkg/spdk/engine.go
Comment thread pkg/spdk/server.go Outdated
Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment on lines +2026 to +2030
return fmt.Errorf("there are already another linked-clone lvol %v in src replica %v. "+
"Each src replica can only has 1 linked-clone lvol at a time", childLvolName, r.Name)
}
}
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.

Why do we need the restriction?

Copy link
Copy Markdown
Contributor Author

@PhanLe1010 PhanLe1010 Sep 5, 2025

Choose a reason for hiding this comment

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

Linked-clone volume is supposed to be a short-live volume for backup solution to read data out and delete it once backup complete. Therefore:

  1. Each src volume does not need to have multiple backups at the same time -> 1 linked-clone is enough
  2. Multiple linked-clone volumes is problematic when user want to delete the source volume or delete snapshot of source volume because SPDK cannot delete snapshot will multiple children (the multiple linked-clone volumes)

Comment thread pkg/spdk/engine.go Outdated
Comment thread pkg/spdk/engine.go Outdated
Comment thread pkg/spdk/server.go
return nil, grpcstatus.Errorf(grpccodes.NotFound, "cannot find replica %s during ReplicaSnapshotCloneDstStart", req.Name)
}

if err := r.SnapshotCloneDstStart(spdkClient, req.SnapshotName, req.SrcReplicaName, req.SrcReplicaAddress, req.CloneMode); err != nil {
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.

Should we pass ctx to downstream calls? Same question for other newly introduced methods.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we should. All current grpc methods do not pass ctx down. Should we create ticket to refactor all of them?

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.

Yes, could you help open an issue to track this?

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.

It seems that other gRPC servers (mainly in longhorn-instance-manager) do not handle the input ctx either. Besides, there is one more context s.ctx.

Comment thread pkg/spdk/replica.go
}
// Create cloning lvol and expose it
cloningLvolName := GetReplicaCloningLvolName(r.Name)
if _, err = spdkClient.BdevLvolCreate("", r.LvsUUID, cloningLvolName, util.BytesToMiB(r.SpecSize),
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.

Will the resources and allocated port get cleaned up if any of the subsequent downstream calls fail?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Replica will be marked as failed, then stopped. The resource will be cleanup as part of stopping logic

Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go Outdated
Comment thread pkg/spdk/replica.go
Comment on lines +2022 to +2024
existingParentOfDstReplica = lvolName
continue
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.

Why not break if existingParentOfDstReplica is found?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because we want to verify if there there are already another linked-clone lvol in src replica:

			if !IsReplicaLvol(r.Name, childLvolName) {
				return fmt.Errorf("there are already another linked-clone lvol %v in src replica %v. "+
					"Each src replica can only has 1 linked-clone lvol at a time", childLvolName, r.Name)
			}

Comment thread pkg/spdk/types.go
derekbit
derekbit previously approved these changes Sep 9, 2025
Copy link
Copy Markdown
Member

@derekbit derekbit left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Copy Markdown
Contributor

@c3y1huang c3y1huang left a comment

Choose a reason for hiding this comment

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

lgtm. Remaining TODOs:

  1. Update go.mod
  2. Clean up commit history
  3. Resolve conflicts

@PhanLe1010
Copy link
Copy Markdown
Contributor Author

@derekbit @c3y1huang

All done:

  • Commit cleanup
  • go.mod cleanup
  • Conflict resolved

longhorn-7794

Signed-off-by: Phan Le <phan.le@suse.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Sep 9, 2025

Codecov Report

❌ Patch coverage is 0% with 880 lines in your changes missing coverage. Please review.
✅ Project coverage is 0.70%. Comparing base (4d9b75a) to head (c599a52).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/spdk/replica.go 0.00% 493 Missing ⚠️
pkg/client/client.go 0.00% 142 Missing ⚠️
pkg/spdk/server.go 0.00% 116 Missing ⚠️
pkg/spdk/engine.go 0.00% 86 Missing ⚠️
pkg/api/types.go 0.00% 25 Missing ⚠️
pkg/util/util.go 0.00% 10 Missing ⚠️
pkg/spdk/types.go 0.00% 8 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##            main    #391      +/-   ##
========================================
- Coverage   0.77%   0.70%   -0.07%     
========================================
  Files         24      24              
  Lines       9866   10737     +871     
========================================
  Hits          76      76              
- Misses      9783   10654     +871     
  Partials       7       7              
Flag Coverage Δ
unittests 0.70% <0.00%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@derekbit derekbit merged commit d94eb93 into longhorn:main Sep 10, 2025
5 of 9 checks passed
Copy link
Copy Markdown
Contributor

@shuo-wu shuo-wu left a comment

Choose a reason for hiding this comment

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

BTW, have we created a ticket that adds checks for volume naming?

Comment thread pkg/spdk/server.go
return nil, grpcstatus.Errorf(grpccodes.NotFound, "cannot find replica %s during ReplicaSnapshotCloneDstStart", req.Name)
}

if err := r.SnapshotCloneDstStart(spdkClient, req.SnapshotName, req.SrcReplicaName, req.SrcReplicaAddress, req.CloneMode); err != nil {
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.

It seems that other gRPC servers (mainly in longhorn-instance-manager) do not handle the input ctx either. Besides, there is one more context s.ctx.

Comment thread pkg/spdk/engine.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment on lines +2162 to +2164
if err := doCleanupForSnapshotCloneSrc(spdkClient, c); err != nil {
return err
}
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.

What if the clone in SPDK is not done yet? Should we print out any warning or error logs now? Will we have an interrupt function in the future?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I expect there there will be error logs when calling disconnectNVMfBdev

Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment on lines +1818 to +1820
if errClose := srcReplicaCli.Close(); errClose != nil {
r.log.WithError(errClose).Errorf("Failed to close src client for %s after status check", srcReplicaName)
}
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 retain srcReplicaCli before closing this for loop? Frequently creating and closing the connection is not a good implementation...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we should recreat. If there is networking error, retry using the old client is useless

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.

The TCP TIME_WAIT delay is the period, typically 240 seconds (4 minutes), a TCP connection stays in the TIME_WAIT state after being closed by the client to ensure the reliable termination of the connection by allowing the receiving end to send final acknowledgements.

Based on this info (provided by AI), by default, we will have 80 TIME_WAIT TCP connections after a full clone start. Is that good?

Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
Comment thread pkg/spdk/replica.go
@PhanLe1010
Copy link
Copy Markdown
Contributor Author

BTW, have we created a ticket that adds checks for volume naming?

Created longhorn/longhorn#11739

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.

4 participants