PoolTimeoutは、HTTPXが設定された待ち時間内にプールから接続を取得できなかったことを示します。プロキシへの新しいTCP接続がタイムアウトした証拠ではありません。2つ目のリクエストは、まだプロセス内で待っている可能性があります。

クライアントの接続上限が1つで、最初のストリーミングレスポンスを開いたままにすると再現しやすくなります。別のリクエストを始めても、その接続が自動で解放されるわけではありません。

ストリーミングレスポンスを誰が閉じるのか明確にする

import httpx

limits = httpx.Limits(
    max_connections=1,
    max_keepalive_connections=1,
)
timeout = httpx.Timeout(
    connect=5, read=10, write=10, pool=1
)

with httpx.Client(
    proxy="http://203.0.113.10:8080",
    trust_env=False,
    limits=limits,
    timeout=timeout,
) as client:
    with client.stream("GET", "https://example.com/") as response:
        print(response.status_code)
        for chunk in response.iter_bytes():
            print(len(chunk))

    next_response = client.get("https://example.com/")
    print(next_response.status_code)

実行前に文書用のプロキシアドレスを置き換えてください。次のリクエストは、ストリームのコンテキストを抜けた後で始まります。ループを途中で抜けたり例外が起きたりしても、コンテキストを抜けるとレスポンスは閉じられます。

client.send(request, stream=True)のような手動ストリーミングでは、最終的に閉じる責任は自分のコードにあります。AsyncClientで対応する手動の後始末を行う場合は、response.aclose()をawaitします。未読のレスポンスを閉じれば容量は空きますが、基礎となる接続を再利用せず破棄する必要が生じることがあります。

例外名を読む

ConnectTimeoutは接続の確立、ReadTimeoutはレスポンスデータの待機、PoolTimeoutは接続枠の取得に関係します。ログに単に「プロキシのタイムアウト」と書くだけでは、対処を選ぶための区別が失われます。

エンドポイントのIPを変える前に、開いたレスポンス、並行タスク、接続上限を調べてください。正当な並行負荷に対してプールの拡大が適切な場合はありますが、レスポンスを閉じない問題は直りません。アプリが占有できる容量を増やすだけです。

一連の処理でクライアントを再利用できるように保ちます。各リクエストが動くように見せるためにクライアントを毎回作り直すと、ライフサイクルの問題を隠し、接続の再利用も失います。

関連記事