當macaron的session配了redis並且遇上了websocket——一個session“不”更新的bug
文章目錄
上個月剛好是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()
如果您有更好的想法,還望不吝賜教