惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
D
DataBreaches.Net
Microsoft Security Blog
Microsoft Security Blog
U
Unit 42
V
Visual Studio Blog
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
博客园 - Franky
C
CXSECURITY Database RSS Feed - CXSecurity.com
大猫的无限游戏
大猫的无限游戏
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
L
LangChain Blog
I
Intezer
V2EX - 技术
V2EX - 技术
Google DeepMind News
Google DeepMind News
T
Threat Research - Cisco Blogs
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
腾讯CDC
博客园 - 【当耐特】
Know Your Adversary
Know Your Adversary
TaoSecurity Blog
TaoSecurity Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
Project Zero
Project Zero
Blog — PlanetScale
Blog — PlanetScale
S
Security @ Cisco Blogs
量子位
M
MIT News - Artificial intelligence
美团技术团队
C
Cisco Blogs
S
Schneier on Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
G
Google Developers Blog
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
The Hacker News
The Hacker News
H
Help Net Security
S
Secure Thoughts
Scott Helme
Scott Helme
SecWiki News
SecWiki News
T
Troy Hunt's Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 叶小钗
O
OpenAI News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 司徒正美
T
Tenable Blog

阿尔的代码屋 | 全栈技术笔记

在 Android Termux 环境下安装 Hermes Agent 的踩坑与完美解决实践 开发日志| 阿尔的代码屋 VS Code 连接 WSL 精确每 10 分钟掉线 排坑笔记 | 阿尔的代码屋 Android 模拟器代理联网与 No Internet WiFi 锁死排坑笔记 | 阿尔的代码屋 [object Object] typing_extensions 有用(四):使用 TypeIs 替代危险的 cast,做最严谨的类型收窄 | 阿尔的代码屋 typing_extensions 有用(三):使用 Unpack 结合 TypedDict 给 **kwargs 装上透视眼 | 阿尔的代码屋 typing_extensions 有用(二):使用 @override 打造重构代码时的“防呆神器” | 阿尔的代码屋 Flutter 并发测试踩坑实录 - IsarCore 动态库下载冲突与 Widget 测试 HTTP 拦截全链路解决 | 阿尔的代码屋 typing_extensions 有用(一):使用 Self 终结继承时的类型推断灾难 | 阿尔的代码屋 基于 Cloudflare Pages 的纯前端 WebAssembly 应用自动化部署实践 | 开发日志 | 阿尔的代码屋 基于 VS Code 远程开发的 GPU Docker 容器自动清理方案实践 开发日志| 阿尔的代码屋 Patrol iOS 集成测试排坑实录 - xcodebuild exit code 70 全链路解决 | 阿尔的代码屋 Flutter E2E 测试从 integration_test 迁移到 Patrol - 实践笔记 | 阿尔的代码屋 Linux/macOS 下 micromamba 报错 Shard Index not available 与极度卡顿 排坑笔记 | 阿尔的代码屋 critical libmamba Shell not initialized micromamba报错 subprocess 无法修改父 Shell - 排坑笔记 | 阿尔的代码屋 VS Code Remote-SSH: 彻底告别重复输入密码 - 排坑笔记 | 阿尔的代码屋 Git 报警: inexact rename detection was skipped - 排坑笔记 | 阿尔的代码屋 解决群晖 NAS SSL 证书自动部署难题 | 阿尔的代码屋 Windows 下 Git error: fatal: refusing to work with credential missing host field - 排坑笔记 | 阿尔的代码屋 VS Code 调试 Python 第三方库源码的配置 - 开发技巧| 阿尔的代码屋 VS Code Remote Tunnels本地连接调试Kaggle GPU - 开发技巧| 阿尔的代码屋 Windows下Git error: open(xxxx) Filename too long - 排坑笔记 | 阿尔的代码屋 记录2026年零预约港卡开户实录避坑 告别孤岛:拥抱 MCP 协议,为大模型打造标准“USB 接口” - 大模型实战 08 完结篇 | 阿尔的代码屋 从 ReAct 到 Workflow:基于云端 API 构建事件驱动的智能体 - 大模型实战 07 额外篇 | 阿尔的代码屋 基于 LlamaIndex ReAct 框架手搓全自动博客监控 Agent - 大模型实战 07 | 阿尔的代码屋 在 Kaggle 上用 Unsloth 极速微调 Qwen3 - 大模型实战 06 | 阿尔的代码屋 大模型微调实战详解 - 大模型实战 05 | 阿尔的代码屋 基于 ChromaDB 打造工程级 RAG 系统 - 大模型实战 04 | 阿尔的代码屋 拆解 Transformers 原理与图解 - 大模型实战 03 | 阿尔的代码屋 云端炼丹房 2:Kaggle 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 云端炼丹房:Google Colab 免费 GPU 上手指南 - 大模型实战 03预备 | 阿尔的代码屋 Open WebUI 部署与 RAG 实战 - 大模型实战 02 | 阿尔的代码屋
GoRouter 结合 Isar 运行 Widget 测试并发/粘性线程死锁卡死排坑笔记 | 阿尔的代码屋
Algieba · 2026-06-20 · via 阿尔的代码屋 | 全栈技术笔记

核心摘要 (TL;DR)

  • 问题现象:本地单线程运行通常可通过,但 CI 高并发(concurrency)下或者特定测试顺序中,router_test.dart 运行到 /add 或其他路由测试时会无限期卡死/挂起,直至被 CI 取消(Error: The operation was canceled)。
  • 根本原因
    1. 异步数据源流监听:路由导航至目标页面(如 DashboardScreenHistoryScreen)后,这些 Widget 在 initState 中开启了 Isar 数据库的异步查询或流监听(watchObject/watch)。
    2. runAsync 逃逸 zone 控制:为防止 FakeAsync 区域内原生端口死锁,测试使用了 tester.runAsync。这导致 Isar 的异步任务在 Dart 真实事件循环上执行。
    3. 数据库提前关闭导致 mdbx 崩溃:用例执行完毕后,tearDown 立即释放容器并关闭了 Isar 实例(isar.close())。但上个测试遗留在真实事件循环中的后台流和事务仍在尝试清理或回调,从而在跨测试线程/关闭的库上操作,触发 MDBX 粘性线程断言崩溃:Shell: Assertion failed: ((txn->flags & MDBX_TXN_FINISHED) || (txn->flags & MDBX_NOSTICKYTHREADS) == ...),导致运行器卡死。
  • 关键解法:修改 [router_test.dart](file:///Users/mac/flutter/UseUp/test/config/router_test.dart) 的 setup 生命周期。使用 setUpAll / tearDownAll 共享同一个 Isar 实例,而对 ProviderContainer 依然保持每个测试单独 setUp / tearDown 进行状态隔离,使遗留后台异步任务能在关闭前平稳退出。

问题概览卡片

基本信息

  • 问题分类:Widget 测试卡死 / 数据库多线程冲突
  • 环境说明:macOS / Ubuntu (CI) / Flutter 3.x / Isar 4.0.0-dev / MDBX 存储引擎
  • 触发条件:Widget 测试中使用 GoRouter 并通过 tester.runAsync 导航 to 监听 Isar Stream 的页面,同时在每个测试用例的 tearDown 中销毁数据库实例。
  • 报错摘要
    1
    2
    Shell: Assertion failed: ((txn->flags & MDBX_TXN_FINISHED) || (txn->flags & MDBX_NOSTICKYTHREADS) == (txn->env->flags & MDBX_NOSTICKYTHREADS)), function check_txn, file mdbx, line 439.
    Error: The operation was canceled.

1. 现象描述与现场还原

在 CI (GitHub Actions) 上进行自动化测试流程时,经常发现在执行到 [router_test.dart](file:///Users/mac/flutter/UseUp/test/config/router_test.dart) 里的特定路由测试用例时(例如 /add 路由或平台页面适配测试),CI Runner 会在输出特定用例启动信息后突然失去响应,不再输出任何进展:

1
2
3
Error: 30] ❌ [ERROR] test/config/router_test.dart > Router Tests /add route without extras shows AddItemScreen
[15:46:30] 🏃 [Start] test/config/router_test.dart > Router Tests /add route with extras passes parameters
Error: The operation was canceled.

在本地手动尝试运行该测试文件时,同样有一定概率触发控制台输出底层原生数据库断言错误:

1
2
3
4
IsarCore using libmdbx: v0.13.8
...
Shell: Assertion failed: ((txn->flags & MDBX_TXN_FINISHED) || (txn->flags & MDBX_NOSTICKYTHREADS) == (txn->env->flags & MDBX_NOSTICKYTHREADS)), function check_txn, file mdbx, line 439.
Bad state: Cannot add event while adding stream.

由于该断言发生在原生 C++ / FFI 线程中,导致 Dart 虚拟机测试运行器未捕捉到正常 Dart 异常而直接陷入挂起或死锁状态,表现为整个测试框架无限期卡死。


2. 根本原因分析

这一死锁崩溃链条是由 Flutter Widget 测试的 zone 运行机制Isar 原生多线程事务模型 共同作用导致的。

2.1 逃逸 FakeAsync

在标准 Widget 测试 testWidgets 中,所有代码都在 FakeAsync 环境下运行,时间流是被 mock 的。然而,Isar 数据库在进行 watch(流监听)时,依赖于底层 C++ 原生端口(FFI Port)跨线程发送数据。
由于 FakeAsync 无法推进真实的原生 Port 事件循环,当组件去监听 Isar Stream 时会发生阻塞。为了让事件循环继续走下去,先前在测试代码中引入了 tester.runAsync 包裹用例:

1
2
3
4
5
6
7
testWidgets('navigates to /settings/categories', (tester) async {
await tester.runAsync(() async {

await tester.pumpWidget(_buildRouterApp(container));
...
});
});

2.2 并发与 tearDown 提前销毁

因为使用了 tester.runAsync,页面中的 initState(例如 CategorySelector / HistoryScreen 内触发的 isar.categorys.watch() / isar.items.watchObject())所产生的异步微任务和原生 Port 消息,被分发到了真实的 Dart 事件循环上。

此时,用例主体执行完毕(例如断言了当前页面类型正确),测试主线程立即进入 tearDown 周期:

1
2
3
4
tearDown(() async {
container.dispose();
await isar.close(deleteFromDisk: true);
});

然而,上一个测试页面在真实事件循环中注册的后台异步流(StreamBuilder / Isar FFI 监听)可能还没有完全注销或正在返回数据。由于 Isar 已经被 tearDown 提前关闭:

  1. 后台流尝试释放或清理未完成的事务(Transaction);
  2. 跨线程的 FFI 端口回调试图访问已被销毁的 Isar 资源;
  3. 或者是事务绑定的线程上下文(Sticky Threads)在并发调度下发生了混乱。

这直接触发了 MDBX 引擎的 check_txn 粘性线程断言失败,导致 native 级别死锁挂起,进程无法退出。


3. 解决方案

要解决这个问题,关键在于避免在后台异步任务还未彻底清理完成时,提前销毁底层的 Isar 数据库实例

由于路由测试只验证页面跳转和渲染关系,并不依赖于强隔离的、每次都重置的 Isar 数据库数据,我们完全可以将 Isar 数据库生命周期提升到文件级共享。

3.1 改造 Isar 生命周期

在 [router_test.dart](file:///Users/mac/flutter/UseUp/test/config/router_test.dart) 中,我们将 Isar 的生命周期修改为 setUpAlltearDownAll。这样在整个测试文件的运行周期内只会创建/关闭一次 Isar,而后端的流即便发生延迟注销,也可以在数据库依然完好的情况下安全退出。

同时,我们仍保留 setUptearDown 来管理 ProviderContainer,以保证每个测试用例的 Riverpod 业务状态隔离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
     late ProviderContainer container;
late Isar isar;

- setUp(() async {
+ setUpAll(() async {
isar = await IsarTestHelper.createTestIsar();
+ });
+
+ tearDownAll(() async {
+ await isar.close(deleteFromDisk: true);
+ });
+
+ setUp(() {
container = ProviderContainer(
overrides: [
databaseProvider.overrideWithValue(isar),
@@ -65,9 +65,8 @@
);
});

- tearDown(() async {
+ tearDown(() {
container.dispose();
- await isar.close(deleteFromDisk: true);
debugDefaultTargetPlatformOverride = null;
});

4. 预防与建议

  1. 测试中的数据库共享:对于只读、重度使用路由/异步组件的 Widget 测试集,共享同一内存数据库实例(利用 setUpAll)是避免跨测试用例异步资源竞争与挂起的最优解。
  2. 谨慎使用 runAsync 包裹整个用例:在 runAsync 内运行的代码,其异步周期脱离了测试 Zone。必须随时提防在测试退出后仍有未收尾的 Future 在真实事件循环中运行所带来的副作用。
  3. 隔离配置环境运行:若测试集数量巨大,尽量控制并发度(例如在 CI 上使用 --concurrency=3),避免过度拥挤抢占 CPU 导致页面渲染延迟进而触发超时。