311 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			311 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
   Copyright The containerd Authors.
 | 
						|
 | 
						|
   Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
   you may not use this file except in compliance with the License.
 | 
						|
   You may obtain a copy of the License at
 | 
						|
 | 
						|
       http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
   Unless required by applicable law or agreed to in writing, software
 | 
						|
   distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
   See the License for the specific language governing permissions and
 | 
						|
   limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
// Package failpoint provides the code point in the path, which can be controlled
 | 
						|
// by user's variable.
 | 
						|
//
 | 
						|
// Inspired by FreeBSD fail(9): https://freebsd.org/cgi/man.cgi?query=fail.
 | 
						|
package failpoint
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
// EvalFn is the func type about delegated evaluation.
 | 
						|
type EvalFn func() error
 | 
						|
 | 
						|
// Type is the type of failpoint to specifies which action to take.
 | 
						|
type Type int
 | 
						|
 | 
						|
const (
 | 
						|
	// TypeInvalid is invalid type
 | 
						|
	TypeInvalid Type = iota
 | 
						|
	// TypeOff takes no action
 | 
						|
	TypeOff
 | 
						|
	// TypeError triggers failpoint error with specified argument
 | 
						|
	TypeError
 | 
						|
	// TypePanic triggers panic with specified argument
 | 
						|
	TypePanic
 | 
						|
	// TypeDelay sleeps with the specified number of milliseconds
 | 
						|
	TypeDelay
 | 
						|
)
 | 
						|
 | 
						|
// String returns the name of type.
 | 
						|
func (t Type) String() string {
 | 
						|
	switch t {
 | 
						|
	case TypeOff:
 | 
						|
		return "off"
 | 
						|
	case TypeError:
 | 
						|
		return "error"
 | 
						|
	case TypePanic:
 | 
						|
		return "panic"
 | 
						|
	case TypeDelay:
 | 
						|
		return "delay"
 | 
						|
	default:
 | 
						|
		return "invalid"
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// Failpoint is used to add code points where error or panic may be injected by
 | 
						|
// user. The user controlled variable will be parsed for how the error injected
 | 
						|
// code should fire. There is the way to set the rule for failpoint.
 | 
						|
//
 | 
						|
//	<count>*<type>[(arg)][-><more terms>]
 | 
						|
//
 | 
						|
// The <type> argument specifies which action to take; it can be one of:
 | 
						|
//
 | 
						|
//	off:	Takes no action (does not trigger failpoint and no argument)
 | 
						|
//	error:	Triggers failpoint error with specified argument(string)
 | 
						|
//	panic:	Triggers panic with specified argument(string)
 | 
						|
//	delay:	Sleep the specified number of milliseconds
 | 
						|
//
 | 
						|
// The <count>* modifiers prior to <type> control when <type> is executed. For
 | 
						|
// example, "5*error(oops)" means "return error oops 5 times total". The
 | 
						|
// operator -> can be used to express cascading terms. If you specify
 | 
						|
// <term1>-><term2>, it means that if <term1> does not execute, <term2> will
 | 
						|
// be evaluated. If you want the error injected code should fire in second
 | 
						|
// call, you can specify "1*off->1*error(oops)".
 | 
						|
//
 | 
						|
// Inspired by FreeBSD fail(9): https://freebsd.org/cgi/man.cgi?query=fail.
 | 
						|
type Failpoint struct {
 | 
						|
	sync.Mutex
 | 
						|
 | 
						|
	fnName  string
 | 
						|
	entries []*failpointEntry
 | 
						|
}
 | 
						|
 | 
						|
// NewFailpoint returns failpoint control.
 | 
						|
func NewFailpoint(fnName string, terms string) (*Failpoint, error) {
 | 
						|
	entries, err := parseTerms([]byte(terms))
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return &Failpoint{
 | 
						|
		fnName:  fnName,
 | 
						|
		entries: entries,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// Evaluate evaluates a failpoint.
 | 
						|
func (fp *Failpoint) Evaluate() error {
 | 
						|
	fn := fp.DelegatedEval()
 | 
						|
	return fn()
 | 
						|
}
 | 
						|
 | 
						|
// DelegatedEval evaluates a failpoint but delegates to caller to fire that.
 | 
						|
func (fp *Failpoint) DelegatedEval() EvalFn {
 | 
						|
	var target *failpointEntry
 | 
						|
 | 
						|
	func() {
 | 
						|
		fp.Lock()
 | 
						|
		defer fp.Unlock()
 | 
						|
 | 
						|
		for _, entry := range fp.entries {
 | 
						|
			if entry.count == 0 {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			entry.count--
 | 
						|
			target = entry
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	if target == nil {
 | 
						|
		return nopEvalFn
 | 
						|
	}
 | 
						|
	return target.evaluate
 | 
						|
}
 | 
						|
 | 
						|
// Failpoint returns the current state of control in string format.
 | 
						|
func (fp *Failpoint) Marshal() string {
 | 
						|
	fp.Lock()
 | 
						|
	defer fp.Unlock()
 | 
						|
 | 
						|
	res := make([]string, 0, len(fp.entries))
 | 
						|
	for _, entry := range fp.entries {
 | 
						|
		res = append(res, entry.marshal())
 | 
						|
	}
 | 
						|
	return strings.Join(res, "->")
 | 
						|
}
 | 
						|
 | 
						|
type failpointEntry struct {
 | 
						|
	typ   Type
 | 
						|
	arg   interface{}
 | 
						|
	count int64
 | 
						|
}
 | 
						|
 | 
						|
func newFailpointEntry() *failpointEntry {
 | 
						|
	return &failpointEntry{
 | 
						|
		typ:   TypeInvalid,
 | 
						|
		count: 0,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (fpe *failpointEntry) marshal() string {
 | 
						|
	base := fmt.Sprintf("%d*%s", fpe.count, fpe.typ)
 | 
						|
	switch fpe.typ {
 | 
						|
	case TypeOff:
 | 
						|
		return base
 | 
						|
	case TypeError, TypePanic:
 | 
						|
		return fmt.Sprintf("%s(%s)", base, fpe.arg.(string))
 | 
						|
	case TypeDelay:
 | 
						|
		return fmt.Sprintf("%s(%d)", base, fpe.arg.(time.Duration)/time.Millisecond)
 | 
						|
	default:
 | 
						|
		return base
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (fpe *failpointEntry) evaluate() error {
 | 
						|
	switch fpe.typ {
 | 
						|
	case TypeOff:
 | 
						|
		return nil
 | 
						|
	case TypeError:
 | 
						|
		return fmt.Errorf("%v", fpe.arg)
 | 
						|
	case TypePanic:
 | 
						|
		panic(fpe.arg)
 | 
						|
	case TypeDelay:
 | 
						|
		time.Sleep(fpe.arg.(time.Duration))
 | 
						|
		return nil
 | 
						|
	default:
 | 
						|
		panic("invalid failpoint type")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func parseTerms(term []byte) ([]*failpointEntry, error) {
 | 
						|
	var entry *failpointEntry
 | 
						|
	var err error
 | 
						|
 | 
						|
	// count*type[(arg)]
 | 
						|
	term, entry, err = parseTerm(term)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	res := []*failpointEntry{entry}
 | 
						|
 | 
						|
	// cascading terms
 | 
						|
	for len(term) > 0 {
 | 
						|
		if !bytes.HasPrefix(term, []byte("->")) {
 | 
						|
			return nil, fmt.Errorf("invalid cascading terms: %s", string(term))
 | 
						|
		}
 | 
						|
 | 
						|
		term = term[2:]
 | 
						|
		term, entry, err = parseTerm(term)
 | 
						|
		if err != nil {
 | 
						|
			return nil, fmt.Errorf("failed to parse cascading term: %w", err)
 | 
						|
		}
 | 
						|
 | 
						|
		res = append(res, entry)
 | 
						|
	}
 | 
						|
	return res, nil
 | 
						|
}
 | 
						|
 | 
						|
func parseTerm(term []byte) ([]byte, *failpointEntry, error) {
 | 
						|
	var err error
 | 
						|
	var entry = newFailpointEntry()
 | 
						|
 | 
						|
	// count*
 | 
						|
	term, err = parseInt64(term, '*', &entry.count)
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// type[(arg)]
 | 
						|
	term, err = parseType(term, entry)
 | 
						|
	return term, entry, err
 | 
						|
}
 | 
						|
 | 
						|
func parseType(term []byte, entry *failpointEntry) ([]byte, error) {
 | 
						|
	var nameToTyp = map[string]Type{
 | 
						|
		"off":    TypeOff,
 | 
						|
		"error(": TypeError,
 | 
						|
		"panic(": TypePanic,
 | 
						|
		"delay(": TypeDelay,
 | 
						|
	}
 | 
						|
 | 
						|
	var found bool
 | 
						|
	for name, typ := range nameToTyp {
 | 
						|
		if bytes.HasPrefix(term, []byte(name)) {
 | 
						|
			found = true
 | 
						|
			term = term[len(name):]
 | 
						|
			entry.typ = typ
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !found {
 | 
						|
		return nil, fmt.Errorf("invalid type format: %s", string(term))
 | 
						|
	}
 | 
						|
 | 
						|
	switch entry.typ {
 | 
						|
	case TypePanic, TypeError:
 | 
						|
		endIdx := bytes.IndexByte(term, ')')
 | 
						|
		if endIdx <= 0 {
 | 
						|
			return nil, fmt.Errorf("invalid argument for %s type", entry.typ)
 | 
						|
		}
 | 
						|
		entry.arg = string(term[:endIdx])
 | 
						|
		return term[endIdx+1:], nil
 | 
						|
	case TypeOff:
 | 
						|
		// do nothing
 | 
						|
		return term, nil
 | 
						|
	case TypeDelay:
 | 
						|
		var msVal int64
 | 
						|
		var err error
 | 
						|
 | 
						|
		term, err = parseInt64(term, ')', &msVal)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		entry.arg = time.Millisecond * time.Duration(msVal)
 | 
						|
		return term, nil
 | 
						|
	default:
 | 
						|
		panic("unreachable")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func parseInt64(term []byte, terminate byte, val *int64) ([]byte, error) {
 | 
						|
	i := 0
 | 
						|
 | 
						|
	for ; i < len(term); i++ {
 | 
						|
		if b := term[i]; b < '0' || b > '9' {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if i == 0 || i == len(term) || term[i] != terminate {
 | 
						|
		return nil, fmt.Errorf("failed to parse int64 because of invalid terminate byte: %s", string(term))
 | 
						|
	}
 | 
						|
 | 
						|
	v, err := strconv.ParseInt(string(term[:i]), 10, 64)
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("failed to parse int64 from %s: %v", string(term[:i]), err)
 | 
						|
	}
 | 
						|
 | 
						|
	*val = v
 | 
						|
	return term[i+1:], nil
 | 
						|
}
 | 
						|
 | 
						|
func nopEvalFn() error {
 | 
						|
	return nil
 | 
						|
}
 |