上個月剛好是go語言9週年,忽然發現入坑go語言也兩年了,把最近一次遇到的bug分享一下,後面有時間再把這兩年的積累慢慢倒出來。
着急解決問題的直接點上面“解決方案”
排錯過程
功能描述:點擊項目名稱切換項目。
實現邏輯:前端調用後端切換項目接口,後端更新session中的項目ID,前端收到返回後刷新頁面。
問題描述:點擊項目名稱,等待刷新後出現原項目頁面。
我在這首先是開F12看下前端傳的參數有沒有問題,當然如果真是參數就不會有這篇文章了。但是這裏有點問題的是,瀏覽器看不到我返回的數據,而postman可以。不過雖然看不到響應體還有響應頭可以搞事情,於是在header裏面加log給回前端,又是一切正常……
但是頁面刷新的第一個接口所帶的session中,確實是切換前的項目。那麼問題來了,切換項目的handler已經把session中的項目ID改了,這個不管是斷點還是F12都已經驗證;而刷新後的第一個接口調到後端,又從session裏拿到原項目的ID,而F12中在這兩者之間又只有靜態資源請求,這是怎麼回事。
這裏說明一下,爲了高可用部署,session是放在redis上集羣共享的。於是就可以在redis上看看session到底啥樣。
於是redis-cli用sessionID來get一下,看到項目ID確實是舊項目的,那麼問題又來了,切換項目的handler明明更新了session且沒有error返回,redis裏的session到底發生了什麼?
這裏提一下,redis沒有history之類的操作,但是有monitor可以提供類似log的作用。於是開着monitor操作了一波後發現,session經歷了兩次修改:原項目ID->新項目ID->原項目ID。在排除了有人同時操作的可能後,session還是經歷了兩次修改。
於是進到macaron源碼中,發現給session的set操作是這樣的:
// Set sets value to given key in session.
func (s *RedisStore) Set(key, val interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
s.data[key] = val
return nil
}
也就是說這一步只是存到內存,而沒有發給redis,不過在它附近發現了這個:
// Release releases resource and save data to provider.
func (s *RedisStore) Release() error {
data, err := session.EncodeGob(s.data)
if err != nil {
return err
}
return s.c.SetEx(s.prefix+s.sid, s.duration, string(data)).Err()
}
然後在return
前加了斷點,操作一下後發現果然執行了兩次,難怪在redis的monitor中看到兩次修改。分別在調用棧裏找請求URL,發現除了switch
正常更新session裏的項目ID外,還有一個state
請求。
這裏插一句說明一下,state
接口是一個websocket
接口,頁面刷新時重建連接。
但是這個F12上看,頁面刷新後第一個請求是info
啊,哪來的state
呢?其實這是因爲刷新操作導致原頁面的websocket
斷開而走到這裏的。
到這就有必要先捋一下macaron裏session這部分的代碼了,在pkg/go-macaron/session/session.go:Sessioner
方法會返回一個macaron.Handler
類型的func,這個func其實是所有前端請求進來的第一站,也是最後一站,開發者通過macaron.Macaron.Get()
等方法註冊的handler,是在這個macaron.Handler
類型的func中的ctx.Next()
去調用的(上面提到的在調用棧中找請求URL就是在這裏找的)。
這個macaron.Handler
類型的func裏操作session的大致邏輯是,最開始先從context
中搞到(獲取或創建)session
,最後再調用session.Release()
保存session(比如保存到redis)。
那麼我是怎麼發現 “其實這是因爲刷新操作導致原頁面的websocket
斷開而走到這裏的” 的呢?因爲ctx裏URL爲state
的那次斷點,沒經過獲取session,而直接到session.Release()
。所以說,在F12的network
清一下再做操作可以幹掉一些干擾項,但同時也可能把有用的幹掉了,比如還沒斷開的websocket
。
到這裏問題就找到了。大致流程如圖(因爲那個類型爲macaron.Handler
的返回值是匿名方法,所以圖上就以macaron.Handler
來表示了):
說明一下圖中的虛線部分,前端頁面收到switch
接口的返回後刷新頁面,刷新頁面導致websocket
斷開,進而導致stateHandler
返回,而此時此macaron.Handler
持有的是websocket
創建時的session,將此session存到redis當然會覆蓋switchHandler
保存在redis的session。
解決方案
問題找到後,解決起來也就簡單了,只需在sess.Release()
之前加個判斷,如果是websocket就不執行即可,而websocket的判斷方法不止一種,這裏是根據請求頭中的Upgrade
字段來實現的,代碼如下:
...
ctx.Next()
if ctx.Req.Header.Get("Upgrade") == "websocket" {
return
}
if err = sess.Release(); err != nil {
...
代碼位置:
pkg/go-macaron/session/session.go:Sessioner()
如果您有更好的想法,還望不吝賜教