捉BUG记(To Catch a Bug)
大约有一年整没有写一篇博客了,由于各种原(jia)因(ban)导致闲暇时间要么拿着IPad看岛国奇怪的片(dong)子(hua)、要么拿着kindle看各种各样的资(xiao)料(shuo)。本来想写的一个介绍MEF的专题也果断在完成50%后砍掉,结果这两天想准备点关于IOC(不是国际奥委会那个IOC)的内部材料,发现之前准备的一些资料也已经顺手删掉了,可惜可惜。
不说别的了,就说这两天自己给自己挖的一个坑。说起来还挺有趣的,原因不复杂,就是最基本的知识点,只是手头的模块略复杂,一开始还真没猜到自己是栽在这坑里。
一个BUG,一个关于IDisposable的故事
事情是这样的,前天下午同事屁颠屁颠跑过来,吼道:“报错啦,报错啦,天塌啦,地陷啦”,嗯,差不多他就是这个意思。
当然,他吼了什么一点都不重要,重要的是一番检查后确认是系统其他地方存在问题,这里抛出异常是合情的合理的合法的美帝大统领看到了也要喊YES WE CAN THROW独国默大妈瞧见了双手摊开轻身细语我要让世界知道这个异常是要抛的股神巴菲特听到了抛字一路比划着一路小跑而来请原谅我忘记了标点的用法因为标点已经阻碍了我表达对这个异常被抛出的正确性的认知。
通常来说,剧情发展到一半的时候,如果前期BOSS被干掉了,就意味着后面还要出现一个幕后黑手——要不然这戏份怎么凑啊编剧?这几年国产片各种捞票作,各种侮辱观众的智商的片子也没这么拍的嘛。
是的,同样的代码,同样的参数调用,第二次这异常就神奇的消失了……在确认天上没有Blink Blink小圆盘地下没有Pink Pink小圆神后,反复重试都是第一次抛异常,后面就默默的什么都没有了。由于代码层次较多,相关数据也很多,在代码战争的汪洋大海中一番折腾后总算找出了一番的BUG(差不多下面的意思):
class Foo : IDisposable
{
DbContext ctx;
...
public void Dispose()
{
ctx.SaveChanges(); //Fuck my life
... //其他资源释放
}
}
原先在设计Foo类的时候,希望调用者能在完成一系列的操作后整体保存修改到数据库中,如果报错则在新的Foo实例中重试(以防止EF的缓存导致数据污染)。由于EF将在SaveChanges时将内存中的修改放在一个事务中统一提交,也为了方便调用者使用此类,故而考虑了在Dispose()中调用SaveChanges()的做法。于是调用者可以采用如下轻松又愉悦的姿势调用:
using(var foo = new Foo())
{
...
}
到此,各位看官看出问题来了么?这个低级的错误刚看到时一时半会还真没想出来:
- using语句块相当于try...finally的简化写法,保证了实现IDisposable的资源在离开语句块时Dispose()方法会被执行。
- 由于处理过程中的异常没有被catch,故而异常被沿着调用堆栈逐级向上抛出——这不是重点,重点是程序离开了using语句块,所以Dispose()方法被!调!用!了!,Dispose()方法被!调!用!了!
- Dispose()方法被!调!用!了!因为很重要所以要说三遍,本条凑数用。
- 由于在异常抛出前,已经有部分实体的状态被修改,故而惨遭提交。Q.E.D.
还好本模块实现了事件溯源,高大上点的说法叫Event Sourcing。最终是发现在异常抛出后事件记录多了几条,才意识到这个坑爹的问题的。看来引入Event Sourcing的概念,虽然目前还没有发挥多少价值,就冲帮我找到了这个BUG上(如果直接查看数据,那真得疯掉了),值了。
发现问题,解决倒也简单。加个标记位简单记录下是否处理过程发生异常即可。正如张〇忌般,历经世事后迎来了回老家画眉(不愧是明教教主,就是躲过了回老家结婚的flag)的平淡结局。
标题NETA了希区柯克的《捉贼记》(To Catch a Thief),当然片子我还没看。