【テクニカル・上級編】【実務中級者向け】他プロセスによるファイル使用中の解放を待機しタイムアウト付きで確実に処理するFSOループ – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

「ファイル競合」を制する: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` 内で何もせず回す(ビジーウェイト)ことは、マルチコア時代の現代において、他のプロセス(=ファイルを解放しようとしているプロセス)の実行リソースを奪うことになり、かえって解決を遅らせる結果を招く。

結言

自動化エンジニアの真価は、成功するコードを書くことではなく、「失敗する状況をいかにエレガントに制御するか」に現れる。

ファイルシステムという、常に不確実性が伴う領域において、リトライ・タイムアウト・ログ出力の三位一体を実装することは、システムのレジリエンス(回復力)を高めるための最低限の義務である。この知見を、あなたの設計に組み込んでいただきたい。

タイトルとURLをコピーしました