
Biblioteca de injeção de falhas para Go que adiciona failpoints controláveis em tempo de execução para disparar panics, sleeps, returns e caminhos de erro condicionais via variáveis de ambiente.
Uma implementação de failpoints para Golang. Fail points são usados para adicionar pontos de código onde erros podem ser injetados de forma controlada pelo usuário. Um fail point é um trecho de código que só é executado quando o failpoint correspondente está ativo.
failpoint-ctl)Compile failpoint-ctl a partir do código-fonte
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
Injete failpoints no seu programa, por exemplo:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Transforme o seu código com failpoint-ctl enable
Compile com go build
Ative failpoints com a variável de ambiente GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Nota: GO_FAILPOINTS não funciona com marcadores do tipo InjectCall.
Se você usar go run para executar o teste, não se esqueça de adicionar o gerado ao seu comando, por exemplo:
failpoint-toolexec)Compile failpoint-toolexec a partir do código-fonte
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
Injete failpoints no seu programa, por exemplo:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Use um cache de build separado para evitar misturar caches sem o failpoint-toolexec e compile
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
Ative failpoints com a variável de ambiente GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Você também pode usar go run ou go test, por exemplo:
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
Defina failpoint em código Golang válido, não em comentários ou qualquer outra coisa
Failpoint não tem nenhum custo extra
A rotina do failpoint é escrevível/legível e deve ser verificada por um compilador
O código gerado pela definição de failpoint é fácil de ler
Mantenha os números de linha iguais aos dos códigos de injeção (mais fácil de depurar)
Suporte a testes paralelos com context.Context
Failpoint
Um failpoint é um trecho de código que só é executado quando o failpoint correspondente está ativo.
A closure nunca será executada se failpoint.Disable("failpoint-name-for-demo") for executado.
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
Funções de marcador
É apenas uma função vazia
É fácil de escrever/ler
Introduz uma verificação do compilador para failpoints que não podem compilar no modo normal se o código do failpoint for inválido
Lista de funções de marcador
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) {}Você pode chamar failpoint.Inject para injetar um failpoint no local da chamada, onde failpoint-name é usado para acionar o failpoint e failpoint-closure será expandido como o corpo da instrução IF.
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
O código convertido fica assim:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value é o valor passado por failpoint.Enable("failpoint-name", "return(5)"), que pode ser ignorado.
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
OU
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
E o código convertido fica assim:
Injete um failpoint na instrução IF INITIAL ou na expressão CONDITIONAL
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)
})
}
O bloco de código acima gerará algo assim:
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)
}
}
Injete um failpoint na instrução SELECT para fazer com que ela bloqueie um CASE se o failpoint estiver ativo
Como você viu acima, _curpkg_ envolverá automaticamente o nome original do failpoint na chamada failpoint.Eval. Você pode pensar em _curpkg_ como um macro que automaticamente prefixa o caminho do pacote atual ao nome do failpoint. Por exemplo,
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 {...}
}
Você não precisa se preocupar com _curpkg_ na sua aplicação. Ele é gerado automaticamente após executar failpoint-ctl enable e é removido com failpoint-ctl disable.
Como todos os failpoints em um pacote compartilham o mesmo namespace, precisamos ter cuidado para evitar conflitos de nomes. Existem algumas regras de nomenclatura recomendadas para melhorar essa situação.
Mantenha o nome único no subpacote atual
Use um nome autoexplicativo para o failpoint
Você pode ativar failpoints por variáveis de 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 para determinar se um failpoint está ativo e executa o código do failpoint se ele estiver ativado
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) {}Variável de ambiente suportada para failpoint
failpoint pode ser ativado exportando variáveis de ambiente com o seguinte padrão, o que é bem semelhante a variáveis SYSCTL do failpoint do FreeBSD
Nota: InjectCall não pode ser ativado por variáveis de ambiente.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
O argumento especifica qual ação tomar; pode ser um dos seguintes:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
Além disso, a closure do failpoint pode ser uma função que recebe context.Context. Você pode fazer algumas coisas personalizadas com context.Context, como controlar se um failpoint está ativo em testes paralelos ou em outros casos. Por exemplo,
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
O código convertido fica assim:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Você pode ignorar context.Context, e isso gerará o mesmo código da versão sem contexto acima. Por exemplo,
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Torna-se
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Você pode usar failpoint.InjectCall para injetar uma chamada de função. Esse tipo de marcador só pode ser ativado usando failpoint.EnableCall e deve ser chamado no mesmo processo do local da chamada InjectCall. Usando esse marcador, você pode evitar que o código do failpoint polua o seu código-fonte. Veja exemplos.
Você pode controlar um failpoint com 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 você usar um failpoint no contexto de um loop, talvez use outras funções de marcador.
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")
}
})
}
}
}
O bloco de código acima gerará o seguinte 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
}
}
}
}
}
Você pode se perguntar por que não usamos label, break, continue e fallthrough diretamente em vez de usar as funções de marcador de failpoint.
Qualquer símbolo não utilizado, como um identificador ou um rótulo, não é permitido em Golang. Será inválido se algum rótulo for usado apenas na closure do failpoint. Por exemplo,
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 só podem ser usados no contexto de loop, o que não é legal no código Golang se os usarmos diretamente na closure.
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")
}
}
O bloco de código acima gerará algo assim:
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")
}
}
Injete um failpoint para estender dinamicamente os braços do 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")
}
O bloco de código acima gerará algo assim:
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 mais complicados