diff --git a/component/size_tracker/size_tracker.go b/component/size_tracker/size_tracker.go index aaed84acd..c8370569d 100644 --- a/component/size_tracker/size_tracker.go +++ b/component/size_tracker/size_tracker.go @@ -43,8 +43,21 @@ type SizeTracker struct { internal.BaseComponent mountSize *MountSize totalBucketCapacity uint64 + displayCapacity uint64 + serverCount uint64 + evictionMode EvictionMode + bucketUsage uint64 + statSizeOffset uint64 } +type EvictionMode int + +const ( + Normal EvictionMode = iota // 0 + Overuse // 1 + Emergency // 2 +) + type SizeTrackerOptions struct { JournalName string `config:"journal-name" yaml:"journal-name,omitempty"` TotalBucketCapacity uint64 `config:"bucket-capacity-fallback" yaml:"bucket-capacity-fallback,omitempty"` @@ -53,7 +66,13 @@ type SizeTrackerOptions struct { const compName = "size_tracker" const blockSize = int64(4096) const defaultJournalName = "mount_size.dat" -const evictionThreshold = 0.95 + +// these usage thresholds determine when the eviction mode changes +const targetUtilization = 0.9 // 90% +const hysteresisMargin = 0.02 // 2% +const overuseThreshold = targetUtilization + hysteresisMargin // 92% +const emergencyThreshold = overuseThreshold + .05 // 97% +const bucketNormalizedThreshold = targetUtilization - hysteresisMargin // 88% var _ internal.Component = &SizeTracker{} @@ -99,7 +118,24 @@ func (st *SizeTracker) Configure(_ bool) error { return fmt.Errorf("SizeTracker: config error [invalid config attributes]") } - st.totalBucketCapacity = conf.TotalBucketCapacity * common.MbToBytes + if conf.TotalBucketCapacity != 0 { + // TODO: document these units + st.totalBucketCapacity = conf.TotalBucketCapacity * common.MbToBytes + // set display capacity + st.displayCapacity = st.totalBucketCapacity + if config.IsSet("libfuse.display-capacity-mb") { + var confDisplayCapacityMb uint64 + err = config.UnmarshalKey("libfuse.display-capacity-mb", &confDisplayCapacityMb) + if err == nil { + st.displayCapacity = confDisplayCapacityMb * common.MbToBytes + } else { + log.Err("SizeTracker::Configure : Invalid display capacity") + } + } + // calculate server count + // round to the nearest whole number + st.serverCount = (st.totalBucketCapacity + st.displayCapacity/2) / st.displayCapacity + } journalName := defaultJournalName if config.IsSet(compName + ".journal-name") { @@ -262,20 +298,44 @@ func (st *SizeTracker) StatFs() (*common.Statfs_t, bool, error) { if err == nil && ret { // Custom logic for use with Nx Plugin - // If the user is over the capacity limit set by Nx, then we need to prevent them from - // accidental overuse of their bucket. So we change our reporting to instead report - // the used capacity of the bucket to enable the VMS to start eviction - if float64( - stat.Blocks*uint64(blockSize), - ) > evictionThreshold*float64( - st.totalBucketCapacity, - ) { - log.Warn( - "SizeTracker::StatFs : changing from size_tracker size to S3 bucket size due to overuse of bucket", - ) - blocks = stat.Blocks + // The Nx VMS evicts data until utilization is at 90% (of display capacity) + // Use a size offset to show the Nx eviction threshold at our desired utilization + // Only update the offset when bucket usage is updated + returnedBucketUsage := stat.Blocks * uint64(blockSize) + isBucketUsageUpdated := returnedBucketUsage != st.bucketUsage + if isBucketUsageUpdated { + // record the updated usage + st.bucketUsage = returnedBucketUsage + // convert everything to float64 + bucketCapacity := float64(st.totalBucketCapacity) + bucketUsage := float64(returnedBucketUsage) + displayCapacity := float64(st.displayCapacity) + serverUsage := float64(st.mountSize.GetSize()) + serverCount := float64(st.serverCount) + sizeOffset := float64(st.statSizeOffset) + nxEvictionThreshold := targetUtilization * displayCapacity + intendedCapacity := bucketCapacity / serverCount + // Use a finite state machine. The evictionMode is the state. + // calculate bucket usage and update eviction mode accordingly + st.updateState(bucketUsage / bucketCapacity) + switch st.evictionMode { + case Normal: + // the server count starts as the bucket capacity divided by the display capacity + // if the server count has been incremented, offset the tracked size + sizeOffset = nxEvictionThreshold - targetUtilization*intendedCapacity + case Overuse: + // drive to a utilization target below the bucketNormalizedThreshold + normalizationTargetFactor := bucketNormalizedThreshold - hysteresisMargin + sizeOffset = nxEvictionThreshold - normalizationTargetFactor*intendedCapacity + case Emergency: + // just report the whole bucket usage + sizeOffset = bucketUsage - serverUsage + } + st.statSizeOffset = uint64(max(0, sizeOffset)) } } + // add the offset + blocks += st.statSizeOffset / uint64(blockSize) } stat := common.Statfs_t{ @@ -303,6 +363,27 @@ func (st *SizeTracker) StatFs() (*common.Statfs_t, bool, error) { return &stat, true, nil } +func (st *SizeTracker) updateState(bucketUsageFactor float64) { + switch st.evictionMode { + case Normal: + if bucketUsageFactor > overuseThreshold { + st.evictionMode = Overuse + } + case Overuse: + if bucketUsageFactor < bucketNormalizedThreshold { + st.evictionMode = Normal + } else if bucketUsageFactor > emergencyThreshold { + st.evictionMode = Emergency + // severe overuse strongly suggests an incorrect server count + st.serverCount++ + } + case Emergency: + if bucketUsageFactor < bucketNormalizedThreshold { + st.evictionMode = Normal + } + } +} + func (st *SizeTracker) CommitData(opt internal.CommitDataOptions) error { log.Trace("SizeTracker::CopyFromFile : %s", opt.Name) var origSize int64 diff --git a/component/size_tracker/size_tracker_mock_test.go b/component/size_tracker/size_tracker_mock_test.go index a31b60035..baa325028 100644 --- a/component/size_tracker/size_tracker_mock_test.go +++ b/component/size_tracker/size_tracker_mock_test.go @@ -26,7 +26,6 @@ package size_tracker import ( "context" - "crypto/rand" "fmt" "os" "strings" @@ -36,7 +35,6 @@ import ( "github.com/Seagate/cloudfuse/common/config" "github.com/Seagate/cloudfuse/common/log" "github.com/Seagate/cloudfuse/internal" - "github.com/Seagate/cloudfuse/internal/handlemap" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -65,7 +63,10 @@ func (suite *sizeTrackerMockTestSuite) SetupTest() { if err != nil { panic(fmt.Sprintf("Unable to set silent logger as default: %v", err)) } - cfg := fmt.Sprintf("size_tracker:\n journal-name: %s", journal_test_name) + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, + ) suite.setupTestHelper(cfg) } @@ -88,52 +89,218 @@ func (suite *sizeTrackerMockTestSuite) cleanupTest() { suite.mockCtrl.Finish() } -// Tests the default configuration of attribute cache +// Tests the default configuration of size tracker func (suite *sizeTrackerMockTestSuite) TestDefault() { defer suite.cleanupTest() suite.assert.Equal("size_tracker", suite.sizeTracker.Name()) suite.assert.Equal(uint64(0), suite.sizeTracker.mountSize.GetSize()) + suite.assert.Equal(Normal, suite.sizeTracker.evictionMode) } -func (suite *sizeTrackerMockTestSuite) TestStatFSFallBackEnabledUnderThreshold() { +// Test behavior when bucket usage is below overuseThreshold (92%) +func (suite *sizeTrackerMockTestSuite) TestStateMachineNormalMode() { defer suite.cleanupTest() - suite.assert.EqualValues(0, suite.sizeTracker.mountSize.GetSize()) - suite.sizeTracker.totalBucketCapacity = 10 * 1024 * 1024 - - // Create File - file := generateFileName() - suite.mock.EXPECT(). - GetAttr(internal.GetAttrOptions{Name: file}). - Return(&internal.ObjAttr{Path: file}, nil) - suite.mock.EXPECT(). - CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644}). - Return(&handlemap.Handle{}, nil) - handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644}) + + // Bucket usage at 80% (below overuseThreshold of 92%) + bucketUsage := uint64(8 * 1024 * 1024) + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + + stat, ret, err := suite.sizeTracker.StatFs() + suite.assert.True(ret) + suite.assert.NoError(err) + suite.assert.NotNil(stat) + + // Should remain in Normal mode + suite.assert.EqualValues(0, stat.Blocks) +} + +// TestStateMachineTransitionToOveruse tests transition from Normal to Overuse mode +func (suite *sizeTrackerMockTestSuite) TestStateMachineTransitionToOveruse() { + defer suite.cleanupTest() + + // Use bucket usage at 93% (above overuseThreshold of 92%) + // With 10MB bucket capacity, 93% = 9.3MB + bucketUsage := uint64(9752371) // 9.3 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + + stat, ret, err := suite.sizeTracker.StatFs() + suite.assert.True(ret) suite.assert.NoError(err) + suite.assert.NotNil(stat) + + // Should transition to Overuse mode + suite.assert.Equal(Overuse, suite.sizeTracker.evictionMode) - // Write File - data := make([]byte, 1024*1024) - _, _ = rand.Read(data) - suite.mock.EXPECT(). - GetAttr(internal.GetAttrOptions{Name: handle.Path}). - Return(&internal.ObjAttr{Path: file}, nil) - suite.mock.EXPECT(). - WriteFile(internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data}). - Return(len(data), nil) - _, err = suite.sizeTracker.WriteFile( - internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data}, + // In Overuse mode: offset = nxEvictionThreshold - normalizationTarget * intendedCapacity + // where: + // nxEvictionThreshold = targetUtilization * displayCapacity = 0.9 * 1MB + // normalizationTarget = bucketNormalizedThreshold - hysteresisMargin = 0.88 - 0.02 = 0.86 + // intendedCapacity = bucketCapacity / serverCount = 10MB / 10 = 1MB + // offset = 0.9 * 1MB - 0.86 * 1MB = 0.04MB = 40960 bytes + // Since mountSize is 0, blocks should be offset / 4096 + expectedBlocks := uint64(40960 / 4096) + suite.assert.Equal(expectedBlocks, stat.Blocks) +} + +// TestStateMachineOveruseMode tests behavior in Overuse mode +func (suite *sizeTrackerMockTestSuite) TestStateMachineOveruseMode() { + suite.cleanupTest() + + // Setup with 1MB mount size to simulate existing usage + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, ) + suite.setupTestHelper(cfg) + defer suite.cleanupTest() + + // Add mount size to simulate usage + suite.sizeTracker.mountSize.Add(1 * 1024 * 1024) + + // First call with 93% usage to trigger transition to Overuse + transitionUsage := uint64(9752371) // 9.3 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: transitionUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + // Now test behavior in Overuse mode with 94% usage + bucketUsage := uint64(9856614) // 9.4 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + + stat, ret, err := suite.sizeTracker.StatFs() + suite.assert.True(ret) suite.assert.NoError(err) + suite.assert.NotNil(stat) + + // Should remain in Overuse mode + suite.assert.Equal(Overuse, suite.sizeTracker.evictionMode) + + // In Overuse mode, target is bucketNormalizedThreshold - hysteresisMargin = 88% - 2% = 86% + // This provides more aggressive offset to drive bucket usage down +} + +// TestStateMachineTransitionToEmergency tests transition from Overuse to Emergency mode +func (suite *sizeTrackerMockTestSuite) TestStateMachineTransitionToEmergency() { + suite.cleanupTest() + + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, + ) + suite.setupTestHelper(cfg) + defer suite.cleanupTest() + + // Add mount size to simulate usage + suite.sizeTracker.mountSize.Add(1 * 1024 * 1024) + + // First transition to Overuse mode with 93% usage + overUseUsage := uint64(9752371) // 9.3 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: overUseUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + initialServerCount := suite.sizeTracker.serverCount + + // Increase bucket usage to 98% (above emergencyThreshold of 97%) + bucketUsage := uint64(10275430) // 9.8 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) - // Flush File - suite.mock.EXPECT().FlushFile(internal.FlushFileOptions{Handle: handle}).Return(nil) - err = suite.sizeTracker.FlushFile(internal.FlushFileOptions{Handle: handle}) + stat, ret, err := suite.sizeTracker.StatFs() + suite.assert.True(ret) suite.assert.NoError(err) - suite.assert.EqualValues(len(data), suite.sizeTracker.mountSize.GetSize()) + suite.assert.NotNil(stat) + + // Should transition to Emergency mode + suite.assert.Equal(Emergency, suite.sizeTracker.evictionMode) + + // Server count should be incremented in Emergency mode + suite.assert.Equal(initialServerCount+1, suite.sizeTracker.serverCount) +} + +// TestStateMachineEmergencyMode tests behavior in Emergency mode +func (suite *sizeTrackerMockTestSuite) TestStateMachineEmergencyMode() { + suite.cleanupTest() + + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, + ) + suite.setupTestHelper(cfg) + defer suite.cleanupTest() - // Call Statfs + // Add mount size to simulate usage + suite.sizeTracker.mountSize.Add(1 * 1024 * 1024) + + // First transition to Overuse mode with 93% usage + overUseUsage := uint64(9752371) // 9.3 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: overUseUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + // Then transition to Emergency mode with 98% usage + emergencyUsage := uint64(10275430) // 9.8 MB suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ - Blocks: 9 * 1024 * 1024 / 4096, + Blocks: emergencyUsage / 4096, Bavail: 0, Bfree: 0, Bsize: 4096, @@ -142,71 +309,244 @@ func (suite *sizeTrackerMockTestSuite) TestStatFSFallBackEnabledUnderThreshold() Frsize: 4096, Namemax: 255, }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + // Bucket usage at 98% (Emergency level) + bucketUsage := uint64(10275430) // 9.8 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + stat, ret, err := suite.sizeTracker.StatFs() suite.assert.True(ret) suite.assert.NoError(err) - suite.assert.NotEqual(&common.Statfs_t{}, stat) - suite.assert.Equal(uint64(1024*1024/4096), stat.Blocks) - suite.assert.Equal(int64(4096), stat.Bsize) - suite.assert.Equal(int64(4096), stat.Frsize) - suite.assert.Equal(uint64(255), stat.Namemax) + suite.assert.NotNil(stat) + + // Should remain in Emergency mode + suite.assert.Equal(Emergency, suite.sizeTracker.evictionMode) + + // In Emergency mode, offset = bucketUsage - serverUsage + // This makes the system report the whole bucket usage + expectedOffset := bucketUsage - suite.sizeTracker.mountSize.GetSize() + expectedBlocks := (suite.sizeTracker.mountSize.GetSize() + expectedOffset) / 4096 + suite.assert.Equal(expectedBlocks, stat.Blocks) } -func (suite *sizeTrackerMockTestSuite) TestStatFSFallBackEnabledOverThreshold() { +// TestStateMachineTransitionBackToNormalFromOveruse tests Overuse -> Normal transition +func (suite *sizeTrackerMockTestSuite) TestStateMachineTransitionBackToNormalFromOveruse() { + suite.cleanupTest() + + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, + ) + suite.setupTestHelper(cfg) defer suite.cleanupTest() - suite.assert.EqualValues(0, suite.sizeTracker.mountSize.GetSize()) - suite.sizeTracker.totalBucketCapacity = 10 * 1024 * 1024 - - // Create File - file := generateFileName() - suite.mock.EXPECT(). - GetAttr(internal.GetAttrOptions{Name: file}). - Return(&internal.ObjAttr{Path: file}, nil) - suite.mock.EXPECT(). - CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644}). - Return(&handlemap.Handle{}, nil) - handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644}) + + // Add mount size to simulate usage + suite.sizeTracker.mountSize.Add(1 * 1024 * 1024) + + // First transition to Overuse mode with 93% usage + overUseUsage := uint64(9752371) // 9.3 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: overUseUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + // Decrease bucket usage to 87% (below bucketNormalizedThreshold of 88%) + bucketUsage := uint64(9122611) // 8.7 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + + stat, ret, err := suite.sizeTracker.StatFs() + suite.assert.True(ret) suite.assert.NoError(err) + suite.assert.NotNil(stat) - // Write File - data := make([]byte, 1024*1024) - _, _ = rand.Read(data) - suite.mock.EXPECT(). - GetAttr(internal.GetAttrOptions{Name: handle.Path}). - Return(&internal.ObjAttr{Path: file}, nil) - suite.mock.EXPECT(). - WriteFile(internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data}). - Return(len(data), nil) - _, err = suite.sizeTracker.WriteFile( - internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data}, + // Should transition back to Normal mode + suite.assert.Equal(Normal, suite.sizeTracker.evictionMode) +} + +// TestStateMachineTransitionBackToNormalFromEmergency tests Emergency -> Normal transition +func (suite *sizeTrackerMockTestSuite) TestStateMachineTransitionBackToNormalFromEmergency() { + suite.cleanupTest() + + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, ) + suite.setupTestHelper(cfg) + defer suite.cleanupTest() + + // Add mount size to simulate usage + suite.sizeTracker.mountSize.Add(1 * 1024 * 1024) + + // First transition to Overuse mode with 93% usage + overUseUsage := uint64(9752371) // 9.3 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: overUseUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + // Then transition to Emergency mode with 98% usage + emergencyUsage := uint64(10275430) // 9.8 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: emergencyUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, _ = suite.sizeTracker.StatFs() + + // Decrease bucket usage to 85% (below bucketNormalizedThreshold of 88%) + bucketUsage := uint64(8912896) // 8.5 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bavail: 0, + Bfree: 0, + Bsize: 4096, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + + stat, ret, err := suite.sizeTracker.StatFs() + suite.assert.True(ret) suite.assert.NoError(err) + suite.assert.NotNil(stat) + + // Should transition back to Normal mode (Emergency can skip Overuse on way down) + suite.assert.Equal(Normal, suite.sizeTracker.evictionMode) +} + +// TestStateMachineHysteresis tests that hysteresis prevents rapid state changes +func (suite *sizeTrackerMockTestSuite) TestStateMachineHysteresis() { + suite.cleanupTest() + + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 10", + journal_test_name, + ) + suite.setupTestHelper(cfg) + defer suite.cleanupTest() + + // Add mount size to simulate usage + suite.sizeTracker.mountSize.Add(1 * 1024 * 1024) - // Flush File - suite.mock.EXPECT().FlushFile(internal.FlushFileOptions{Handle: handle}).Return(nil) - err = suite.sizeTracker.FlushFile(internal.FlushFileOptions{Handle: handle}) + // Start in Normal mode at 91% (below 92% overuseThreshold) + suite.assert.Equal(Normal, suite.sizeTracker.evictionMode) + bucketUsage := uint64(9542042) // 9.1 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, + Bsize: 4096, + Bavail: 0, + Bfree: 0, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, err := suite.sizeTracker.StatFs() suite.assert.NoError(err) - suite.assert.EqualValues(len(data), suite.sizeTracker.mountSize.GetSize()) + suite.assert.Equal(Normal, suite.sizeTracker.evictionMode) - // Call Statfs + // Transition to Overuse at 93% + bucketUsage = uint64(9752371) // 9.3 MB suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ - Blocks: 10 * 1024 * 1024 / 4096, + Blocks: bucketUsage / 4096, + Bsize: 4096, Bavail: 0, Bfree: 0, + Ffree: 1e9, + Files: 1e9, + Frsize: 4096, + Namemax: 255, + }, true, nil) + _, _, err = suite.sizeTracker.StatFs() + suite.assert.NoError(err) + suite.assert.Equal(Overuse, suite.sizeTracker.evictionMode) + + // Stay at 91% - should remain in Overuse due to hysteresis + // (needs to go below 88% bucketNormalizedThreshold to return to Normal) + bucketUsage = uint64(9542042) // 9.1 MB + suite.mock.EXPECT().StatFs().Return(&common.Statfs_t{ + Blocks: bucketUsage / 4096, Bsize: 4096, + Bavail: 0, + Bfree: 0, Ffree: 1e9, Files: 1e9, Frsize: 4096, Namemax: 255, }, true, nil) + _, _, err = suite.sizeTracker.StatFs() + suite.assert.NoError(err) + suite.assert.Equal( + Overuse, + suite.sizeTracker.evictionMode, + "Hysteresis should prevent immediate transition back", + ) +} + +// TestStatFsWithoutBucketCapacity tests that StatFs works when bucket capacity is not configured +func (suite *sizeTrackerMockTestSuite) TestStatFsWithoutBucketCapacity() { + suite.cleanupTest() + + // Setup without bucket-capacity-fallback + cfg := fmt.Sprintf( + "libfuse:\n display-capacity-mb: 1\nsize_tracker:\n journal-name: %s", + journal_test_name, + ) + suite.setupTestHelper(cfg) + defer suite.cleanupTest() + + // Add mount size + suite.sizeTracker.mountSize.Add(5 * 1024 * 1024) + stat, ret, err := suite.sizeTracker.StatFs() suite.assert.True(ret) suite.assert.NoError(err) - suite.assert.NotEqual(&common.Statfs_t{}, stat) - suite.assert.Equal(uint64(10*1024*1024/4096), stat.Blocks) - suite.assert.Equal(int64(4096), stat.Bsize) - suite.assert.Equal(int64(4096), stat.Frsize) - suite.assert.Equal(uint64(255), stat.Namemax) + suite.assert.NotNil(stat) + + // Should just report mount size without any offset + expectedBlocks := (5 * 1024 * 1024) / 4096 + suite.assert.Equal(uint64(expectedBlocks), stat.Blocks) + suite.assert.Equal(uint64(0), stat.Bavail) + suite.assert.Equal(uint64(0), stat.Bfree) } // In order for 'go test' to run this suite, we need to create