Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions component/size_tracker/size_tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"context"
"fmt"
"os"
"syscall"

"github.com/Seagate/cloudfuse/common"
"github.com/Seagate/cloudfuse/common/config"
Expand All @@ -48,6 +49,7 @@ type SizeTracker struct {
evictionMode EvictionMode
bucketUsage uint64
statSizeOffset uint64
hardLimitEnabled bool
}

type EvictionMode int
Expand All @@ -61,6 +63,7 @@ const (
type SizeTrackerOptions struct {
JournalName string `config:"journal-name" yaml:"journal-name,omitempty"`
TotalBucketCapacity uint64 `config:"bucket-capacity-fallback" yaml:"bucket-capacity-fallback,omitempty"`
HardLimit bool `config:"hard-limit" yaml:"hard-limit,omitempty"`
}

const compName = "size_tracker"
Expand Down Expand Up @@ -137,6 +140,13 @@ func (st *SizeTracker) Configure(_ bool) error {
st.serverCount = (st.totalBucketCapacity + st.displayCapacity/2) / st.displayCapacity
}

st.hardLimitEnabled = conf.HardLimit
if st.hardLimitEnabled && st.totalBucketCapacity == 0 {
log.Warn(
"SizeTracker::Configure : hard-limit enabled but bucket-capacity-fallback is not set",
)
}

journalName := defaultJournalName
if config.IsSet(compName + ".journal-name") {
journalName = conf.JournalName
Expand Down Expand Up @@ -218,6 +228,21 @@ func (st *SizeTracker) RenameFile(options internal.RenameFileOptions) error {
return err
}

func (st *SizeTracker) checkCapacityDelta(delta int64) error {
if !st.hardLimitEnabled || st.totalBucketCapacity == 0 || delta <= 0 {
return nil
}
current := st.mountSize.GetSize()
if current >= st.totalBucketCapacity {
return syscall.ENOSPC
}
remaining := st.totalBucketCapacity - current
if uint64(delta) > remaining {
return syscall.ENOSPC
}
return nil
}

func (st *SizeTracker) WriteFile(options *internal.WriteFileOptions) (int, error) {
var oldSize int64
attr, getAttrErr1 := st.NextComponent().
Expand All @@ -232,14 +257,15 @@ func (st *SizeTracker) WriteFile(options *internal.WriteFileOptions) (int, error
)
}

newSize := max(oldSize, options.Offset+int64(len(options.Data)))
diff := newSize - oldSize
if err := st.checkCapacityDelta(diff); err != nil {
return 0, err
}
bytesWritten, err := st.NextComponent().WriteFile(options)
if err != nil {
return bytesWritten, err
}
newSize := max(oldSize, options.Offset+int64(len(options.Data)))

diff := newSize - oldSize

// File already exists and WriteFile succeeded subtract difference in file size
st.mountSize.Add(diff)

Expand All @@ -260,6 +286,10 @@ func (st *SizeTracker) TruncateFile(options internal.TruncateFileOptions) error
)
}

if err := st.checkCapacityDelta(options.NewSize - origSize); err != nil {
return err
}

err := st.NextComponent().TruncateFile(options)
if err != nil {
return err
Expand All @@ -278,15 +308,20 @@ func (st *SizeTracker) CopyFromFile(options internal.CopyFromFileOptions) error
origSize = attr.Size
}

fileInfo, statErr := options.File.Stat()
if statErr == nil {
if err := st.checkCapacityDelta(fileInfo.Size() - origSize); err != nil {
return err
}
}

err = st.NextComponent().CopyFromFile(options)
if err != nil {
return err
}
fileInfo, err := options.File.Stat()
if err != nil {
return nil
if statErr == nil {
st.mountSize.Add(fileInfo.Size() - origSize)
}
st.mountSize.Add(fileInfo.Size() - origSize)
return nil
}

Expand Down
214 changes: 214 additions & 0 deletions component/size_tracker/size_tracker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"runtime"
"strconv"
"strings"
"syscall"
"testing"

"github.com/Seagate/cloudfuse/common"
Expand Down Expand Up @@ -306,6 +307,219 @@ func (suite *sizeTrackerTestSuite) TestWriteFile() {
suite.assert.NoError(err)
}

func (suite *sizeTrackerTestSuite) TestWriteFileHardLimit() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

file := generateFileName()
handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644})
suite.assert.NoError(err)

data := make([]byte, MB+1)
_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data},
)
suite.assert.EqualValues(syscall.ENOSPC, err)
suite.assert.EqualValues(0, suite.sizeTracker.mountSize.GetSize())
}

func (suite *sizeTrackerTestSuite) TestWriteFileHardLimitWithinCapacity() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

file := generateFileName()
handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644})
suite.assert.NoError(err)

data := make([]byte, MB/2)
_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data},
)
suite.assert.NoError(err)
suite.assert.EqualValues(len(data), suite.sizeTracker.mountSize.GetSize())

err = suite.sizeTracker.ReleaseFile(internal.ReleaseFileOptions{Handle: handle})
suite.assert.NoError(err)

err = suite.sizeTracker.DeleteFile(internal.DeleteFileOptions{Name: file})
suite.assert.NoError(err)
}

func (suite *sizeTrackerTestSuite) TestWriteFileHardLimitExactCapacity() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

file := generateFileName()
handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644})
suite.assert.NoError(err)

data := make([]byte, MB-1)
_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data},
)
suite.assert.NoError(err)

_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: int64(len(data)), Data: []byte{0}},
)
suite.assert.NoError(err)
suite.assert.EqualValues(MB, suite.sizeTracker.mountSize.GetSize())

err = suite.sizeTracker.ReleaseFile(internal.ReleaseFileOptions{Handle: handle})
suite.assert.NoError(err)
}

func (suite *sizeTrackerTestSuite) TestWriteFileHardLimitExceedsByOne() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

file := generateFileName()
handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644})
suite.assert.NoError(err)

data := make([]byte, MB)
_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data},
)
suite.assert.NoError(err)
suite.assert.EqualValues(MB, suite.sizeTracker.mountSize.GetSize())

_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: int64(len(data)), Data: []byte{0}},
)
suite.assert.EqualValues(syscall.ENOSPC, err)
suite.assert.EqualValues(MB, suite.sizeTracker.mountSize.GetSize())

err = suite.sizeTracker.ReleaseFile(internal.ReleaseFileOptions{Handle: handle})
suite.assert.NoError(err)
}

func (suite *sizeTrackerTestSuite) TestTruncateFileHardLimitExactCapacity() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

file := generateFileName()
handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644})
suite.assert.NoError(err)

data := make([]byte, MB/2)
_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data},
)
suite.assert.NoError(err)

err = suite.sizeTracker.ReleaseFile(internal.ReleaseFileOptions{Handle: handle})
suite.assert.NoError(err)

err = suite.sizeTracker.TruncateFile(
internal.TruncateFileOptions{Name: file, NewSize: int64(MB)},
)
suite.assert.NoError(err)
suite.assert.EqualValues(MB, suite.sizeTracker.mountSize.GetSize())
}

func (suite *sizeTrackerTestSuite) TestTruncateFileHardLimit() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

file := generateFileName()
handle, err := suite.sizeTracker.CreateFile(internal.CreateFileOptions{Name: file, Mode: 0644})
suite.assert.NoError(err)

data := make([]byte, MB/2)
_, err = suite.sizeTracker.WriteFile(
&internal.WriteFileOptions{Handle: handle, Offset: 0, Data: data},
)
suite.assert.NoError(err)
suite.assert.EqualValues(len(data), suite.sizeTracker.mountSize.GetSize())

err = suite.sizeTracker.ReleaseFile(internal.ReleaseFileOptions{Handle: handle})
suite.assert.NoError(err)

err = suite.sizeTracker.TruncateFile(
internal.TruncateFileOptions{Name: file, NewSize: int64(2 * MB)},
)
suite.assert.EqualValues(syscall.ENOSPC, err)
suite.assert.EqualValues(len(data), suite.sizeTracker.mountSize.GetSize())
}

func (suite *sizeTrackerTestSuite) TestCopyFromFileHardLimit() {
suite.cleanupTest()

suite.loopback_storage_path = getFakeStoragePath("loopback")
cfg := fmt.Sprintf(
"loopbackfs:\n path: %s\n\nsize_tracker:\n journal-name: %s\n bucket-capacity-fallback: 1\n hard-limit: true",
suite.loopback_storage_path,
journal_test_name,
)
suite.setupTestHelper(cfg)
defer suite.cleanupTest()

localFile, err := os.CreateTemp("", "size-tracker-copy-*")
suite.assert.NoError(err)
defer os.Remove(localFile.Name())

truncateErr := localFile.Truncate(int64(2 * MB))
suite.assert.NoError(truncateErr)
_, seekErr := localFile.Seek(0, 0)
suite.assert.NoError(seekErr)

file := generateFileName()
err = suite.sizeTracker.CopyFromFile(
internal.CopyFromFileOptions{Name: file, File: localFile},
)
suite.assert.EqualValues(syscall.ENOSPC, err)
suite.assert.EqualValues(0, suite.sizeTracker.mountSize.GetSize())
}

func (suite *sizeTrackerTestSuite) TestWriteFileMultiple() {
defer suite.cleanupTest()
suite.assert.EqualValues(0, suite.sizeTracker.mountSize.GetSize())
Expand Down
1 change: 1 addition & 0 deletions setup/advancedConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ attr_cache:
size_tracker:
journal-name: <custom name for the size journal file. Default - generated from container/bucket name>
bucket-capacity-fallback: <total bucket capacity in MB for StatFs calculations. Used for display and eviction mode management. Default - not set>
hard-limit: true|false <enforce bucket capacity; writes beyond capacity return ENOSPC. Default - false>

# Loopback configuration
loopbackfs:
Expand Down
Loading