ceremonyclient/pebble/vfs/errorfs/errorfs.go

519 lines
13 KiB
Go
Raw Permalink Normal View History

2024-01-03 07:31:42 +00:00
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package errorfs
import (
"fmt"
"io"
"os"
"strings"
"sync/atomic"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/internal/dsl"
"github.com/cockroachdb/pebble/vfs"
)
// ErrInjected is an error artificially injected for testing fs error paths.
var ErrInjected = LabelledError{
error: errors.New("injected error"),
Label: "ErrInjected",
}
// Op describes a filesystem operation.
type Op struct {
// Kind describes the particular kind of operation being performed.
Kind OpKind
// Path is the path of the file of the file being operated on.
Path string
// Offset is the offset of an operation. It's set for OpFileReadAt and
// OpFileWriteAt operations.
Offset int64
}
// OpKind is an enum describing the type of operation.
type OpKind int
const (
// OpCreate describes a create file operation.
OpCreate OpKind = iota
// OpLink describes a hardlink operation.
OpLink
// OpOpen describes a file open operation.
OpOpen
// OpOpenDir describes a directory open operation.
OpOpenDir
// OpRemove describes a remove file operation.
OpRemove
// OpRemoveAll describes a recursive remove operation.
OpRemoveAll
// OpRename describes a rename operation.
OpRename
// OpReuseForWrite describes a reuse for write operation.
OpReuseForWrite
// OpMkdirAll describes a make directory including parents operation.
OpMkdirAll
// OpLock describes a lock file operation.
OpLock
// OpList describes a list directory operation.
OpList
// OpFilePreallocate describes a file preallocate operation.
OpFilePreallocate
// OpStat describes a path-based stat operation.
OpStat
// OpGetDiskUsage describes a disk usage operation.
OpGetDiskUsage
// OpFileClose describes a close file operation.
OpFileClose
// OpFileRead describes a file read operation.
OpFileRead
// OpFileReadAt describes a file seek read operation.
OpFileReadAt
// OpFileWrite describes a file write operation.
OpFileWrite
// OpFileWriteAt describes a file seek write operation.
OpFileWriteAt
// OpFileStat describes a file stat operation.
OpFileStat
// OpFileSync describes a file sync operation.
OpFileSync
// OpFileFlush describes a file flush operation.
OpFileFlush
)
// ReadOrWrite returns the operation's kind.
func (o OpKind) ReadOrWrite() OpReadWrite {
switch o {
case OpOpen, OpOpenDir, OpList, OpStat, OpGetDiskUsage, OpFileRead, OpFileReadAt, OpFileStat:
return OpIsRead
case OpCreate, OpLink, OpRemove, OpRemoveAll, OpRename, OpReuseForWrite, OpMkdirAll, OpLock, OpFileClose, OpFileWrite, OpFileWriteAt, OpFileSync, OpFileFlush, OpFilePreallocate:
return OpIsWrite
default:
panic(fmt.Sprintf("unrecognized op %v\n", o))
}
}
// OpReadWrite is an enum describing whether an operation is a read or write
// operation.
type OpReadWrite int
const (
// OpIsRead describes read operations.
OpIsRead OpReadWrite = iota
// OpIsWrite describes write operations.
OpIsWrite
)
// String implements fmt.Stringer.
func (kind OpReadWrite) String() string {
switch kind {
case OpIsRead:
return "Reads"
case OpIsWrite:
return "Writes"
default:
panic(fmt.Sprintf("unrecognized OpKind %d", kind))
}
}
// OnIndex is a convenience function for constructing a dsl.OnIndex for use with
// an error-injecting filesystem.
func OnIndex(index int32) *InjectIndex {
return &InjectIndex{dsl.OnIndex[Op](index)}
}
// InjectIndex implements Injector, injecting an error at a specific index.
type InjectIndex struct {
*dsl.Index[Op]
}
// MaybeError implements the Injector interface.
//
// TODO(jackson): Remove this implementation and update callers to compose it
// with other injectors.
func (ii *InjectIndex) MaybeError(op Op) error {
if !ii.Evaluate(op) {
return nil
}
return ErrInjected
}
// InjectorFunc implements the Injector interface for a function with
// MaybeError's signature.
type InjectorFunc func(Op) error
// String implements fmt.Stringer.
func (f InjectorFunc) String() string { return "<opaque func>" }
// MaybeError implements the Injector interface.
func (f InjectorFunc) MaybeError(op Op) error { return f(op) }
// Injector injects errors into FS operations.
type Injector interface {
fmt.Stringer
// MaybeError is invoked by an errorfs before an operation is executed. It
// is passed an enum indicating the type of operation and a path of the
// subject file or directory. If the operation takes two paths (eg,
// Rename, Link), the original source path is provided.
MaybeError(op Op) error
}
// Any returns an injector that injects an error if any of the provided
// injectors inject an error. The error returned by the first injector to return
// an error is used.
func Any(injectors ...Injector) Injector {
return anyInjector(injectors)
}
type anyInjector []Injector
func (a anyInjector) String() string {
var sb strings.Builder
sb.WriteString("(Any")
for _, inj := range a {
sb.WriteString(" ")
sb.WriteString(inj.String())
}
sb.WriteString(")")
return sb.String()
}
func (a anyInjector) MaybeError(op Op) error {
for _, inj := range a {
if err := inj.MaybeError(op); err != nil {
return err
}
}
return nil
}
// Counter wraps an Injector, counting the number of errors injected. It may be
// used in tests to ensure that when an error is injected, the error is
// surfaced through the user interface.
type Counter struct {
Injector
atomic.Uint64
}
// String implements fmt.Stringer.
func (c *Counter) String() string {
return c.Injector.String()
}
// MaybeError implements Injector.
func (c *Counter) MaybeError(op Op) error {
err := c.Injector.MaybeError(op)
if err != nil {
c.Uint64.Add(1)
}
return err
}
// Toggle wraps an Injector. By default, Toggle injects nothing. When toggled on
// through its On method, it begins injecting errors when the contained injector
// injects them. It may be returned to its original state through Off.
type Toggle struct {
Injector
on atomic.Bool
}
// String implements fmt.Stringer.
func (t *Toggle) String() string {
return t.Injector.String()
}
// MaybeError implements Injector.
func (t *Toggle) MaybeError(op Op) error {
if !t.on.Load() {
return nil
}
return t.Injector.MaybeError(op)
}
// On enables error injection.
func (t *Toggle) On() { t.on.Store(true) }
// Off disables error injection.
func (t *Toggle) Off() { t.on.Store(false) }
// FS implements vfs.FS, injecting errors into
// its operations.
type FS struct {
fs vfs.FS
inj Injector
}
// Wrap wraps an existing vfs.FS implementation, returning a new
// vfs.FS implementation that shadows operations to the provided FS.
// It uses the provided Injector for deciding when to inject errors.
// If an error is injected, FS propagates the error instead of
// shadowing the operation.
func Wrap(fs vfs.FS, inj Injector) *FS {
return &FS{
fs: fs,
inj: inj,
}
}
// WrapFile wraps an existing vfs.File, returning a new vfs.File that shadows
// operations to the provided vfs.File. It uses the provided Injector for
// deciding when to inject errors. If an error is injected, the file
// propagates the error instead of shadowing the operation.
func WrapFile(f vfs.File, inj Injector) vfs.File {
return &errorFile{file: f, inj: inj}
}
// Unwrap returns the FS implementation underlying fs.
// See pebble/vfs.Root.
func (fs *FS) Unwrap() vfs.FS {
return fs.fs
}
// Create implements FS.Create.
func (fs *FS) Create(name string) (vfs.File, error) {
if err := fs.inj.MaybeError(Op{Kind: OpCreate, Path: name}); err != nil {
return nil, err
}
f, err := fs.fs.Create(name)
if err != nil {
return nil, err
}
return &errorFile{name, f, fs.inj}, nil
}
// Link implements FS.Link.
func (fs *FS) Link(oldname, newname string) error {
if err := fs.inj.MaybeError(Op{Kind: OpLink, Path: oldname}); err != nil {
return err
}
return fs.fs.Link(oldname, newname)
}
// Open implements FS.Open.
func (fs *FS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) {
if err := fs.inj.MaybeError(Op{Kind: OpOpen, Path: name}); err != nil {
return nil, err
}
f, err := fs.fs.Open(name)
if err != nil {
return nil, err
}
ef := &errorFile{name, f, fs.inj}
for _, opt := range opts {
opt.Apply(ef)
}
return ef, nil
}
// OpenReadWrite implements FS.OpenReadWrite.
func (fs *FS) OpenReadWrite(name string, opts ...vfs.OpenOption) (vfs.File, error) {
if err := fs.inj.MaybeError(Op{Kind: OpOpen, Path: name}); err != nil {
return nil, err
}
f, err := fs.fs.OpenReadWrite(name)
if err != nil {
return nil, err
}
ef := &errorFile{name, f, fs.inj}
for _, opt := range opts {
opt.Apply(ef)
}
return ef, nil
}
// OpenDir implements FS.OpenDir.
func (fs *FS) OpenDir(name string) (vfs.File, error) {
if err := fs.inj.MaybeError(Op{Kind: OpOpenDir, Path: name}); err != nil {
return nil, err
}
f, err := fs.fs.OpenDir(name)
if err != nil {
return nil, err
}
return &errorFile{name, f, fs.inj}, nil
}
// GetDiskUsage implements FS.GetDiskUsage.
func (fs *FS) GetDiskUsage(path string) (vfs.DiskUsage, error) {
if err := fs.inj.MaybeError(Op{Kind: OpGetDiskUsage, Path: path}); err != nil {
return vfs.DiskUsage{}, err
}
return fs.fs.GetDiskUsage(path)
}
// PathBase implements FS.PathBase.
func (fs *FS) PathBase(p string) string {
return fs.fs.PathBase(p)
}
// PathDir implements FS.PathDir.
func (fs *FS) PathDir(p string) string {
return fs.fs.PathDir(p)
}
// PathJoin implements FS.PathJoin.
func (fs *FS) PathJoin(elem ...string) string {
return fs.fs.PathJoin(elem...)
}
// Remove implements FS.Remove.
func (fs *FS) Remove(name string) error {
if _, err := fs.fs.Stat(name); oserror.IsNotExist(err) {
return nil
}
if err := fs.inj.MaybeError(Op{Kind: OpRemove, Path: name}); err != nil {
return err
}
return fs.fs.Remove(name)
}
// RemoveAll implements FS.RemoveAll.
func (fs *FS) RemoveAll(fullname string) error {
if err := fs.inj.MaybeError(Op{Kind: OpRemoveAll, Path: fullname}); err != nil {
return err
}
return fs.fs.RemoveAll(fullname)
}
// Rename implements FS.Rename.
func (fs *FS) Rename(oldname, newname string) error {
if err := fs.inj.MaybeError(Op{Kind: OpRename, Path: oldname}); err != nil {
return err
}
return fs.fs.Rename(oldname, newname)
}
// ReuseForWrite implements FS.ReuseForWrite.
func (fs *FS) ReuseForWrite(oldname, newname string) (vfs.File, error) {
if err := fs.inj.MaybeError(Op{Kind: OpReuseForWrite, Path: oldname}); err != nil {
return nil, err
}
return fs.fs.ReuseForWrite(oldname, newname)
}
// MkdirAll implements FS.MkdirAll.
func (fs *FS) MkdirAll(dir string, perm os.FileMode) error {
if err := fs.inj.MaybeError(Op{Kind: OpMkdirAll, Path: dir}); err != nil {
return err
}
return fs.fs.MkdirAll(dir, perm)
}
// Lock implements FS.Lock.
func (fs *FS) Lock(name string) (io.Closer, error) {
if err := fs.inj.MaybeError(Op{Kind: OpLock, Path: name}); err != nil {
return nil, err
}
return fs.fs.Lock(name)
}
// List implements FS.List.
func (fs *FS) List(dir string) ([]string, error) {
if err := fs.inj.MaybeError(Op{Kind: OpList, Path: dir}); err != nil {
return nil, err
}
return fs.fs.List(dir)
}
// Stat implements FS.Stat.
func (fs *FS) Stat(name string) (os.FileInfo, error) {
if err := fs.inj.MaybeError(Op{Kind: OpStat, Path: name}); err != nil {
return nil, err
}
return fs.fs.Stat(name)
}
// errorFile implements vfs.File. The interface is implemented on the pointer
// type to allow pointer equality comparisons.
type errorFile struct {
path string
file vfs.File
inj Injector
}
func (f *errorFile) Close() error {
// We don't inject errors during close as those calls should never fail in
// practice.
return f.file.Close()
}
func (f *errorFile) Read(p []byte) (int, error) {
if err := f.inj.MaybeError(Op{Kind: OpFileRead, Path: f.path}); err != nil {
return 0, err
}
return f.file.Read(p)
}
func (f *errorFile) ReadAt(p []byte, off int64) (int, error) {
if err := f.inj.MaybeError(Op{
Kind: OpFileReadAt,
Path: f.path,
Offset: off,
}); err != nil {
return 0, err
}
return f.file.ReadAt(p, off)
}
func (f *errorFile) Write(p []byte) (int, error) {
if err := f.inj.MaybeError(Op{Kind: OpFileWrite, Path: f.path}); err != nil {
return 0, err
}
return f.file.Write(p)
}
func (f *errorFile) WriteAt(p []byte, off int64) (int, error) {
if err := f.inj.MaybeError(Op{
Kind: OpFileWriteAt,
Path: f.path,
Offset: off,
}); err != nil {
return 0, err
}
return f.file.WriteAt(p, off)
}
func (f *errorFile) Stat() (os.FileInfo, error) {
if err := f.inj.MaybeError(Op{Kind: OpFileStat, Path: f.path}); err != nil {
return nil, err
}
return f.file.Stat()
}
func (f *errorFile) Prefetch(offset, length int64) error {
// TODO(radu): Consider error injection.
return f.file.Prefetch(offset, length)
}
func (f *errorFile) Preallocate(offset, length int64) error {
if err := f.inj.MaybeError(Op{Kind: OpFilePreallocate, Path: f.path}); err != nil {
return err
}
return f.file.Preallocate(offset, length)
}
func (f *errorFile) Sync() error {
if err := f.inj.MaybeError(Op{Kind: OpFileSync, Path: f.path}); err != nil {
return err
}
return f.file.Sync()
}
func (f *errorFile) SyncData() error {
// TODO(jackson): Consider error injection.
return f.file.SyncData()
}
func (f *errorFile) SyncTo(length int64) (fullSync bool, err error) {
// TODO(jackson): Consider error injection.
return f.file.SyncTo(length)
}
func (f *errorFile) Fd() uintptr {
return f.file.Fd()
}