mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-08 04:57:50 +01:00
* storage/topology: keep disk-full read-only volumes vacuumable The vacuum sweep skipped every read-only replica, so a volume that went read-only because its disk filled could never reclaim its garbage — the exact situation compaction exists for. The volume server now reports disk_space_low in VacuumVolumeCheckResponse, and the sweep skips a read-only replica only when the flag is clear. An explicit volumeId vacuum is unaffected: it already bypassed the read-only rule. The field takes number 4: 2 and 3 are downstream-allocated for tombstone retention, keeping the wire merge clean. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * storage: measure vacuum free space against live bytes The pre-compaction space check required the current .dat + .idx size free, which includes the garbage being reclaimed — on a nearly full disk that estimate can never fit, so the volume stayed garbage-bound forever. Measure against the estimated compacted output instead: superblock plus live index entries plus live content bytes, with the existing ten percent buffer unchanged. Mirrors the same check in the Rust volume server. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * vacuum: count per-needle framing in the compacted-size estimate The live-bytes estimate covered each live needle's content and index entry but not its .dat framing (header, checksum, timestamp, padding — ~32 bytes on version 3). For small-needle volumes that is more than the 10% headroom, so a disk with space between the estimate and the real output still ran out mid-compaction. Rust side mirrors the same formula. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * storage: report disk_space_low only when it is the sole read-only cause Review feedback (ihnokim, greptile, devin): a volume read-only for low disk space AND an operator mark or I/O quarantine was still eligible for the automatic sweep, rewriting a copy meant to stay protected. The flag now reports only the benign sole-cause case in both servers. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * topology: fail closed when the read-only lookup misses in the sweep A heartbeat can drop the volume from the DataNode cache between the location-list copy and VacuumVolumeCheck; a lookup error previously skipped the read-only check entirely. Review feedback (coderabbit). 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>
159 lines
5.5 KiB
Go
159 lines
5.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
func TestSpaceCalculation(t *testing.T) {
|
|
// Test the space calculation logic
|
|
testCases := []struct {
|
|
name string
|
|
volumeSize uint64
|
|
indexSize uint64
|
|
preallocate int64
|
|
expectedMin int64
|
|
}{
|
|
{
|
|
name: "Large volume, small preallocate",
|
|
volumeSize: 244 * 1024 * 1024 * 1024, // 244GB
|
|
indexSize: 1024 * 1024, // 1MB
|
|
preallocate: 1024, // 1KB
|
|
expectedMin: int64((244*1024*1024*1024 + 1024*1024) * 11 / 10), // +10% buffer
|
|
},
|
|
{
|
|
name: "Small volume, large preallocate",
|
|
volumeSize: 100 * 1024 * 1024, // 100MB
|
|
indexSize: 1024, // 1KB
|
|
preallocate: 1024 * 1024 * 1024, // 1GB
|
|
expectedMin: int64(1024 * 1024 * 1024 * 11 / 10), // preallocate + 10%
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Calculate space needed using the same logic as our fix
|
|
estimatedCompactSize := int64(tc.volumeSize + tc.indexSize)
|
|
spaceNeeded := tc.preallocate
|
|
if estimatedCompactSize > tc.preallocate {
|
|
spaceNeeded = estimatedCompactSize
|
|
}
|
|
// Add 10% safety buffer
|
|
spaceNeeded = spaceNeeded + (spaceNeeded / 10)
|
|
|
|
if spaceNeeded < tc.expectedMin {
|
|
t.Errorf("Space calculation too low: got %d, expected at least %d", spaceNeeded, tc.expectedMin)
|
|
}
|
|
|
|
t.Logf("Volume size: %d bytes, Space needed: %d bytes (%.2f%% of volume size)",
|
|
tc.volumeSize, spaceNeeded, float64(spaceNeeded)/float64(tc.volumeSize)*100)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Compaction writes live needles only, so the space check must be measured
|
|
// against the live size, not the .dat the garbage occupies — a full disk
|
|
// needs the estimate to shrink or it can never reclaim.
|
|
func TestEstimatedCompactedSizeCountsLiveNeedles(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
const count = 20
|
|
for i := 1; i <= count; i++ {
|
|
if _, _, _, err := v.writeNeedle2(newRandomNeedle(uint64(i)), true, false, false); err != nil {
|
|
t.Fatalf("write needle %d: %v", i, err)
|
|
}
|
|
}
|
|
datSize, _, _ := v.FileStat()
|
|
|
|
fullEstimate := estimatedCompactedSize(v)
|
|
if fullEstimate <= super_block.SuperBlockSize {
|
|
t.Fatalf("estimate for all-live volume = %d, want > superblock", fullEstimate)
|
|
}
|
|
|
|
for i := 1; i < count; i++ {
|
|
if _, err := v.doDeleteRequest(newEmptyNeedle(uint64(i))); err != nil {
|
|
t.Fatalf("delete needle %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
estimate := estimatedCompactedSize(v)
|
|
if estimate >= int64(datSize) {
|
|
t.Fatalf("estimate %d not below .dat size %d with 19/20 needles deleted", estimate, datSize)
|
|
}
|
|
if estimate <= super_block.SuperBlockSize {
|
|
t.Fatalf("estimate %d lost the one live needle", estimate)
|
|
}
|
|
live := int64(v.FileCount()-v.DeletedCount())*types.NeedleMapEntrySize + super_block.SuperBlockSize
|
|
if estimate < live {
|
|
t.Fatalf("estimate %d below superblock + live index entries %d", estimate, live)
|
|
}
|
|
|
|
if _, err := v.doDeleteRequest(newEmptyNeedle(uint64(count))); err != nil {
|
|
t.Fatalf("delete last needle: %v", err)
|
|
}
|
|
if estimate := estimatedCompactedSize(v); estimate != super_block.SuperBlockSize {
|
|
t.Fatalf("all-deleted estimate = %d, want superblock only (%d)", estimate, super_block.SuperBlockSize)
|
|
}
|
|
}
|
|
|
|
// The estimate must cover what compaction writes on disk: each live needle's
|
|
// content plus its header, checksum, timestamp and padding. An all-live
|
|
// volume's compacted .dat is byte-for-byte its current one, so the estimate
|
|
// may not fall below the current file.
|
|
func TestEstimatedCompactedSizeCoversNeedleFraming(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
v, err := NewVolume(dir, dir, "", 1, NeedleMapInMemory, &super_block.ReplicaPlacement{}, &needle.TTL{}, 0, needle.GetCurrentVersion(), 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("volume creation: %v", err)
|
|
}
|
|
defer v.Close()
|
|
|
|
for i := 1; i <= 100; i++ {
|
|
if _, _, _, err := v.writeNeedle2(newRandomNeedle(uint64(i)), true, false, false); err != nil {
|
|
t.Fatalf("write needle %d: %v", i, err)
|
|
}
|
|
}
|
|
datSize, _, _ := v.FileStat()
|
|
|
|
if estimate := estimatedCompactedSize(v); estimate < int64(datSize) {
|
|
t.Fatalf("estimate %d below .dat size %d for an all-live volume: missing per-needle framing", estimate, datSize)
|
|
}
|
|
}
|
|
|
|
// disk_space_low is only reported when low space is the sole read-only cause,
|
|
// so a volume also marked read-only by an operator or quarantined by failed
|
|
// I/O stays out of the sweep.
|
|
func TestCheckCompactVolumeDiskLowSoleCauseOnly(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := newSingleDirStore(t, dir)
|
|
defer store.Close()
|
|
const vid = needle.VolumeId(7)
|
|
require.NoError(t, store.AddVolume(vid, "", NeedleMapInMemory, "000", "", 0, needle.GetCurrentVersion(), 0, types.HardDriveType, 0))
|
|
|
|
_, low, err := store.CheckCompactVolume(vid)
|
|
require.NoError(t, err)
|
|
require.False(t, low)
|
|
|
|
store.Locations[0].isDiskSpaceLow.Store(true)
|
|
_, low, err = store.CheckCompactVolume(vid)
|
|
require.NoError(t, err)
|
|
require.True(t, low)
|
|
|
|
require.NoError(t, store.MarkVolumeReadonly(vid, false, false))
|
|
_, low, err = store.CheckCompactVolume(vid)
|
|
require.NoError(t, err)
|
|
require.False(t, low)
|
|
}
|