mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* operation: add shared volume_move package for volume and EC shard moves The shell commands (volume.move, volume.balance, ec.balance, tier moves) and the maintenance workers (balance, ec_balance) each carried their own copy of the move RPC sequences, and the copies had drifted: the worker verified the target before deleting the source but dropped the disk type and IO throttle; the shell passed those but deleted the source unverified. volume_move.Mover carries the merged sequences, keeping the stricter behavior from each side: - LiveMoveVolume: check-then-hard-freeze the source (VolumeStatus's IsReadOnly also covers low-disk and readonly-but-can-delete states, which still accept needle deletes), copy with disk type and IO throttle, tail, verify the target is not behind the source before the destructive source delete (a target that is ahead holds writes it accepted during the tail and the move commits to keep them), and restore the source's writability when a failure precedes the delete and this move did the freezing. Aborts clean up the incomplete target copy; a failed cleanup or an ambiguous source delete keeps the source readonly (ErrSourceKeptReadonly) so callers do not thaw a source next to a possibly-authoritative copy. With a readonly source, an existing or unknown-state target refuses the move outright: no client-side observation can prove such a copy is a stale remnant rather than the authoritative copy of an unfinished move. - MoveEcShards: copy with the .ecx/.ecj/.vif/.ecsum sidecars, mount, verify the target registered every shard before unmount+delete on the source, and reject same-server moves (the EC delete is server-wide). Server identity is the grpc endpoint (SameServer), so node:8080 and node:8080.18080 compare equal while test servers sharing a degenerate HTTP address stay distinct; addresses are validated non-fatally before dialing and before being embedded in copy/tail requests, since both the client dialer and the receiving server normalize them through a parser that aborts the process on a malformed port. The Rust volume server's codes.NotFound counts as a definitively absent probe answer alongside the Go server's plain-error code Unknown. All RPCs go through an injectable ClientFunc, so the sequences are unit tested against a fake volume server client: RPC order, request fields, and that verification failures keep the source intact. * shell, worker: delegate volume and EC shard moves to operation/volume_move LiveMoveVolume and the copy/tail/delete/mark-writable helpers become thin wrappers over the shared mover, keeping their signatures; the EC helpers keep their per-step output and delegate the RPCs. BalanceTask and ECBalanceTask keep their parameter validation, progress reporting, and guards (same-node cross-disk rejection, dedup keep-node verification, shard ids range-checked before the uint8 narrowing) and hand the RPC sequences to the mover. volume.tier.move skips its thaw-on-failure when the mover deliberately kept the source readonly, since reopening the replicas beside a possibly-authoritative target copy would fork the volume. The tail-failure tolerance moves inside the mover: a failed tail is tolerated only when the volume was already readonly before the move began, backstopped by a stability re-read across the idle window, so volume.balance's -skipTailError-by-readonly heuristic and tier-move's unconditional skip both become the same authoritative rule. * volume_move: keep the source readonly when a failed copy leaves a target of unknown origin A failed copy can leave a complete, mounted copy on the target (the server finishes after the client loses the stream). The abort probed the target only when its pre-copy state was known-absent; an unknown prior state skipped both the probe and the cleanup and then reopened the source - two writable replicas of one volume, diverging from the next write on. The abort now probes the target on every failed copy and restores the source only when the target provably holds nothing. A copy whose provenance cannot be proven (unknown prior state, a pre-existing replica, or an unreachable target) is never deleted, and the source stays readonly with ErrSourceKeptReadonly naming the recovery. * test: teach the plugin worker harness the shared move sequence The fake volume server lacked VolumeStatus, which the shared mover now issues before freezing the source, and the batch execution test's status-read accounting predates the pre-copy target probe and the verification reads. Mirrors the harness the enterprise tree already carries.
502 lines
23 KiB
Go
502 lines
23 KiB
Go
package volume_move
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// ErrSourceKeptReadonly marks a move failure whose recovery deliberately keeps
|
|
// the source volume readonly — the target copy may hold data the source lacks
|
|
// (an ambiguous source delete, or an undeletable stale copy). Callers that
|
|
// froze the source themselves must not thaw it on this error.
|
|
var ErrSourceKeptReadonly = errors.New("the source volume is deliberately kept readonly")
|
|
|
|
// VolumeMoveOptions control LiveMoveVolume.
|
|
type VolumeMoveOptions struct {
|
|
// DiskType changes the volume's disk type on the target ("" keeps it).
|
|
DiskType string
|
|
// IoBytePerSecond throttles the copy (0 = unlimited).
|
|
IoBytePerSecond int64
|
|
// IdleTimeout is how long the tail phase waits for in-flight requests to
|
|
// drain (default 5s).
|
|
IdleTimeout time.Duration
|
|
// Writer receives human-readable progress lines (nil discards them).
|
|
Writer io.Writer
|
|
// Progress, when set, receives percent/stage callbacks as the move advances.
|
|
Progress func(percent float64, stage string)
|
|
}
|
|
|
|
func (o *VolumeMoveOptions) fillDefaults() {
|
|
if o.IdleTimeout <= 0 {
|
|
o.IdleTimeout = 5 * time.Second
|
|
}
|
|
if o.Writer == nil {
|
|
o.Writer = io.Discard
|
|
}
|
|
if o.Progress == nil {
|
|
o.Progress = func(float64, string) {}
|
|
}
|
|
}
|
|
|
|
// LiveMoveVolume moves one volume between volume servers while it keeps serving
|
|
// reads: freeze the source (readonly), copy, tail to drain in-flight requests,
|
|
// verify the target matches the source, then delete the source. A failure
|
|
// before the source delete restores the source's writability if this move is
|
|
// what froze it.
|
|
func (m *Mover) LiveMoveVolume(ctx context.Context, volumeId needle.VolumeId, source, target pb.ServerAddress, opts VolumeMoveOptions) (err error) {
|
|
opts.fillDefaults()
|
|
|
|
// VolumeCopy tears down any existing copy on the target first, so a
|
|
// same-server "move" would delete the volume and then fail to read it.
|
|
// SameServer, not ==: "node:8080" and "node:8080.18080" are one server.
|
|
if SameServer(source, target) {
|
|
return fmt.Errorf("refusing to move volume %d onto its own server %s", volumeId, source)
|
|
}
|
|
|
|
opts.Progress(10, fmt.Sprintf("marking volume %d readonly on %s", volumeId, source))
|
|
sourceWasWritable, err := m.ensureVolumeReadonly(ctx, volumeId, source, true)
|
|
// The source stays authoritative until its delete succeeds; on any earlier
|
|
// failure undo the freeze this move added and clean up the target copy.
|
|
// Installed before the error check: the marking RPC itself can fail after
|
|
// the server applied the mark (e.g. its master notification failed).
|
|
var copyStarted, copyCompleted, sourceDeleteStarted bool
|
|
var targetHadVolume, targetStateKnown bool
|
|
defer func() {
|
|
if err != nil {
|
|
if sourceDeleteStarted {
|
|
// Past verification: the target is complete and may already hold
|
|
// new writes, and the delete may or may not have reached the
|
|
// source. Deleting the target or reopening the source could fork
|
|
// the volume; keep the source readonly.
|
|
fmt.Fprintf(opts.Writer, "volume %d is left readonly on %s: the source delete failed with the verified copy on %s mounted\n", volumeId, source, target)
|
|
glog.Warningf("volume %d is left readonly on %s: the source delete failed with the verified copy on %s mounted", volumeId, source, target)
|
|
err = fmt.Errorf("%w: %w", ErrSourceKeptReadonly, err)
|
|
return
|
|
}
|
|
// The cleanup runs on its own deadline so an abort via cancelled
|
|
// context still cleans up.
|
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
|
defer cleanupCancel()
|
|
cleanupTarget := copyCompleted
|
|
if copyStarted && !copyCompleted {
|
|
// The server can finish the copy and mount the target even when
|
|
// the client loses the stream, so probe rather than assume.
|
|
exists, known := m.probeVolume(cleanupCtx, volumeId, target)
|
|
switch {
|
|
case known && !exists:
|
|
// nothing mounted on the target; just undo the freeze below
|
|
case known && exists && targetStateKnown && !targetHadVolume:
|
|
// the failed copy created it; remove it so the source stays
|
|
// the only replica
|
|
cleanupTarget = true
|
|
default:
|
|
// A copy may sit mounted on the target and its provenance
|
|
// cannot be proven: the prior state is unknown, the target
|
|
// held a replica before the move, or the probe failed.
|
|
// Deleting it risks someone else's replica; reopening the
|
|
// source beside it risks two writable replicas taking
|
|
// divergent writes. Keep the source readonly.
|
|
fmt.Fprintf(opts.Writer, "volume %d is left readonly on %s: a copy may exist on %s but its origin cannot be determined; delete one side explicitly, then re-run the move\n", volumeId, source, target)
|
|
glog.Warningf("volume %d is left readonly on %s: a copy may exist on %s but its origin cannot be determined; delete one side explicitly, then re-run the move", volumeId, source, target)
|
|
err = fmt.Errorf("%w: %w", ErrSourceKeptReadonly, err)
|
|
return
|
|
}
|
|
}
|
|
if cleanupTarget {
|
|
// The target copy may be missing tailed entries; remove it so
|
|
// the source stays the only replica.
|
|
if dErr := m.DeleteVolume(cleanupCtx, volumeId, target, false, true); dErr != nil {
|
|
// Restoring the source while the stale target stays mounted
|
|
// risks divergent replicas; keep the source readonly. A
|
|
// re-run refuses while the copy exists, so name the fix.
|
|
fmt.Fprintf(opts.Writer, "volume %d is left readonly on %s: failed to delete the incomplete copy on %s: %v; delete that copy, then re-run the move\n", volumeId, source, target, dErr)
|
|
glog.Warningf("volume %d is left readonly on %s: failed to delete the incomplete copy on %s: %v; delete that copy, then re-run the move", volumeId, source, target, dErr)
|
|
err = fmt.Errorf("%w: %w", ErrSourceKeptReadonly, err)
|
|
return
|
|
}
|
|
}
|
|
if sourceWasWritable {
|
|
m.restoreVolumeWritable(volumeId, source)
|
|
} else {
|
|
// The hard freeze added on top of a readonly-reporting status is
|
|
// kept: it is indistinguishable from an operator's mark. It is
|
|
// not persisted, so a volume server restart clears it.
|
|
fmt.Fprintf(opts.Writer, "volume %d on %s keeps the readonly mark the move added; volume.mark -writable clears it if the prior readonly state was transient\n", volumeId, source)
|
|
}
|
|
}
|
|
}()
|
|
if err != nil {
|
|
return fmt.Errorf("mark volume %d readonly on %s: %v", volumeId, source, err)
|
|
}
|
|
|
|
targetHadVolume, targetStateKnown = m.probeVolume(ctx, volumeId, target)
|
|
if !sourceWasWritable {
|
|
// A readonly source next to an existing copy is the signature of a
|
|
// previous move whose source delete failed — that copy may be the
|
|
// authoritative one, serving writes the source never saw, and the
|
|
// copy below would tear it down. No client-side observation proves
|
|
// otherwise: compaction revision is shared by ordinary replicas,
|
|
// aggregate sizes shrink under compaction, and any snapshot can be
|
|
// invalidated by a write right after it. Fail closed on any existing
|
|
// copy or unknown state; the operator deletes one side explicitly.
|
|
if !targetStateKnown {
|
|
err = fmt.Errorf("%w: cannot determine whether %s already holds a copy of volume %d; refusing to overwrite it while the source is readonly", ErrSourceKeptReadonly, target, volumeId)
|
|
return err
|
|
}
|
|
if targetHadVolume {
|
|
err = fmt.Errorf("%w: volume %d already has a copy on %s while the source on %s is readonly — likely a previous move that did not finish; delete the source to keep that copy, or delete the copy before re-running the move", ErrSourceKeptReadonly, volumeId, target, source)
|
|
return err
|
|
}
|
|
}
|
|
|
|
opts.Progress(20, fmt.Sprintf("copying volume %d from %s to %s", volumeId, source, target))
|
|
fmt.Fprintf(opts.Writer, "copying volume %d from %s to %s\n", volumeId, source, target)
|
|
copyStarted = true
|
|
lastAppendAtNs, err := m.copyVolumeData(ctx, volumeId, source, target, opts.DiskType, opts.IoBytePerSecond, opts.Writer)
|
|
if err != nil {
|
|
return fmt.Errorf("copy volume %d from %s to %s: %v", volumeId, source, target, err)
|
|
}
|
|
copyCompleted = true
|
|
|
|
opts.Progress(70, fmt.Sprintf("tailing volume %d from %s to %s", volumeId, source, target))
|
|
fmt.Fprintf(opts.Writer, "tailing volume %d from %s to %s\n", volumeId, source, target)
|
|
tailFailed := false
|
|
if tailErr := m.TailVolume(ctx, volumeId, source, target, lastAppendAtNs, opts.IdleTimeout); tailErr != nil {
|
|
// A tail failure is tolerable only when the volume was already readonly
|
|
// before this move began: frozen for the whole copy, it should have no
|
|
// in-flight writes for the tail to drain — and the stability check
|
|
// below still proves it. A volume this move froze can have stragglers
|
|
// admitted just before the freeze that only the tail delivers, so
|
|
// losing the tail there must abort the move.
|
|
if sourceWasWritable {
|
|
return fmt.Errorf("tail volume %d from %s to %s: %v", volumeId, source, target, tailErr)
|
|
}
|
|
tailFailed = true
|
|
fmt.Fprintf(opts.Writer, "tail volume %d from %s to %s: %v\n", volumeId, source, target, tailErr)
|
|
glog.Warningf("tail volume %d from %s to %s: %v", volumeId, source, target, tailErr)
|
|
}
|
|
|
|
// Verify before the point of no return: the source is deleted only when the
|
|
// target holds at least everything the source does. The source status is
|
|
// read here, after the tail — a write in flight when the source was frozen
|
|
// can still land after an earlier read, and a stale snapshot would wave
|
|
// through an incomplete target.
|
|
opts.Progress(85, fmt.Sprintf("verifying volume %d on %s", volumeId, target))
|
|
sourceStatus, err := m.ReadVolumeFileStatus(ctx, volumeId, source)
|
|
if err != nil {
|
|
return fmt.Errorf("read volume %d status on %s: %v", volumeId, source, err)
|
|
}
|
|
if tailFailed {
|
|
// A successful tail proves the source held still for the idle window; a
|
|
// tolerated tail failure proved nothing, so substitute the same drain
|
|
// barrier here: the source status must not change across the window.
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(opts.IdleTimeout):
|
|
}
|
|
secondStatus, statusErr := m.ReadVolumeFileStatus(ctx, volumeId, source)
|
|
if statusErr != nil {
|
|
return fmt.Errorf("re-read volume %d status on %s: %v", volumeId, source, statusErr)
|
|
}
|
|
if secondStatus.DatFileSize != sourceStatus.DatFileSize || secondStatus.IdxFileSize != sourceStatus.IdxFileSize || secondStatus.FileCount != sourceStatus.FileCount {
|
|
return fmt.Errorf("volume %d on %s is still changing after a failed tail; aborting the move", volumeId, source)
|
|
}
|
|
sourceStatus = secondStatus
|
|
}
|
|
targetStatus, err := m.ReadVolumeFileStatus(ctx, volumeId, target)
|
|
if err != nil {
|
|
return fmt.Errorf("verify volume %d on target %s before deleting source: %v", volumeId, target, err)
|
|
}
|
|
if err = verifyTargetNotBehind(volumeId, sourceStatus, targetStatus); err != nil {
|
|
return err
|
|
}
|
|
if targetStatus.DatFileSize > sourceStatus.DatFileSize || targetStatus.IdxFileSize > sourceStatus.IdxFileSize || targetStatus.FileCount > sourceStatus.FileCount {
|
|
// The target announced itself writable when the copy mounted it, so
|
|
// clients may have written to it during the tail. Those writes live on
|
|
// the surviving copy; committing keeps them.
|
|
fmt.Fprintf(opts.Writer, "volume %d on %s has writes beyond the source; they stay with the moved volume\n", volumeId, target)
|
|
}
|
|
|
|
opts.Progress(90, fmt.Sprintf("deleting volume %d from %s", volumeId, source))
|
|
fmt.Fprintf(opts.Writer, "deleting volume %d from %s\n", volumeId, source)
|
|
sourceDeleteStarted = true
|
|
if err = m.DeleteVolume(ctx, volumeId, source, false, true); err != nil {
|
|
return fmt.Errorf("delete volume %d from %s: %v", volumeId, source, err)
|
|
}
|
|
|
|
opts.Progress(100, fmt.Sprintf("moved volume %d from %s to %s", volumeId, source, target))
|
|
fmt.Fprintf(opts.Writer, "moved volume %d from %s to %s\n", volumeId, source, target)
|
|
return nil
|
|
}
|
|
|
|
// verifyTargetNotBehind fails when the target holds less than the (frozen)
|
|
// source — the copy or tail missed data and deleting the source would lose it.
|
|
// A target that is ahead is not an error: the target serves writes during the
|
|
// tail, and those belong to the copy that survives the move.
|
|
func verifyTargetNotBehind(volumeId needle.VolumeId, source, target *volume_server_pb.ReadVolumeFileStatusResponse) error {
|
|
if target.DatFileSize < source.DatFileSize {
|
|
return fmt.Errorf("volume %d target is behind the source: .dat %d < %d bytes", volumeId, target.DatFileSize, source.DatFileSize)
|
|
}
|
|
if target.IdxFileSize < source.IdxFileSize {
|
|
return fmt.Errorf("volume %d target is behind the source: .idx %d < %d bytes", volumeId, target.IdxFileSize, source.IdxFileSize)
|
|
}
|
|
if target.FileCount < source.FileCount {
|
|
return fmt.Errorf("volume %d target is behind the source: %d < %d files", volumeId, target.FileCount, source.FileCount)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CopyVolume freezes the volume on source, copies it to target, and reports the
|
|
// stamp of the last entry copied. restoreWritable also restores the source's
|
|
// writability on success, for copies that leave the source serving; a failed
|
|
// copy always undoes the freeze this call added.
|
|
func (m *Mover) CopyVolume(ctx context.Context, volumeId needle.VolumeId, source, target pb.ServerAddress, diskType string, ioBytePerSecond int64, restoreWritable bool, writer io.Writer) (lastAppendAtNs uint64, err error) {
|
|
if writer == nil {
|
|
writer = io.Discard
|
|
}
|
|
if SameServer(source, target) {
|
|
return 0, fmt.Errorf("refusing to copy volume %d onto its own server %s", volumeId, source)
|
|
}
|
|
// The copy is non-destructive, so a readonly-reporting source is taken as
|
|
// is (force=false): missed concurrent deletes only make the new replica
|
|
// trivially stale, and hard-marking here could pin a transiently readonly
|
|
// (e.g. low-disk) source readonly with nothing to gate on it.
|
|
sourceWasWritable, err := m.ensureVolumeReadonly(ctx, volumeId, source, false)
|
|
defer func() {
|
|
if sourceWasWritable && (err != nil || restoreWritable) {
|
|
m.restoreVolumeWritable(volumeId, source)
|
|
}
|
|
}()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return m.copyVolumeData(ctx, volumeId, source, target, diskType, ioBytePerSecond, writer)
|
|
}
|
|
|
|
// ensureVolumeReadonly freezes the volume on server and reports whether it was
|
|
// writable beforehand — only then may a failure path undo the freeze, since a
|
|
// volume that was already readonly (e.g. full, or operator-set) must stay so.
|
|
// With force, the mark is issued even when the status already reports
|
|
// readonly: that answer also covers transient low-disk state and the
|
|
// readonly-but-can-delete flag, and neither blocks needle deletes, so only the
|
|
// hard mark makes the volume immutable — required before deleting the source
|
|
// of a move. Without force, a readonly-reporting volume is left untouched.
|
|
func (m *Mover) ensureVolumeReadonly(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress, force bool) (wasWritable bool, err error) {
|
|
err = m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
|
resp, statusErr := client.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{
|
|
VolumeId: uint32(volumeId),
|
|
})
|
|
if statusErr != nil {
|
|
return statusErr
|
|
}
|
|
wasWritable = !resp.IsReadOnly
|
|
if !wasWritable && !force {
|
|
return nil
|
|
}
|
|
_, readonlyErr := client.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
|
VolumeId: uint32(volumeId),
|
|
Persist: false,
|
|
})
|
|
return readonlyErr
|
|
})
|
|
return
|
|
}
|
|
|
|
// probeVolume reports whether server currently has the volume, and whether the
|
|
// answer is trustworthy. The Go server answers a missing volume with a plain
|
|
// error (gRPC code Unknown — the only way this RPC fails there); the Rust
|
|
// server answers with codes.NotFound. Both mean the server responded and the
|
|
// volume is absent. A transport-level failure means the state is unknown, and
|
|
// callers deciding to delete must treat unknown as hands-off.
|
|
func (m *Mover) probeVolume(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress) (exists bool, known bool) {
|
|
_, err := m.ReadVolumeFileStatus(ctx, volumeId, server)
|
|
if err == nil {
|
|
return true, true
|
|
}
|
|
if s, ok := status.FromError(err); ok && (s.Code() == codes.Unknown || s.Code() == codes.NotFound) {
|
|
return false, true
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// restoreVolumeWritable undoes a readonly mark after a failed move. It runs on
|
|
// its own deadline so an abort via cancelled context still restores the source,
|
|
// and only logs a failure — the caller is already returning the move error.
|
|
func (m *Mover) restoreVolumeWritable(volumeId needle.VolumeId, server pb.ServerAddress) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := m.MarkVolumeWritable(ctx, volumeId, server, true, false); err != nil {
|
|
glog.Warningf("restore volume %d writable on %s: %v", volumeId, server, err)
|
|
}
|
|
}
|
|
|
|
// copyVolumeData streams a VolumeCopy on the target, which pulls the volume
|
|
// from source and mounts it, and returns the stamp of the last entry copied
|
|
// for the tail phase.
|
|
func (m *Mover) copyVolumeData(ctx context.Context, volumeId needle.VolumeId, source, target pb.ServerAddress, diskType string, ioBytePerSecond int64, writer io.Writer) (lastAppendAtNs uint64, err error) {
|
|
// The target dials the embedded source itself; a malformed one would
|
|
// abort the target server, not this client.
|
|
if err = checkDialable(source); err != nil {
|
|
return 0, err
|
|
}
|
|
err = m.withClient(true, target, func(client volume_server_pb.VolumeServerClient) error {
|
|
stream, replicateErr := client.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
|
|
VolumeId: uint32(volumeId),
|
|
SourceDataNode: string(source),
|
|
DiskType: diskType,
|
|
IoBytePerSecond: ioBytePerSecond,
|
|
})
|
|
if replicateErr != nil {
|
|
return replicateErr
|
|
}
|
|
for {
|
|
resp, recvErr := stream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr == io.EOF {
|
|
break
|
|
}
|
|
return recvErr
|
|
}
|
|
if resp.LastAppendAtNs != 0 {
|
|
lastAppendAtNs = resp.LastAppendAtNs
|
|
} else {
|
|
fmt.Fprintf(writer, "%s => %s volume %d processed %s\n", source, target, volumeId, util.BytesToHumanReadable(uint64(resp.ProcessedBytes)))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
// TailVolume has the target follow the source's appends since sinceNs until the
|
|
// source stays idle for idleTimeout, draining requests in flight when the move
|
|
// froze the source.
|
|
func (m *Mover) TailVolume(ctx context.Context, volumeId needle.VolumeId, source, target pb.ServerAddress, sinceNs uint64, idleTimeout time.Duration) error {
|
|
// The target dials the embedded source itself; a malformed one would
|
|
// abort the target server, not this client.
|
|
if err := checkDialable(source); err != nil {
|
|
return err
|
|
}
|
|
return m.withClient(true, target, func(client volume_server_pb.VolumeServerClient) error {
|
|
_, replicateErr := client.VolumeTailReceiver(ctx, &volume_server_pb.VolumeTailReceiverRequest{
|
|
VolumeId: uint32(volumeId),
|
|
SinceNs: sinceNs,
|
|
IdleTimeoutSeconds: uint32(idleTimeout.Seconds()),
|
|
SourceVolumeServer: string(source),
|
|
})
|
|
return replicateErr
|
|
})
|
|
}
|
|
|
|
// ReadVolumeFileStatus reads the volume's file sizes and needle count on server.
|
|
func (m *Mover) ReadVolumeFileStatus(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress) (resp *volume_server_pb.ReadVolumeFileStatusResponse, err error) {
|
|
err = m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
|
var statusErr error
|
|
resp, statusErr = client.ReadVolumeFileStatus(ctx, &volume_server_pb.ReadVolumeFileStatusRequest{
|
|
VolumeId: uint32(volumeId),
|
|
})
|
|
return statusErr
|
|
})
|
|
return
|
|
}
|
|
|
|
// DeleteVolume removes the volume from server. When keepRemoteData is true, the
|
|
// cloud-tier object backing the volume is left intact — used on the source side
|
|
// of a move where another server is taking over the same .vif.
|
|
func (m *Mover) DeleteVolume(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress, onlyEmpty bool, keepRemoteData bool) error {
|
|
return m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
|
_, deleteErr := client.VolumeDelete(ctx, &volume_server_pb.VolumeDeleteRequest{
|
|
VolumeId: uint32(volumeId),
|
|
OnlyEmpty: onlyEmpty,
|
|
KeepRemoteData: keepRemoteData,
|
|
})
|
|
return deleteErr
|
|
})
|
|
}
|
|
|
|
// MarkVolumeWritable marks the volume writable (or readonly when writable is
|
|
// false, persisted per persist) on server.
|
|
func (m *Mover) MarkVolumeWritable(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress, writable, persist bool) (err error) {
|
|
return m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
|
if writable {
|
|
_, err = client.VolumeMarkWritable(ctx, &volume_server_pb.VolumeMarkWritableRequest{
|
|
VolumeId: uint32(volumeId),
|
|
})
|
|
} else {
|
|
_, err = client.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
|
VolumeId: uint32(volumeId),
|
|
Persist: persist,
|
|
})
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// ReplicateVolume copies a volume from source to target without touching the
|
|
// source — the replica-creation half of a move.
|
|
func (m *Mover) ReplicateVolume(ctx context.Context, volumeId needle.VolumeId, source, target pb.ServerAddress, diskType string, writer io.Writer) error {
|
|
if writer == nil {
|
|
writer = io.Discard
|
|
}
|
|
if SameServer(source, target) {
|
|
return fmt.Errorf("refusing to replicate volume %d onto its own server %s", volumeId, source)
|
|
}
|
|
// The target dials the embedded source itself; a malformed one would
|
|
// abort the target server, not this client.
|
|
if err := checkDialable(source); err != nil {
|
|
return err
|
|
}
|
|
return m.withClient(false, target, func(client volume_server_pb.VolumeServerClient) error {
|
|
stream, replicateErr := client.VolumeCopy(ctx, &volume_server_pb.VolumeCopyRequest{
|
|
VolumeId: uint32(volumeId),
|
|
SourceDataNode: string(source),
|
|
DiskType: diskType,
|
|
})
|
|
if replicateErr != nil {
|
|
return replicateErr
|
|
}
|
|
for {
|
|
resp, recvErr := stream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr == io.EOF {
|
|
break
|
|
}
|
|
return recvErr
|
|
}
|
|
if resp.ProcessedBytes > 0 {
|
|
fmt.Fprintf(writer, "volume %d processed %s bytes\n", volumeId, util.BytesToHumanReadable(uint64(resp.ProcessedBytes)))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ConfigureVolumeReplication sets the replication setting on the volume at server.
|
|
func (m *Mover) ConfigureVolumeReplication(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress, replication string) error {
|
|
return m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
|
resp, configureErr := client.VolumeConfigure(ctx, &volume_server_pb.VolumeConfigureRequest{
|
|
VolumeId: uint32(volumeId),
|
|
Replication: replication,
|
|
})
|
|
if configureErr != nil {
|
|
return configureErr
|
|
}
|
|
if resp.Error != "" {
|
|
return errors.New(resp.Error)
|
|
}
|
|
return nil
|
|
})
|
|
}
|