mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 17:00:48 +01:00
* topology: add AvailableSpaceWithinLocation for location-scoped free slots AvailableSpaceFor answers for the whole topology; a location-constrained assign needs to know whether the requested data center, rack, or data node still has room before deciding growth can satisfy it. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: let location-constrained assigns wait for pending growth An assign carrying a data center or rack constraint failed on the first unsatisfied pick, even when the same request had just queued volume growth for that location — so the first write into a fresh collection errored out instead of picking up the volume growth landed a moment later. Keep the fail-fast path only when growth cannot help: growth disabled, no growth pending, or no free slots left inside the constraint. Otherwise the request takes the same wait/shed path as an unconstrained assign. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * topology: sum leaf free slots in AvailableSpaceWithinLocation Rolled-up rack and data center counters keep a maintenance-mode server's capacity, and one overcommitted node can cancel a sibling's slots. Sum the matching data nodes directly so the location check agrees with what growth can actually use. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: let constrained growth initiators reach the shed path When a constrained assign's own growth concluded unfulfilled, the fast-fail check saw only that no grow was pending and returned the plain pick error — a code the assign client does not retry, so the first write died instead of re-triggering growth. Exclude the request's own initiated grow from the no-pending-growth condition so it reaches the existing re-pick and ResourceExhausted shed like an unconstrained assign. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: judge targeted grow requests by the location they name A grow request carrying a data center, rack, or data node constraint was discarded whenever the layout's writables elsewhere were healthy, so a location with free slots but no writable could never receive the volume its assign was waiting for. Add ShouldGrowVolumesWithinLocation and use it for constrained requests; the layout-wide check still governs untargeted ones. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * topology: only positive leaf slots count in AvailableSpaceWithinLocation An over-committed node reports negative free slots; summed plainly it\ncancelled a sibling's room and a constrained assign could fail fast\nwhile the location still had space to grow into. * master: fail constrained assigns fast when growth cannot place A constrained assign with free slots in its location still waited out\ngrowth that could never satisfy the replica placement — a single-rack\ndata center asked for 010 sheds the same doomed grow on every client\nretry. Run the grow-time node search as a dry run and return its\nplacement error instead of a retryable shed. * master: judge constrained growth by layout, not free slots A slot-level feasibility probe races with the grow it judges: a\npending grow consuming the last slot in one rack reports the\nplacement impossible while it mints the request's own volume, and\nthe hard error skips the re-pick that would find it. GrowthBlocker\nnow counts racks and data nodes inside the named location — shape a\ngrow cannot consume — so only genuinely unplaceable layouts fail\nfast. * topology: GrowthBlocker mirrors the grow selector's feasibility The location filter bound every replica tier, so a 010 request naming one rack needed two racks inside it while the selector constrains only the first placement and counts the rest by room. And counting nodes without free slots let a full location queue a grow it could never serve, so the assign shed ResourceExhausted at timeout instead of failing fast. GrowthBlocker now applies the option's location to the primary placement chain only and leaf-sums free slots the way the selector does, so it reports a blocker exactly when the grow can never place. * master: repick once before surfacing a growth blocker An in-flight grow can spend the last free slot minting the request's own volume: the blocker's slot counts predate that registration by a heartbeat, so returning the placement error on the first failed pick could reject a write whose volume lands a moment later. A pending grow now earns one repick grace before the blocker error is returned. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
834 lines
23 KiB
Go
834 lines
23 KiB
Go
package topology
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/sequence"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
// setupWithLimit is like setup() but allows specifying the volumeSizeLimit
|
|
// so that VolumeLayouts are created with the correct limit from the start.
|
|
func setupWithLimit(t testing.TB, topologyLayout string, volumeSizeLimit uint64) *Topology {
|
|
t.Helper()
|
|
var data interface{}
|
|
if err := json.Unmarshal([]byte(topologyLayout), &data); err != nil {
|
|
t.Fatalf("setupWithLimit: json.Unmarshal: %v", err)
|
|
}
|
|
mTopology, ok := data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("setupWithLimit: expected map[string]interface{}, got %T", data)
|
|
}
|
|
|
|
topo := NewTopology("weedfs", sequence.NewMemorySequencer(), volumeSizeLimit, 5, false)
|
|
for dcKey, dcValue := range mTopology {
|
|
dc := NewDataCenter(dcKey)
|
|
dcMap := dcValue.(map[string]interface{})
|
|
topo.LinkChildNode(dc)
|
|
for rackKey, rackValue := range dcMap {
|
|
dcRack := NewRack(rackKey)
|
|
rackMap := rackValue.(map[string]interface{})
|
|
dc.LinkChildNode(dcRack)
|
|
for serverKey, serverValue := range rackMap {
|
|
server := NewDataNode(serverKey)
|
|
serverMap := serverValue.(map[string]interface{})
|
|
if ip, ok := serverMap["ip"]; ok {
|
|
server.Ip = ip.(string)
|
|
}
|
|
dcRack.LinkChildNode(server)
|
|
for _, v := range serverMap["volumes"].([]interface{}) {
|
|
m := v.(map[string]interface{})
|
|
vi := storage.VolumeInfo{
|
|
Id: needle.VolumeId(int64(m["id"].(float64))),
|
|
Size: uint64(m["size"].(float64)),
|
|
Version: needle.GetCurrentVersion(),
|
|
}
|
|
if mVal, ok := m["fileCount"]; ok {
|
|
vi.FileCount = uint32(mVal.(float64))
|
|
}
|
|
if mVal, ok := m["collection"]; ok {
|
|
vi.Collection = mVal.(string)
|
|
}
|
|
if mVal, ok := m["replication"]; ok {
|
|
rp, _ := super_block.NewReplicaPlacementFromString(mVal.(string))
|
|
vi.ReplicaPlacement = rp
|
|
}
|
|
if vi.ReplicaPlacement != nil {
|
|
vl := topo.GetVolumeLayout(vi.Collection, vi.ReplicaPlacement, needle.EMPTY_TTL, types.HardDriveType)
|
|
vl.RegisterVolume(&vi, server, nil)
|
|
vl.setVolumeWritable(vi.Id)
|
|
}
|
|
server.AddOrUpdateVolume(vi)
|
|
}
|
|
disk := server.getOrCreateDisk("")
|
|
disk.UpAdjustDiskUsageDelta("", &DiskUsageCounts{
|
|
maxVolumeCount: int64(serverMap["limit"].(float64)),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return topo
|
|
}
|
|
|
|
func setupPickTest(t testing.TB, layout string, volumeSizeLimit uint64) (*Topology, *VolumeLayout) {
|
|
t.Helper()
|
|
topo := setupWithLimit(t, layout, volumeSizeLimit)
|
|
rp, _ := super_block.NewReplicaPlacementFromString("000")
|
|
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
|
|
return topo, vl
|
|
}
|
|
|
|
func TestPickForWriteWeightedDistribution(t *testing.T) {
|
|
// 3 volumes at 20%, 50%, 80% full (sizes 2000, 5000, 8000 of limit 10000)
|
|
// remaining: 8000, 5000, 2000 => ratios ~53%, 33%, 13%
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":2000, "replication":"000"},
|
|
{"id":2, "size":5000, "replication":"000"},
|
|
{"id":3, "size":8000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
counts := make(map[needle.VolumeId]int)
|
|
option := &VolumeGrowOption{}
|
|
n := 60000
|
|
|
|
for i := 0; i < n; i++ {
|
|
vid, _, _, _, err := vl.PickForWrite(1, option)
|
|
if err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
counts[vid]++
|
|
}
|
|
|
|
// vid 1 (remaining 8000) > vid 2 (remaining 5000) > vid 3 (remaining 2000)
|
|
if counts[1] <= counts[3] {
|
|
t.Errorf("expected vid 1 picked more than vid 3: vid1=%d, vid3=%d", counts[1], counts[3])
|
|
}
|
|
if counts[2] <= counts[3] {
|
|
t.Errorf("expected vid 2 picked more than vid 3: vid2=%d, vid3=%d", counts[2], counts[3])
|
|
}
|
|
|
|
// Check proportions: expected 8000/5000/2000 out of 15000
|
|
expected := map[needle.VolumeId]float64{
|
|
1: 8000.0 / 15000.0,
|
|
2: 5000.0 / 15000.0,
|
|
3: 2000.0 / 15000.0,
|
|
}
|
|
for vid, expectedPct := range expected {
|
|
actualPct := float64(counts[vid]) / float64(n)
|
|
if math.Abs(actualPct-expectedPct) > 0.03 {
|
|
t.Errorf("vid %d: expected ~%.1f%%, got %.1f%%", vid, expectedPct*100, actualPct*100)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPickForWriteWithPendingSize(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 large pending to vid 1, making it effectively 9000/10000
|
|
vl.RecordAssign(1, 8000)
|
|
|
|
counts := make(map[needle.VolumeId]int)
|
|
option := &VolumeGrowOption{}
|
|
n := 10000
|
|
|
|
for i := 0; i < n; i++ {
|
|
vid, _, _, _, err := vl.PickForWrite(1, option)
|
|
if err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
counts[vid]++
|
|
}
|
|
|
|
// vid 2 (remaining ~9000) should be picked much more than vid 1 (remaining ~1000)
|
|
ratio := float64(counts[2]) / float64(counts[1])
|
|
if ratio < 3.0 {
|
|
t.Errorf("vid2/vid1 ratio %.2f expected >= 3.0 (vid1=%d, vid2=%d)", ratio, counts[1], counts[2])
|
|
}
|
|
}
|
|
|
|
// A flat 1MB charge per hintless file id overcharges a small-file workload by
|
|
// orders of magnitude, marking volumes full while they hold a fraction of the
|
|
// limit.
|
|
func TestPickForWriteEstimatesPendingSizeFromVolumeAverage(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":409600, "fileCount":100, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
topo, vl := setupPickTest(t, layout, 30*1024*1024)
|
|
rp, _ := super_block.NewReplicaPlacementFromString("000")
|
|
option := &VolumeGrowOption{ReplicaPlacement: rp}
|
|
|
|
pendingAfterAssign := func(expectedDataSize uint64) uint64 {
|
|
vl.accessLock.RLock()
|
|
before := vl.sizeTracking[1].effectiveSize
|
|
vl.accessLock.RUnlock()
|
|
if _, _, _, _, err := topo.PickForWrite(1, option, vl, expectedDataSize); err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
vl.accessLock.RLock()
|
|
defer vl.accessLock.RUnlock()
|
|
return vl.sizeTracking[1].effectiveSize - before
|
|
}
|
|
|
|
if got := pendingAfterAssign(0); got != 4096 {
|
|
t.Errorf("a hintless assign charged %d, want the volume's 4096-byte average", got)
|
|
}
|
|
if got := pendingAfterAssign(8192); got != 8192 {
|
|
t.Errorf("a hinted assign charged %d, want the 8192-byte hint to win over the average", got)
|
|
}
|
|
}
|
|
|
|
// A volume with no files yet has no average to draw on, so the 1MB fallback
|
|
// still applies.
|
|
func TestPickForWriteEstimateFallsBackWithoutHistory(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":0, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
topo, vl := setupPickTest(t, layout, 30*1024*1024)
|
|
rp, _ := super_block.NewReplicaPlacementFromString("000")
|
|
option := &VolumeGrowOption{ReplicaPlacement: rp}
|
|
|
|
if _, _, _, _, err := topo.PickForWrite(1, option, vl, 0); err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
vl.accessLock.RLock()
|
|
pending := vl.sizeTracking[1].effectiveSize - vl.sizeTracking[1].reportedSize
|
|
vl.accessLock.RUnlock()
|
|
if pending != DefaultNeedleSizeEstimate {
|
|
t.Errorf("a hintless assign on an empty volume charged %d, want the %d fallback", pending, DefaultNeedleSizeEstimate)
|
|
}
|
|
}
|
|
|
|
func TestPickForWriteSingleWritable(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":5000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
option := &VolumeGrowOption{}
|
|
for i := 0; i < 100; i++ {
|
|
vid, _, _, _, err := vl.PickForWrite(1, option)
|
|
if err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
if vid != 1 {
|
|
t.Fatalf("expected vid 1, got %d", vid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPickForWriteAllNearFull(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":9999, "replication":"000"},
|
|
{"id":2, "size":9999, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
option := &VolumeGrowOption{}
|
|
for i := 0; i < 100; i++ {
|
|
vid, _, _, _, err := vl.PickForWrite(1, option)
|
|
if err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
if vid != 1 && vid != 2 {
|
|
t.Fatalf("expected vid 1 or 2, got %d", vid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPickForWriteConstrainedWeighted(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"ip":"10.0.0.1",
|
|
"volumes":[
|
|
{"id":1, "size":2000, "replication":"000"},
|
|
{"id":2, "size":8000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
},
|
|
"rack2":{
|
|
"server2":{
|
|
"ip":"10.0.0.2",
|
|
"volumes":[
|
|
{"id":3, "size":5000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
counts := make(map[needle.VolumeId]int)
|
|
option := &VolumeGrowOption{DataCenter: "dc1"}
|
|
n := 20000
|
|
|
|
for i := 0; i < n; i++ {
|
|
vid, _, _, _, err := vl.PickForWrite(1, option)
|
|
if err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
counts[vid]++
|
|
}
|
|
|
|
// vid 1 (remaining 8000) should be picked most, vid 2 (remaining 2000) least
|
|
if counts[1] <= counts[2] {
|
|
t.Errorf("expected vid 1 picked more than vid 2: vid1=%d, vid2=%d", counts[1], counts[2])
|
|
}
|
|
}
|
|
|
|
func TestRecordAssignMarksCrowded(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":8500, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// Volume at 85% — not crowded yet (threshold is 90%)
|
|
_, crowded := vl.GetWritableVolumeCount()
|
|
if crowded != 0 {
|
|
t.Fatalf("expected 0 crowded, got %d", crowded)
|
|
}
|
|
|
|
// Add pending that pushes past 90%
|
|
vl.RecordAssign(1, 1000)
|
|
|
|
_, crowded = vl.GetWritableVolumeCount()
|
|
if crowded != 1 {
|
|
t.Errorf("expected 1 crowded after pending push past 90%%, got %d", crowded)
|
|
}
|
|
}
|
|
|
|
func TestRecordAssignReachingCapacityRemovesFromWritable(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":5000, "replication":"000"},
|
|
{"id":2, "size":5000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
topo, vl := setupPickTest(t, layout, 10000)
|
|
|
|
writable, _ := vl.GetWritableVolumeCount()
|
|
if writable != 2 {
|
|
t.Fatalf("expected 2 writable volumes initially, got %d", writable)
|
|
}
|
|
// Each volume counts as active initially.
|
|
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
|
|
|
|
// Push vid 1 past the hard limit (5000 + 5000 = 10000 == limit).
|
|
reachedCapacity := vl.RecordAssign(1, 5000)
|
|
if !reachedCapacity {
|
|
t.Fatalf("RecordAssign should return true when effectiveSize reaches limit")
|
|
}
|
|
vl.AdjustActiveVolumeCountForFull(1)
|
|
|
|
writable, _ = vl.GetWritableVolumeCount()
|
|
if writable != 1 {
|
|
t.Errorf("expected 1 writable after eager removal, got %d", writable)
|
|
}
|
|
// activeVolumeCount should be decremented for the data node holding vid 1.
|
|
afterActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
|
|
if afterActive != initialActive-1 {
|
|
t.Errorf("expected activeVolumeCount=%d, got %d", initialActive-1, afterActive)
|
|
}
|
|
|
|
// A second RecordAssign on the already-removed volume should not return
|
|
// true again (no double accounting).
|
|
if vl.RecordAssign(1, 10000) {
|
|
t.Errorf("RecordAssign should not report reachedCapacity twice for the same removal")
|
|
}
|
|
afterSecond := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
|
|
if afterSecond != afterActive {
|
|
t.Errorf("activeVolumeCount changed on second RecordAssign: before=%d after=%d", afterActive, afterSecond)
|
|
}
|
|
}
|
|
|
|
// advanceSizeTrackingClock backdates a volume's time-sensitive fields by d
|
|
// so heartbeat decay and the recovery delay fire on the next update.
|
|
func advanceSizeTrackingClock(vl *VolumeLayout, vid needle.VolumeId, d time.Duration) {
|
|
vl.accessLock.Lock()
|
|
defer vl.accessLock.Unlock()
|
|
st := vl.sizeTracking[vid]
|
|
if st == nil {
|
|
return
|
|
}
|
|
if !st.lastUpdateTime.IsZero() {
|
|
st.lastUpdateTime = st.lastUpdateTime.Add(-d)
|
|
}
|
|
if !st.fullSince.IsZero() {
|
|
st.fullSince = st.fullSince.Add(-d)
|
|
}
|
|
}
|
|
|
|
func TestUpdateVolumeSizeRecoversEagerlyRemovedVolume(t *testing.T) {
|
|
// Two writable volumes, each at 40% of a 10000-byte limit. Push vid 1
|
|
// past the hard limit via RecordAssign, then heartbeat with an
|
|
// unchanged reported size so decay shrinks effectiveSize below the
|
|
// crowded threshold (90% of limit). After the recovery delay, the
|
|
// volume should be re-added to writables and activeVolumeCount
|
|
// restored.
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":4000, "replication":"000"},
|
|
{"id":2, "size":4000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
topo, vl := setupPickTest(t, layout, 10000)
|
|
|
|
initialActive := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount
|
|
initialWritables, _ := vl.GetWritableVolumeCount()
|
|
|
|
// Push vid 1 past the limit (effective = 4000 + 6000 = 10000).
|
|
if !vl.RecordAssign(1, 6000) {
|
|
t.Fatalf("RecordAssign should return true at the limit")
|
|
}
|
|
vl.AdjustActiveVolumeCountForFull(1)
|
|
|
|
w, _ := vl.GetWritableVolumeCount()
|
|
if w != initialWritables-1 {
|
|
t.Fatalf("expected %d writables after eager removal, got %d", initialWritables-1, w)
|
|
}
|
|
|
|
// Before the recovery delay, a heartbeat that lets decay run should
|
|
// *not* re-add the volume (even though effectiveSize would now be
|
|
// under the threshold).
|
|
advanceSizeTrackingClock(vl, 1, 3*time.Second) // past the 2s dedup window, but before 30s delay
|
|
if vl.UpdateVolumeSize(1, 4000, 0, true) {
|
|
t.Fatalf("recovery should not fire before capacityRecoveryDelay")
|
|
}
|
|
w, _ = vl.GetWritableVolumeCount()
|
|
if w != initialWritables-1 {
|
|
t.Errorf("writable count should not change before delay, got %d", w)
|
|
}
|
|
|
|
// Now skip past the recovery delay. Decay the gap further until
|
|
// effectiveSize drops below the crowded threshold (9000).
|
|
for i := 0; i < 6; i++ {
|
|
advanceSizeTrackingClock(vl, 1, 10*time.Second)
|
|
recovered := vl.UpdateVolumeSize(1, 4000, 0, true)
|
|
if recovered {
|
|
vl.AdjustActiveVolumeCountAfterRecovery(1)
|
|
break
|
|
}
|
|
}
|
|
|
|
w, _ = vl.GetWritableVolumeCount()
|
|
if w != initialWritables {
|
|
t.Errorf("expected recovery to restore %d writables, got %d", initialWritables, w)
|
|
}
|
|
if got := topo.diskUsages.usages[types.HardDriveType].activeVolumeCount; got != initialActive {
|
|
t.Errorf("expected activeVolumeCount restored to %d, got %d", initialActive, got)
|
|
}
|
|
|
|
// fullSince should have been cleared so a subsequent heartbeat doesn't
|
|
// try to recover again.
|
|
advanceSizeTrackingClock(vl, 1, 60*time.Second)
|
|
if vl.UpdateVolumeSize(1, 4000, 0, true) {
|
|
t.Errorf("recovery should not re-fire after the volume is already writable")
|
|
}
|
|
}
|
|
|
|
func TestUpdateVolumeSizeNoRecoveryWhenDiskStillOversized(t *testing.T) {
|
|
// Volume reported at 95% of limit and pushed past limit by RecordAssign.
|
|
// Even after the recovery delay and decay, reportedSize remains >= limit
|
|
// (real on-disk size is over limit), so recovery must not fire.
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":9500, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
if !vl.RecordAssign(1, 500) {
|
|
t.Fatalf("RecordAssign should hit the limit (9500 + 500 = 10000)")
|
|
}
|
|
vl.AdjustActiveVolumeCountForFull(1)
|
|
|
|
// Plenty of time elapsed — but reported stays at 10500 (over limit).
|
|
for i := 0; i < 5; i++ {
|
|
advanceSizeTrackingClock(vl, 1, 10*time.Second)
|
|
if vl.UpdateVolumeSize(1, 10500, 0, true) {
|
|
t.Fatalf("recovery must not fire when reported >= limit")
|
|
}
|
|
}
|
|
w, _ := vl.GetWritableVolumeCount()
|
|
if w != 0 {
|
|
t.Errorf("expected 0 writables (volume legitimately full), got %d", w)
|
|
}
|
|
}
|
|
|
|
func TestHeartbeatDecaysPendingSize(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)
|
|
|
|
// vid2size starts at 1000 (reported). Add 8000 pending → 9000.
|
|
vl.RecordAssign(1, 8000)
|
|
|
|
vl.accessLock.RLock()
|
|
if vl.sizeTracking[1].effectiveSize != 9000 {
|
|
t.Fatalf("expected vid2size=9000 after RecordAssign, got %d", vl.sizeTracking[1].effectiveSize)
|
|
}
|
|
vl.accessLock.RUnlock()
|
|
|
|
// Helper to simulate a new heartbeat cycle (advance past dedup window)
|
|
advanceCycle := func() {
|
|
vl.accessLock.Lock()
|
|
vl.sizeTracking[1].lastUpdateTime = time.Now().Add(-3 * time.Second)
|
|
vl.accessLock.Unlock()
|
|
}
|
|
|
|
// Heartbeat: volume server reports size=3000 (some writes landed).
|
|
// Old effective=9000, new reported=3000 → excess=6000 → decayed to 3000.
|
|
// So vid2size should become 3000 + 6000/2 = 6000, not just 3000.
|
|
vl.UpdateVolumeSize(1, 3000, 0, true)
|
|
|
|
vl.accessLock.RLock()
|
|
if vl.sizeTracking[1].effectiveSize != 6000 {
|
|
t.Errorf("expected vid2size=6000 after decay (3000 + 6000/2), got %d", vl.sizeTracking[1].effectiveSize)
|
|
}
|
|
vl.accessLock.RUnlock()
|
|
|
|
// Second heartbeat: size=5000. Old effective=6000 → excess=1000 → decay to 500.
|
|
// vid2size should become 5000 + 1000/2 = 5500.
|
|
advanceCycle()
|
|
vl.UpdateVolumeSize(1, 5000, 0, true)
|
|
|
|
vl.accessLock.RLock()
|
|
if vl.sizeTracking[1].effectiveSize != 5500 {
|
|
t.Errorf("expected vid2size=5500 after second decay (5000 + 1000/2), got %d", vl.sizeTracking[1].effectiveSize)
|
|
}
|
|
vl.accessLock.RUnlock()
|
|
|
|
// Third heartbeat: size=5500. Old effective=5500 → no excess.
|
|
// vid2size should be exactly 5500.
|
|
advanceCycle()
|
|
vl.UpdateVolumeSize(1, 5500, 0, true)
|
|
|
|
vl.accessLock.RLock()
|
|
if vl.sizeTracking[1].effectiveSize != 5500 {
|
|
t.Errorf("expected vid2size=5500 (no excess), got %d", vl.sizeTracking[1].effectiveSize)
|
|
}
|
|
vl.accessLock.RUnlock()
|
|
|
|
// vid 2 (remaining 9000) should be picked more than vid 1 (remaining 4500)
|
|
counts := make(map[needle.VolumeId]int)
|
|
option := &VolumeGrowOption{}
|
|
for i := 0; i < 10000; i++ {
|
|
vid, _, _, _, err := vl.PickForWrite(1, option)
|
|
if err != nil {
|
|
t.Fatalf("PickForWrite: %v", err)
|
|
}
|
|
counts[vid]++
|
|
}
|
|
if counts[2] <= counts[1] {
|
|
t.Errorf("vid 2 (remaining 9000) should be picked more than vid 1 (remaining 4500): vid1=%d, vid2=%d", counts[1], counts[2])
|
|
}
|
|
}
|
|
|
|
func TestHeartbeatDecayDedupReplicas(t *testing.T) {
|
|
// Volume 1 replicated on server1 and server2.
|
|
// Both servers report size=3000 in the same heartbeat cycle.
|
|
// Decay should run only once, not once per replica.
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"ip":"10.0.0.1",
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"001"}
|
|
],
|
|
"limit":10
|
|
},
|
|
"server2":{
|
|
"ip":"10.0.0.2",
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"001"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
topo := setupWithLimit(t, layout, 10000)
|
|
rp, _ := super_block.NewReplicaPlacementFromString("001")
|
|
vl := topo.GetVolumeLayout("", rp, needle.EMPTY_TTL, types.HardDriveType)
|
|
|
|
// Add pending: effective = 1000 + 8000 = 9000
|
|
vl.RecordAssign(1, 8000)
|
|
|
|
vl.accessLock.RLock()
|
|
if vl.sizeTracking[1].effectiveSize != 9000 {
|
|
t.Fatalf("expected vid2size=9000, got %d", vl.sizeTracking[1].effectiveSize)
|
|
}
|
|
vl.accessLock.RUnlock()
|
|
|
|
// Both replicas report size=3000. Decay should happen once: 3000 + (9000-3000)/2 = 6000.
|
|
// Calling UpdateVolumeSize twice simulates two replicas reporting in the same cycle.
|
|
vl.UpdateVolumeSize(1, 3000, 0, true)
|
|
vl.UpdateVolumeSize(1, 3000, 0, true) // second replica, same size — should be a no-op
|
|
|
|
vl.accessLock.RLock()
|
|
got := vl.sizeTracking[1].effectiveSize
|
|
vl.accessLock.RUnlock()
|
|
// Without dedup: would be 3000 + (6000-3000)/2 = 4500 (double decay).
|
|
// With dedup: should be 6000 (single decay).
|
|
if got != 6000 {
|
|
t.Errorf("expected vid2size=6000 (single decay), got %d (double decay would give 4500)", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdateVolumeSize_DecaysEvenWhenReportedSizeUnchanged(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"volumes":[
|
|
{"id":1, "size":1000, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
// Add pending: effective = 1000 + 8000 = 9000
|
|
vl.RecordAssign(1, 8000)
|
|
if p := vl.GetPendingSize(1); p != 8000 {
|
|
t.Fatalf("expected 8000 pending, got %d", p)
|
|
}
|
|
|
|
// First heartbeat: reported size unchanged at 1000 (writes haven't landed).
|
|
// Decay should still run: 1000 + (9000-1000)/2 = 5000.
|
|
vl.UpdateVolumeSize(1, 1000, 0, true)
|
|
if p := vl.GetPendingSize(1); p != 4000 {
|
|
t.Errorf("expected 4000 pending after first decay, got %d", p)
|
|
}
|
|
|
|
// Simulate next heartbeat cycle (>2s later) with same reported size.
|
|
// Need to advance lastUpdateTime — manipulate directly under lock.
|
|
vl.accessLock.Lock()
|
|
vl.sizeTracking[1].lastUpdateTime = time.Now().Add(-3 * time.Second)
|
|
vl.accessLock.Unlock()
|
|
|
|
// Second heartbeat: still 1000. Decay again: 1000 + (5000-1000)/2 = 3000.
|
|
vl.UpdateVolumeSize(1, 1000, 0, true)
|
|
if p := vl.GetPendingSize(1); p != 2000 {
|
|
t.Errorf("expected 2000 pending after second decay, got %d", p)
|
|
}
|
|
}
|
|
|
|
func TestShouldGrowVolumesByDcAndRack_WithPendingSize(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"ip":"10.0.0.1",
|
|
"volumes":[
|
|
{"id":1, "size":8500, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
writables := vl.CloneWritableVolumes()
|
|
if vl.ShouldGrowVolumesByDcAndRack(&writables, "dc1", "rack1") {
|
|
t.Error("should not grow before pending makes volume crowded")
|
|
}
|
|
|
|
// Add pending that pushes effective size past 9000 threshold
|
|
vl.RecordAssign(1, 600)
|
|
|
|
if !vl.ShouldGrowVolumesByDcAndRack(&writables, "dc1", "rack1") {
|
|
t.Error("should grow after pending pushes volume past crowded threshold")
|
|
}
|
|
}
|
|
|
|
// A targeted grow asks whether the location it names still lacks a usable
|
|
// writable, so healthy writables in other data centers must not mask the need.
|
|
func TestShouldGrowVolumesWithinLocation(t *testing.T) {
|
|
layout := `
|
|
{
|
|
"dc1":{
|
|
"rack1":{
|
|
"server1":{
|
|
"ip":"10.0.0.1",
|
|
"volumes":[
|
|
{"id":1, "size":2000, "replication":"000"},
|
|
{"id":2, "size":9500, "replication":"000"}
|
|
],
|
|
"limit":10
|
|
}
|
|
}
|
|
},
|
|
"dc2":{
|
|
"rack2":{
|
|
"server2":{
|
|
"ip":"10.0.0.2",
|
|
"volumes":[],
|
|
"limit":10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
_, vl := setupPickTest(t, layout, 10000)
|
|
|
|
if vl.ShouldGrowVolumesWithinLocation(&VolumeGrowOption{DataCenter: "dc1"}) {
|
|
t.Error("dc1 holds a non-crowded writable: a dc1 grow is not needed")
|
|
}
|
|
if !vl.ShouldGrowVolumesWithinLocation(&VolumeGrowOption{DataCenter: "dc1", Rack: "rack2"}) {
|
|
t.Error("no writable has a replica in dc1/rack2: a rack grow is needed")
|
|
}
|
|
if !vl.ShouldGrowVolumesWithinLocation(&VolumeGrowOption{DataCenter: "dc2"}) {
|
|
t.Error("dc2 has no writable at all: a dc2 grow is needed")
|
|
}
|
|
if vl.ShouldGrowVolumesWithinLocation(&VolumeGrowOption{DataCenter: "dc1", DataNode: "server1"}) {
|
|
t.Error("server1 holds a non-crowded writable: a datanode grow is not needed")
|
|
}
|
|
if vl.ShouldGrowVolumesWithinLocation(&VolumeGrowOption{}) {
|
|
t.Error("an unconstrained grow sees the non-crowded writable and is not needed")
|
|
}
|
|
}
|