現実は理想とは異なる
月曜日の午前3時、私の電話が鳴り止みませんでした。順調に動いていたはずの日常業務を自動化するデータクローリング用ボットが、突然「息絶えて」いたのです。ログを確認すると、そこには ConnectionTimeout というエラーメッセージが1行だけ残されていました。ローカル環境では100%完璧に動作していましたが、サーバーにデプロイすると、APIのレスポンス遅延が原因で平均して週に3回は機嫌を損ねていました。
このプロジェクトは当初、わずか200行のコードから始まりました。しかし、3ヶ月の運用を経て2,000行以上に膨れ上がりました。悲しいことに、その半分はビジネスロジックではなく、不安定なネットワークに対処するための「継ぎ接ぎ」のコードでした。これは、すべての開発者が経験するであろう、堅牢なシステム(resilient system)を構築するための手痛い教訓となりました。
なぜコードはすぐに「早死に」するのか?
分散システムの境界では、私たちは Transient Faults(一時的な障害)に直面します。これらは、現れては消えるエラーです。数秒後にリトライ(retry)すれば、何事もなかったかのようにスムーズに動作する可能性が高いものです。典型的なケースには以下のようなものがあります。
- 一瞬のインターネット接続断(マイクロアウトレージ)。
- 相手側サーバーの過負荷によるエラー 429(レート制限)。
- 定期バックアップのためのデータベースの一時的なロック。
- 帯域の混雑によるマイクロサービスのレスポンス遅延。
「正常系(happy path)」だけを考えたコードを書くのは致命的なミスです。ネットワークが1〜2秒ラグを起こすだけで、後続のプロセス全体がドミノ倒しのように崩壊します。ネットワークインフラを制御することはできませんが、コードがそれにどう反応するかは制御できます。
よくある「スマートではない」エラー処理
方法1:WhileループとTry-Except(コードの不吉な臭い)
これは最も直感的な方法です。Pythonを学びたての頃は、誰もがこのように書いたことがあるでしょう。
import time
attempts = 0
while attempts < 3:
try:
result = call_api_service()
break
except Exception as e:
attempts += 1
print(f"{attempts}回目リトライ中...")
time.sleep(2)
else:
print("完全に失敗しました。")
このコードは動作はしますが、非常に「汚い」です。呼び出す必要のあるエンドポイントが50個あると想像してみてください。コードファイルはwhileループとカウンター変数で溢れかえります。メインロジックが薄まり、メンテナンスが極めて困難になるだけでなく、カウンターのインクリメントを忘れて無限ループに陥るようなロジックミスも誘発しやすくなります。
方法2:自作デコレータ(まだ最適ではない)
少し改善して、再利用可能なデコレータを自作することもできます。しかし、Exponential Backoff(指数バックオフ:待機時間を徐々に増やす)の実装や、リトライすべき例外の種類を正確にフィルタリングするのは骨の折れる作業です。コミュニティが長年かけて最適化してきたものを、わざわざ車輪の再発明をする必要はありません。
プロフェッショナルな解決策:Tenacityライブラリ
Tenacityは、リトライの問題をエレガントに解決するために生まれたPythonライブラリです。命令的な(imperative)コードを書く代わりに、デコレータを通じて自分の望みを宣言する(declarative)だけで済みます。
クイックインストール
pip install tenacity
1. 基本的なリトライメカニズム
関数の頭に @retry を付けるだけで完了です!デフォルトでは、関数が成功するまで無限にリトライを繰り返します。
from tenacity import retry
@retry
def do_something_unreliable():
print("接続を試行中...")
raise Exception("ネットワークエラー!")
2. 試行回数と時間の制御
現実的には、無限に待つわけにはいきません。アプリケーションの許容限界を設定する必要があります。
from tenacity import retry, stop_after_attempt, stop_after_delay
# 最大5回まで試行
@retry(stop=stop_after_attempt(5))
def call_api():
raise IOError("Fail")
# 試行回数に関わらず10秒後に停止
@retry(stop=stop_after_delay(10))
def connect_db():
pass
3. スマートな待機戦略(Wait strategies)
すぐにリトライしてはいけません!サーバーが過負荷の時にリクエストを連打すると、即座にIPブロックされる可能性があります。そこで Exponential Backoff を使用します。
from tenacity import retry, wait_exponential
# 1回目は1秒、2回目は2秒、3回目は4秒待機... 最大10秒まで
@retry(wait=wait_exponential(multiplier=1, min=1, max=10))
def fetch_data():
raise Exception("サーバービジー")
4. 必要な時だけリトライする
404 Not Found エラーが発生した場合、1,000回リトライしても無意味です。Tenacityでは、リトライすべきエラーの型を正確に指定できます。
import requests
from tenacity import retry, retry_if_exception_type
@retry(retry=retry_if_exception_type(requests.exceptions.ConnectionError))
def get_weather():
return requests.get("https://api.weather.com/v1/...")
実践応用:標準的なAPI呼び出し関数の構築
これは、OpenAI APIの呼び出しや電子決済プロジェクトで私がよく使用するコードパターンです。適切なAPI呼び出しの実装とロギングを組み合わせることで、バックグラウンドでシステムが何を行っているかを簡単に追跡できます。
import logging
import requests
from tenacity import retry, stop_after_attempt, wait_fixed, before_sleep_log, retry_if_exception
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def is_transient_error(exception):
if isinstance(exception, requests.exceptions.HTTPError):
# 429(レート制限)または5xxサーバーエラーの場合のみリトライ
return exception.response.status_code in [429, 500, 502, 503, 504]
return isinstance(exception, (requests.exceptions.ConnectionError, requests.exceptions.Timeout))
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(2),
before_sleep=before_sleep_log(logger, logging.INFO),
retry=retry_if_exception(is_transient_error)
)
def safe_get_data(url):
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.json()
try:
data = safe_get_data("https://api.example.com/data")
print("データの取得に成功しました!")
except Exception as e:
print(f"3回の試行後に失敗しました: {e}")
結びに代えて
シニア開発者とジュニア開発者の違いは、不完全な状況をどう扱うかに現れます。Tenacityを使用することで、コードが綺麗になる(クリーンコード)だけでなく、システムに魔法のような自己修復能力を持たせることができます。今日からあなたのプロジェクトに取り入れてみてください。信じてください、深夜のエラー電話に怯えることなく、もっとぐっすり眠れるようになるはずです!

