「ファイル競合」を制する:FSOにおけるリトライ・タイムアウト設計の極致
業務自動化の現場において、もっとも低俗ながら、もっともエンジニアの神経を逆撫でする問題がある。それは「ファイルが別のプロセスによって使用されています」という、あまりにも無慈悲な実行時エラー(Error 70: Permission Denied)だ。
Excelがバックグラウンドで残っている、ウイルス対策ソフトがスキャンを開始した、あるいはネットワークストレージの遅延によってハンドルが解放されていない——。これらの事象は、単一のスクリプトを走らせる分には些細な問題に見えるが、数千のファイルを捌くバッチ処理や、ミッションクリティカルなシステム間連携においては、システムの信頼性を根底から覆す致命的な脆弱性となる。
本稿では、FileSystemObject (FSO) を用いたファイル操作において、単なる「リトライループ」を超えた、堅牢なタイムアウト制御とリソース管理の実装手法を解説する。これは、レガシーなVBScript環境であっても、現代的な堅牢性を担保するための「設計思想」の伝承である。
1. 敗北のパターン:なぜ「On Error Resume Next」だけでは足りないのか
初学者が陥る典型的な失敗は、エラーを無視してループを回すだけのコードだ。
' --- 悪例:アンチパターン ---
On Error Resume Next
Do While Not FSO.FileExists(destPath)
FSO.MoveFile srcPath, destPath
' エラーを無視し続け、無限ループに陥るか、
' あるいはエラーに気づかず処理が先に進んでしまう
Loop
この実装には、以下の3つの致命的な欠陥がある。
1. 無限ループの懸念: ファイルが永久にロックされた場合、プロセスがゾンビ化し、CPUリソースを浪費し続ける。
2. コンテキストの喪失: なぜ失敗しているのか、いつ諦めるべきなのかという「意思決定」がコードに含まれていない。
3. リソースの枯渇: エラー発生時にオブジェクトのクリーンアップが適切に行われず、COMオブジェクトの参照カウントが残る可能性がある。
シニアエンジニアが書くべきは、「いつ、どのような条件で、どのように失敗を宣言するか」が定義された、決定論的なロジックである。
2. 極限の設計:堅牢なリトライ・タイムアウト・ロジック
堅牢な実装には、「経過時間の監視」「指数バックオフ(または一定間隔の待機)」「明示的なエラーハンドリング」の3要素が不可欠である。
以下に、実務レベルでそのまま転用可能な、高品位なVBScriptモジュールを示す。
' ==============================================================================
' Module: RobustFileOperations
' Description: 他プロセスによるロックを考慮した、タイムアウト付きファイル移動関数
' ==============================================================================
Option Explicit
' 定数定義
Const ERR_PERMISSION_DENIED = 70
Const DEFAULT_TIMEOUT_SEC = 30
Const DEFAULT_RETRY_INTERVAL_MS = 1000 ' 1秒
' メイン処理実行例
Call Main()
Sub Main()
Dim fso, src, dest
Set fso = CreateObject("Scripting.FileSystemObject")
src = "C:\Temp\source_data.csv"
dest = "C:\Temp\archive_data.csv"
' 堅牢な移動処理の呼び出し
If RobustMoveFile(fso, src, dest, DEFAULT_TIMEOUT_SEC, DEFAULT_RETRY_INTERVAL_MS) Then
WScript.Echo "SUCCESS: File moved successfully."
Else
WScript.Echo "FATAL: File move failed after timeout. Check logs."
WScript.Quit 1
End If
' オブジェクトの明示的解放
Set fso = Nothing
End Sub
' ------------------------------------------------------------------------------
' Function: RobustMoveFile
' Purpose: タイムアウト付きでファイルを移動する。ロック中であれば待機してリトライする。
' ------------------------------------------------------------------------------
Function RobustMoveFile(objFSO, strSource, strDest, intTimeoutSec, intRetryIntervalMs)
Dim intStartTime, intEndTime, boolSuccess, errCode
boolSuccess = False
' WScript.Timer はミリ秒単位の経過時間を返す
intStartTime = Timer
intEndTime = intStartTime + intTimeoutSec
Do While Timer < intEndTime
On Error Resume Next
' ファイルの存在確認(移動元が消失していないか)
If Not objFSO.FileExists(strSource) Then
WScript.Echo "ERROR: Source file not found: " & strSource
RobustMoveFile = False
Exit Function
End If
' 移動試行
objFSO.MoveFile strSource, strDest
' エラーハンドリング
errCode = Err.Number
If errCode = 0 Then
boolSuccess = True
On Error GoTo 0
Exit Do
ElseIf errCode = ERR_PERMISSION_DENIED Then
' ロック中の場合は、指定時間待機してリトライ
' WScript.Sleep はミリ秒指定
WScript.Sleep intRetryIntervalMs
Else
' 想定外のエラー(ディスクフル、パス不正等)は即時中断
WScript.Echo "ERROR: Unexpected error " & errCode & " during move."
On Error GoTo 0
Exit Function
End If
On Error GoTo 0
Loop
' タイムアウト判定
If Not boolSuccess Then
WScript.Echo "TIMEOUT: File is locked by another process. Target: " & strSource
End If
RobustMoveFile = boolSuccess
End Function
3. アーキテクトの視点:実装における深層的知見
このコードがなぜ「実務に耐える」のか。その裏側にある技術的意図を解説する。
3.1. `WScript.Timer` による非ブロッキングに近い時間管理
`Date` 関数ではなく `Timer` 関数を使用している点は重要だ。`Timer` は午前0時からの経過秒数を浮動小数点数で返すため、精密な経過時間の計算に適している。これにより、システム時刻の変更による影響を最小限に抑えつつ、正確なタイムアウト判定が可能となる。
3.2. エラーコードの選別(Error Discrimination)
すべてのエラーをリトライ対象にしてはならない。
- Error 70 (Permission Denied): これは「一時的な競合」である可能性が高い。リトライの価値がある。
- Error 58 (File not found) または Error 76 (Path not found): これらは「論理的な欠陥」である。リトライしても結果は変わらないため、即座にフェイルファスト(Fail-Fast)させるべきだ。
この選別こそが、システムの無駄な稼働を防ぐプロフェッショナルの流儀である。
3.3. オブジェクトのライフサイクルとメモリ管理
VBScriptはCOM(Component Object Model)を利用している。`Set obj = Nothing` を明示的に記述することは、単なる作法ではない。
特に、ループ内でオブジェクトを生成・破棄する場合、参照カウントが正しくデクリメントされないと、メモリリークを引き起こし、長期間稼働するサーバープロセスにおいて「徐々にメモリを食いつぶす」という、デバッグ困難な不具合の温床となる。本稿のコードでは、`objFSO` を引数として渡し、呼び出し元で管理する設計にすることで、所有権(Ownership)を明確にしている。
3.4. レガシー環境への配慮
`WScript.Sleep` を利用した待機は、CPU使用率を極限まで抑える。`Do…Loop` 内で何もせず回す(ビジーウェイト)ことは、マルチコア時代の現代において、他のプロセス(=ファイルを解放しようとしているプロセス)の実行リソースを奪うことになり、かえって解決を遅らせる結果を招く。
結言
自動化エンジニアの真価は、成功するコードを書くことではなく、「失敗する状況をいかにエレガントに制御するか」に現れる。
ファイルシステムという、常に不確実性が伴う領域において、リトライ・タイムアウト・ログ出力の三位一体を実装することは、システムのレジリエンス(回復力)を高めるための最低限の義務である。この知見を、あなたの設計に組み込んでいただきたい。
