openlang

После перехода на TaskGroup `except ValueError` перестал ловить

Небольшой скрипт подсчёта оценки переехал с asyncio.gather на asyncio.TaskGroup, и запасной путь перестал работать. До изменения пустой ответ с весами вызывал ValueError при загрузке, except его ловил, и оценка считалась с весами по умолчанию. После изменения тот же сбой проходит мимо обработчика.

import asyncio

async def fetch_weights():
    raise ValueError("weights missing")

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(fetch_weights())
    except ValueError:
        print("fallback weights")

asyncio.run(main())

На 3.13 это ничего не печатает. Трассировка заканчивается на ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception), а ValueError лежит внутри.

Значит, исключение всё-таки дошло. Дошло обёрнутым, хотя упала всего одна задача. Прежде чем трогать обработчик, хочется понять, всегда ли TaskGroup так оборачивает или только когда упасть могут несколько задач.

Разговор продолжается 5

Следуя доводам
  1. Ссылка на ответ

    Насколько я знаю, всегда. TaskGroup бросает группу, как только падает любой участник, даже если сбой один, так что простой except ValueError снаружи совпасть не может. Для этого есть except*.

    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(fetch_weights())
    except* ValueError as eg:
        print(type(eg).__name__, eg.exceptions)

    Вывод ExceptionGroup (ValueError('weights missing'),). Меня подловило имя. Внутри except* целью оказывается совпавшая подгруппа, а не голый ValueError, поэтому str(eg) и обращения к атрибутам ведут себя не так, как в старом обработчике. Всё, что раньше читалось из исключения, теперь читается из eg.exceptions[0] или в цикле по ним.

    И ещё в одном try нельзя смешивать except и except*. Я всё время об этом забываю и не уверен, что сообщение об ошибке объяснило бы почему.

  2. Ссылка на ответ

    Поэтому старая версия и работала. gather без return_exceptions пробрасывает первое исключение как есть, голый ValueError, без группы. То есть переход поменял тип исключения, а не только планирование. Оговорка, gather при этом оставлял другую задачу работать.

  3. Ссылка на ответ

    С except* запасной путь снова работает. По дороге была одна заминка. Старый обработчик заканчивался на return default_weights(), а внутри except* это SyntaxError, как и break с continue. Правило понятно, если вспомнить, что группа может совпасть с несколькими ветками, и return из одной пропустил бы остальные.

    Теперь обработчик только ставит флаг, а запасной расчёт идёт после блока try, и читается это даже проще, чем раньше. Мысль kpw про тип - как раз то, что я упустил из виду. Переезд виделся мне только изменением планирования.

  4. Ссылка на ответ

    Стало интересно, что лежит в группе, когда падают обе загрузки. Две задачи с исключением, одна бросает сразу, другая после await asyncio.sleep(0.1). len(eg.exceptions) выходит 1. Первый сбой отменяет группу, спящая задача завершается отменённой, и группа не считает это ошибкой. Уберите sleep, чтобы обе падали на первом шаге, и выйдет 2.

    Так что в группе лежат сбои, успевшие до отмены, а не все загрузки, которые упали бы. Для флага запасного пути это нормально. Хуже, если кто-нибудь начнёт писать группу в лог как список всего, что пошло не так.

  5. Ссылка на ответ

    Со стороны всплывающего сообщения после перехода любой str(e) выше по стеку показывает unhandled errors in a TaskGroup (1 sub-exception). Формально верно! Так что текст, который попадает на экран, должен браться из внутреннего исключения.

Другие стороны того же вопроса

  1. `String::from_utf8` при неудаче возвращает байты

    API, которое забирает значение, должно решить, что с ним будет при неудаче. String::from_utf8 принимает Vec<u8> по значению,…

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

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

    Ответы 1gleam
  3. Где остаётся курсор после неполной записи?

    Через Result вызывающий код потокового парсера узнаёт, получилась ли запись. Но сдвинул ли парсер курсор, тронул ли рабочую п…

    Ответы 9rustocamlzig