
Библиотека внедрения сбоев для Go, добавляющая управляемые во время выполнения точки отказа для запуска паник, задержек, возвратов и условных путей ошибок через переменные окружения.
Реализация failpoints для Golang. Fail points используются для добавления точек в коде, где ошибки могут быть внедрены под управлением пользователя. Fail point — это фрагмент кода, который выполняется только тогда, когда соответствующий failpoint активен.
failpoint-ctl)Соберите failpoint-ctl из исходников
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
Внедрите failpoints в вашу программу, например:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Преобразуйте ваш код с помощью failpoint-ctl enable
Соберите с помощью go build
Включите failpoints с помощью переменной окружения GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Примечание: GO_FAILPOINTS не работает с маркером типа InjectCall.
Если вы используете go run для запуска теста, не забудьте добавить сгенерированный в вашу команду, например:
failpoint-toolexec)Соберите failpoint-toolexec из исходников
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
Внедрите failpoints в вашу программу, например:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
Используйте отдельный кэш сборки, чтобы избежать смешивания кэшей без failpoint-toolexec, и выполните сборку
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
Включите failpoints с помощью переменной окружения GO_FAILPOINTS
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
Вы также можете использовать go run или go test, например:
Определяйте failpoint в валидном коде Golang, а не в комментариях или где-либо ещё
Failpoint не имеет дополнительных затрат
Код failpoint можно писать и читать, и он должен проверяться компилятором
Сгенерированный код по определению failpoint легко читается
Сохраняйте номера строк такими же, как у кода с внедрением (легче отлаживать)
Поддержка параллельных тестов с context.Context
Failpoint
Failpoint — это фрагмент кода, который выполняется только тогда, когда соответствующий failpoint активен.
Замыкание никогда не будет выполнено, если выполняется 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)
})
Функции-маркеры
Это просто пустая функция
Её легко писать и читать
Добавляет проверку компилятором для failpoints: код не скомпилируется в обычном режиме, если код failpoint невалиден
Список функций-маркеров
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) {}Вы можете вызвать failpoint.Inject, чтобы внедрить failpoint в место вызова, где failpoint-name используется для запуска failpoint, а failpoint-closure будет развёрнут как тело оператора IF.
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
Преобразованный код выглядит так:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
failpoint.Value — это значение, которое передаётся через failpoint.Enable("failpoint-name", "return(5)"); его можно игнорировать.
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
ИЛИ
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
Преобразованный код выглядит так:
Внедрение failpoint в оператор инициализации IF или условное выражение
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)
})
}
Приведённый выше блок кода сгенерирует что-то вроде этого:
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)
}
}
Внедрение failpoint в оператор SELECT, чтобы он блокировал один из CASE, если failpoint активен
Как вы видите выше, _curpkg_ автоматически оборачивает исходное имя failpoint в вызов failpoint.Eval. Можно думать о _curpkg_ как о макросе, который автоматически добавляет путь текущего пакета к имени failpoint. Например,
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 {...}
}
Вам не нужно заботиться о _curpkg_ в вашем приложении. Он автоматически генерируется после запуска failpoint-ctl enable и удаляется с помощью failpoint-ctl disable.
Поскольку все failpoints в пакете используют одно и то же пространство имён, нужно быть внимательными, чтобы избежать конфликтов имён. Существуют некоторые рекомендуемые правила именования, улучшающие эту ситуацию.
Сохраняйте имя уникальным в текущем подпакете
Используйте самодокументируемое имя для failpoint
Вы можете включить failpoints с помощью переменных окружения
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, чтобы определить, активен ли failpoint, и выполняет код failpoint, если он включён
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) {}Поддерживаемая переменная окружения failpoint
failpoint можно включить с помощью экспортируемых переменных окружения по следующему шаблону, который очень похож на SYSCTL-переменные failpoint FreeBSD
Примечание: InjectCall нельзя включить с помощью переменных окружения.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
Аргумент определяет, какое действие выполнить; он может быть одним из:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
Кроме того, замыкание failpoint может быть функцией, принимающей context.Context. Вы можете делать с context.Context различные настраиваемые вещи, например управлять тем, активен ли failpoint в параллельных тестах или в других случаях. Например,
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Преобразованный код выглядит так:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Вы можете игнорировать context.Context, и это сгенерирует тот же код, что и версия без контекста выше. Например,
failpoint.InjectContext(nil, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
Станет
if val, _err_ := failpoint.EvalContext(nil, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
Вы можете использовать failpoint.InjectCall для внедрения вызова функции. Этот тип маркера можно включить только с помощью failpoint.EnableCall, и он должен вызываться в том же процессе, что и место вызова InjectCall. Используя этот маркер, вы можете избежать загрязнения исходного кода кодом failpoint. См. examples.
Вы можете управлять failpoint с помощью 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))
}
Если вы используете failpoint в контексте цикла, возможно, вам понадобятся другие функции-маркеры.
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")
}
})
}
}
}
Приведённый выше блок кода сгенерирует следующий код:
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
}
}
}
}
}
Возможно, вы сомневаетесь, почему мы не используем label, break, continue и fallthrough напрямую, вместо того чтобы использовать функции-маркеры failpoint.
В Golang не допускаются неиспользуемые символы, такие как идентификатор или метка. Код будет невалидным, если какая-то метка используется только в замыкании failpoint. Например,
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 и continue могут использоваться только в контексте цикла, что недопустимо в коде Golang, если использовать их в замыкании напрямую.
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")
}
}
Приведённый выше блок кода сгенерирует что-то вроде этого:
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")
}
}
Внедрение failpoint для динамического расширения веток 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")
}
Приведённый выше блок кода сгенерирует что-то вроде этого:
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