github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/vfs/errorfs/dsl.go (about)

     1  // Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package errorfs
     6  
     7  import (
     8  	"encoding/binary"
     9  	"fmt"
    10  	"go/token"
    11  	"hash/maphash"
    12  	"math/rand"
    13  	"path/filepath"
    14  	"strconv"
    15  	"sync"
    16  
    17  	"github.com/cockroachdb/errors"
    18  	"github.com/cockroachdb/pebble/internal/dsl"
    19  )
    20  
    21  // Predicate encodes conditional logic that determines whether to inject an
    22  // error.
    23  type Predicate = dsl.Predicate[Op]
    24  
    25  // PathMatch returns a predicate that returns true if an operation's file path
    26  // matches the provided pattern according to filepath.Match.
    27  func PathMatch(pattern string) Predicate {
    28  	return &pathMatch{pattern: pattern}
    29  }
    30  
    31  type pathMatch struct {
    32  	pattern string
    33  }
    34  
    35  func (pm *pathMatch) String() string {
    36  	return fmt.Sprintf("(PathMatch %q)", pm.pattern)
    37  }
    38  
    39  func (pm *pathMatch) Evaluate(op Op) bool {
    40  	matched, err := filepath.Match(pm.pattern, op.Path)
    41  	if err != nil {
    42  		// Only possible error is ErrBadPattern, indicating an issue with
    43  		// the test itself.
    44  		panic(err)
    45  	}
    46  	return matched
    47  }
    48  
    49  var (
    50  	// Reads is a predicate that returns true iff an operation is a read
    51  	// operation.
    52  	Reads Predicate = opKindPred{kind: OpIsRead}
    53  	// Writes is a predicate that returns true iff an operation is a write
    54  	// operation.
    55  	Writes Predicate = opKindPred{kind: OpIsWrite}
    56  )
    57  
    58  type opFileReadAt struct {
    59  	// offset configures the predicate to evaluate to true only if the
    60  	// operation's offset exactly matches offset.
    61  	offset int64
    62  }
    63  
    64  func (o *opFileReadAt) String() string {
    65  	return fmt.Sprintf("(FileReadAt %d)", o.offset)
    66  }
    67  
    68  func (o *opFileReadAt) Evaluate(op Op) bool {
    69  	return op.Kind == OpFileReadAt && o.offset == op.Offset
    70  }
    71  
    72  type opKindPred struct {
    73  	kind OpReadWrite
    74  }
    75  
    76  func (p opKindPred) String() string      { return p.kind.String() }
    77  func (p opKindPred) Evaluate(op Op) bool { return p.kind == op.Kind.ReadOrWrite() }
    78  
    79  // Randomly constructs a new predicate that pseudorandomly evaluates to true
    80  // with probability p using randomness determinstically derived from seed.
    81  //
    82  // The predicate is deterministic with respect to file paths: its behavior for a
    83  // particular file is deterministic regardless of intervening evaluations for
    84  // operations on other files. This can be used to ensure determinism despite
    85  // nondeterministic concurrency if the concurrency is constrained to separate
    86  // files.
    87  func Randomly(p float64, seed int64) Predicate {
    88  	rs := &randomSeed{p: p, rootSeed: seed}
    89  	rs.mu.perFilePrng = make(map[string]*rand.Rand)
    90  	return rs
    91  }
    92  
    93  type randomSeed struct {
    94  	// p defines the probability of an error being injected.
    95  	p        float64
    96  	rootSeed int64
    97  	mu       struct {
    98  		sync.Mutex
    99  		h           maphash.Hash
   100  		perFilePrng map[string]*rand.Rand
   101  	}
   102  }
   103  
   104  func (rs *randomSeed) String() string {
   105  	if rs.rootSeed == 0 {
   106  		return fmt.Sprintf("(Randomly %.2f)", rs.p)
   107  	}
   108  	return fmt.Sprintf("(Randomly %.2f %d)", rs.p, rs.rootSeed)
   109  }
   110  
   111  func (rs *randomSeed) Evaluate(op Op) bool {
   112  	rs.mu.Lock()
   113  	defer rs.mu.Unlock()
   114  	prng, ok := rs.mu.perFilePrng[op.Path]
   115  	if !ok {
   116  		// This is the first time an operation has been performed on the file at
   117  		// this path. Initialize the per-file prng by computing a deterministic
   118  		// hash of the path.
   119  		rs.mu.h.Reset()
   120  		var b [8]byte
   121  		binary.LittleEndian.PutUint64(b[:], uint64(rs.rootSeed))
   122  		if _, err := rs.mu.h.Write(b[:]); err != nil {
   123  			panic(err)
   124  		}
   125  		if _, err := rs.mu.h.WriteString(op.Path); err != nil {
   126  			panic(err)
   127  		}
   128  		seed := rs.mu.h.Sum64()
   129  		prng = rand.New(rand.NewSource(int64(seed)))
   130  		rs.mu.perFilePrng[op.Path] = prng
   131  	}
   132  	return prng.Float64() < rs.p
   133  }
   134  
   135  // ParseDSL parses the provided string using the default DSL parser.
   136  func ParseDSL(s string) (Injector, error) {
   137  	return defaultParser.Parse(s)
   138  }
   139  
   140  var defaultParser = NewParser()
   141  
   142  // NewParser constructs a new parser for an encoding of a lisp-like DSL
   143  // describing error injectors.
   144  //
   145  // Errors:
   146  // - ErrInjected is the only error currently supported by the DSL.
   147  //
   148  // Injectors:
   149  //   - <ERROR>: An error by itself is an injector that injects an error every
   150  //     time.
   151  //   - (<ERROR> <PREDICATE>) is an injector that injects an error only when
   152  //     the operation satisfies the predicate.
   153  //
   154  // Predicates:
   155  //   - Reads is a constant predicate that evalutes to true iff the operation is a
   156  //     read operation (eg, Open, Read, ReadAt, Stat)
   157  //   - Writes is a constant predicate that evaluates to true iff the operation is
   158  //     a write operation (eg, Create, Rename, Write, WriteAt, etc).
   159  //   - (PathMatch <STRING>) is a predicate that evalutes to true iff the
   160  //     operation's file path matches the provided shell pattern.
   161  //   - (OnIndex <INTEGER>) is a predicate that evaluates to true only on the n-th
   162  //     invocation.
   163  //   - (And <PREDICATE> [PREDICATE]...) is a predicate that evaluates to true
   164  //     iff all the provided predicates evaluate to true. And short circuits on
   165  //     the first predicate to evaluate to false.
   166  //   - (Or <PREDICATE> [PREDICATE]...) is a predicate that evaluates to true iff
   167  //     at least one of the provided predicates evaluates to true. Or short
   168  //     circuits on the first predicate to evaluate to true.
   169  //   - (Not <PREDICATE>) is a predicate that evaluates to true iff its provided
   170  //     predicates evaluates to false.
   171  //   - (Randomly <FLOAT> [INTEGER]) is a predicate that pseudorandomly evaluates
   172  //     to true. The probability of evaluating to true is determined by the
   173  //     required float argument (must be ≤1). The optional second parameter is a
   174  //     pseudorandom seed, for adjusting the deterministic randomness.
   175  //   - Operation-specific:
   176  //     (OpFileReadAt <INTEGER>) is a predicate that evaluates to true iff
   177  //     an operation is a file ReadAt call with an offset that's exactly equal.
   178  //
   179  // Example: (ErrInjected (And (PathMatch "*.sst") (OnIndex 5))) is a rule set
   180  // that will inject an error on the 5-th I/O operation involving an sstable.
   181  func NewParser() *Parser {
   182  	p := &Parser{
   183  		predicates: dsl.NewPredicateParser[Op](),
   184  		injectors:  dsl.NewParser[Injector](),
   185  	}
   186  	p.predicates.DefineConstant("Reads", func() dsl.Predicate[Op] { return Reads })
   187  	p.predicates.DefineConstant("Writes", func() dsl.Predicate[Op] { return Writes })
   188  	p.predicates.DefineFunc("PathMatch",
   189  		func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] {
   190  			pattern := s.ConsumeString()
   191  			s.Consume(token.RPAREN)
   192  			return PathMatch(pattern)
   193  		})
   194  	p.predicates.DefineFunc("OpFileReadAt",
   195  		func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] {
   196  			return parseFileReadAtOp(s)
   197  		})
   198  	p.predicates.DefineFunc("Randomly",
   199  		func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] {
   200  			return parseRandomly(s)
   201  		})
   202  	p.AddError(ErrInjected)
   203  	return p
   204  }
   205  
   206  // A Parser parses the error-injecting DSL. It may be extended to include
   207  // additional errors through AddError.
   208  type Parser struct {
   209  	predicates *dsl.Parser[dsl.Predicate[Op]]
   210  	injectors  *dsl.Parser[Injector]
   211  }
   212  
   213  // Parse parses the error injection DSL, returning the parsed injector.
   214  func (p *Parser) Parse(s string) (Injector, error) {
   215  	return p.injectors.Parse(s)
   216  }
   217  
   218  // AddError defines a new error that may be used within the DSL parsed by
   219  // Parse and will inject the provided error.
   220  func (p *Parser) AddError(le LabelledError) {
   221  	// Define the error both as a constant that unconditionally injects the
   222  	// error, and as a function that injects the error only if the provided
   223  	// predicate evaluates to true.
   224  	p.injectors.DefineConstant(le.Label, func() Injector { return le })
   225  	p.injectors.DefineFunc(le.Label,
   226  		func(_ *dsl.Parser[Injector], s *dsl.Scanner) Injector {
   227  			pred := p.predicates.ParseFromPos(s, s.Scan())
   228  			s.Consume(token.RPAREN)
   229  			return le.If(pred)
   230  		})
   231  }
   232  
   233  // LabelledError is an error that also implements Injector, unconditionally
   234  // injecting itself. It implements String() by returning its label. It
   235  // implements Error() by returning its underlying error.
   236  type LabelledError struct {
   237  	error
   238  	Label     string
   239  	predicate Predicate
   240  }
   241  
   242  // String implements fmt.Stringer.
   243  func (le LabelledError) String() string {
   244  	if le.predicate == nil {
   245  		return le.Label
   246  	}
   247  	return fmt.Sprintf("(%s %s)", le.Label, le.predicate.String())
   248  }
   249  
   250  // MaybeError implements Injector.
   251  func (le LabelledError) MaybeError(op Op) error {
   252  	if le.predicate == nil || le.predicate.Evaluate(op) {
   253  		return le
   254  	}
   255  	return nil
   256  }
   257  
   258  // If returns an Injector that returns the receiver error if the provided
   259  // predicate evalutes to true.
   260  func (le LabelledError) If(p Predicate) Injector {
   261  	le.predicate = p
   262  	return le
   263  }
   264  
   265  func parseFileReadAtOp(s *dsl.Scanner) *opFileReadAt {
   266  	lit := s.Consume(token.INT).Lit
   267  	off, err := strconv.ParseInt(lit, 10, 64)
   268  	if err != nil {
   269  		panic(err)
   270  	}
   271  	s.Consume(token.RPAREN)
   272  	return &opFileReadAt{offset: off}
   273  }
   274  
   275  func parseRandomly(s *dsl.Scanner) Predicate {
   276  	lit := s.Consume(token.FLOAT).Lit
   277  	p, err := strconv.ParseFloat(lit, 64)
   278  	if err != nil {
   279  		panic(err)
   280  	} else if p > 1.0 {
   281  		// NB: It's not possible for p to be less than zero because we don't
   282  		// try to parse the '-' token.
   283  		panic(errors.Newf("errorfs: Randomly proability p must be within p ≤ 1.0"))
   284  	}
   285  
   286  	var seed int64
   287  	tok := s.Scan()
   288  	switch tok.Kind {
   289  	case token.RPAREN:
   290  	case token.INT:
   291  		seed, err = strconv.ParseInt(tok.Lit, 10, 64)
   292  		if err != nil {
   293  			panic(err)
   294  		}
   295  		s.Consume(token.RPAREN)
   296  	default:
   297  		panic(errors.Errorf("errorfs: unexpected token %s; expected RPAREN | FLOAT", tok.String()))
   298  	}
   299  	return Randomly(p, seed)
   300  }