mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-15 00:09:00 +01:00
volume_move: persist the move's source freeze across reloads
A live move froze the source in memory only, so an unmount/mount or a volume server restart mid-move reopened it for writes that the move then deletes with the source. Persist the freeze when the move froze a writable source, and reopen the target right after the copy since the copied .vif carries the freeze. An already-readonly source keeps an in-memory mark, and a failed move still restores the source writable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2b2ed769b
commit
49bb6bb8c4
3 files changed
+91
-10
No files matched your search
@@ -40,9 +40,10 @@ type fakeCluster struct {
|
|||||||
|
|
||||||
lastAppendAtNs uint64
|
lastAppendAtNs uint64
|
||||||
|
|
||||||
copyReqs []*volume_server_pb.VolumeCopyRequest
|
copyReqs []*volume_server_pb.VolumeCopyRequest
|
||||||
deleteReqs []*volume_server_pb.VolumeDeleteRequest
|
readonlyReqs []*volume_server_pb.VolumeMarkReadonlyRequest
|
||||||
ecCopyReqs []*volume_server_pb.VolumeEcShardsCopyRequest
|
deleteReqs []*volume_server_pb.VolumeDeleteRequest
|
||||||
|
ecCopyReqs []*volume_server_pb.VolumeEcShardsCopyRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFakeCluster() *fakeCluster {
|
func newFakeCluster() *fakeCluster {
|
||||||
@@ -94,6 +95,9 @@ func (f *fakeClient) VolumeMarkReadonly(ctx context.Context, req *volume_server_
|
|||||||
if err := f.cluster.record(f.addr, "VolumeMarkReadonly"); err != nil {
|
if err := f.cluster.record(f.addr, "VolumeMarkReadonly"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
f.cluster.mu.Lock()
|
||||||
|
f.cluster.readonlyReqs = append(f.cluster.readonlyReqs, req)
|
||||||
|
f.cluster.mu.Unlock()
|
||||||
return &volume_server_pb.VolumeMarkReadonlyResponse{}, nil
|
return &volume_server_pb.VolumeMarkReadonlyResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (m *Mover) LiveMoveVolume(ctx context.Context, volumeId needle.VolumeId, so
|
|||||||
}
|
}
|
||||||
|
|
||||||
opts.Progress(10, fmt.Sprintf("marking volume %d readonly on %s", volumeId, source))
|
opts.Progress(10, fmt.Sprintf("marking volume %d readonly on %s", volumeId, source))
|
||||||
sourceWasWritable, err := m.ensureVolumeReadonly(ctx, volumeId, source, true)
|
sourceWasWritable, err := m.ensureVolumeReadonly(ctx, volumeId, source, true, true)
|
||||||
// The source stays authoritative until its delete succeeds; on any earlier
|
// The source stays authoritative until its delete succeeds; on any earlier
|
||||||
// failure undo the freeze this move added and clean up the target copy.
|
// 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
|
// Installed before the error check: the marking RPC itself can fail after
|
||||||
@@ -130,8 +130,9 @@ func (m *Mover) LiveMoveVolume(ctx context.Context, volumeId needle.VolumeId, so
|
|||||||
m.restoreVolumeWritable(volumeId, source)
|
m.restoreVolumeWritable(volumeId, source)
|
||||||
} else {
|
} else {
|
||||||
// The hard freeze added on top of a readonly-reporting status is
|
// The hard freeze added on top of a readonly-reporting status is
|
||||||
// kept: it is indistinguishable from an operator's mark. It is
|
// kept: it is indistinguishable from an operator's mark. Only a
|
||||||
// not persisted, so a volume server restart clears it.
|
// freeze of a writable volume is persisted, so a volume server
|
||||||
|
// restart clears this one.
|
||||||
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)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +140,9 @@ func (m *Mover) LiveMoveVolume(ctx context.Context, volumeId needle.VolumeId, so
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("mark volume %d readonly on %s: %v", volumeId, source, err)
|
return fmt.Errorf("mark volume %d readonly on %s: %v", volumeId, source, err)
|
||||||
}
|
}
|
||||||
|
if sourceWasWritable {
|
||||||
|
fmt.Fprintf(opts.Writer, "volume %d on %s stays readonly across restarts until the move ends; if the move is interrupted, volume.mark -node %s -volumeId %d -writable reopens it\n", volumeId, source, source, volumeId)
|
||||||
|
}
|
||||||
|
|
||||||
targetHadVolume, targetStateKnown = m.probeVolume(ctx, volumeId, target)
|
targetHadVolume, targetStateKnown = m.probeVolume(ctx, volumeId, target)
|
||||||
if !sourceWasWritable {
|
if !sourceWasWritable {
|
||||||
@@ -168,6 +172,13 @@ func (m *Mover) LiveMoveVolume(ctx context.Context, volumeId needle.VolumeId, so
|
|||||||
return fmt.Errorf("copy volume %d from %s to %s: %v", volumeId, source, target, err)
|
return fmt.Errorf("copy volume %d from %s to %s: %v", volumeId, source, target, err)
|
||||||
}
|
}
|
||||||
copyCompleted = true
|
copyCompleted = true
|
||||||
|
if sourceWasWritable {
|
||||||
|
// The copy carries the source's persisted freeze; the target must take
|
||||||
|
// the tailed writes and serve as the moved volume.
|
||||||
|
if err = m.MarkVolumeWritable(ctx, volumeId, target, true, false); err != nil {
|
||||||
|
return fmt.Errorf("mark volume %d writable on %s: %v", volumeId, target, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
opts.Progress(70, fmt.Sprintf("tailing volume %d from %s to %s", volumeId, source, target))
|
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)
|
fmt.Fprintf(opts.Writer, "tailing volume %d from %s to %s\n", volumeId, source, target)
|
||||||
@@ -273,7 +284,7 @@ func (m *Mover) CopyVolume(ctx context.Context, volumeId needle.VolumeId, source
|
|||||||
// is (force=false): missed concurrent deletes only make the new replica
|
// is (force=false): missed concurrent deletes only make the new replica
|
||||||
// trivially stale, and hard-marking here could pin a transiently readonly
|
// trivially stale, and hard-marking here could pin a transiently readonly
|
||||||
// (e.g. low-disk) source readonly with nothing to gate on it.
|
// (e.g. low-disk) source readonly with nothing to gate on it.
|
||||||
sourceWasWritable, err := m.ensureVolumeReadonly(ctx, volumeId, source, false)
|
sourceWasWritable, err := m.ensureVolumeReadonly(ctx, volumeId, source, false, false)
|
||||||
defer func() {
|
defer func() {
|
||||||
if sourceWasWritable && (err != nil || restoreWritable) {
|
if sourceWasWritable && (err != nil || restoreWritable) {
|
||||||
m.restoreVolumeWritable(volumeId, source)
|
m.restoreVolumeWritable(volumeId, source)
|
||||||
@@ -293,7 +304,9 @@ func (m *Mover) CopyVolume(ctx context.Context, volumeId needle.VolumeId, source
|
|||||||
// readonly-but-can-delete flag, and neither blocks needle deletes, so only 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
|
// hard mark makes the volume immutable — required before deleting the source
|
||||||
// of a move. Without force, a readonly-reporting volume is left untouched.
|
// 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) {
|
// persist keeps the freeze of a writable volume across a volume reload or
|
||||||
|
// server restart; a mark over an already-readonly volume is never persisted.
|
||||||
|
func (m *Mover) ensureVolumeReadonly(ctx context.Context, volumeId needle.VolumeId, server pb.ServerAddress, force, persist bool) (wasWritable bool, err error) {
|
||||||
err = m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
err = m.withClient(false, server, func(client volume_server_pb.VolumeServerClient) error {
|
||||||
resp, statusErr := client.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{
|
resp, statusErr := client.VolumeStatus(ctx, &volume_server_pb.VolumeStatusRequest{
|
||||||
VolumeId: uint32(volumeId),
|
VolumeId: uint32(volumeId),
|
||||||
@@ -307,7 +320,7 @@ func (m *Mover) ensureVolumeReadonly(ctx context.Context, volumeId needle.Volume
|
|||||||
}
|
}
|
||||||
_, readonlyErr := client.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
_, readonlyErr := client.VolumeMarkReadonly(ctx, &volume_server_pb.VolumeMarkReadonlyRequest{
|
||||||
VolumeId: uint32(volumeId),
|
VolumeId: uint32(volumeId),
|
||||||
Persist: false,
|
Persist: persist && wasWritable,
|
||||||
})
|
})
|
||||||
return readonlyErr
|
return readonlyErr
|
||||||
})
|
})
|
||||||
@@ -331,7 +344,8 @@ func (m *Mover) probeVolume(ctx context.Context, volumeId needle.VolumeId, serve
|
|||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreVolumeWritable undoes a readonly mark after a failed move. It runs on
|
// restoreVolumeWritable undoes a readonly mark after a failed move, including
|
||||||
|
// a persisted one: the volume servers always persist a writable mark. It runs on
|
||||||
// its own deadline so an abort via cancelled context still restores the source,
|
// 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.
|
// and only logs a failure — the caller is already returning the move error.
|
||||||
func (m *Mover) restoreVolumeWritable(volumeId needle.VolumeId, server pb.ServerAddress) {
|
func (m *Mover) restoreVolumeWritable(volumeId needle.VolumeId, server pb.ServerAddress) {
|
||||||
|
|||||||
@@ -147,12 +147,19 @@ func TestLiveMoveVolumeSequence(t *testing.T) {
|
|||||||
"src:8080 VolumeMarkReadonly",
|
"src:8080 VolumeMarkReadonly",
|
||||||
"dst:8080 ReadVolumeFileStatus",
|
"dst:8080 ReadVolumeFileStatus",
|
||||||
"dst:8080 VolumeCopy",
|
"dst:8080 VolumeCopy",
|
||||||
|
"dst:8080 VolumeMarkWritable",
|
||||||
"dst:8080 VolumeTailReceiver",
|
"dst:8080 VolumeTailReceiver",
|
||||||
"src:8080 ReadVolumeFileStatus",
|
"src:8080 ReadVolumeFileStatus",
|
||||||
"dst:8080 ReadVolumeFileStatus",
|
"dst:8080 ReadVolumeFileStatus",
|
||||||
"src:8080 VolumeDelete",
|
"src:8080 VolumeDelete",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The freeze survives a reload of the source; the copy inherits it, so the
|
||||||
|
// target is reopened before the tail writes to it.
|
||||||
|
if !cluster.readonlyReqs[0].Persist {
|
||||||
|
t.Errorf("freeze of a writable source not persisted: %+v", cluster.readonlyReqs[0])
|
||||||
|
}
|
||||||
|
|
||||||
copyReq := cluster.copyReqs[0]
|
copyReq := cluster.copyReqs[0]
|
||||||
if copyReq.DiskType != "ssd" || copyReq.IoBytePerSecond != 42 || copyReq.SourceDataNode != string(srcAddr) {
|
if copyReq.DiskType != "ssd" || copyReq.IoBytePerSecond != 42 || copyReq.SourceDataNode != string(srcAddr) {
|
||||||
t.Errorf("copy request not propagated: %+v", copyReq)
|
t.Errorf("copy request not propagated: %+v", copyReq)
|
||||||
@@ -631,3 +638,59 @@ func TestCopyVolumeRestoreWritable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLiveMoveVolumeReadonlySourceFreezeNotPersisted(t *testing.T) {
|
||||||
|
// A readonly-reporting source may be only transiently so (e.g. low disk);
|
||||||
|
// the hard mark over it must not outlive a restart, and the copy keeps
|
||||||
|
// whatever state the source had.
|
||||||
|
cluster := newFakeCluster()
|
||||||
|
cluster.readonly[string(srcAddr)] = true
|
||||||
|
cluster.status[string(srcAddr)] = volumeStatus(1000, 100, 10)
|
||||||
|
cluster.status[string(dstAddr)] = volumeStatus(1000, 100, 10)
|
||||||
|
cluster.statusFailures[string(dstAddr)] = 1
|
||||||
|
|
||||||
|
if err := cluster.mover().LiveMoveVolume(context.Background(), 7, srcAddr, dstAddr, VolumeMoveOptions{}); err != nil {
|
||||||
|
t.Fatalf("LiveMoveVolume: %v", err)
|
||||||
|
}
|
||||||
|
if cluster.readonlyReqs[0].Persist {
|
||||||
|
t.Errorf("freeze of an already-readonly source persisted: %+v", cluster.readonlyReqs[0])
|
||||||
|
}
|
||||||
|
for _, call := range cluster.callList() {
|
||||||
|
if call == "dst:8080 VolumeMarkWritable" {
|
||||||
|
t.Fatalf("copy of a readonly source made writable: %v", cluster.callList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveMoveVolumeTargetReopenFailureRestores(t *testing.T) {
|
||||||
|
// A target left with the copied freeze would refuse the tailed writes;
|
||||||
|
// abort, drop the copy and reopen the source.
|
||||||
|
cluster := newFakeCluster()
|
||||||
|
cluster.status[string(srcAddr)] = volumeStatus(1000, 100, 10)
|
||||||
|
cluster.errs["dst:8080 VolumeMarkWritable"] = errors.New("target unreachable")
|
||||||
|
|
||||||
|
err := cluster.mover().LiveMoveVolume(context.Background(), 7, srcAddr, dstAddr, VolumeMoveOptions{})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "mark volume 7 writable on dst:8080") {
|
||||||
|
t.Fatalf("expected target reopen error, got: %v", err)
|
||||||
|
}
|
||||||
|
calls := cluster.callList()
|
||||||
|
for _, call := range calls {
|
||||||
|
if call == "dst:8080 VolumeTailReceiver" {
|
||||||
|
t.Fatalf("tailed into a target that is still frozen: %v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fmt.Sprint(calls[len(calls)-2:]) != fmt.Sprint([]string{"dst:8080 VolumeDelete", "src:8080 VolumeMarkWritable"}) {
|
||||||
|
t.Fatalf("expected target cleanup then source restore, got: %v", calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCopyVolumeFreezeNotPersisted(t *testing.T) {
|
||||||
|
// A copy leaves the source serving, so its freeze must not outlive a crash.
|
||||||
|
cluster := newFakeCluster()
|
||||||
|
if _, err := cluster.mover().CopyVolume(context.Background(), 7, srcAddr, dstAddr, "", 0, false, nil); err != nil {
|
||||||
|
t.Fatalf("CopyVolume: %v", err)
|
||||||
|
}
|
||||||
|
if cluster.readonlyReqs[0].Persist {
|
||||||
|
t.Errorf("copy freeze persisted: %+v", cluster.readonlyReqs[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user