mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* master: estimate a hintless assign's size from the volume's average file size An assign that carries no dataSize hint charged a flat 1MB per file id against the volume's effective size. A small-file workload overpays by orders of magnitude: bulk-writing 4KB files marks volumes holding a few hundred MB of real data as crowded and then full, so the master grows unnecessary volumes and, once every volume is spuriously full, fails all assigns. Estimate from the volume's own average file size instead, and keep the 1MB fallback only for volumes with no history. * master: decay pending assign sizes for volumes gone quiet The decay that corrects pending assign estimates runs only when a heartbeat reports the volume, and a heartbeat only reports a volume whose content changed. A volume held out of the writable list takes no writes, so once inflated estimates mark every volume full, nothing is ever reported again, nothing decays, and the cluster refuses all writes until a restart. Run the decay from the master's periodic loop for volumes no heartbeat has reported within two pulses, feeding the last reported size back through the same path an unchanged heartbeat would take. * master: trim the comments on the assign size estimate * master: keep the periodic decay out of the replica-dedup window UpdateVolumeSize ignores a report arriving within two seconds of the last one, so replicas of the same volume do not each halve the pending estimate. The periodic decay went through the same path and stamped that window, so a real heartbeat landing right behind it was dropped along with its reported size and compact revision. Only a volume whose content changed is reported at all, so nothing would send that size again and the master kept a stale one. Let the dedup window belong to volume server reports alone. * master: let the decay read the size record under the lock it mutates The periodic decay picked its volumes under a read lock and replayed them under a write one, carrying the size it had read across the gap. A heartbeat landing in between was rolled back: the replay wrote the older size and compact revision over the fresh ones, and a compaction report lost that way is never resent, since only a volume whose content changed is reported. The decay has no size of its own to contribute, so it now reads the record under the same lock it mutates. * master: let a heartbeat that beat the decay stand for the cycle The decay chooses its volumes under a read lock and applies them under a write one. A heartbeat landing in that gap already did the halving the cycle owed, so applying the decay on top of it halved twice and forgot pending bytes the volume has not written yet - the double-halving the replica-dedup window exists to prevent. Both callers now give way to a report already handled for this cycle; only a real report still advances lastUpdateTime, so a quiet volume keeps decaying every pulse. * master: keep genuinely full volumes out of the decay pass A volume the disk really did fill keeps its fullSince set for good, so it was selected every pulse for a decay that cannot help it: UpdateVolumeSize refuses to recover a volume whose reported size is at the limit, and replaying a size that cannot move leaves the record as it found it. Full and quiet is the ordinary resting state of a cluster, so this was most of the pass, taking the layout write lock away from the heartbeats to do nothing. On a million tracked volumes with a hundredth of them phantom-full it costs ten thousand write locks a pulse instead of a million. * master: put the stale-replay test back on the path it guards Giving the decay the dedup window left this test short-circuiting there, so it no longer reached the locked read it was written for and passed with that read removed. Age the record past the window, which is the only case where reading it under the lock is what saves the report.
262 lines
6.1 KiB
Go
262 lines
6.1 KiB
Go
package topology
|
|
|
|
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"
|
|
)
|
|
|
|
func TestGetPendingSize(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// Initially no pending
|
|
if p := vl.GetPendingSize(1); p != 0 {
|
|
t.Fatalf("expected 0 pending, got %d", p)
|
|
}
|
|
|
|
// RecordAssign increases pending
|
|
vl.RecordAssign(1, 5000)
|
|
if p := vl.GetPendingSize(1); p != 5000 {
|
|
t.Fatalf("expected 5000 pending, got %d", p)
|
|
}
|
|
|
|
// UpdateVolumeSize (heartbeat) decays pending
|
|
vl.UpdateVolumeSize(1, 3000, 0, true)
|
|
// effective was 6000, reported 3000 → decay to 3000 + (6000-3000)/2 = 4500
|
|
// pending = 4500 - 3000 = 1500
|
|
if p := vl.GetPendingSize(1); p != 1500 {
|
|
t.Fatalf("expected 1500 pending after decay, got %d", p)
|
|
}
|
|
}
|
|
|
|
func TestGetPendingSize_CompactionResets(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":5000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// Add large pending
|
|
vl.RecordAssign(1, 4000)
|
|
if p := vl.GetPendingSize(1); p != 4000 {
|
|
t.Fatalf("expected 4000 pending, got %d", p)
|
|
}
|
|
|
|
// Compaction happens — size drops from 5000 to 2000, revision changes.
|
|
// Without compaction awareness, decay would give: 2000 + (9000-2000)/2 = 5500.
|
|
// With compaction awareness, vid2size resets to 2000 (the real size).
|
|
vl.UpdateVolumeSize(1, 2000, 1, true) // revision 0 → 1
|
|
|
|
if p := vl.GetPendingSize(1); p != 0 {
|
|
t.Errorf("expected 0 pending after compaction reset, got %d", p)
|
|
}
|
|
|
|
// Verify vid2size is the reported size, not a decayed value
|
|
vl.accessLock.RLock()
|
|
if vl.sizeTracking[1].effectiveSize != 2000 {
|
|
t.Errorf("expected vid2size=2000 after compaction, got %d", vl.sizeTracking[1].effectiveSize)
|
|
}
|
|
vl.accessLock.RUnlock()
|
|
}
|
|
|
|
func TestDrainAndRemoveFromWritable_NoPending(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// No pending — drain should return immediately
|
|
start := time.Now()
|
|
vl.DrainAndRemoveFromWritable(1)
|
|
if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
|
|
t.Errorf("drain with no pending took %v, expected near-instant", elapsed)
|
|
}
|
|
|
|
// Verify volume is no longer writable
|
|
writable, _ := vl.GetWritableVolumeCount()
|
|
if writable != 0 {
|
|
t.Errorf("expected 0 writable after drain, got %d", writable)
|
|
}
|
|
}
|
|
|
|
func TestDrainAndRemoveFromWritable_WithPending(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"000"},
|
|
{"id":2, "size":1000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// Add pending below threshold
|
|
vl.RecordAssign(1, int64(pendingSizeThreshold-1))
|
|
|
|
start := time.Now()
|
|
vl.DrainAndRemoveFromWritable(1)
|
|
elapsed := time.Since(start)
|
|
|
|
// Should return quickly since pending is below threshold
|
|
if elapsed > 500*time.Millisecond {
|
|
t.Errorf("drain with pending below threshold took %v", elapsed)
|
|
}
|
|
|
|
// Volume removed from writable
|
|
writables := vl.CloneWritableVolumes()
|
|
for _, vid := range writables {
|
|
if vid == 1 {
|
|
t.Error("volume 1 should not be writable after drain")
|
|
}
|
|
}
|
|
// Volume 2 still writable
|
|
found := false
|
|
for _, vid := range writables {
|
|
if vid == 2 {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("volume 2 should still be writable")
|
|
}
|
|
}
|
|
|
|
func TestDrainAndRemoveFromWritable_DecaysViaConcurrentHeartbeat(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// Add large pending (well above threshold)
|
|
vl.RecordAssign(1, 100*1024*1024) // 100 MB
|
|
|
|
// Simulate heartbeats in background that will decay the pending.
|
|
// Advance lastUpdateTime before each call to bypass the 2s replica dedup.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
for i := 0; i < 10; i++ {
|
|
time.Sleep(500 * time.Millisecond)
|
|
vl.accessLock.Lock()
|
|
if st := vl.sizeTracking[1]; st != nil {
|
|
st.lastUpdateTime = time.Time{} // reset to allow update
|
|
}
|
|
vl.accessLock.Unlock()
|
|
vl.UpdateVolumeSize(1, 1000+uint64(i+1)*1000, 0, true)
|
|
}
|
|
}()
|
|
|
|
start := time.Now()
|
|
vl.DrainAndRemoveFromWritable(1)
|
|
elapsed := time.Since(start)
|
|
|
|
// Should have drained within a few seconds (heartbeats every 500ms).
|
|
// Use generous margin for slow CI.
|
|
if elapsed > 15*time.Second {
|
|
t.Errorf("drain took %v, expected faster with concurrent heartbeats", elapsed)
|
|
}
|
|
|
|
<-done
|
|
|
|
// Verify volume is no longer writable
|
|
writable, _ := vl.GetWritableVolumeCount()
|
|
if writable != 0 {
|
|
t.Errorf("expected 0 writable after drain, got %d", writable)
|
|
}
|
|
}
|
|
|
|
func TestSetVolumeReadOnly_PreservesPending(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
topo := setupWithLimit(t, layout, 10000)
|
|
rp, _ := super_block.NewReplicaPlacementFromString("000")
|
|
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
|
|
dn := vl.Lookup(1)[0]
|
|
|
|
// Add some pending
|
|
vl.RecordAssign(1, 5000)
|
|
|
|
// SetVolumeReadOnly should succeed immediately (non-blocking)
|
|
result := vl.SetVolumeReadOnly(dn, 1)
|
|
if !result {
|
|
t.Error("expected SetVolumeReadOnly to return true")
|
|
}
|
|
|
|
// Pending is still there (not drained), but volume is readonly
|
|
writable, _ := vl.GetWritableVolumeCount()
|
|
if writable != 0 {
|
|
t.Errorf("expected 0 writable after readonly, got %d", writable)
|
|
}
|
|
if p := vl.GetPendingSize(1); p != 5000 {
|
|
t.Errorf("expected 5000 pending (not drained), got %d", p)
|
|
}
|
|
}
|