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 }