Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
504 lines
23 KiB
Go
504 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. ioBytePerSecond limits the
|
|
// copy rate; 0 falls back to the volume server's maintenance rate.
|
|
func (m *Mover) ReplicateVolume(ctx context.Context, volumeId needle.VolumeId, source, target pb.ServerAddress, diskType string, ioBytePerSecond int64, 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,
|
|
IoBytePerSecond: ioBytePerSecond,
|
|
})
|
|
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
|
|
})
|
|
}
|