
Bibliothèque d'injection de fautes pour Go qui ajoute des failpoints contrôlables au runtime pour déclencher des panics, des mises en veille, des retours et des chemins d'erreur conditionnels via des variables d'environnement.
Une implémentation des failpoints pour Golang. Les failpoints sont utilisés pour ajouter des points de code où des erreurs peuvent être injectées de manière contrôlée par l'utilisateur. Un failpoint est un extrait de code qui n'est exécuté que lorsque le failpoint correspondant est actif.
failpoint-ctl)Compilez failpoint-ctl à partir des sources
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
Injectez des failpoints dans votre programme, par exemple :
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Transformez votre code avec failpoint-ctl enable
Compilez avec go build
Activez les failpoints avec la variable d'environnement GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Remarque : GO_FAILPOINTS ne fonctionne pas avec le type de marqueur InjectCall.
Si vous utilisez go run pour exécuter le test, n'oubliez pas d'ajouter le fichier généré à votre commande, par exemple :
failpoint-toolexec)Compilez failpoint-toolexec à partir des sources
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
Injectez des failpoints dans votre programme, par exemple :
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Utilisez un cache de build séparé pour éviter de mélanger les caches sans failpoint-toolexec, et compilez
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
Activez les failpoints avec la variable d'environnement GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Vous pouvez aussi utiliser go run ou go test, par exemple :
Définir le failpoint dans du code Golang valide, pas dans des commentaires ou autre chose
Un failpoint n'a aucun coût supplémentaire
Le code du failpoint est lisible/modifiable et doit être vérifié par un compilateur
Le code généré par la définition d'un failpoint est facile à lire
Conserve les numéros de ligne identiques aux codes d'injection (plus facile à déboguer)
Prend en charge les tests parallèles avec context.Context
Failpoint
Un failpoint est un extrait de code qui n'est exécuté que lorsque le failpoint correspondant est actif.
La fermeture ne sera jamais exécutée si failpoint.Disable("failpoint-name-for-demo") est exécuté.
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
Fonctions de marqueur
Ce n'est qu'une fonction vide
C'est facile à écrire/lire
Introduit une vérification par le compilateur pour les failpoints qui ne peuvent pas compiler en mode normal si le code du failpoint est invalide
Liste des fonctions de marqueur
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) {}Vous pouvez appeler failpoint.Inject pour injecter un failpoint au site d'appel, où failpoint-name est utilisé pour déclencher le failpoint et failpoint-closure sera développé comme corps de l'instruction IF.
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
Le code converti ressemble à :
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value est la valeur transmise par failpoint.Enable("failpoint-name", "return(5)"), qui peut être ignorée.
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
OU
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
Et le code converti ressemble à :
Injection d'un failpoint dans l'instruction IF INITIALE ou l'expression CONDITIONNELLE
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)
})
}
Le bloc de code ci-dessus générera quelque chose comme ceci :
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)
}
}
Injection d'un failpoint dans l'instruction SELECT pour qu'elle bloque un CASE si le failpoint est actif
Comme vous le voyez ci-dessus, _curpkg_ enveloppe automatiquement le nom original du failpoint dans un appel failpoint.Eval. Vous pouvez considérer _curpkg_ comme une macro qui préfixe automatiquement le chemin du package courant au nom du failpoint. Par exemple,
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 {...}
}
Vous n'avez pas besoin de vous soucier de _curpkg_ dans votre application. Il est généré automatiquement après avoir exécuté failpoint-ctl enable et supprimé avec failpoint-ctl disable.
Comme tous les failpoints d'un package partagent le même espace de noms, nous devons faire attention à éviter les conflits de noms. Voici quelques règles de nommage recommandées pour améliorer cette situation.
Gardez un nom unique dans le sous-package actuel
Utilisez un nom explicite pour le failpoint
Vous pouvez activer les failpoints grâce aux variables d'environnement
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 pour déterminer si un failpoint est actif et exécute le code du failpoint si le failpoint est activé
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) {}Variable d'environnement de failpoint prise en charge
Un failpoint peut être activé en exportant des variables d'environnement selon le modèle suivant, ce qui ressemble beaucoup aux variables SYSCTL du failpoint FreeBSD
Remarque : InjectCall ne peut pas être activé par des variables d'environnement.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
L'argument spécifie l'action à effectuer ; il peut être l'un des suivants :
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
De plus, la fermeture du failpoint peut être une fonction qui prend un context.Context. Vous pouvez personnaliser certaines choses avec context.Context, comme contrôler si un failpoint est actif dans les tests parallèles ou dans d'autres cas. Par exemple,
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Le code converti ressemble à :
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Vous pouvez ignorer context.Context, et cela générera le même code que la version sans contexte ci-dessus. Par exemple,
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Devient
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Vous pouvez utiliser failpoint.InjectCall pour injecter un appel de fonction. Ce type de marqueur ne peut être activé qu'avec failpoint.EnableCall et doit être appelé dans le même processus que le site d'appel InjectCall. Avec ce marqueur, vous pouvez éviter que le code du failpoint ne pollue votre code source. Voir examples.
Vous pouvez contrôler un failpoint grâce à 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))
}
Si vous utilisez un failpoint dans le contexte d'une boucle, vous utiliserez peut-être d'autres fonctions de marqueur.
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")
}
})
}
}
}
Le bloc de code ci-dessus générera le code suivant :
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
}
}
}
}
}
Vous vous demandez peut-être pourquoi nous n'utilisons pas directement label, break, continue et fallthrough au lieu des fonctions de marqueur de failpoint.
Tout symbole inutilisé, comme un identifiant ou une étiquette, n'est pas autorisé en Golang. Ce serait invalide si une étiquette n'était utilisée que dans la fermeture du failpoint. Par exemple,
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 et continue ne peuvent être utilisés que dans le contexte d'une boucle, ce qui n'est pas valide dans le code Golang si on les utilise directement dans une fermeture.
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")
}
}
Le bloc de code ci-dessus générera quelque chose comme ceci :
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")
}
}
Injection d'un failpoint pour étendre dynamiquement les branches 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")
}
Le bloc de code ci-dessus générera quelque chose comme ceci :
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")
}
Failpoints plus complexes