mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-13 15:29:12 +01:00
volume.deleteEmpty judged each copy on its own, so it removed an empty copy while another copy still held data or was offline, leaving that one as the volume's only copy. Low-disk copies now report readOnlyCanDelete and passed the check as well. Apply the master sweep's rule: every copy is listed and every copy is empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
4.1 KiB
Go
85 lines
4.1 KiB
Go
package shell
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestIsEmptyVolumeDeleteCandidate(t *testing.T) {
|
|
matcher, err := wildcard.CompileCollectionMatcher("")
|
|
assert.NoError(t, err)
|
|
|
|
const quietSeconds = int64(60)
|
|
const now = int64(1_000_000)
|
|
old := now - quietSeconds - 1
|
|
|
|
tests := []struct {
|
|
name string
|
|
v *master_pb.VolumeInformationMessage
|
|
want bool
|
|
}{
|
|
{"small .dat is empty", &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ModifiedAtSecond: old}, true},
|
|
{"all needles deleted is empty", &master_pb.VolumeInformationMessage{Size: 1 << 30, FileCount: 100, DeleteCount: 100, ModifiedAtSecond: old}, true},
|
|
{"live needles keep the volume", &master_pb.VolumeInformationMessage{Size: 1 << 30, FileCount: 100, DeleteCount: 99, ModifiedAtSecond: old}, false},
|
|
{"overwrites alone are not empty", &master_pb.VolumeInformationMessage{Size: 1 << 30, FileCount: 200, DeleteCount: 100, ModifiedAtSecond: old}, false},
|
|
{"recent all-deleted volume is kept", &master_pb.VolumeInformationMessage{Size: 1 << 30, FileCount: 100, DeleteCount: 100, ModifiedAtSecond: now}, false},
|
|
{"never written volume is kept", &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ModifiedAtSecond: 0}, false},
|
|
{"remote-backed replica is kept", &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, RemoteStorageName: "s3", RemoteStorageKey: "v", ModifiedAtSecond: old}, false},
|
|
{"remote-backed garbage is kept", &master_pb.VolumeInformationMessage{Size: 1 << 30, FileCount: 100, DeleteCount: 100, RemoteStorageName: "s3", ModifiedAtSecond: old}, false},
|
|
{"protected read-only volume is kept", &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ReadOnly: true, ModifiedAtSecond: old}, false},
|
|
{"deletable read-only volume can go", &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ReadOnly: true, ReadOnlyCanDelete: true, ModifiedAtSecond: old}, true},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, isEmptyVolumeDeleteCandidate(tc.v, quietSeconds, now, matcher))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsEmptyVolumeCopiesDeleteCandidate(t *testing.T) {
|
|
matcher, err := wildcard.CompileCollectionMatcher("")
|
|
assert.NoError(t, err)
|
|
|
|
const quietSeconds = int64(60)
|
|
const now = int64(1_000_000)
|
|
old := now - quietSeconds - 1
|
|
copyOf := func(rp uint32, v *master_pb.VolumeInformationMessage) *VolumeReplica {
|
|
v.ReplicaPlacement = rp
|
|
return &VolumeReplica{location: &location{dataNode: &master_pb.DataNodeInfo{Id: "dn"}}, info: v}
|
|
}
|
|
empty := func() *master_pb.VolumeInformationMessage {
|
|
return &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ModifiedAtSecond: old}
|
|
}
|
|
lowDisk := func() *master_pb.VolumeInformationMessage {
|
|
return &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ReadOnly: true, ReadOnlyCanDelete: true, ModifiedAtSecond: old}
|
|
}
|
|
withData := func() *master_pb.VolumeInformationMessage {
|
|
return &master_pb.VolumeInformationMessage{Size: 1 << 20, FileCount: 3, ModifiedAtSecond: old}
|
|
}
|
|
protected := func() *master_pb.VolumeInformationMessage {
|
|
return &master_pb.VolumeInformationMessage{Size: super_block.SuperBlockSize, ReadOnly: true, ModifiedAtSecond: old}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
replicas []*VolumeReplica
|
|
want bool
|
|
}{
|
|
{"single copy volume", []*VolumeReplica{copyOf(0, empty())}, true},
|
|
{"every copy empty", []*VolumeReplica{copyOf(1, empty()), copyOf(1, lowDisk())}, true},
|
|
{"other copy holds data", []*VolumeReplica{copyOf(1, lowDisk()), copyOf(1, withData())}, false},
|
|
{"other copy is missing", []*VolumeReplica{copyOf(1, lowDisk())}, false},
|
|
{"other copy is protected read-only", []*VolumeReplica{copyOf(1, empty()), copyOf(1, protected())}, false},
|
|
{"no copies", nil, false},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, isEmptyVolumeCopiesDeleteCandidate(tc.replicas, quietSeconds, now, matcher))
|
|
})
|
|
}
|
|
}
|