[Volume] Validate record counts after volume copy (#11238)

* validate Volume Copy record counts

* Delete s3api_object_versioning_bench_test.go

* reply ai comments
This commit is contained in:
ssshr-66
2026-09-09 02:18:18 -07:00
committed by GitHub
parent 168b9c39f8
commit 966692fa23
4 changed files with 269 additions and 9 deletions
+54 -9
View File
@@ -50,11 +50,20 @@ func (vs *VolumeServer) VolumeCopy(req *volume_server_pb.VolumeCopyRequest, stre
// send .dat file
// confirm size and timestamp
var volFileInfoResp *volume_server_pb.ReadVolumeFileStatusResponse
var sourceVolumeStatus *volume_server_pb.VolumeStatusResponse
var sourceVolumeStatusAfterCopy *volume_server_pb.VolumeStatusResponse
var dataBaseFileName, indexBaseFileName, idxFileName, datFileName string
var hasRemoteDatFile bool
err := operation.WithVolumeServerClient(true, pb.ServerAddress(req.SourceDataNode), vs.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
var err error
volFileInfoResp, err = client.ReadVolumeFileStatus(context.Background(),
sourceVolumeStatus, err = client.VolumeStatus(stream.Context(), &volume_server_pb.VolumeStatusRequest{
VolumeId: req.VolumeId,
})
if err != nil {
return fmt.Errorf("read volume status failed, %w", err)
}
volFileInfoResp, err = client.ReadVolumeFileStatus(stream.Context(),
&volume_server_pb.ReadVolumeFileStatusRequest{
VolumeId: req.VolumeId,
})
@@ -189,6 +198,15 @@ func (vs *VolumeServer) VolumeCopy(req *volume_server_pb.VolumeCopyRequest, stre
return fmt.Errorf("remove .note for volume %d: %w", req.VolumeId, noteErr)
}
var statusErr error
sourceVolumeStatusAfterCopy, statusErr = client.VolumeStatus(stream.Context(), &volume_server_pb.VolumeStatusRequest{
VolumeId: req.VolumeId,
})
if statusErr != nil {
glog.Warningf("failed to read source volume %d status after copy; skip record count validation: %v", req.VolumeId, statusErr)
sourceVolumeStatusAfterCopy = nil
}
return nil
})
@@ -223,10 +241,24 @@ func (vs *VolumeServer) VolumeCopy(req *volume_server_pb.VolumeCopyRequest, stre
}
}
// mount the volume
err = vs.store.MountVolume(needle.VolumeId(req.VolumeId))
shouldValidateCopyCounts := copyCountsStable(sourceVolumeStatus, sourceVolumeStatusAfterCopy)
if sourceVolumeStatusAfterCopy == nil {
glog.V(1).Infof("source volume %d status was unavailable after copy; skip record count validation", req.VolumeId)
} else if !shouldValidateCopyCounts {
glog.V(1).Infof("source volume %d changed during copy; skip record count validation", req.VolumeId)
}
// Load and validate the volume before announcing it to the master. A failed
// validation is unloaded by the store without ever making the replica
// routable.
err = vs.store.MountVolumeWithValidator(needle.VolumeId(req.VolumeId), func(targetVolume *storage.Volume) error {
if !shouldValidateCopyCounts {
return nil
}
return checkCopyCounts(sourceVolumeStatusAfterCopy, targetVolume.FileCount(), targetVolume.DeletedCount())
})
if err != nil {
return fmt.Errorf("failed to mount volume %d: %v", req.VolumeId, err)
return fmt.Errorf("failed to mount or validate volume %d: %w", req.VolumeId, err)
}
if err = stream.Send(&volume_server_pb.VolumeCopyResponse{
@@ -266,11 +298,8 @@ func (vs *VolumeServer) doCopyFileWithThrottler(client volume_server_pb.VolumeSe
}
/*
*
only check the differ of the file size
todo: maybe should check the received count and deleted count of the volume
*/
// checkCopyFiles verifies the copied file sizes. Record counts are checked
// after the target volume is mounted, when the target needle map is available.
func checkCopyFiles(originFileInf *volume_server_pb.ReadVolumeFileStatusResponse, hasRemoteDatFile bool, idxFileName, datFileName string) error {
stat, err := os.Stat(idxFileName)
if err != nil {
@@ -300,6 +329,22 @@ func checkCopyFiles(originFileInf *volume_server_pb.ReadVolumeFileStatusResponse
return nil
}
func checkCopyCounts(origin *volume_server_pb.VolumeStatusResponse, targetFileCount, targetDeletedCount uint64) error {
if origin.FileCount != targetFileCount {
return fmt.Errorf("target file count [%d] is not same as origin file count [%d]", targetFileCount, origin.FileCount)
}
if origin.FileDeletedCount != targetDeletedCount {
return fmt.Errorf("target deleted count [%d] is not same as origin deleted count [%d]", targetDeletedCount, origin.FileDeletedCount)
}
return nil
}
func copyCountsStable(before, after *volume_server_pb.VolumeStatusResponse) bool {
return before != nil && after != nil &&
before.FileCount == after.FileCount &&
before.FileDeletedCount == after.FileDeletedCount
}
func findLastAppendAtNsFromCopiedFiles(idxFileName, datFileName string, version needle.Version) (uint64, error) {
if version < needle.Version3 {
return 0, nil
+148
View File
@@ -0,0 +1,148 @@
package weed_server
import (
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
func TestCheckCopyCounts(t *testing.T) {
tests := []struct {
name string
sourceFileCount uint64
sourceDeleteCount uint64
targetFileCount uint64
targetDeleteCount uint64
wantErr string
}{
{
name: "empty volume counts match",
},
{
name: "file and deleted counts match",
sourceFileCount: 10,
sourceDeleteCount: 3,
targetFileCount: 10,
targetDeleteCount: 3,
},
{
name: "file count differs",
sourceFileCount: 10,
sourceDeleteCount: 3,
targetFileCount: 9,
targetDeleteCount: 3,
wantErr: "file count",
},
{
name: "deleted count differs",
sourceFileCount: 10,
sourceDeleteCount: 3,
targetFileCount: 10,
targetDeleteCount: 2,
wantErr: "deleted count",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkCopyCounts(
&volume_server_pb.VolumeStatusResponse{
FileCount: tt.sourceFileCount,
FileDeletedCount: tt.sourceDeleteCount,
},
tt.targetFileCount,
tt.targetDeleteCount,
)
if tt.wantErr == "" {
if err != nil {
t.Fatalf("checkCopyCounts() error = %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("checkCopyCounts() error = %v, want substring %q", err, tt.wantErr)
}
})
}
}
func TestCopyCountsStable(t *testing.T) {
tests := []struct {
name string
before *volume_server_pb.VolumeStatusResponse
after *volume_server_pb.VolumeStatusResponse
want bool
}{
{
name: "empty volume is stable",
before: &volume_server_pb.VolumeStatusResponse{},
after: &volume_server_pb.VolumeStatusResponse{},
want: true,
},
{
name: "matching file and deleted counts are stable",
before: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 3,
},
after: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 3,
},
want: true,
},
{
name: "read-only state changes without count changes are stable",
before: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 3,
IsReadOnly: true,
},
after: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 3,
IsReadOnly: false,
},
want: true,
},
{
name: "changed file count is not stable",
before: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 3,
},
after: &volume_server_pb.VolumeStatusResponse{
FileCount: 9,
FileDeletedCount: 3,
},
},
{
name: "changed deleted count is not stable",
before: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 3,
},
after: &volume_server_pb.VolumeStatusResponse{
FileCount: 10,
FileDeletedCount: 2,
},
},
{
name: "missing before status is not stable",
after: &volume_server_pb.VolumeStatusResponse{},
},
{
name: "missing after status is not stable",
before: &volume_server_pb.VolumeStatusResponse{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := copyCountsStable(tt.before, tt.after); got != tt.want {
t.Fatalf("copyCountsStable() = %v, want %v", got, tt.want)
}
})
}
}