حل مشكلة: جو روتين ستاك تخطى الحد الأقصى للحجم | لغة جو
كنت أكتب برنامج سطر أوامر (CLI app) لمعالجة وعمل بعض التعديلات التلقائية على البيانات. في هذا الكود ، أحتاج إلى تنفيذ الفانكشن مرات كثيرة حتى أنتهي من معالجة كل سجلات البيانات. عدد السجلات 40,572,219 وهو رقم كبير طبعاً.
كتبت الفانكشن بهذا الشكل.
func dbAutoProcessData(db *sql.DB, inId int) {
rows, err := db.Query(`SELECT * FROM tbl WHERE info LIKE '%something%' AND id > ? LIMIT 1;`, inId)
if err != nil {
log.Fatal(err)
}
// add it manually before re-calling the func
//defer rows.Close()
var id, uid int
var info string
for rows.Next() {
if err := rows.Scan(&id, &uid, &info); err != nil {
log.Fatal(err)
}
}
// some data processing
updatedInfo := info
_, err = db.Exec(`UPDATE tbl SET info = ? WHERE id = ?;`, updatedInfo, id)
if err != nil {
log.Fatal(err)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
rows.Close() // close rows first
// re-run the same function
dbAutoProcessData(db, id)
}
شغلت هذا البرنامج لساعات. لاحظت توقف البرنامج مع رسالة خطأ تبدأ بهذه السطور.
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc020512380 stack=[0xc020512000, 0xc040512000]
fatal error: stack overflow
مع وجود stack trace به معلومات أكثر عن تفاصيل الخطأ ومكان حدوثه في الكود.
المشكلة ببساطة هي أنني أنفذ نفس الفانكشن من داخل نفسها كثيراً جداً (recursion). والـ stack إمتلأ بعناوين وبيانات الفانكشنز السابقة التي استدعت الفانكشن الحالية ، وتعدت الحد المسموح (overflowed). لتجنب تخطي الحد الأقصى لحجم الستاك (stack) للـ جو روتين (goroutine), نقوم بكتابة الكود بطريقة لا تستدعي نفسها (iterative).
لحسن الحظ، لا نحتاج إلى استخدام فانشكن تستدعي نفسها مرة بعد الأخرى. يُمكن كتابة الكود في شكل حلقة تكرارية لا نهائية (مستمرة) كما يلي.
var id int = 1
for id > 0 {
id = dbAutoProcessData(db, id)
time.Sleep(5 * time.Millisecond)
}
كتبت حلقة تكرارية بـ for واستدعيت الفانكشن لتنفيذها ؛ وهذه الفانكشن تأخذ id وترجع id جديد. ثم تتوقف لمدة ٥ مللي ثانية. ثم يتم إعادة تنفيذ الحلقة التكرارية بقيمة الـ id الجديد. لو الـ id أكبر من صفر ، فإن السجل موجود في قاعدة البيانات. أما إن كان الـ id يساوي صفر ، فإنه لا يوجد سجل في نتائج الاستعلام في قاعدة البيانات، وبذلك تتوقف الحلقة التكرارية. وهذا هو المطلوب تنفيذه.
الفانكشن dbAutoProcessData()
بعد التعديل عليها أصبحت كما يلي.
func dbAutoProcessData(db *sql.DB, inId int) int {
rows, err := db.Query(`SELECT * FROM tbl WHERE info LIKE '%something%' AND id > ? LIMIT 1;`, inId)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var id, uid int
var info string
for rows.Next() {
if err := rows.Scan(&id, &uid, &info); err != nil {
log.Fatal(err)
}
}
// some data processing
updatedInfo := info
_, err = db.Exec(`UPDATE tbl SET info = ? WHERE id = ?;`, updatedInfo, id)
if err != nil {
log.Fatal(err)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
return id
}
التغييرات بإختصار هي:
- الفانكشن ترجع لنا رقم id لآخر سجل تم معالجة بياناته
- نستخدم كود
defer
بالشكل المعتاد، دون الحاجة إلى كتابته في نهاية الفانكشن يدوياً
واجهت هذا الخطأ ، وكتبت هذا المقال البسيط لتوثيق المشكلة والحل لكي تستفيد منه إن كنت تواجه نفس المشكلة أو مشكلة مشابهة. أتمنى أن تكون قد استفدت من الموضوع. إن أردت معرفة الموضوعات الجديدة بعد نشرها مباشرةً على موقع أبانوب حنا ، تابعني على يوتيوب و تيليجرام ، و فيسبوك ، و واتساب .