當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()

如果您有更好的想法,還望不吝賜教

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章