
Fehlerinjektions-Bibliothek für Go, die zur Laufzeit steuerbare Failpoints hinzufügt, um über Umgebungsvariablen Panics, Sleeps, Returns und bedingte Fehlerpfade auszulösen.
Eine Implementierung von Failpoints für Golang. Failpoints werden verwendet, um Codepunkte einzufügen, an denen Fehler in einer benutzergesteuerten Weise injiziert werden können. Ein Failpoint ist ein Codeausschnitt, der nur ausgeführt wird, wenn der entsprechende Failpoint aktiv ist.
failpoint-ctl)Baue failpoint-ctl aus dem Quellcode
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
Injiziere Failpoints in dein Programm, z. B.:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Transformiere deinen Code mit failpoint-ctl enable
Baue mit go build
Aktiviere Failpoints mit der Umgebungsvariable GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Hinweis: GO_FAILPOINTS funktioniert nicht mit Markern vom Typ InjectCall.
Wenn du go run zum Ausführen des Tests verwendest, vergiss nicht, die generierte Datei in deinen Befehl aufzunehmen, z. B.:
failpoint-toolexec)Baue failpoint-toolexec aus dem Quellcode
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
Injiziere Failpoints in dein Programm, z. B.:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Verwende einen separaten Build-Cache, um eine Vermischung mit Caches ohne failpoint-toolexec zu vermeiden, und baue:
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
Aktiviere Failpoints mit der Umgebungsvariable GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Du kannst auch go run oder go test verwenden, z. B.:
Definiere Failpoints als gültigen Golang-Code, nicht als Kommentare oder etwas anderes
Failpoints verursachen keine zusätzlichen Kosten
Failpoint-Routinen sind schreib-/lesbar und sollten von einem Compiler geprüft werden
Der durch die Failpoint-Definition erzeugte Code ist leicht lesbar
Die Zeilennummern bleiben identisch mit den injizierten Codes (einfacheres Debugging)
Unterstützung paralleler Tests mit context.Context
Failpoint
Ein Failpoint ist ein Codeausschnitt, der nur ausgeführt wird, wenn der entsprechende Failpoint aktiv ist.
Die Closure wird niemals ausgeführt, wenn failpoint.Disable("failpoint-name-for-demo") ausgeführt wird.
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
Markerfunktionen
Sie sind nur leere Funktionen
Sie sind leicht zu schreiben/lesen
Sie führen eine Compilerprüfung für Failpoints ein, die im regulären Modus nicht kompilieren können, wenn der Failpoint-Code ungültig ist
Liste der Markerfunktionen
func Inject(fpname string, fpblock func(val Value)) {}func InjectContext(fpname string, ctx context.Context, fpblock func(val Value)) {}func InjectCall(fpname string, args ...any) {}func Break(label ...string) {}Du kannst failpoint.Inject aufrufen, um einen Failpoint an der Aufrufstelle zu injizieren, wobei failpoint-name
zum Auslösen des Failpoints verwendet wird und failpoint-closure als Rumpf der IF-Anweisung expandiert wird.
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
Der konvertierte Code sieht wie folgt aus:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value ist der Wert, der durch failpoint.Enable("failpoint-name", "return(5)") übergeben wird,
und kann ignoriert werden.
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
ODER
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
Injiziere einen Failpoint in die IF-INITIAL-Anweisung oder den CONDITIONAL-Ausdruck
if a, b := func() {
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
}, func() int { return rand.Intn(200) }(); b > func() int {
failpoint.Inject("failpoint-name", func(val failpoint.Value) int {
return val.(int)
})
return rand.Intn(3000)
}() && b < func() int {
failpoint.Inject("failpoint-name-2", func(val failpoint.Value) {
return rand.Intn(val.(int))
})
return rand.Intn(6000)
}() {
a()
failpoint.Inject("failpoint-name-3", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
}
Der obige Codeblock erzeugt etwas wie folgt:
if a, b := func() {
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
}, func() int { return rand.Intn(200) }(); b > func() int {
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return val.(int)
}
return rand.Intn(3000)
}() && b < func() int {
if val, ok := failpoint.Eval(_curpkg_("failpoint-name-2")); ok {
return rand.Intn(val.(int))
}
return rand.Intn(6000)
}() {
a()
if val, ok := failpoint.Eval(_curpkg_("failpoint-name-3")); ok {
fmt.Println("unit-test", val)
}
}
Injiziere einen Failpoint in die SELECT-Anweisung, um einen CASE zu blockieren, wenn der Failpoint aktiv ist
Wie oben zu sehen, umschließt _curpkg_ den ursprünglichen Failpoint-Namen im failpoint.Eval-Aufruf automatisch.
Du kannst dir _curpkg_ wie ein Makro vorstellen, das dem Failpoint-Namen automatisch den aktuellen Paketpfad voranstellt. Zum Beispiel:
package ddl // which parent package is `github.com/pingcap/tidb`
func demo() {
// _curpkg_("the-original-failpoint-name") will be expanded as `github.com/pingcap/tidb/ddl/the-original-failpoint-name`
if val, ok := failpoint.Eval(_curpkg_("the-original-failpoint-name")); ok {...}
}
Du musst dich in deiner Anwendung nicht um _curpkg_ kümmern. Es wird nach dem Ausführen von failpoint-ctl enable automatisch erzeugt
und mit failpoint-ctl disable wieder entfernt.
Da alle Failpoints in einem Paket denselben Namespace teilen, müssen wir vorsichtig sein, um Namenskonflikte zu vermeiden. Es gibt einige empfohlene Namensregeln, um diese Situation zu verbessern.
Halte den Namen im aktuellen Unterpaket eindeutig
Verwende einen selbsterklärenden Namen für den Failpoint
Du kannst Failpoints über Umgebungsvariablen aktivieren
GO_FAILPOINTS="github.com/pingcap/tidb/ddl/renameTableErr=return(100);github.com/pingcap/tidb/planner/core/illegalPushDown=return(true);github.com/pingcap/pd/server/schedulers/balanceLeaderFailed=return(true)"
failpoint.Eval aufruft, um zu bestimmen, ob ein
Failpoint aktiv ist, und führt den Failpoint-Code aus, wenn der Failpoint aktiviert ist
binding__failpoint_binding__.goGO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
func Goto(label string) {}func Continue(label ...string) {}func Fallthrough() {}func Return(results ...interface{}) {}func Label(label string) {}Unterstützte Failpoint-Umgebungsvariablen
Failpoints können durch Exportieren von Umgebungsvariablen mit dem folgenden Muster aktiviert werden, das den FreeBSD-Failpoint-SYSCTL-VARIABLEN recht ähnlich ist.
Hinweis: InjectCall kann nicht über Umgebungsvariablen aktiviert werden.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
Das Argument gibt an, welche Aktion ausgeführt werden soll; es kann eines der folgenden sein:
Und der konvertierte Code sieht wie folgt aus:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
Außerdem kann die Failpoint-Closure eine Funktion sein, die context.Context entgegennimmt. Du kannst
mit context.Context eigene Dinge tun, z. B. steuern, ob ein Failpoint in
parallelen Tests oder anderen Fällen aktiv ist. Zum Beispiel:
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Der konvertierte Code sieht wie folgt aus:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Du kannst context.Context ignorieren, und dies erzeugt denselben Code wie die obige Nicht-Kontext-Version. Zum Beispiel:
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
wird zu
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Du kannst failpoint.InjectCall verwenden, um einen Funktionsaufruf zu injizieren. Dieser Markertyp kann nur mit failpoint.EnableCall aktiviert werden und muss im selben Prozess wie die InjectCall-Aufrufstelle aufgerufen werden. Mit diesem Marker kannst du vermeiden, dass Failpoint-Code deinen Quellcode verschmutzt. Siehe Beispiele.
Du kannst einen Failpoint mit failpoint.WithHook steuern
func (s *dmlSuite) TestCRUDParallel() {
sctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
return ctx.Value(fpname) != nil // Determine by ctx key
})
insertFailpoints = map[string]struct{} {
"insert-record-fp": {},
"insert-index-fp": {},
"on-duplicate-fp": {},
}
ictx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
_, found := insertFailpoints[fpname] // Only enables some failpoints.
return found
})
deleteFailpoints = map[string]struct{} {
"tikv-is-busy-fp": {},
"fetch-tso-timeout": {},
}
dctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
_, found := deleteFailpoints[fpname] // Only disables failpoints.
return !found
})
// other DML parallel test cases.
s.RunParallel(buildSelectTests(sctx))
s.RunParallel(buildInsertTests(ictx))
s.RunParallel(buildDeleteTests(dctx))
}
Wenn du einen Failpoint im Schleifenkontext verwendest, benötigst du möglicherweise andere Markerfunktionen.
failpoint.Label("outer")
for i := 0; i < 100; i++ {
inner:
for j := 0; j < 1000; j++ {
switch rand.Intn(j) + i {
case j / 5:
failpoint.Break()
case j / 7:
failpoint.Continue("outer")
case j / 9:
failpoint.Fallthrough()
case j / 10:
failpoint.Goto("outer")
default:
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val.(int))
if val == j/11 {
failpoint.Break("inner")
} else {
failpoint.Goto("outer")
}
})
}
}
}
Der obige Codeblock erzeugt den folgenden Code:
outer:
for i := 0; i < 100; i++ {
inner:
for j := 0; j < 1000; j++ {
switch rand.Intn(j) + i {
case j / 5:
break
case j / 7:
continue outer
case j / 9:
fallthrough
case j / 10:
goto outer
default:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val.(int))
if val == j/11 {
break inner
} else {
goto outer
}
}
}
}
}
Du fragst dich vielleicht, warum wir nicht direkt label, break, continue und fallthrough verwenden,
anstatt Failpoint-Markerfunktionen zu nutzen.
Jedes ungenutzte Symbol wie eine Kennung oder ein Label ist in Golang nicht erlaubt. Es ist ungültig, wenn ein Label nur in der Failpoint-Closure verwendet wird. Zum Beispiel:
label1: // compiler error: unused label1
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
if val.(int) == 1000 {
goto label1 // illegal to use goto here
}
fmt.Println("unit-test", val)
})
break und continue können nur im Schleifenkontext verwendet werden, was im Golang-Code nicht zulässig ist,
wenn wir sie direkt in einer Closure verwenden.
func (s *StoreService) ExecuteStoreTask() {
select {
case <-func() chan *StoreTask {
failpoint.Inject("priority-fp", func(_ failpoint.Value) {
return make(chan *StoreTask)
})
return s.priorityHighCh
}():
fmt.Println("execute high priority task")
case <- s.priorityNormalCh:
fmt.Println("execute normal priority task")
case <- s.priorityLowCh:
fmt.Println("execute normal low task")
}
}
Der obige Codeblock erzeugt etwas wie folgt:
func (s *StoreService) ExecuteStoreTask() {
select {
case <-func() chan *StoreTask {
if _, ok := failpoint.Eval(_curpkg_("priority-fp")); ok {
return make(chan *StoreTask)
})
return s.priorityHighCh
}():
fmt.Println("execute high priority task")
case <- s.priorityNormalCh:
fmt.Println("execute normal priority task")
case <- s.priorityLowCh:
fmt.Println("execute normal low task")
}
}
Injiziere einen Failpoint, um SWITCH-CASE-Zweige dynamisch zu erweitern
switch opType := operator.Type(); {
case opType == "balance-leader":
fmt.Println("create balance leader steps")
case opType == "balance-region":
fmt.Println("create balance region steps")
case opType == "scatter-region":
fmt.Println("create scatter region steps")
case func() bool {
failpoint.Inject("dynamic-op-type", func(val failpoint.Value) bool {
return strings.Contains(val.(string), opType)
})
return false
}():
fmt.Println("do something")
default:
panic("unsupported operator type")
}
Der obige Codeblock erzeugt etwas wie folgt:
switch opType := operator.Type(); {
case opType == "balance-leader":
fmt.Println("create balance leader steps")
case opType == "balance-region":
fmt.Println("create balance region steps")
case opType == "scatter-region":
fmt.Println("create scatter region steps")
case func() bool {
if val, ok := failpoint.Eval(_curpkg_("dynamic-op-type")); ok {
return strings.Contains(val.(string), opType)
}
return false
}():
fmt.Println("do something")
default:
panic("unsupported operator type")
}
Kompliziertere Failpoints