LCOV - code coverage report
Current view: top level - pebble/vfs/errorfs - dsl.go (source / functions) Hit Total Coverage
Test: 2023-11-26 08:15Z 4cbc6446 - meta test only.lcov Lines: 52 132 39.4 %
Date: 2023-11-26 08:16:44 Functions: 0 0 -

          Line data    Source code
       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           0 : func PathMatch(pattern string) Predicate {
      28           0 :         return &pathMatch{pattern: pattern}
      29           0 : }
      30             : 
      31             : type pathMatch struct {
      32             :         pattern string
      33             : }
      34             : 
      35           0 : func (pm *pathMatch) String() string {
      36           0 :         return fmt.Sprintf("(PathMatch %q)", pm.pattern)
      37           0 : }
      38             : 
      39           0 : func (pm *pathMatch) Evaluate(op Op) bool {
      40           0 :         matched, err := filepath.Match(pm.pattern, op.Path)
      41           0 :         if err != nil {
      42           0 :                 // Only possible error is ErrBadPattern, indicating an issue with
      43           0 :                 // the test itself.
      44           0 :                 panic(err)
      45             :         }
      46           0 :         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           0 : func (o *opFileReadAt) String() string {
      65           0 :         return fmt.Sprintf("(FileReadAt %d)", o.offset)
      66           0 : }
      67             : 
      68           0 : func (o *opFileReadAt) Evaluate(op Op) bool {
      69           0 :         return op.Kind == OpFileReadAt && o.offset == op.Offset
      70           0 : }
      71             : 
      72             : type opKindPred struct {
      73             :         kind OpReadWrite
      74             : }
      75             : 
      76           0 : func (p opKindPred) String() string      { return p.kind.String() }
      77           1 : 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           1 : func Randomly(p float64, seed int64) Predicate {
      88           1 :         rs := &randomSeed{p: p, rootSeed: seed}
      89           1 :         rs.mu.perFilePrng = make(map[string]*rand.Rand)
      90           1 :         return rs
      91           1 : }
      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           0 : func (rs *randomSeed) String() string {
     105           0 :         if rs.rootSeed == 0 {
     106           0 :                 return fmt.Sprintf("(Randomly %.2f)", rs.p)
     107           0 :         }
     108           0 :         return fmt.Sprintf("(Randomly %.2f %d)", rs.p, rs.rootSeed)
     109             : }
     110             : 
     111           1 : func (rs *randomSeed) Evaluate(op Op) bool {
     112           1 :         rs.mu.Lock()
     113           1 :         defer rs.mu.Unlock()
     114           1 :         prng, ok := rs.mu.perFilePrng[op.Path]
     115           1 :         if !ok {
     116           1 :                 // This is the first time an operation has been performed on the file at
     117           1 :                 // this path. Initialize the per-file prng by computing a deterministic
     118           1 :                 // hash of the path.
     119           1 :                 rs.mu.h.Reset()
     120           1 :                 var b [8]byte
     121           1 :                 binary.LittleEndian.PutUint64(b[:], uint64(rs.rootSeed))
     122           1 :                 if _, err := rs.mu.h.Write(b[:]); err != nil {
     123           0 :                         panic(err)
     124             :                 }
     125           1 :                 if _, err := rs.mu.h.WriteString(op.Path); err != nil {
     126           0 :                         panic(err)
     127             :                 }
     128           1 :                 seed := rs.mu.h.Sum64()
     129           1 :                 prng = rand.New(rand.NewSource(int64(seed)))
     130           1 :                 rs.mu.perFilePrng[op.Path] = prng
     131             :         }
     132           1 :         return prng.Float64() < rs.p
     133             : }
     134             : 
     135             : // ParseDSL parses the provided string using the default DSL parser.
     136           0 : func ParseDSL(s string) (Injector, error) {
     137           0 :         return defaultParser.Parse(s)
     138           0 : }
     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           1 : func NewParser() *Parser {
     182           1 :         p := &Parser{
     183           1 :                 predicates: dsl.NewPredicateParser[Op](),
     184           1 :                 injectors:  dsl.NewParser[Injector](),
     185           1 :         }
     186           1 :         p.predicates.DefineConstant("Reads", func() dsl.Predicate[Op] { return Reads })
     187           1 :         p.predicates.DefineConstant("Writes", func() dsl.Predicate[Op] { return Writes })
     188           1 :         p.predicates.DefineFunc("PathMatch",
     189           1 :                 func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] {
     190           0 :                         pattern := s.ConsumeString()
     191           0 :                         s.Consume(token.RPAREN)
     192           0 :                         return PathMatch(pattern)
     193           0 :                 })
     194           1 :         p.predicates.DefineFunc("OpFileReadAt",
     195           1 :                 func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] {
     196           0 :                         return parseFileReadAtOp(s)
     197           0 :                 })
     198           1 :         p.predicates.DefineFunc("Randomly",
     199           1 :                 func(p *dsl.Parser[dsl.Predicate[Op]], s *dsl.Scanner) dsl.Predicate[Op] {
     200           0 :                         return parseRandomly(s)
     201           0 :                 })
     202           1 :         p.AddError(ErrInjected)
     203           1 :         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           0 : func (p *Parser) Parse(s string) (Injector, error) {
     215           0 :         return p.injectors.Parse(s)
     216           0 : }
     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           1 : func (p *Parser) AddError(le LabelledError) {
     221           1 :         // Define the error both as a constant that unconditionally injects the
     222           1 :         // error, and as a function that injects the error only if the provided
     223           1 :         // predicate evaluates to true.
     224           1 :         p.injectors.DefineConstant(le.Label, func() Injector { return le })
     225           1 :         p.injectors.DefineFunc(le.Label,
     226           1 :                 func(_ *dsl.Parser[Injector], s *dsl.Scanner) Injector {
     227           0 :                         pred := p.predicates.ParseFromPos(s, s.Scan())
     228           0 :                         s.Consume(token.RPAREN)
     229           0 :                         return le.If(pred)
     230           0 :                 })
     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           0 : func (le LabelledError) String() string {
     244           0 :         if le.predicate == nil {
     245           0 :                 return le.Label
     246           0 :         }
     247           0 :         return fmt.Sprintf("(%s %s)", le.Label, le.predicate.String())
     248             : }
     249             : 
     250             : // MaybeError implements Injector.
     251           1 : func (le LabelledError) MaybeError(op Op) error {
     252           1 :         if le.predicate == nil || le.predicate.Evaluate(op) {
     253           0 :                 return le
     254           0 :         }
     255           1 :         return nil
     256             : }
     257             : 
     258             : // If returns an Injector that returns the receiver error if the provided
     259             : // predicate evalutes to true.
     260           1 : func (le LabelledError) If(p Predicate) Injector {
     261           1 :         le.predicate = p
     262           1 :         return le
     263           1 : }
     264             : 
     265           0 : func parseFileReadAtOp(s *dsl.Scanner) *opFileReadAt {
     266           0 :         lit := s.Consume(token.INT).Lit
     267           0 :         off, err := strconv.ParseInt(lit, 10, 64)
     268           0 :         if err != nil {
     269           0 :                 panic(err)
     270             :         }
     271           0 :         s.Consume(token.RPAREN)
     272           0 :         return &opFileReadAt{offset: off}
     273             : }
     274             : 
     275           0 : func parseRandomly(s *dsl.Scanner) Predicate {
     276           0 :         lit := s.Consume(token.FLOAT).Lit
     277           0 :         p, err := strconv.ParseFloat(lit, 64)
     278           0 :         if err != nil {
     279           0 :                 panic(err)
     280           0 :         } else if p > 1.0 {
     281           0 :                 // NB: It's not possible for p to be less than zero because we don't
     282           0 :                 // try to parse the '-' token.
     283           0 :                 panic(errors.Newf("errorfs: Randomly proability p must be within p ≤ 1.0"))
     284             :         }
     285             : 
     286           0 :         var seed int64
     287           0 :         tok := s.Scan()
     288           0 :         switch tok.Kind {
     289           0 :         case token.RPAREN:
     290           0 :         case token.INT:
     291           0 :                 seed, err = strconv.ParseInt(tok.Lit, 10, 64)
     292           0 :                 if err != nil {
     293           0 :                         panic(err)
     294             :                 }
     295           0 :                 s.Consume(token.RPAREN)
     296           0 :         default:
     297           0 :                 panic(errors.Errorf("errorfs: unexpected token %s; expected RPAREN | FLOAT", tok.String()))
     298             :         }
     299           0 :         return Randomly(p, seed)
     300             : }

Generated by: LCOV version 1.14