openlang

except ValueError 在 gather(return_exceptions=True) 外面永远不执行,然后 ValueError 引发 TypeError

我在客户回访前跑一个小脚本,根据三项指标给每个客户的网站打分,每个客户的权重从他们的分析接口获取。获取是通过 asyncio.gather 做的,并且设了 return_exceptions=True,所以一个客户出问题不会取消其他的,对吧?响应为空时 fetch_weights 会抛出 ValueError,我把 gather 包在 try/except ValueError 里,打一条警告日志,然后回退到默认权重。

第一个客户能打印出分数,然后脚本在 eastside-cycles 上挂掉了,它的响应是空的。完全没有 “falling back to defaults” 的警告。我把它改成了 except Exception,还用 python3 -X dev main.py 跑过,都没有任何变化。

我不明白的是,TypeError 里提到的是一个 ValueError 对象,可 ValueError 明明是在 fetch_weights 里抛出的。

juniper-florals: 83.0
Traceback (most recent call last):
  File "main.py", line 59, in <module>
    main()
  File "main.py", line 55, in main
    asyncio.run(score_all(clients))
  ...
  File "main.py", line 40, in score_all
    value = score(client["metrics"], weights)
  File "main.py", line 26, in score
    return round(sum(metrics[k] * weights[k] for k in weights), 1)
TypeError: 'ValueError' object is not iterable

main.py 里相关的代码在下面。

async def score_all(clients):
    try:
        all_weights = await asyncio.gather(
            *(fetch_weights(c) for c in clients),
            return_exceptions=True,
        )
    except ValueError:
        log.warning("weights fetch failed, falling back to defaults")
        all_weights = [DEFAULT_WEIGHTS for _ in clients]

    for client, weights in zip(clients, all_weights):
        value = score(client["metrics"], weights)
        print(client["slug"] + ": " + str(value))

为什么这个异常处理被跳过了?

从英语翻译查看原文

后续讨论 5

顺着彼此的依据读下去
  1. 固定链接

    设了 return_exceptions=True,gather 就永远不会抛出 ValueError。它会把异常作为结果列表里的一项返回,所以 except 根本不会触发。然后 score() 在需要权重的地方拿到了异常对象,于是就报了 TypeError。大概可以对每一项检查 isinstance(w, Exception),在那里回退。

    all_weights = [
        DEFAULT_WEIGHTS if isinstance(w, Exception) else w
        for w in all_weights
    ]
    从英语翻译查看原文
  2. 固定链接

    补充一点,我自己这段代码的 isinstance(w, Exception) 可能范围太宽了。任何失败,比如超时或者认证令牌失效,都会悄悄拿到默认权重,看起来像一个正常的分数。既然警告才是重点,我会专门检查 ValueError,并且把客户的 slug 记到日志里。其他异常就可以直接大声报错了。

    for c, w in zip(clients, all_weights):
        if isinstance(w, ValueError):
            log.warning("empty weights for %s, using defaults", c["slug"])

    我在维护工作里见过这种兜底式的回退把一个坏掉的接口藏了好几周。

    从英语翻译查看原文
  3. 固定链接

    谢谢,逐项检查确实说得通,对吧?对于其他异常,是应该马上重新抛出,还是在循环结束后统一记录?

    从英语翻译查看原文
  4. 固定链接

    大概率是对任何不是 ValueError 的异常都马上重新抛出。如果等循环结束后再统一记录,那时已经用坏数据给客户打过分了。被跳过的那些也一样,所以我会选择快速失败。

    for c, w in zip(clients, all_weights):
        if isinstance(w, ValueError):
            log.warning("empty weights for %s, using defaults", c["slug"])
            w = DEFAULT_WEIGHTS
        elif isinstance(w, BaseException):
            raise w
    从英语翻译查看原文

同一个问题的其他角度

  1. `String::from_utf8` 失败时会把字节还给你

    消耗型 API 得决定失败时那个值怎么处理。String::from_utf8 按值接收 Vec<u8>,失败时由错误持有它。 它会打印 bad byte at 2, 4 bytes back。没有拷贝,调用方在一个值里同时拿回缓冲区和出错的偏移。m…

    回复 0最近活动 rust
  2. use 链里每一行都挂着 `result.map_error`

    use 配 result.try 是 Gleam 里我最喜欢的部分,但我开始有点烦它了。每一步调的是不同的模块,每个模块有自己的错误类型,结果每一行末尾都得挂一个 result.map_error。 把构造器当函数传确实能让它短一点。但每行有一半都是…

    回复 1最近活动 gleam
  3. errdefer 里的 free 在成功路径上泄漏了

    一个测试泄漏了内存,而且泄漏出在成功的那条路径上。函数分配了一个临时缓冲区,解析出一个归调用方所有的结果,并用 errdefer allocator.free(scratch) 释放这个临时缓冲区。所有错误路径都释放了它。成功返回却没有,测试结束时…

    回复 2最近活动 zig