防止瞬时性 Bug
本页面介绍了开发者可以遵循的架构模式和技巧,以防止瞬时性 bug。
常见根本原因
我们在处理瞬时性 bug 时发现了一些频繁出现的根本原因。
- 后端或前端需要更好的状态管理。
- 前端代码需要改进。
- 缺少测试覆盖。
- 竞态条件。
前端
不要依赖响应顺序
在处理多个请求时,很容易假设响应的顺序与它们被触发的顺序相匹配。
但事实并非总是如此,这可能导致仅在顺序切换时才会出现的 bug。
示例:
diffs_metadata.json(较轻量)diffs_batch.json(较重量)
如果你的功能需要两者的数据,请确保在处理数据之前两者都已加载完成。
手动测试时模拟慢速连接
在浏览器的开发者工具中添加网络条件模板,让你能够在慢速和快速连接之间切换。
示例:
- 龟速:
- 下载:50kb/s
- 上传:20kb/s
- 延迟:10000ms
折叠元素
在设置事件监听器时,如果不能使用事件委托,请确保为展开的内容设置所有相关的事件监听器。
包括当展开的内容是:
- 不可见(
display: none;)。某些 JavaScript 要求元素可见才能正常工作,例如在测量时。 - 动态内容(AJAX/DOM 操作)。
使用断言检测因条件未满足导致的瞬时性 bug
瞬时性 bug 发生在代码执行时,假设应用程序状态满足一个或多个条件的上下文中。我们可能会编写一个功能,假设服务器端 API 响应总是包含一组属性,或者只有在应用程序成功转换到新状态时才执行某个操作。
瞬时性 bug 很难调试,因为没有机制会向用户或开发者警告条件未满足。这些条件通常不会在代码中明确表达。对于这种情况,一个有用的调试技术是放置断言来使任何假设变得明确。它们可以帮助检测是哪个未满足的条件导致了 bug。
在状态变更上断言前置条件
导致瞬时性 bug 的常见场景是存在一个轮询服务,它应该仅在用户操作完成时才变更状态。我们可以使用断言来使这个前置条件明确:
// 此操作由轮询服务调用。它假设在操作分发时所有前置条件都已满足。
export const updateMergeableStatus = ({ commit }, payload) => {
commit(types.SET_MERGEABLE_STATUS, payload);
};
// 我们可以通过添加断言来使任何前置条件明确
export const updateMergeableStatus = ({ state, commit }, payload) => {
console.assert(
state.isResolvingDiscussion === true,
'在更新可合并状态之前,必须完成讨论请求的解决'
);
commit(types.SET_MERGEABLE_STATUS, payload);
};断言 API 合约
使用断言的另一个有用方式是检测服务器端端点返回的响应负载是否满足 API 合约。
相关阅读
Debug it! 探讨了诊断和修复非确定性 bug 的技术,以及编写更容易调试的软件的方法。
后端
带锁的 Sidekiq 作业
通过 Sidekiq 处理异步工作时,可能出现两个具有相同参数的作业同时被执行的情况。如果处理不当,这可能导致过时或不准确的状态。
例如,考虑一个更新对象状态的 worker。在 worker 更新对象的状态(例如 #update_state)之前,它需要检查应该是什么适当的状态(例如 #check_state)。
当有两个作业同时执行时,操作顺序可能是这样的:
- (Worker A) 调用
#check_state - (Worker B) 调用
#check_state - (Worker B) 调用
#update_state - (Worker A) 调用
#update_state
在这个例子中,Worker B 本应设置更新后的状态。但 Worker A 调用 #update_state 稍微晚了一点。
这可以通过使用数据库锁或 Gitlab::ExclusiveLease 来避免。这样,作业将一次一个地执行。这也允许它们被标记为 幂等。
重试机制处理
有时对象/记录会处于可以重新检查的失败状态。
如果对象处于可以重新检查的状态,请确保向用户显示适当的提示信息,让他们知道该做什么。同时,确保重试功能在触发时能够正确重置状态。
错误日志记录
错误日志记录不一定能直接防止瞬时性 bug,但可以帮助调试它们。
在编码时,有时我们期望抛出一些异常,然后我们会捕获它们。
每当捕获错误时进行记录,有助于发现可能导致用户看到的瞬时性 bug。在调查错误报告时,可能需要工程师查看发生时的日志。看到错误被记录下来可能是某些处理方式不当的信号。