Skip to content

Commit

Permalink
test(bsr/fstest): Add new FS for testing out of space errors (#3305)
Browse files Browse the repository at this point in the history
This provides a new storage.FS implementation that expands the on
fstest.MemFS by allowing tests to mark the filesystem as out of space.
When this happens any write/create operation will start returning an
error indicating that the filesystem is out of space.
  • Loading branch information
tmessi authored Jun 9, 2023
1 parent f7823d3 commit 837734b
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 0 deletions.
79 changes: 79 additions & 0 deletions internal/bsr/internal/fstest/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
"github.com/hashicorp/boundary/internal/storage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -144,6 +145,10 @@ func TestFSOpen(t *testing.T) {
return fstest.NewLocalFS(ctx, d)
},
},
{
"LimitedSpaceFS",
func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() },
},
}
cases := []struct {
name string
Expand Down Expand Up @@ -210,6 +215,10 @@ func TestContainerOpenFile(t *testing.T) {
return fstest.NewLocalFS(ctx, d)
},
},
{
"LimitedSpaceFS",
func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() },
},
}

cases := []struct {
Expand Down Expand Up @@ -320,6 +329,10 @@ func TestContainerCreate(t *testing.T) {
return fstest.NewLocalFS(ctx, d)
},
},
{
"LimitedSpaceFS",
func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() },
},
}

cases := []struct {
Expand Down Expand Up @@ -392,6 +405,10 @@ func TestMemContainerSubContainer(t *testing.T) {
return fstest.NewLocalFS(ctx, d)
},
},
{
"LimitedSpaceFS",
func(t *testing.T) storage.FS { return fstest.NewLimitedSpaceFS() },
},
}
cases := []struct {
name string
Expand Down Expand Up @@ -478,3 +495,65 @@ func TestMemContainerSubContainer(t *testing.T) {
})
}
}

func TestOutOfSpace(t *testing.T) {
ctx := context.Background()

line := []byte("foo")

fs := fstest.NewLimitedSpaceFS()

// Create some containers, subcontains and files.
r, err := fs.New(ctx, "root")
require.NoError(t, err)

sub, err := r.SubContainer(ctx, "sub", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
require.NoError(t, err)

f1, err := r.Create(ctx, "f1")
require.NoError(t, err)
c, err := f1.Write(line)
require.NoError(t, err)
assert.Equal(t, c, len(line))

f2, err := sub.Create(ctx, "f2")
require.NoError(t, err)
c, err = f2.Write(line)
require.NoError(t, err)
assert.Equal(t, c, len(line))

// Now mark out of space
fs.SetOutOfSpace(true)

// eixsing files can no longer write
c, err = f1.Write(line)
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Equal(t, 0, c)

c, err = f2.Write(line)
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Equal(t, 0, c)

// existing containers cannot create new files
f3, err := r.Create(ctx, "f3")
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Nil(t, f3)

f4, err := sub.Create(ctx, "f4")
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Nil(t, f4)

// existing containers cannot create sub containers
sub2, err := r.SubContainer(ctx, "sub2", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Nil(t, sub2)

sub3, err := sub.SubContainer(ctx, "sub3", storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite))
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Nil(t, sub3)

// fs cannot make a new container
r2, err := fs.New(ctx, "root2")
require.ErrorIs(t, err, fstest.ErrOutOfSpace)
assert.Nil(t, r2)
}
153 changes: 153 additions & 0 deletions internal/bsr/internal/fstest/space.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// Package fstest provides test implementations of the fs interfaces.
package fstest

import (
"context"
"errors"
"fmt"
sfs "io/fs"
"sync"

"github.com/hashicorp/boundary/internal/storage"
)

// ErrOutOfSpace is used when the FS is out of space.
var ErrOutOfSpace = errors.New("no space left on device")

// LimitedSpaceFS is a test FS that can simulate running out of disk space.
type LimitedSpaceFS struct {
*MemFS

outOfSpace bool
sync.RWMutex
}

// SetOutOfSpace is a helper to mark the file system as being out of space.
func (l *LimitedSpaceFS) SetOutOfSpace(b bool) {
l.Lock()
defer l.Unlock()
l.outOfSpace = b
}

// OutOfSpace is used to check if the filesystem is out of space in a concurrent safe way.
func (l *LimitedSpaceFS) OutOfSpace() bool {
l.RLock()
defer l.RUnlock()
return l.outOfSpace
}

// NewLimitedSpaceFS creates a LimitedSpaceFS. It supports WithNewFunc, WithReadOnly.
func NewLimitedSpaceFS(options ...Option) *LimitedSpaceFS {
return &LimitedSpaceFS{
MemFS: NewMemFS(options...),
}
}

// New creates a storage.Container in the LimitedSpaceFS.
func (l *LimitedSpaceFS) New(ctx context.Context, n string) (storage.Container, error) {
if l.OutOfSpace() {
return nil, ErrOutOfSpace
}
c, err := l.MemFS.New(ctx, n)
if err != nil {
return nil, err
}

cc := c.(*MemContainer)
return &LimitedSpaceContainer{
MemContainer: cc,
fs: l,
}, nil
}

// Open opens an existing a storage.Container from the LimitedSpaceFS.
func (l *LimitedSpaceFS) Open(ctx context.Context, n string) (storage.Container, error) {
return l.MemFS.Open(ctx, n)
}

// LimitedSpaceContainer is a storage.Container that resides in memory.
type LimitedSpaceContainer struct {
*MemContainer

fs *LimitedSpaceFS
}

// Close closes the container.
func (l *LimitedSpaceContainer) Close() error {
return l.MemContainer.Close()
}

// Create makes a new storage.File in the container.
func (l *LimitedSpaceContainer) Create(ctx context.Context, n string) (storage.File, error) {
if l.fs.OutOfSpace() {
return nil, ErrOutOfSpace
}
f, err := l.MemContainer.Create(ctx, n)
if err != nil {
return nil, err
}
ff := f.(*MemFile)
return &LimitedSpaceFile{
MemFile: ff,
fs: l.fs,
}, nil
}

// OpenFile creates a storage.File in the container using the provided options
// It supports WithCloseSyncMode.
func (l *LimitedSpaceContainer) OpenFile(ctx context.Context, n string, option ...storage.Option) (storage.File, error) {
return l.MemContainer.OpenFile(ctx, n, option...)
}

// SubContainer creates a new storage.Container in the container.
func (l *LimitedSpaceContainer) SubContainer(ctx context.Context, n string, option ...storage.Option) (storage.Container, error) {
if l.fs.OutOfSpace() {
return nil, ErrOutOfSpace
}
c, err := l.MemContainer.SubContainer(ctx, n, option...)
if err != nil {
return nil, err
}

cc := c.(*MemContainer)
return &LimitedSpaceContainer{
MemContainer: cc,
fs: l.fs,
}, nil
}

// LimitedSpaceFile is a storage.File that resides in memory.
type LimitedSpaceFile struct {
*MemFile

fs *LimitedSpaceFS
}

// Stat returns the FileInfo for the file.
func (l *LimitedSpaceFile) Stat() (sfs.FileInfo, error) {
return l.MemFile.Stat()
}

func (l *LimitedSpaceFile) Read(p []byte) (int, error) {
return l.MemFile.Read(p)
}

// Close closes the file.
func (l *LimitedSpaceFile) Close() error {
return l.MemFile.Close()
}

// WriteString implements io.StringWriter.
func (l *LimitedSpaceFile) WriteString(s string) (n int, err error) {
return l.Write([]byte(s))
}

func (l *LimitedSpaceFile) Write(p []byte) (n int, err error) {
if l.fs.OutOfSpace() {
return 0, fmt.Errorf("%s %w", l.MemFile.name, ErrOutOfSpace)
}
return l.MemFile.Write(p)
}

0 comments on commit 837734b

Please sign in to comment.