別々の自動化案件で、まったく同じリトライ処理を書いていた
複数のRuby + Seleniumの自動化スクリプトを見返していて気づいたことがあります。対象業種はオークション系、EC系、求人系とまったく違うのに、ページ遷移まわりのエラーハンドリングだけは判で押したように同じ形をしていました。
begin
driver.get(url)
rescue
begin
driver.quit
rescue
end
driver = Selenium::WebDriver.for :chrome
retry
end
driver.getが例外を投げたら、まずdriver.quitでドライバを終了させ(それすら失敗する可能性があるので二重にrescueする)、新しいドライバインスタンスを作り直し、retryで同じgetをもう一度試みる。この形が案件をまたいで独立に何度も出てきていたので、なぜこの設計に自然と収束するのか整理してみます。
「例外を握りつぶして単純リトライ」では足りない理由
素朴には、こう書きたくなります。
begin
driver.get(url)
rescue
retry
end
しかしこれは長時間の巡回バッチでは危険です。driver.getが失敗する原因には、一時的なネットワーク断だけでなく、ブラウザプロセスそのものが応答不能になっているケース(レンダラークラッシュ、メモリ不足、ドライバとブラウザ間の通信断など)が含まれます。こうなったドライバは何度getを呼び直しても同じ場所で固まり続け、単純な無限リトライがそのままハングにつながってしまいます。同じ例外を投げ続けているように見えても、リトライで直るエラーと、ドライバの再生成でしか直らないエラーは実は別物だということです。
driverオブジェクトごと作り直すという発想
ここで効くのが、「怪しくなったら疑わしきはリセットする」という考え方です。ドライバが本当に不健全な状態かどうかを厳密に診断するのは難しいので、診断そのものを諦めて丸ごと作り直してしまいます。
MAX_RETRY = 5
def get_with_retry(driver, url, max_retry: MAX_RETRY)
retries = 0
begin
driver.get(url)
driver
rescue => e
retries += 1
raise e if retries > max_retry
warn "driver.get失敗(#{retries}回目)。ドライバを再生成します: #{e.class}"
begin
driver.quit
rescue
# quit自体が失敗しても無視して次に進む
end
driver = Selenium::WebDriver.for :chrome
retry
end
end
呼び出し側は、戻り値のドライバで必ず変数を更新するようにします。
driver = Selenium::WebDriver.for :chrome
driver = get_with_retry(driver, "https://example.com/list")
driver.quitを必ずrescueで囲っているのも、独立した案件間で共通していたポイントです。すでに壊れかけているブラウザプロセスに終了処理を呼ぶと、それ自体が例外を投げることがあります。ここで無防備に例外を伝播させてしまうと、肝心の再生成にたどり着けずスクリプト全体が止まってしまいます。「後片付けの失敗で本題を止めない」という優先順位が、この設計のいちばん重要な部分です。
リトライ回数に上限を設ける
案件ごとの実装を見ていると、無限retryになっているものも少なくありませんでした。しかし対象サイト自体がダウンしている、ネットワークが恒久的に切れているといった場合は、何度ドライバを作り直しても無駄です。上限回数を超えたら例外を再送出し、呼び出し元(バッチの監視やログ)に異常を伝える形にしておくことで、無限ループでリソースを食い潰す事故を防げます。上限を何回にするかは対象環境の不安定さ次第ですが、まずは3〜5回程度から始めて、実際のログを見ながら調整するのが現実的です。
なぜ複数の自動化で独立に同じ形に収束するのか
オークション系、EC系、求人系のように扱うデータもドメインもまったく違う自動化が、この一点だけ同じ設計にたどり着くのは偶然ではありません。長時間・大量ページを巡回するSeleniumスクリプトは共通して「ブラウザプロセスは一定確率で壊れる」という前提を置かざるを得ず、その対処として選べる手段が「壊れたコンポーネント(ドライバ)を丸ごと作り直す」という一択にほぼ収束するからです。個々のビジネスロジックはまったく違っても、インフラ層の障害モデルが同じであれば対処パターンも同じになる、という良い例だと思います。
まとめ
driver.get失敗時は「ドライバを疑わしきものとして丸ごと作り直す」のが、原因を厳密に診断できない状況での現実的な対処driver.quit自体が失敗する可能性があるので必ずrescueで囲み、後片付けの失敗が再生成の妨げにならないようにする- リトライには上限を設け、恒久的な障害では例外を再送出して呼び出し元に異常を伝える
- ドメインの異なる複数の自動化案件で、この設計に独立に収束していたのは「ブラウザプロセスは壊れうる」という共通の障害モデルに対する自然な解だから


コメント