已关闭
[Bug]: DB 写失败时 write 异常未处理,cache 被脏刷新 #112
Vxiaomao52创建于  7月17日关闭于  7月29日
Vxiaomao52
Vxiaomao52
7月17日 创建

Checklist

🐞 问题详细描述

DB 完全宕机时 DbDataStore.write 吞掉 create 异常,write 不向外抛,CacheBackedDataStore 继续刷新 Redis,出现"DB 未写入但 Redis 被刷新"的脏缓存。

根因
DbDataStore.write(db_data_store.py:105-136)的设计意图是"先 create,主键冲突则回退 update",用 try/except Exception 包裹 create。但 except 分支吞掉所有异常(包括非主键冲突的 DB 宕机异常),无条件走 get+update 回退路径。当 DB 完全宕机时:

  1. create 抛 RuntimeError("DB down") → 被 except Exception 捕获(line 120)
  2. get 返回 None(DB 不可用)
  3. update 在 store 空时返回 None,不抛错(line 125)
  4. write 正常返回,不向上传播
  5. CacheBackedDataStore.write(cache_backed_data_store.py:42-52)继续执行 cache.set_json,Redis 被刷新

触发条件

  • runtime_db_enabled=True
  • DB handler 的 create 抛任意异常(连接断开、权限错误、磁盘满等,不止主键冲突)
  • 典型场景:DB 主机宕机、网络分区、连接池耗尽

影响范围

  • 数据一致性:DB 无记录但 Redis 有缓存,出现"幻读"——调用方读到数据但 DB 兜底失效
  • 持久化目标落空:串讲文档核心目标"Redis 故障时业务状态可从 DB 恢复"无法达成,因为 DB 根本没写入
  • 故障传播失效:运维无法通过 write 失败感知 DB 故障,延迟发现
  • 触发概率:中(DB 完全宕机是 P0 故障,但本应"失败可见"而非"静默成功")

代码位置

  • db_data_store.py:120 except Exception as e: logger.debug(...) ← 吞异常过宽
  • db_data_store.py:123-135 回退路径无写入结果校验
  • cache_backed_data_store.py:42 await self._db.write(...) 后无条件执行 line 52 cache.set_json

复现证据(TC-R01)

self.db_handler.create = _always_fail_create  # create 永远抛
self.db_handler.get = _always_return_none_get
self.db_handler.update = _always_return_none_update
await self.adapter.put("conv-r1", {"v": 1})  # 不抛
self.assertNotIn(("session_task", "conv-r1"), self.db_handler._store)  # DB 无记录
self.assertEqual(len(self.cache.set_calls), 1)  # cache 被刷新 ← 脏刷新

修复建议

  1. 区分异常类型:create 失败时仅当异常是主键冲突(IntegrityError/UniqueViolation)才回退 update,其他异常向上抛
  2. 或在回退路径增加写入结果校验:if update 返回 None 且 get 返回 None: raise RuntimeError("DB write failed: record not created")
  3. CacheBackedDataStore.write 可增加 db.write 返回值校验,确保 DB 写入成功后才刷 cache

详细的环境信息描述

单机环境本地部署

其他辅助信息

代码片段,agent-runtime\applications\a2a_service\common\db_data_store.py

        try:
            await self._db.create(
                self._table,
                {
                    "state_domain": namespace,
                    "state_key": key,
                    "payload": value,
                    "version": 1,
                    "state_metadata": metadata,
                    "updated_at": now,
                    "expire_at": expire_at,
                },
            )
            logger.debug("[DbDataStore] write_insert namespace=%s key=%s", namespace, key)
            return
        except Exception as e:
            logger.debug("[DbDataStore] create_failed fallback to update: %s", e)

版本信息

感谢您的贡献 🎉!

likedislike
openJiuwen-bot成员
7月17日 评论:

欢迎来到 openJiuwen 社区

Hey @Vxiaomao52 , 感谢你对社区的贡献.

机器人使用手册

有关指令的使用,可以点击 此处 查看详情。开发人员可以在每个PR或Issue下方评论特定指令来触发机器人任务。

likedislike
Vxiaomao52Vxiaomao52
7月29日 issue状态由 TODO 改变为 DONE
Vxiaomao52Vxiaomao52
7月29日 关闭了 issue
Vxiaomao52
Vxiaomao52
7月29日 评论:

回归结果

6 passed in 0.66s
用例 场景 预期 实际 结果
REG-001a createOperationalError(DB 连接拒绝) 异常传播,cache 不刷新 异常传播,cache.set_calls=0
REG-001b createRuntimeError(DB 完全宕机) 异常传播,cache 不刷新 异常传播,cache.set_calls=0
REG-002 createIntegrityError + get 返回 None RuntimeError 传播,cache 不刷新 RuntimeError("record not found"),cache.set_calls=0
REG-003 createIntegrityError → 正常回退 update upsert 成功,version 自增,cache 刷新 version=2,cache.set_calls=2
REG-004 create 静默返回 None(残留风险) 标记已知残留 cache.set_calls=1(符合预期标记)
REG-005 DB 正常写入(基线对照) DB + cache 双写成功 数据一致,version=1

修复验证结论

原始缺陷路径:except Exception 吞所有异常 → 脏缓存。修复后 except IntegrityError 仅捕获主键冲突:

  • DB 宕机(OperationalError / RuntimeError):异常正确传播至调用方,CacheBackedDataStore.write 在 line 42 处即抛出,不执行 line 52 cache.set_json脏缓存阻断
  • 主键冲突 + DB 不可用(IntegrityError + get=None)existing is None 触发 RuntimeError("record not found for update") → 异常传播 → 脏缓存阻断
  • 正常 upsert(IntegrityError + get 成功 + update 成功):回退路径正常工作,version 自增 → 业务正常
likedislike
Vxiaomao52Vxiaomao52
7月29日 issue状态由 DONE 改变为 CLOSED