选举2029:不可能的异常——已解决

本文详细记录了作者在调试选举系统数据排序异常时的完整过程,包括异常消息的误解、代码逻辑的错误分析,以及最终通过修改映射组合顺序解决数据加载问题的技术细节。

选举2029:不可能的异常——已解决

在写完上一篇文章后不久,一位同事联系我说她找到了问题所在——至少在最直接的层面上,即异常本身。排序代码没有问题——只是异常消息太容易被误读了。她完全正确,我对自己感到非常懊恼。

再次看一下异常消息:

1
Incorrect ordering for PredictionSets: mic-01 should occur before focaldata-01

以及创建该异常的代码:

1
2
3
4
5
6
7
string currentText = selector(current);
string nextText = selector(next);
if (StringComparer.Ordinal.Compare(currentText, nextText) >= 0)
{
    throw new InvalidOperationException(
        $"Incorrect ordering for {message}: {currentText} should occur before {nextText}");
}

在我之前的文章中,我声称:

异常消息暗示在异常发生时,currentText的值是"focaldata-01",而nextText的值是"mic-01"。

不,并不是这样!它暗示的正好相反,currentText的值是"mic-01",而nextText的值是"focaldata-01"…换句话说,数据确实是错误的。

唉。即使一直想着"当我的代码行为不当时,几乎总是我的错",我仍然无法真正退后一步并适当双重检查我的逻辑。

但这很奇怪,对吧?因为之前无效的数据(20:15:57)后来神奇地"变成"了有效(20:26:22),对吧?这就是我在上一篇文章中声称的。我应该更仔细地查看日志…在20:22:58启动了一个新实例。那个新实例正确加载了数据,因此重新加载已经有效的数据是没问题的。

真正的问题是什么?

我在实际修复代码之前就开始写这篇文章,但我现在确定问题在于"部分"重新加载——向数据库添加新的预测集,然后从存储系统重新加载数据,而该系统的缓存中已经有现有数据。这应该相对容易测试——

首先,值得修复那条消息。与其谈论什么"应该出现",不如说明实际情况,包括集合中出错的索引:

1
2
3
4
5
6
7
8
9
foreach (var (index, (current, next)) in source.Zip(source.Skip(1)).Index())
{
    string currentText = selector(current);
    string nextText = selector(next);
    if (StringComparer.Ordinal.Compare(currentText, nextText) >= 0)
    {
        throw new InvalidOperationException($"Incorrect ordering: {message}[{index}]={currentText}; {message}[{index + 1}]={nextText}");
    }
}

接下来,让我们在上传新数据时添加另一层检查:除了从干净启动重新加载两次外,让我们添加"之前然后之后"的重新加载。这方面的代码并不有趣(尽管由于依赖注入的原因很繁琐)。然后只需测试添加一个ID为"aaaa"的"绝对第一个"预测集…

太好了,我重现了问题!

1
Incorrect ordering: PredictionSets[4]=name-length; PredictionSets[5]=aaaa

之后,花了不太长时间(通过更多日志记录)就找到了问题。一旦我找到了问题,修复就非常容易。无需过多不必要的细节,我在组合新旧映射时破坏了我的内部"哈希到完整数据"映射。

1
2
3
var predictionSetsByHash = newHashes.Concat(currentHashes)
    .Zip(currentPredictionSets.Concat(newPredictionSets))
    .ToOrdinalDictionary(pair => pair.First, pair => pair.Second);

应该改为:

1
2
3
var predictionSetsByHash = newHashes.Concat(currentHashes)
    .Zip(newPredictionSets.Concat(currentPredictionSets))
    .ToOrdinalDictionary(pair => pair.First, pair => pair.Second);

这只有在加载带有新预测集的上下文时才会成为问题,当我们之前已经有预测集时。

这就是我的选举网站没有很多自动化测试(这些可能是集成测试而不是单元测试)的不足之处…尽管公平地说,这是少数几次出现这种情况。

可能是时候开始编写更多测试了——特别是在这种情况下,这是一个在凌晨重写的完整上下文存储系统。

结论

所以,学到了一些教训:

是的,当我的代码行为不当时,几乎总是我的错。即使我盯着它看,认为我 somehow 发现了真正奇怪的东西。 我应该编写更多测试。 使异常消息尽可能明确非常重要。 我应该总是听 Amanda 的。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计