はじめに
並列実行で出る一時エラーにリトライをかけたい。でも INSERT ベースのロードに「失敗したら毎回リトライ」は危険——という話です。冪等でない処理へ安全にリトライをかけるための設計判断を、PowerShell の実装例とあわせてまとめます。
私はデータ基盤の運用保守に携わっており、主にバッチ処理まわりの保守や追加開発を担っています。
Snowflake CLI(snow コマンド)を PowerShell から呼び出して、TSV ファイルをターゲットテーブルへロードする処理を運用しています。
並列実行するとタイミング依存の一時エラーがまれに出るため、ここにリトライ機能を組み込みました。
このとき素直に「コマンドが失敗したら毎回リトライ」と書きたくなりますが、私の環境ではそれが危険でした。ロード処理が INSERT 文ベースだったためです。
結論から言うと、私はリトライ対象を 「SQL が処理される前に必ず失敗する一時エラー」だけに限定しました。具体的には、ファイルロックを解放できない系・ロギング系のエラーです。これらは SQL 本体が実行される前に落ちるため、再実行してもターゲットテーブルに重複行が入りません。判定は PowerShell の -like で行い、対象エラーの文言は外部ファイルで管理しています。
この記事は、「なぜ全エラー一律のリトライにしなかったのか」という設計判断を中心に、同じように冪等でない処理へリトライをかけたい方に向けてまとめます。
📚 本記事は Snowflake CLI 運用シリーズの一編です。シリーズ第1回「Snowflake CLI で並列接続を安全に動かすための実践ガイド」では、並列実行で遭遇する 落とし穴①(日次初回のログローテーション競合) と 落とし穴②(ファイルロックを解放できないエラー)、その対策(ログ分割・起動分散・リトライ)を扱いました。本記事は、その 対策③「リトライ」を、冪等でない
INSERTロードへ安全にかけるには という設計側の掘り下げです。
環境・前提
- OS / シェル: Windows / PowerShell(コマンド例は Windows 基準です。macOS や Linux は公式をご確認ください)
- Snowflake CLI(
snowコマンド)をスクリプトから呼び出し、TSV ファイルをロード - データのロードは
INSERT文で実行(既存の本番スクリプトの事情によるもの。新規開発であれば後述のとおりCOPY INTOが推奨です) - 並列実行時に、ファイルロック・ロギング系の一時エラーがまれに発生する
💡 ここで対象にする一時エラーは、シリーズ第1回で扱った 落とし穴①(ログローテーション競合)=ロギング系 と 落とし穴②(ファイルロック解放)=ファイルロック系 に対応します。エラーそのものの発生メカニズムは第1回に、本記事はその「リトライをどう安全にかけるか」という設計に焦点を当てています。
本題:なぜ "全エラー一律リトライ" にしなかったか
落とし穴:INSERT は冪等でない
リトライの怖いところは、失敗したと判定した処理が、実際にはサーバ側で成功していた場合です。INSERT 文をそのまま再実行すると、同じ行がもう一度入り、重複データが生まれます。Snowflake の INSERT は、すでに同じ行が存在していてもエラーにはならず、そのまま追加されます。
つまり「失敗したら無条件にリトライ」は、INSERT ベースのロードでは重複を生むリスクを抱えることになります。
COPY INTO ならこの問題は起きにくい
新規にデータロードを設計するなら、Snowflake では COPY INTO が推奨です。COPY INTO はロード済みファイルの情報をテーブルのロードメタデータに記録し、すでにロードした同じファイルを既定でスキップします。これにより、リトライや再実行で同じファイルを読んでも重複ロードを防げます(ロード履歴による判定。FORCE を指定するとこの保護を無効化して再ロードできます)。
ただし今回は、既存の本番スクリプトが INSERT を使ってロードしているという制約がありました。COPY INTO のロード履歴による保護は効きません。プラットフォームが推奨する書き方をそのまま採れない、というよくある状況です。
対策:リトライを「SQL 実行前のエラー」に限定する
そこで私が採った設計は、リトライ対象を「SQL が処理される前に必ず失敗するエラー」だけに絞るというものです。私の環境でリトライ対象にしたのは、次の 2 種類でした。
- ファイルロックを解放できないことに起因するエラー(シリーズ第1回の 落とし穴② に相当)
- ロギング(ログ出力・ローテーション)に起因するエラー(シリーズ第1回の 落とし穴① に相当)
これらはいずれも CLI 内部のファイル競合が原因で発生する一時エラーで、SQL 本体が実行される前に落ちます。SQL が完了しない以上、INSERT がターゲットテーブルへ行を書くこともありません。したがって再実行しても重複は生じない、と判断しました。対象エラーが SQL 処理前に発生する性質であることは、Snowflake サポートにも確認しています。
⚠️ 逆に言うと、SQL 実行中・実行後に起こりうるエラーをこのリトライ対象に含めてはいけません。それらは「成功していたかもしれない処理」を再実行することになり、重複の原因になります。リトライしてよいエラーかどうかは、その発生タイミングを必ず確認したうえで決めています。
なぜ「ジョブ管理ツールでのリトライ」にしなかったか
別案として、ジョブ管理ツール側で処理単位(ジョブ全体)をまるごとリトライする方法も検討しました。最終的にこれは採用していません。理由は 2 つです。
-
コマンド単位でリトライしたい。 失敗した
snowコマンドだけを再実行したいのであって、ジョブ全体をやり直したいわけではありません。 - リトライしてはいけないコマンドが混在しうる。 処理単位での再実行では、「再実行してはいけないコマンド」を個別にハンドリングできません。
コマンド単位でリトライ可否を判定できる今の設計のほうが、上記の要件に合っていました。これは私のケースでの判断で、ジョブ全体のリトライが適する構成もあると思います。
実装の骨子
判定対象のエラー文言は、スクリプトに直書きせず外部ファイルで管理しています。文言が変わってもコードを直さず、リストだけ更新すれば済むようにするためです。一致判定には -like を使い、ワイルドカード * で部分一致させています(-like は PowerShell の比較演算子で、* が任意の文字列にマッチします)。
以下は、実際のコードを公開用に書き起こした最小サンプルです。
# リトライ対象のエラー文言(部分一致用のパターン)を外部ファイルから読む
$retryablePatterns = Get-Content -Path ".\retryable_errors.txt"
function Invoke-SnowWithRetry {
param(
[scriptblock]$Command, # 実行する snow コマンドを scriptblock で渡す
[int]$MaxRetries = 3, # リトライ上限
[int]$BackoffSeconds = 60 # 待機秒数(既定60秒。コマンドごとに調整可能)
)
for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
$errorText = $null
try {
$output = & $Command 2>&1
if ($LASTEXITCODE -eq 0) { return $output } # 成功したら結果を返す
$errorText = "$output"
}
catch {
$errorText = $_.Exception.Message
}
# 「SQL 処理前に発生する一時エラー」に一致するときだけリトライ対象とする
$isRetryable = $false
foreach ($pattern in $retryablePatterns) {
if ($errorText -like "*$pattern*") { $isRetryable = $true; break }
}
if (-not $isRetryable) {
throw "リトライ対象外のエラーのため中断します: $errorText"
}
Write-Host "[$(Get-Date -Format s)] retry $attempt/$MaxRetries (${BackoffSeconds}s 待機): $errorText"
Start-Sleep -Seconds $BackoffSeconds
}
throw "リトライ上限($MaxRetries 回)に達しました。"
}
ポイントは次のとおりです。
-
snowコマンドをscriptblockとして関数に渡し、その中で実行・結果判定する形にしています。これで、リトライしたいコマンドを差し替えやすくなります。 - 待機秒数とリトライ上限を引数化し、コマンドごとに調整できるようにしました(既定は 60 秒)。競合が続く場面では、待機を少しずつ延ばすバックオフにすると全体への負荷を抑えられます。
- リトライした事実はログに記録し、後から発生状況を追えるようにしています。
- リスト(
retryable_errors.txt)には、対象としたエラー文言の断片だけを置きます。文言は環境やバージョンで変わりうるので、コードと分離しておくと保守が楽です。
まとめ・学び
冪等でない INSERT ベースのロードにリトライをかけるとき、私がつかんだ要点を整理します。
-
「失敗=即リトライ」は危険。
INSERTは冪等でないため、成功していた処理を再実行すると重複行が入る。 - 新規設計なら
COPY INTOが無難。ロードメタデータで既ロードファイルをスキップし、重複を防いでくれる。 - 既存事情で
INSERTを使わざるを得ない場合は、リトライ対象を「SQL 実行前に必ず失敗する一時エラー」に限定する。発生タイミングを確認したうえで、再実行しても安全なものだけを対象にする。 - エラー判定の文言は外部ファイルで管理し、
-likeで部分一致。コードと文言を分離しておくと保守しやすい。
「どのエラーならリトライしてよいか」を、エラーの発生タイミング(SQL の前か後か)で線引きする——という観点は、Snowflake に限らず冪等でない処理一般に応用できる考え方だと思います。