
Biblioteca de inyección de fallos para Go que añade puntos de fallo controlables en tiempo de ejecución para provocar pánicos, suspensiones, retornos y rutas de error condicionales mediante variables de entorno.
Una implementación de failpoints para Golang. Los failpoints se utilizan para añadir puntos de código donde se pueden inyectar errores de forma controlada por el usuario. Un failpoint es un fragmento de código que solo se ejecuta cuando el failpoint correspondiente está activo.
failpoint-ctl)Compila failpoint-ctl desde el código fuente
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
Inyecta failpoints en tu programa, por ejemplo:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Transforma tu código con failpoint-ctl enable
Compila con go build
Activa los failpoints con la variable de entorno GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Nota: GO_FAILPOINTS no funciona con el tipo de marcador InjectCall.
Si usas go run para ejecutar el test, no olvides añadir el archivo generado a tu comando, así:
failpoint-toolexec)Compila failpoint-toolexec desde el código fuente
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
Inyecta failpoints en tu programa, por ejemplo:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Usa un caché de compilación separado para evitar mezclar cachés sin failpoint-toolexec, y compila
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
Activa los failpoints con la variable de entorno GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
También puedes usar go run o go test, por ejemplo:
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
Define el failpoint en código Golang válido, no en comentarios ni en nada más
El failpoint no tiene ningún costo adicional
La rutina del failpoint es escribible/legible y debe ser verificada por un compilador
El código generado por la definición del failpoint es fácil de leer
Mantiene los mismos números de línea que los códigos de inyección (más fácil de depurar)
Soporta pruebas paralelas con context.Context
Failpoint
Un failpoint es un fragmento de código que solo se ejecuta cuando el failpoint correspondiente está activo.
El cierre nunca se ejecutará si se ejecuta 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)
})
Funciones marcadoras
Son solo funciones vacías
Son fáciles de escribir/leer
Introducen una verificación del compilador para los failpoints que no se pueden compilar en el modo normal si el código del failpoint es inválido
Lista de funciones marcadoras
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) {}Puedes llamar a failpoint.Inject para inyectar un failpoint en el punto de llamada, donde failpoint-name se
usa para disparar el failpoint y failpoint-closure se expandirá como el cuerpo de la sentencia IF.
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
El código convertido se ve así:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value es el valor que se pasa mediante failpoint.Enable("failpoint-name", "return(5)"),
el cual se puede ignorar.
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
O
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
Y el código convertido se ve así:
Inyectar un failpoint en la sentencia IF INITIAL o en la expresión CONDICIONAL
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)
})
}
El bloque de código anterior generará algo como esto:
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)
}
}
Inyectar un failpoint en la sentencia SELECT para bloquear un CASE si el failpoint está activo
Como viste arriba, _curpkg_ envolverá automáticamente el nombre original del failpoint en la llamada a failpoint.Eval.
Puedes pensar en _curpkg_ como una macro que automáticamente antepone la ruta del paquete actual al nombre del failpoint. Por ejemplo:
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 {...}
}
No necesitas preocuparte por _curpkg_ en tu aplicación. Se genera automáticamente después de ejecutar failpoint-ctl enable
y se elimina con failpoint-ctl disable.
Debido a que todos los failpoints en un paquete comparten el mismo espacio de nombres, debemos tener cuidado de evitar conflictos de nombres. Hay algunas reglas de nomenclatura recomendadas para mejorar esta situación.
Mantén el nombre único dentro del subpaquete actual
Usa un nombre autoexplicativo para el failpoint
Puedes habilitar failpoints mediante variables de entorno
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 para determinar si un
failpoint está activo y ejecuta el código del failpoint si está habilitado
binding__failpoint_binding__.goGO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
func Goto(label string) {}func Continue(label ...string) {}func Fallthrough() {}func Return(results ...interface{}) {}func Label(label string) {}Variables de entorno de failpoint soportadas
Los failpoints se pueden habilitar exportando variables de entorno con el siguiente patrón, que es bastante similar a las VARIABLES SYSCTL de failpoints en FreeBSD
Nota: InjectCall no se puede habilitar mediante variables de entorno.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
El argumento especifica qué acción tomar; puede ser uno de:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
Además, el cierre del failpoint puede ser una función que reciba context.Context. Puedes
hacer cosas personalizadas con context.Context, como controlar si un failpoint está
activo en pruebas paralelas u otros casos. Por ejemplo:
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
El código convertido se ve así:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Puedes ignorar context.Context, y esto generará el mismo código que la versión sin contexto anterior. Por ejemplo:
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Se convierte en
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Puedes usar failpoint.InjectCall para inyectar una llamada a función. Este tipo de marcador solo se puede habilitar usando failpoint.EnableCall y debe llamarse en el mismo proceso que el punto de llamada de InjectCall. Con este marcador, puedes evitar que el código del failpoint contamine tu código fuente. Ver ejemplos.
Puedes controlar un failpoint con 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 usas un failpoint en el contexto de un bucle, quizás necesites usar otras funciones marcadoras.
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")
}
})
}
}
}
El bloque de código anterior generará el siguiente código:
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
}
}
}
}
}
Quizás te preguntes por qué no usamos label, break, continue y fallthrough directamente
en lugar de usar funciones marcadoras de failpoint.
Cualquier símbolo no utilizado, como un identificador o una etiqueta, no está permitido en Golang. Será inválido si alguna etiqueta solo se usa en el cierre del failpoint. Por ejemplo:
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 y continue solo se pueden usar en el contexto de un bucle, lo cual no es legal en código Golang
si los usamos directamente en un cierre.
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")
}
}
El bloque de código anterior generará algo como esto:
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")
}
}
Inyectar un failpoint para extender dinámicamente los brazos de un 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")
}
El bloque de código anterior generará algo como esto:
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 más complicados