mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-12 23:09:33 +01:00
* shell: parallel moves and a per-run limit for volumeServer.evacuate -maxParallelization runs up to N EC shard moves at once (default 1), with the ec.balance scheduler: never two shards of one volume at a time, free workers go to the least busy targets. The moves are planned first with the same target choice as before, then applied through volume_move.Mover.MoveEcShards, which verifies the target registered a shard before deleting it on the source. -movesPerExec limits how many volumes and EC shards one run moves (default 0 for unlimited), like -volumesPerExec in volume.balance, so a big server can be emptied in several runs. With -skipNonMoveable a shard that fails to move (a read error on a failing disk, for example) no longer drops the other shards of its volume; they are moved and the failures are listed at the end. Without it the run stops at the first failure as before. Losing the admin lock stops the run in either mode. Refs #12501 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * shell: plan evacuation again after failed moves; fail on a lost lock A failed shard move kept the target slot it was planned on, so a readable shard that found no destination in the plan stayed on the server although the room was never used. Failed moves now give their targets back to the model and the shards without a destination are planned again, up to maxEcEvacuationRounds times; the failed shards are not tried again. With -skipNonMoveable a lost admin lock was printed as a skipped failure and the run returned success. ec.ErrLockLost is now exported and always fails the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * shell: volumeServer.evacuate reports -movesPerExec after an applied EC run The round loop only checked the limit after a failed move, so an applied run that stopped at -movesPerExec without failures never said to run it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * shell: volumeServer.evacuate plans again for as long as failed moves leave room The ten-round cap left readable shards on the server once more than ten rounds of unreadable shards had taken the free room first, and a rerun fails the same shards first again. Every round moves or fails each shard it plans and a failed shard is not planned again, so the rounds end without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * shell: volumeServer.evacuate gives a failed move back every slot it took When the target already held the shard in another disk type, adding it to the planned disk type still took a slot, but the undo gave back only the explicit one. Record the slots the plan took and restore exactly those. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ec: an evacuation move that landed on its target keeps the target slot A move can fail after the target registered the shard, when only the source unmount or delete failed. Evacuate then gave the target's slot back and could plan another shard onto a full server. The runner now checks the target after a failed move and leaves a shard the target has out of FailedMoves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * shell: volumeServer.evacuate gives back the slot of a failed move onto a shard the target had A target that held the shard before the plan reports it after any failed move, so the runner took such a move for one that landed and evacuate kept the slot the plan had taken. The runner now lists those moves in LandedMoves, and evacuate undoes the ones whose target had the shard, then plans again with the room they gave back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
160 lines
4.7 KiB
Go
160 lines
4.7 KiB
Go
package ec
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding/ecbalancer"
|
|
)
|
|
|
|
// evacuationMoves is what volumeServer.evacuate plans: every shard of each
|
|
// volume leaves the same source node.
|
|
func evacuationMoves(volumes, shards int) []ecbalancer.Move {
|
|
var moves []ecbalancer.Move
|
|
for v := 0; v < volumes; v++ {
|
|
for s := 0; s < shards; s++ {
|
|
moves = append(moves, ecbalancer.Move{
|
|
VolumeID: uint32(100 + v),
|
|
ShardID: s,
|
|
SourceNode: "source",
|
|
TargetNode: []string{"node-a", "node-b", "node-c"}[(v+s)%3],
|
|
})
|
|
}
|
|
}
|
|
return moves
|
|
}
|
|
|
|
func TestRunEcShardMovesStopsOnFirstError(t *testing.T) {
|
|
moves := evacuationMoves(3, 2)
|
|
var ran []ecbalancer.Move
|
|
result, err := runEcShardMoves(moves, 1, true, func(m ecbalancer.Move) error {
|
|
ran = append(ran, m)
|
|
if len(ran) == 2 {
|
|
return errors.New("input/output error")
|
|
}
|
|
return nil
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected the failed move's error")
|
|
}
|
|
if len(ran) != 2 {
|
|
t.Fatalf("expected no move to start after the failure, %d ran", len(ran))
|
|
}
|
|
if result.Moved != 1 || result.Failed != 1 || result.NotStarted != len(moves)-2 {
|
|
t.Fatalf("unexpected result %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestRunEcShardMovesGoesOnAfterFailure(t *testing.T) {
|
|
moves := evacuationMoves(3, 3)
|
|
broken := moves[0]
|
|
ran := map[uint32]int{}
|
|
result, err := runEcShardMoves(moves, 1, false, func(m ecbalancer.Move) error {
|
|
ran[m.VolumeID]++
|
|
if m.VolumeID == broken.VolumeID && m.ShardID == broken.ShardID {
|
|
return errors.New("input/output error")
|
|
}
|
|
return nil
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected the failed move's error")
|
|
}
|
|
if ran[broken.VolumeID] != 3 {
|
|
t.Fatalf("expected the other shards of volume %d to move after its failed shard, %d moves ran", broken.VolumeID, ran[broken.VolumeID])
|
|
}
|
|
if result.Moved != len(moves)-1 || result.Failed != 1 || result.NotStarted != 0 {
|
|
t.Fatalf("unexpected result %+v", result)
|
|
}
|
|
if len(result.FailedMoves) != 1 || result.FailedMoves[0] != broken {
|
|
t.Fatalf("expected the broken move to be reported, got %+v", result.FailedMoves)
|
|
}
|
|
}
|
|
|
|
func TestRunEcShardMovesInParallel(t *testing.T) {
|
|
const parallelization = 4
|
|
moves := evacuationMoves(20, 4)
|
|
started := make(chan struct{}, len(moves))
|
|
release := make(chan struct{})
|
|
var mu sync.Mutex
|
|
running, most := 0, 0
|
|
var result EcShardMovesResult
|
|
var err error
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
result, err = runEcShardMoves(moves, parallelization, true, func(m ecbalancer.Move) error {
|
|
mu.Lock()
|
|
running++
|
|
most = max(most, running)
|
|
mu.Unlock()
|
|
started <- struct{}{}
|
|
<-release
|
|
mu.Lock()
|
|
running--
|
|
mu.Unlock()
|
|
return nil
|
|
})
|
|
}()
|
|
// The first moves are held until parallelization of them have started.
|
|
for i := 0; i < parallelization; i++ {
|
|
select {
|
|
case <-started:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("only %d moves started at once, expected %d", i, parallelization)
|
|
}
|
|
}
|
|
close(release)
|
|
<-done
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Moved != len(moves) || result.Failed != 0 || result.NotStarted != 0 {
|
|
t.Fatalf("unexpected result %+v", result)
|
|
}
|
|
if most != parallelization {
|
|
t.Fatalf("%d moves ran at once, expected %d", most, parallelization)
|
|
}
|
|
}
|
|
|
|
func TestRunEcShardMovesStopsWhenLockIsLost(t *testing.T) {
|
|
moves := evacuationMoves(3, 2)
|
|
ran := 0
|
|
result, err := runEcShardMoves(moves, 1, false, func(m ecbalancer.Move) error {
|
|
ran++
|
|
if ran == 2 {
|
|
return ErrLockLost
|
|
}
|
|
return nil
|
|
})
|
|
if !errors.Is(err, ErrLockLost) {
|
|
t.Fatalf("expected the lost lock to be reported, got %v", err)
|
|
}
|
|
if ran != 2 || result.Moved != 1 || result.Failed != 1 || result.NotStarted != len(moves)-2 {
|
|
t.Fatalf("expected no move after the lock was lost even with skipping on: ran %d, %+v", ran, result)
|
|
}
|
|
}
|
|
|
|
// A move that failed after its target registered the shard counts as failed but
|
|
// is not reported for planning again: the target's slot is taken.
|
|
func TestRunEcShardMovesKeepsShardsOnTheTargetOutOfFailedMoves(t *testing.T) {
|
|
moves := evacuationMoves(2, 1)
|
|
result, err := runEcShardMoves(moves, 1, false, func(m ecbalancer.Move) error {
|
|
if m.VolumeID == moves[0].VolumeID {
|
|
return fmt.Errorf("move ec shard: %w: delete: input/output error", errShardOnTarget)
|
|
}
|
|
return errors.New("copy: input/output error")
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected the failed moves' errors")
|
|
}
|
|
if result.Failed != 2 || len(result.FailedMoves) != 1 || result.FailedMoves[0] != moves[1] {
|
|
t.Fatalf("expected only the move without a target copy in FailedMoves, got %+v", result)
|
|
}
|
|
if len(result.LandedMoves) != 1 || result.LandedMoves[0] != moves[0] {
|
|
t.Fatalf("expected the move with a target copy in LandedMoves, got %+v", result)
|
|
}
|
|
}
|