
مكتبة حقن أعطال للغة Go تضيف نقاط فشل قابلة للتحكم في وقت التشغيل لإحداث حالات panic، ونوم (sleeps)، وإرجاعات (returns)، ومسارات أخطاء شرطية عبر متغيرات البيئة.
تنفيذ لنقاط الفشل failpoints بلغة Golang. تُستخدم نقاط الفشل لإضافة نقاط في الكود يمكن عندها حقن الأخطاء بطريقة يتحكم بها المستخدم. نقطة الفشل هي مقتطف برمجي لا يُنفَّذ إلا عندما تكون نقطة الفشل المقابلة نشطة.
failpoint-ctl)ابنِ failpoint-ctl من المصدر
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
احقن نقاط الفشل في برنامجك، مثال:
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
حوّل الكود الخاص بك باستخدام failpoint-ctl enable
ابنِ باستخدام go build
فعّل نقاط الفشل باستخدام متغير البيئة 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
احقن نقاط الفشل في برنامجك، مثال:
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
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 صالح، وليس في تعليقات أو أي شيء آخر
لا تكلف نقطة الفشل أي عبء إضافي
روتين نقطة الفشل قابل للكتابة/القراءة ويجب أن يفحصه المترجم
الكود المُولَّد من تعريف نقطة الفشل سهل القراءة
الحفاظ على نفس أرقام الأسطر كما في الأكواد المُحقنة (أسهل للتنقيح)
دعم الاختبارات المتوازية باستخدام context.Context
نقطة الفشل
نقطة الفشل هي مقتطف برمجي لا يُنفَّذ إلا عندما تكون نقطة الفشل المقابلة نشطة.
لن يُنفَّذ الإغلاق أبدًا إذا تم تنفيذ 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)
})
دوال العلامات
هي مجرد دوال فارغة
سهلة الكتابة/القراءة
توفير فحص مترجم لنقاط الفشل التي قد لا تُترجم في الوضع العادي إذا كان كود نقطة الفشل غير صالح
قائمة دوال العلامات
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) {}يمكنك استدعاء failpoint.Inject لحقن نقطة فشل في موقع الاستدعاء، حيث يُستخدم failpoint-name لتفعيل نقطة الفشل وسيتم توسيع 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")
}
حقن نقطة فشل في جملة INIT التالية لـ 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)
}
}
حقن نقطة فشل في جملة SELECT لحجب إحدى حالات CASE إذا كانت نقطة الفشل نشطة
كما ترى أعلاه، يقوم _curpkg_ تلقائيًا بلف اسم نقطة الفشل الأصلي في استدعاء failpoint.Eval.
يمكنك اعتبار _curpkg_ ككليشيه يقوم تلقائيًا بإضافة مسار الحزمة الحالية إلى اسم نقطة الفشل. على سبيل المثال،
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.
نظرًا لأن جميع نقاط الفشل في حزمة واحدة تشترك في نفس مساحة الأسماء، يجب أن نكون حريصين على تجنب تعارض الأسماء. هناك بعض قواعد التسمية الموصى بها لتحسين هذا الوضع.
حافظ على الاسم فريدًا في الحزمة الفرعية الحالية
استخدم اسمًا واضحًا بذاته لنقطة الفشل
يمكنك تفعيل نقاط الفشل عبر متغيرات البيئة
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 لتحديد ما إذا كانت
نقطة الفشل نشطة وتنفذ كود نقطة الفشل إذا كانت مفعّلة
binding__failpoint_binding__.goGO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
func Continue(label ...string) {}func Fallthrough() {}func Return(results ...interface{}) {}func Label(label string) {}متغير بيئة نقطة الفشل المدعوم
يمكن تفعيل نقطة الفشل بتصدير متغيرات بيئة بالنمط التالي، وهو مشابه جدًا لمتغيرات freebsd failpoint SYSCTL VARIABLES
ملاحظة: لا يمكن تفعيل InjectCall عبر متغيرات البيئة.
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
وسيطة تحدد الإجراء الذي سيتم اتخاذه؛ يمكن أن تكون واحدة من:
أيضًا، يمكن أن يكون إغلاق نقطة الفشل دالة تأخذ context.Context. يمكنك القيام بأشياء مخصصة باستخدام 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. باستخدام هذه العلامة، يمكنك تجنب تلويث كود المصدر بكود نقطة الفشل. راجع examples.
يمكنك التحكم في نقطة فشل باستخدام 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.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 مباشرة بدلاً من استخدام دوال علامات نقطة الفشل.
أي رمز غير مستخدم مثل معرف أو تسمية غير مسموح به في Golang. سيكون غير صالح إذا كانت بعض التسميات مستخدمة فقط في إغلاق نقطة الفشل. على سبيل المثال،
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 ديناميكيًا
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")
}
نقاط فشل أكثر تعقيدًا