openlang

except ValueError가 gather(return_exceptions=True) 주변에서 실행되지 않고, ValueError 때문에 TypeError가 나요

클라이언트 체크인 전에 작은 스크립트를 돌려서 각 클라이언트 사이트를 세 가지 지표로 점수를 매기는데, 클라이언트별 가중치는 해당 클라이언트의 analytics 엔드포인트에서 가져와요. 가져오는 부분은 return_exceptions=True를 준 asyncio.gather로 하고 있어서, 클라이언트 하나가 망가져도 나머지가 취소되지 않아야 하는 거죠? fetch_weights는 응답이 비어 있으면 ValueError를 raise하고, 저는 gather를 try/except ValueError로 감싸서 경고를 로그로 남기고 기본 가중치로 대체하도록 했어요.

첫 번째 클라이언트는 점수가 출력되는데, 응답이 빈 eastside-cycles에서 스크립트가 죽어요. "falling back to defaults" 경고는 아예 안 나와요. except Exception으로 바꿔 보고 python3 -X dev main.py로도 돌려 봤는데, 둘 다 달라진 게 없어요.

이해가 안 되는 건, ValueError는 분명히 fetch_weights에서 raise되는데 TypeError가 ValueError 객체를 언급한다는 거예요.

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를 raise하지 않아요. 결과 리스트의 항목으로 돌려주기 때문에 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"])

    유지보수 작업을 하면서 이런 식의 catch-all 폴백이 망가진 엔드포인트를 몇 주 동안 가려 버리는 걸 본 적이 있어요.

    영어에서 번역원문 보기
  3. 고유 링크

    고마워요, 항목마다 확인하는 게 맞는 것 같네요. 다른 예외는 바로 다시 raise해야 할까요, 아니면 루프가 끝난 뒤에 한꺼번에 로그로 남겨야 할까요?

    영어에서 번역원문 보기
  4. 고유 링크

    ValueError가 아닌 건 바로 다시 raise하는 게 좋을 것 같아요. 루프 끝난 뒤에 전부 로그로 남기면 그때는 이미 잘못된 데이터로 클라이언트들의 점수를 매겨 버린 뒤예요. 건너뛰는 것들도 마찬가지라서, 거기서는 빨리 실패하게 하겠어요.

    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…

    답글 0최근 활동 rust
  2. use 체인 줄마다 붙는 `result.map_error`

    use와 result.try 조합은 Gleam에서 제일 좋아하는 부분인데, 요즘 조금 미워지기 시작했어요. 단계마다 다른 모듈을 부르고 모듈마다 자기 오류 타입이 있으니, 줄마다 result.map_error를 달고 다녀요.…

    답글 1최근 활동 gleam
  3. 성공 경로에서 샌 errdefer 해제

    테스트에서 메모리가 샜는데, 새는 곳이 성공한 경로였습니다. 함수는 임시 버퍼를 할당하고, 호출자가 소유할 결과로 파싱한 뒤, 임시 버퍼를 errdefer allocator.free(scratch)로 해제했습니다. 오류 경로…

    답글 2최근 활동 zig