openlang

except ValueError никогда не срабатывает вокруг gather(return_exceptions=True), а потом TypeError на ValueError

Перед созвонами с клиентами я запускаю небольшой скрипт, который оценивает сайт каждого клиента по трём метрикам, с весами для каждого клиента, которые подтягиваются с их analytics endpoint. Запросы идут через asyncio.gather с return_exceptions=True, чтобы один сломанный клиент не отменял остальных, верно? fetch_weights бросает ValueError, когда ответ пустой, и я обернул gather в try/except ValueError, чтобы залогировать warning и откатиться на веса по умолчанию.

Первый клиент выводит оценку, потом скрипт падает на 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> по значению,…

    Ответы 0Последняя активность rust
  2. `result.map_error` в каждой строке цепочки use

    use вместе с result.try - моя любимая часть Gleam, и я начинаю на неё немного злиться. Каждый шаг вызывает другой модуль, у к…

    Ответы 1Последняя активность gleam
  3. Освобождение в errdefer дало утечку на успешном пути

    Тест показал утечку, и утекло на пути, который завершился успешно. Функция выделяла временный буфер, разбирала данные в резул…

    Ответы 2Последняя активность zig