קריאה אסינכרונית משרת HTTP בGO
-
באמת נראה תמוה, ניסיתי גם לדבג, שים לב שזה קורה רק בחבילה הזו של פיבר, בשרת HTTP המובנה זה לא יקרה.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path[1:] go myfunc(path) }) http.ListenAndServe(":4000", nil)
מה שנראה, שהמשתנה number שאמור להיות פרטי פר בקשה, הופך להיות רפרנס ומתעדכן בכל בקשה, ניסיתי בכל בקשה לטעון אותו למערך גלובלי, וגיליתי שהבקשות האחרונות משנות את האיברים הראשונים של המערך.
var arr []string ... number := c.Params("number") arr = append(arr, number) go myfunc(number) c.Send(number)
אני לא מספיק מכיר את גו, לא יודע איך זה קורה והאם זה מכוון או כשל בחבילה. אם אתה שואל בסטאק אנא עדכן
-
@יוסף-בן-שמעון
אם זה בעיה רק בשרת של פיבר
צריך לפתוח פניה בגיטהאב
כי כנראה זה בעיה קריטית בשרת
אני יתחיל לנסות לנסח את הפניה כראוי
ויעלה את זה קודם כל באן בשביל לבדוק שאני עושה את זה נכון -
אני לא מבין איך זה קשור לפייבר. אני גם לא יודע לומר מה קורה. אם המבנה של התשובה בכל בקשה משותף גם השורה המודפסת הראשונה הייתה צריכה להידרס, מוכח שהפונקציה מקבלת בכל בקשה ערך שונה.
ואם זה קשור לפייבר זה מטיל צל על כל הgo, כי הטעות הזו גורמת לקוד אחר לחלוטין לעבור באופן בלתי צפוי. -
הצלחתי לשחזר את הבאג.
הפייבר עושה שימוש באותו ctx, כנראה עם נעילה (mutex וכדומה) כך שברור שאין שימוש בקודם. הבעיה שההשהיה מנטרלת את הנעילה, כלומר הפוקנציה באמת נגמרה (והיא לא ביצעה עוד את השורה המושהית) ופייבר ממשיך לבקשה הבאה.
לגבי העתקת הערך לפונקציה שלכאורה "מנתקת" את הערך ע"י "העתקתו", אז הנה למדנו משהו על go. אלא שאני לא מצליח "ללמוד" פה, ואשמח להסבר על זה.
(בדוגמה שלי להלן יש המחשה לבעיה אבל שמה גם ההדפסה הראשונה שגויה בגלל שאין נעילה בכלל גם בהתחלה, משא"כ בבקשות הhttp של פייבר שיש נעילה עד לסיום הפונקציה המטפלת של הget. משא"כ ההדפסה הראשונה משוחררת מנעילה ומבוצעת מיידית).https://play.golang.org/p/OLboA4D9qng
מה שקורה פה זה עם לולאה, מעבירים מצביע למבנה לפונקציה, שהיא לכאורה מנתקת את הערך ע"י השמה למשתנה אבל זה לא עוזר.
-
@yossiz אמר בקריאה אסינכרונית משרת HTTP בGO:
@nigun אגב, פרוייקט פייבר נראה מאוד צעיר ואולי לא מוכן לפרודקשן
אתה צודק זה פרוייקט בן חודשיים סך הכל
כנראה הם ישמחו אם אני ידווח להם על הבאג
אבל אני לא יודע לנסח את הכותרת
אתה יכול עזור לי בזה קצת? (לפחות לפתיח את ההמשך אני יכול לכתוב לבד) -
@dovid
קודם כל אני מופתע שאתה מונח טוב בGO (מספיק בשביל לדבג)
בדקתי עכשיו מה קורה אם אני משתמש בשרת fasthttp שעליו פיבר מבוסס (הוא גם משתמש בctx)
ושם התוצאה תקינה
כנראה שהבעיה היא בפיברpackage main import ( "flag" "fmt" "log" "time" "github.com/valyala/fasthttp" ) var ( addr = flag.String("addr", ":3000", "TCP address to listen to") compress = flag.Bool("compress", false, "Whether to enable transparent response compression") ) func main() { flag.Parse() h := requestHandler if *compress { h = fasthttp.CompressHandler(h) } if err := fasthttp.ListenAndServe(*addr, h); err != nil { log.Fatalf("Error in ListenAndServe: %s", err) } } func requestHandler(ctx *fasthttp.RequestCtx) { go myfunc(string(ctx.Path())) fmt.Fprintf(ctx, "Requested path is %q\n", ctx.Path()) } func myfunc(number string) { fmt.Printf("number is %s \n", number) time.Sleep(1 * time.Second) fmt.Printf("number is now %s \n", number) }
-
פוסט זה נמחק!
-
מצאתי את מקור הבעיה
https://github.com/gofiber/fiber/blob/efe89d243de6d8cf964581f5832b4341e2baa427/utils.go#L128-L130
הפונקציה הנ"ל בשימוש רחב בקובץ context.go
וזה הרי כבר מתועד בתיעוד הרשמי של fasthttp שאסור להשתמש ב-ctx
אחרי שה-handler חוזר. וכן בהערה כאן, ועיין עוד: https://github.com/valyala/fasthttp/issues/146אם fiber לא היו מחפים על השימוש ב-byte array על ידי המרה בצורה לא בטוחה למחרוזת היה יותר קל למצוא את הבעיה.
@dovid אמר בקריאה אסינכרונית משרת HTTP בGO:
אז הנה למדנו משהו על go
לפי דרכי למדנו שלא כדאי להשתמש ב-
unsafe
בלי להבין טוב טוב את ההשלכות... (דבר פשוט מאוד...) -
@dovid אם הבנתי נכון מה שאתה שואל אז התשובה היא שמשתנה מסוג string ב-GO לא מכיל את הבייטים של המחרוזת, הוא רק מצביע למקום בזכרון. לכן על אף שלכל קריאה ל-myfunc מיוצר string חדש, אבל כולם מצביעים על אותו איזור בזיכרון.
עיין כאן: https://www.reddit.com/r/golang/comments/57ica9 -
@yossiz אני שואל איך יכול להיות שהnumber נהרס ע"י גישה ישירה לזיכרון לtype של הctx.
הרי הוא עותק. שהרי כשאתה משים משתנה מחרוזת למשתנה אחר זה כן כתובת שונה ותוכן בלתי תלוי, אז מה זה שונה.
בכל מקרה גם בשימוש בunsafe לא מתקבל על הדעת שיהיה דרך בשפה עילית מודרנית לשנות ערכים של זיכרון שלא באחריות הקוד שלי, אני ממש לא מבין איך זה ייתכן.
אני מבין שאני מפספס משהו, מה קורה כעושים number:= ctx.XXX. -
@dovid אמר בקריאה אסינכרונית משרת HTTP בGO:
שהרי כשאתה משים משתנה מחרוזת למשתנה אחר זה כן כתובת שונה ותוכן בלתי תלוי, אז מה זה שונה
הבייטים של המחרוזת הם אותם בייטים, אממה הכתובת של המשתנה הוא לא הכתובת של הבייטים אלא הכתובת של המצביע. (ומכיון ש-string ב-GO הוא immutable - בלי משחקים ב-unsafe - זה בטוח לגמרי, אבל ברגע שמשחקים עם הבייטים עצמם על ידי קוד unsafe כאן יתחילו בעיות)
תעשה ניסוי קטן:
package main import ( "fmt" "unsafe" ) func main() { b := []byte("ABC") s := *(*string)(unsafe.Pointer(&b)) s2 := s s3 := string(b) // this is safe (it makes a copy of the original bytes) b[0] = 'X' b[1] = 'Y' b[2] = 'Z' fmt.Println(s) fmt.Println(s2) fmt.Println(s3) }
https://play.golang.org/p/EpPwsoM-z8g
זה פחות או יותר מה שקורה בתוכנה של @nigun
-
@yossiz
נראה לי שעכשיו הבנתי קצת מה קורה כאן
המשתנה number הוא מערך של bytes
והוא מצביע על מערך אחר של bytes (שנמצא בCtx ???)
כשאני מדפיס בפעם הראשונה את number זה מיד אחרי שקראתי לfunc(c *fiber.Ctx)
ולפני שנעשה קריאה חדשה לfunc(c *fiber.Ctx)
ולכן הוא מדפיס את הערך האחרון (שהוא הנכון)
אבל בפעם השניה הערך הוא המערך במקורי הוא[57 49]
שזה "19" (כי זה היה הקריאה האחרונה)
אבל המצביע number נשאר עם מערך של איבר אחד
לכן הוא ממיר לסטרינג רק את האיבר הראשון במערך ולכן הוא מדפיס "1"
עד שהוא מגיע למערכים בני שני איברים (כי בקריאה המקורית זה היה עם שני ספרות) ואז הוא מדפיס "19"
והסיבה שהוא נשאר מצביע ולא משתנה חדש זה בגלל שהוא נוצר עם unsafe (שנשאר להצביע תמיד על הכתובת הראשונה)`