Files
seaweedfs/weed/storage/store_duplicate_vid_test.go
T
Chris LuandDevin 8ad2f29e3e shell: let volume.deleteEmpty drop volumes with no live needles (#11437)
* shell: let volume.deleteEmpty drop volumes with no live needles

The candidate check only accepted a .dat at superblock size, so a volume
whose every needle was deleted still had to be vacuumed first — minutes
of compaction to rewrite bytes that were all garbage anyway. FileCount
counts every indexed entry and DeleteCount every entry made garbage by
overwrite or delete, so FileCount <= DeleteCount means nothing live
remains and the volume can be unlinked directly. The quietFor guard is
unchanged.

* volume server: add only_garbage VolumeDelete guard

VolumeDelete(only_empty) refuses every volume that ever held data, so a
volume whose needles are all deleted could only be removed after a
vacuum rewrote it. The new only_garbage flag deletes only when the byte
counters show nothing live: DeletedSize covering all of ContentSize, the
same all-garbage state vacuum measures. Byte counters are used because
the file/delete counts drift on index reload.

* rust volume: mirror only_garbage VolumeDelete guard

Same check as the Go server: a volume deletes under only_garbage when
its deleted bytes cover all content bytes. The grpc handler rejects
before the store drops the volume from its map, since destroy errors
after removal would still unmount it.

* volume delete: let either enabled check pass, keep onlyEmpty on the wire

An upgraded shell sending only_garbage to a pre-upgrade server would be
read as an unconditional delete (field ignored, only_empty false). The
request now keeps only_empty set so old servers check emptiness and
refuse, while new servers delete when either check passes.

* volume.deleteEmpty: skip remote-backed and protected read-only volumes

A remote-tiered replica shares its cloud object with the other replicas,
so keepRemoteData=false on one delete removes data they still reference.
Protected read-only volumes are quarantined or under maintenance, which
is exactly when a replica should not be dropped.

* volume delete: validate guarded copies across disks before deleting

* volume delete: hold copy locks across guarded validate-and-delete

CheckVolumeDeletable released each copy's locks before Destroy ran, so a
write landing on a later copy between the two passes refused its destroy
after earlier copies were already removed. Pin every copy's
dataFileAccessLock (and its location's volumesLock) across validation and
removal so a refused delete leaves all copies intact.

* volume delete: send deleted-volume notices after releasing locks

A blocking send on a full DeletedVolumesChan under volumesLock can stall
the heartbeat loop that drains it while it waits on the same locks.
Collect the notices under the lock span and send after release.

* pb: restore generated-file cosmetics to match the repo's protoc version

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-26 15:53:39 +08:00

131 lines
4.9 KiB
Go

package storage
import (
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
"github.com/stretchr/testify/require"
)
// A volume id can end up mounted on more than one disk of a server (a stale twin
// re-attached after a disk repair, since NewStore has no cross-disk duplicate
// guard). UnmountVolume must remove EVERY copy, not just the first match, or the
// stale twin survives and re-registers as the volume's content on the next
// heartbeat.
func TestUnmountVolumeRemovesAllDuplicateCopies(t *testing.T) {
store := newTestStore(t, 2)
const vid = needle.VolumeId(4242)
store.Locations[0].SetVolume(vid, createTestVolume(vid, false))
store.Locations[1].SetVolume(vid, createTestVolume(vid, false))
require.NoError(t, store.UnmountVolume(vid))
_, found0 := store.Locations[0].FindVolume(vid)
_, found1 := store.Locations[1].FindVolume(vid)
require.False(t, found0, "copy on disk 0 must be unmounted")
require.False(t, found1, "the stale twin on disk 1 must also be unmounted")
}
// DeleteVolume must likewise destroy every copy of a duplicate volume id, not just
// the first match, so the stale twin cannot survive the delete.
func TestDeleteVolumeRemovesAllDuplicateCopies(t *testing.T) {
store := newTestStore(t, 2)
const vid = needle.VolumeId(4243)
// Real volumes (not stubs) so Destroy can close and unlink them cleanly.
for _, loc := range store.Locations {
v, err := NewVolume(loc.Directory, loc.IdxDirectory, "", vid, NeedleMapInMemory,
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
require.NoError(t, err)
loc.SetVolume(vid, v)
}
require.NoError(t, store.DeleteVolume(vid, false, false, false))
_, found0 := store.Locations[0].FindVolume(vid)
_, found1 := store.Locations[1].FindVolume(vid)
require.False(t, found0, "copy on disk 0 must be deleted")
require.False(t, found1, "the stale twin on disk 1 must also be deleted")
}
// A guarded delete must check every copy before destroying any: an earlier
// garbage copy must survive when a later duplicate still holds live data.
func TestDeleteVolumeGuardChecksAllCopiesBeforeDeleting(t *testing.T) {
store := newTestStore(t, 2)
const vid = needle.VolumeId(4244)
for _, loc := range store.Locations {
v, err := NewVolume(loc.Directory, loc.IdxDirectory, "", vid, NeedleMapInMemory,
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
require.NoError(t, err)
loc.SetVolume(vid, v)
}
// Disk 0's copy is fully deleted; disk 1's twin still holds a live needle.
copy0, _ := store.Locations[0].FindVolume(vid)
copy1, _ := store.Locations[1].FindVolume(vid)
n := &needle.Needle{Id: types.Uint64ToNeedleId(1), Data: []byte("x")}
_, _, _, err := copy0.writeNeedle2(n, false, false, false)
require.NoError(t, err)
_, _, _, err = copy1.writeNeedle2(n, false, false, false)
require.NoError(t, err)
_, err = copy0.deleteNeedle2(n)
require.NoError(t, err)
err = store.DeleteVolume(vid, false, true, false)
require.ErrorIs(t, err, ErrVolumeNotEmpty)
_, found0 := store.Locations[0].FindVolume(vid)
_, found1 := store.Locations[1].FindVolume(vid)
require.True(t, found0, "the garbage copy must survive a refused delete")
require.True(t, found1, "the live copy must survive a refused delete")
}
// A guarded delete must serialize with writes in flight on any copy: while a
// copy's data lock is held (a write in progress), the delete cannot start
// destroying other copies, or a write landing between validation and removal
// would refuse the later copy and leave a partial delete.
func TestDeleteVolumeGuardWaitsForInFlightCopyWrite(t *testing.T) {
store := newTestStore(t, 2)
const vid = needle.VolumeId(4245)
for _, loc := range store.Locations {
v, err := NewVolume(loc.Directory, loc.IdxDirectory, "", vid, NeedleMapInMemory,
&super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
require.NoError(t, err)
loc.SetVolume(vid, v)
}
copy1, _ := store.Locations[1].FindVolume(vid)
copy1.dataFileAccessLock.Lock()
done := make(chan error, 1)
go func() {
done <- store.DeleteVolume(vid, false, true, false)
}()
select {
case err := <-done:
t.Fatalf("guarded delete proceeded while a copy was locked for write: %v", err)
case <-time.After(200 * time.Millisecond):
}
// The write wins; the delete must see the new needle and refuse, leaving
// every copy intact.
n := &needle.Needle{Id: types.Uint64ToNeedleId(1), Data: []byte("x")}
_, _, _, err := copy1.doWriteRequest(n, false)
require.NoError(t, err)
copy1.dataFileAccessLock.Unlock()
require.ErrorIs(t, <-done, ErrVolumeNotEmpty)
_, found0 := store.Locations[0].FindVolume(vid)
_, found1 := store.Locations[1].FindVolume(vid)
require.True(t, found0, "the earlier copy must survive a refused delete")
require.True(t, found1, "the written copy must survive a refused delete")
}