
Libreria di fault-injection per Go che aggiunge failpoint controllabili a runtime per innescare panic, sleep, return e percorsi di errore condizionali tramite variabili d'ambiente.
Un'implementazione dei failpoint per Golang. I failpoint vengono usati per aggiungere punti nel codice in cui gli errori possono essere iniettati in modo controllato dall'utente. Un failpoint è uno snippet di codice che viene eseguito solo quando il corrispondente failpoint è attivo.
failpoint-ctlCompila failpoint-ctl dal sorgente
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
Inietta i failpoint nel tuo programma, ad esempio:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Trasforma il tuo codice con failpoint-ctl enable
Compila con go build
Abilita i failpoint con la variabile d'ambiente GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Nota: GO_FAILPOINTS non funziona con il tipo di marcatore InjectCall.
Se usi go run per eseguire il test, non dimenticare di aggiungere il file generato binding__failpoint_binding__.go al tuo comando, ad esempio:
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
failpoint-toolexec)Compila failpoint-toolexec dal sorgente
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
Inietta i failpoint nel tuo programma, ad esempio:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Usa una cache di build separata per evitare di mescolare le cache senza failpoint-toolexec, e compila
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
Abilita i failpoint con la variabile d'ambiente GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Puoi anche usare go run o go test, ad esempio:
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
Definisci il failpoint in codice Golang valido, non in commenti o altro
Un failpoint non comporta alcun costo aggiuntivo
La routine del failpoint è scrivibile/leggibile e deve essere controllata da un compilatore
Il codice generato dalla definizione del failpoint è facile da leggere
Mantieni gli stessi numeri di riga dei codici di iniezione (più facile da debuggare)
Supporto per test paralleli con context.Context
Failpoint
Un failpoint è uno snippet di codice che viene eseguito solo quando il corrispondente failpoint è attivo.
La closure non verrà mai eseguita se viene eseguito failpoint.Disable("failpoint-name-for-demo").
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
Funzioni marcatore
Sono solo funzioni vuote
Sono facili da scrivere/leggere
Introducono un controllo del compilatore per i failpoint che non possono compilare in modalità normale se il codice del failpoint non è valido
Elenco delle funzioni marcatore
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) {}func Goto(label string) {}func Continue(label ...string) {}func Fallthrough() {}func Return(results ...interface{}) {}func Label(label string) {}Variabili d'ambiente supportate per i failpoint
Un failpoint può essere abilitato esportando variabili d'ambiente con il seguente pattern, molto simile a freebsd failpoint SYSCTL VARIABLES
Nota: InjectCall non può essere abilitato tramite variabili d'ambiente.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
L'argomento specifica quale azione intraprendere; può essere uno dei seguenti:
Puoi chiamare failpoint.Inject per iniettare un failpoint nel punto di chiamata, dove failpoint-name viene usato per attivare il failpoint e failpoint-closure verrà espansa come corpo dell'istruzione IF.
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
Il codice convertito è simile a:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value è il valore passato da failpoint.Enable("failpoint-name", "return(5)") che può essere ignorato.
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
OPPURE
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
E il codice convertito è simile a:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
Inoltre, la closure del failpoint può essere una funzione che accetta context.Context. Puoi fare cose personalizzate con context.Context, come controllare se un failpoint è attivo nei test paralleli o in altri casi. Per esempio,
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Il codice convertito è simile a:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Puoi ignorare context.Context, e questo genererà lo stesso codice della versione senza contesto sopra. Per esempio,
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Diventa
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Puoi usare failpoint.InjectCall per iniettare una chiamata di funzione; questo tipo di marcatore può essere abilitato solo usando failpoint.EnableCall e deve essere chiamato nello stesso processo del punto di chiamata InjectCall. Usando questo marcatore, puoi evitare che il codice del failpoint inquini il tuo codice sorgente. Vedi esempi.
Puoi controllare un failpoint tramite failpoint.WithHook
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))
}
Se usi un failpoint nel contesto di un loop, forse userai altre funzioni marcatore.
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")
}
})
}
}
}
Il blocco di codice sopra genererà il seguente codice:
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
}
}
}
}
}
Potresti chiederti perché non usiamo direttamente label, break, continue e fallthrough invece delle funzioni marcatore dei failpoint.
Qualsiasi simbolo inutilizzato come un identificatore o un'etichetta non è consentito in Golang. Sarà invalido se un'etichetta viene usata solo nella closure del failpoint. Per esempio,
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 e continue possono essere usati solo nel contesto di un loop, il che non è legale nel codice Golang se li usiamo direttamente in una closure.
Inietta un failpoint nell'istruzione IF INITIAL o nell'espressione CONDIZIONALE
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)
})
}
Il blocco di codice sopra genererà qualcosa come questo:
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)
}
}
Inietta un failpoint nell'istruzione SELECT per bloccare un CASE se il failpoint è attivo
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")
}
}
Il blocco di codice sopra genererà qualcosa come questo:
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")
}
}
Inietta un failpoint per estendere dinamicamente i rami SWITCH CASE
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")
}
Il blocco di codice sopra genererà qualcosa come questo:
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")
}
Failpoint più complicati
Come visto sopra, _curpkg_ avvolgerà automaticamente il nome originale del failpoint nella chiamata failpoint.Eval. Puoi pensare a _curpkg_ come a una macro che antepone automaticamente il percorso del pacchetto corrente al nome del failpoint. Per esempio,
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 {...}
}
Non devi preoccuparti di _curpkg_ nella tua applicazione. Viene generato automaticamente dopo aver eseguito failpoint-ctl enable e viene eliminato con failpoint-ctl disable.
Poiché tutti i failpoint in un pacchetto condividono lo stesso namespace, dobbiamo fare attenzione a evitare conflitti di nomi. Ci sono alcune regole di denominazione consigliate per migliorare questa situazione.
Mantieni il nome univoco nel sottopacchetto corrente
Usa un nome autoesplicativo per il failpoint
Puoi abilitare i failpoint tramite variabili d'ambiente
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 per determinare se un failpoint è attivo ed esegue il codice del failpoint se il failpoint è abilitato