
Go के लिए फॉल्ट-इंजेक्शन लाइब्रेरी जो एनवायरनमेंट वेरिएबल्स के माध्यम से पैनिक्स, स्लीप्स, रिटर्न्स और कंडीशनल एरर पाथ को ट्रिगर करने के लिए रनटाइम-नियंत्रणीय फेलपॉइंट्स जोड़ती है।
Golang के लिए failpoints का एक कार्यान्वयन। फेल पॉइंट्स (fail points) का उपयोग कोड पॉइंट्स जोड़ने के लिए किया जाता है जहाँ त्रुटियाँ उपयोगकर्ता-नियंत्रित तरीके से इंजेक्ट की जा सकती हैं। फेल पॉइंट एक कोड स्निपेट है जो केवल तब निष्पादित होता है जब संबंधित 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 के साथ बनाएँ
GO_FAILPOINTS पर्यावरण चर के साथ 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
GO_FAILPOINTS पर्यावरण चर के साथ failpoints सक्षम करें
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
Failpoint को मान्य Golang कोड में परिभाषित करें, टिप्पणियों या किसी और चीज़ में नहीं
Failpoint की कोई अतिरिक्त लागत नहीं होती
Failpoint रूटीन लिखने/पढ़ने योग्य है और कंपाइलर द्वारा जाँचा जाना चाहिए
Failpoint परिभाषा द्वारा जनरेट किया गया कोड पढ़ने में आसान है
इंजेक्टिंग कोड के साथ लाइन नंबर समान रखें (डिबग करना आसान)
context.Context के साथ समानांतर परीक्षणों का समर्थन करें
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)
})
Marker functions
यह सिर्फ एक खाली फ़ंक्शन है
यह लिखना/पढ़ना आसान है
Failpoints के लिए एक कंपाइलर जाँच प्रस्तुत करता है जो failpoint कोड अमान्य होने पर नियमित मोड में संकलित नहीं हो सकता
Marker funtion list
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 इंजेक्ट करने के लिए failpoint.Inject को कॉल कर सकते हैं, जहाँ 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")
})
OR
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
IF इनिशियल स्टेटमेंट या 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 के साथ हटा दिया जाता है।
चूँकि एक पैकेज में सभी 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 सक्षम है तो failpoint कोड निष्पादित करता है।
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) {}Supported failpoint environment variable
failpoint को निम्न पैटर्न वाले निर्यात पर्यावरण चर द्वारा सक्षम किया जा सकता है, जो freebsd failpoint SYSCTL VARIABLES के समान है।
नोट: 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 कोड को अपने सोर्स कोड को प्रदूषित करने से बचा सकते हैं। उदाहरण देखें।
आप 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")
}
More complicated failpoints