当macaron的session配了redis并且遇上了websocket——一个session“不”更新的bug
Posted -_-void
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了当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()
如果您有更好的想法,还望不吝赐教
以上是关于当macaron的session配了redis并且遇上了websocket——一个session“不”更新的bug的主要内容,如果未能解决你的问题,请参考以下文章