可以看到,这段代码是非常stupid的,因为当初为了写的快一点,我大量的使用了lock_table_[rid]来获取LockRequestQueue的引用,而operator []的操作并不是常量级的,这会引入非常多的开销(本意是测试通过之后再修改)。更重要的是这里是我的第二个疏忽,正是前后这两个疏忽造成了BUG。
BUG产生情景在完成编码后我在本地跑了上千次测试,都可以完美PASS测试样例,但当我把代码提交到gradescope时却失败了,提示某些请求一直没有得到调度导致超时了。于是我在代码里加了点小trick,把调度失败时lock_table_的状态打印出来,于是产生了下面的日志:
先解释一下日志的输出。一个RID可以由一个二元组(page_num, slot_num)唯一表示,一个请求可以由三元组(txn_id, is_granted, lock_type)表示。我希望通过前文所述的二元组和三元组展示一下LockManager中锁调度的状态。上图中(txn 5, granted, S-LOCKED)表示事务5申请了RID(9, 9)的S-LOCK,且lock_manager已经将S-LOCK授权给了它。
注意到这个日志里有两个 RID(1, 1),且它们的hashval是一模一样的,这说明std::unordered_map中出现了两个同样的键!。我后面又多次提交代码查看lock_table_的状态,但都获得了类似的结果,即std::unordered_map中总是会有两个相同的键,或者说,拥有同样的hasval的键被分到了两个bucket中,而我们通过下标访问lock_table_[rid]时,至多只能访问到其中的一个bucket,因此也只有这个bucket中的LockRequest可能被调度,而另一个bucket由于无法被访问到,因此其中的请求就可能永远都不会被调度了,这就导致了测试代码的超时。
我们知道,std::unordered_map通过Key获取对应的Value的规则是首先计算这个Key对应的hashval % bucket_num获取得到K-V对所在的bucket,虽然不同的Key会有不同的hashval,但他们可能会有相同的hashval % bucket_num,因此可能会被放入到同一个bucket中;为了从bucket中找到唯一的K-V对,又需要调用operator ==来找到唯一的目标Key;因此发现这个BUG后,我第一个想法就是RID的实现可能存在问题,于是去仔细查看了RID的operator ()方法和operator ==的实现,然后打消了这个念头。其实前文中我也提到了,我在日志中打印了RID对应的hashval,两个键的hashval都是一样的,却在不同的bucket中,这种情景基本不可能是operator ()方法和operator ==实现错误所能触发的、
【世界名画之:我代码错了,肯定是XXX的问题】
然后我又考虑是不是因为我前面试图降低锁粒度的方法存在问题,但用纸笔模拟了多种情景、又拿状态机之类的理论推导了一下,最终也宣告了我的怀疑破产。
这样一来我的思路就完全断掉了,于是我希望能获取到更多的这个BUG产生时的程序上下文信息。由于测试代码只涉及到了10个RID,而这种情况出现时,lock_table_的size会膨胀到11,因此这个时机可以作为一个排查BUG、获取当前lock_table_状态的切入点,因此我又往自己的代码里添加了一系列的逻辑,边打印日志边准备捕获这个瞬间,但测试代码又被TIMEOUT了,因为gradescope的执行速度比较慢,打印太多日志会导致超时,拿不到我想要的东西。
这样一时间我的调试就陷入了僵局,我猜不到这个BUG产生的可能原因,无法在本地复现这个BUG,甚至无法通过日志的方式获取到更多的信息。
BUG的解决这个BUG的解决也很富有戏剧性,大概有两天我的思路没有进展,直到第二天晚上偶然打开cppreference时注意到了std::unordered_map的一个之前没注意到的细节:rehash。最初始时,std::unordered_map最初一般只有7个bucket,但随着插入量的增长,同一个bucket中的元素越来越多,越来越多的时间会被花费在bucket内部的线性查找上,因此std::unordered_map会在适当时机进行扩容操作,增添bucket的数量,并将之前的k-v pair重新分配到其对应的桶中。
https://en.cppreference.com/w/cpp/container/unordered_map
我自己写了一点测试代码了解rehash的行为后,猜测可能是并发访问下rehash造成了std::unordered_map的undefined行为,但这种想法一旦成立,也就意味着我前文中降低锁粒度所思考的逻辑存在着严重的问题。验证方法也很简单,在lock_table_创建时,把桶的数量开到足够大,这样就不会出现rehash的情景了:
LockManager() { enable_cycle_detection_ = true; cycle_detection_thread_ = new std::thread(&LockManager::RunCycleDetection, this); // reserve enough buckets to avoid rehash lock_table_.reserve(100); LOG_INFO("Cycle detection thread launched"); }修改后再次提交到gradescope,顺利通过。这样基本石锤了时rehash导致lock_table中出现了两个相同的key;
BUG的分析