別々の自動化案件で、まったく同じリトライ処理を書いていた
複数のRuby + Seleniumの自動化スクリプトを見返していて気づいたことがあります。対象がまったく違う複数の案件なのに、ページ遷移まわりのエラーハンドリングだけは判で押したように同じ形をしていました。
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を使い続けてしまいます。
driver = Selenium::WebDriver.for :chrome
driver = get_with_retry(driver, "https://example.com/list")
呼び出し側でやることはこれだけです。簡単ですね。実行中のログにdriver.get失敗(1回目)。ドライバを再生成しますと出れば、意図通り動いている証拠です。
driver.quitを必ずrescueで囲っているのも、独立した案件間で共通していたポイントでした。すでに壊れかけているブラウザプロセスに終了処理を呼ぶと、それ自体が例外を投げることがあります。ここで無防備に例外を伝播させると、肝心の再生成にたどり着けずスクリプト全体が止まってしまうことに注意してください。「後片付けの失敗で本題を止めない」。この優先順位が、この設計でいちばん大事なところです。
リトライ回数に上限を設ける
案件ごとの実装を見ていると、無限retryになっているものも少なくありませんでした。しかし対象サイト自体がダウンしている、ネットワークが恒久的に切れているといった場合は、何度ドライバを作り直しても無駄です。上限回数を超えたら例外を再送出して、呼び出し元(バッチの監視やログ)に異常を伝えるようにしましょう。これだけで、無限ループでリソースを食い潰す事故を防げます。上限を何回にするかは対象環境の不安定さ次第ですが、まずは3〜5回くらいから始めて、実際のログを見ながら調整するのが現実的です。シンプルな仕組みですが、これで十分いけます。
なぜ複数の自動化で独立に同じ形に収束するのか
扱うデータもドメインもまったく違う複数の自動化が、この一点だけ同じ設計にたどり着くのは偶然ではありません。長時間・大量ページを巡回するSeleniumスクリプトは、共通して「ブラウザプロセスは一定確率で壊れる」という前提を置かざるを得ないからです。その対処として選べる手段は、「壊れたコンポーネント(ドライバ)を丸ごと作り直す」という一択にほぼ収束します。ここは断言してもいいと思っています。ブラウザ自動化を長時間・大量に回すなら、案件を問わずいずれこの設計にたどり着きます。個々のビジネスロジックはまったく違っても、インフラ層の障害モデルが同じであれば対処パターンも同じになる、という良い例だと思います。
同じようなリトライ処理を書いている方は、一度自分のコードとここまでの内容を見比べてみてください。もっとシンプルにいけている例があれば、ぜひコメントで教えてください。


コメント