
Go용 장애 주입 라이브러리로, 환경 변수를 통해 런타임에 제어 가능한 실패 지점을 추가하여 패닉, 지연, 반환 및 조건부 오류 경로를 트리거합니다.
Golang용 failpoints 구현입니다. Fail point는 사용자가 제어하는 방식으로 오류를 주입할 수 있는 코드 지점을 추가하는 데 사용됩니다. Fail point는 해당 failpoint가 활성화된 경우에만 실행되는 코드 스니펫입니다.
failpoint-ctl 사용)소스에서 failpoint-ctl 빌드
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
프로그램에 failpoint를 주입하세요, 예:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
failpoint-ctl enable로 코드를 변환하세요
go build로 빌드하세요
GO_FAILPOINTS 환경 변수로 failpoint를 활성화하세요
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
참고: GO_FAILPOINTS는 InjectCall 유형의 마커에서는 동작하지 않습니다.
테스트 실행에 go run을 사용하는 경우, 생성된 binding__failpoint_binding__.go를 명령에 포함하는 것을 잊지 마세요. 예:
failpoint-toolexec 사용)소스에서 failpoint-toolexec 빌드
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
프로그램에 failpoint를 주입하세요, 예:
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
GO_FAILPOINTS 환경 변수로 failpoint를 활성화하세요
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
go run이나 go test를 사용할 수도 있습니다. 예:
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
유효한 Golang 코드로 failpoint를 정의하세요. 주석이나 다른 것이 아니라.
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)
})
마커 함수
그것은 단지 빈 함수입니다
쓰기/읽기가 쉽습니다
failpoint에 대한 컴파일러 검사를 도입합니다. 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) {}func Goto(label string) {}func Continue(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")
})
그리고 변환된 코드는 다음과 같습니다:
if _, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test")
}
IF INITIAL 문 또는 CONDITIONAL 표현식에 failpoint 주입
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)
}
}
SELECT 문에 failpoint를 주입하여 failpoint가 활성화되면 하나의 CASE를 차단하도록 합니다
위에서 보았듯이 _curpkg_는 failpoint.Eval 호출에서 원래 failpoint 이름을 자동으로 감쌉니다. _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로 삭제됩니다.
패키지의 모든 failpoint가 동일한 네임스페이스를 공유하므로 이름 충돌을 피하기 위해 주의해야 합니다. 이 상황을 개선하기 위해 권장되는 몇 가지 명명 규칙이 있습니다.
현재 하위 패키지에서 이름을 고유하게 유지하세요
failpoint에 자명한 이름을 사용하세요
환경 변수로 failpoint를 활성화할 수 있습니다
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을 호출하는 IF 문으로 재작성되어 failpoint가 활성화되었는지 확인하고, failpoint가 활성화된 경우 failpoint 코드를 실행합니다
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
func Fallthrough() {}func Return(results ...interface{}) {}func Label(label string) {}지원되는 failpoint 환경 변수
failpoint는 다음 패턴의 환경 변수를 export하여 활성화할 수 있습니다. 이는 freebsd failpoint SYSCTL VARIABLES와 매우 유사합니다.
참고: InjectCall은 환경 변수로 활성화할 수 없습니다.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
<type> 인자는 수행할 동작을 지정하며, 다음 중 하나일 수 있습니다:
또한 failpoint 클로저는 context.Context를 받는 함수일 수 있습니다. 병렬 테스트나 다른 경우에 failpoint가 활성화될지 제어하는 것처럼 context.Context로 몇 가지 맞춤 작업을 할 수 있습니다. 예를 들어,
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.WithHook으로 failpoint를 제어할 수 있습니다
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
}
}
}
}
}
왜 failpoint 마커 함수를 사용하는 대신 label, break, continue, fallthrough를 직접 사용하지 않는지 의문이 들 수 있습니다.
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")
}
}
SWITCH CASE 분기를 동적으로 확장하도록 failpoint 주입
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")
}
더 복잡한 failpoint