【実務・中級編】【実務中級者向け】「名前を付けて保存」失敗時のリトライアル:ネットワーク遅延によるファイルロックを待機するループ処理 – AutoCAD VBA解析バイブル

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

「たかが保存」がシステムを殺す。ネットワーク遅延とファイルロックを制する堅牢なリトライ設計

現場のエンジニアが書くコードは、往々にして「ハッピーパス(正常系)」のみを想定している。
「ファイルパスが正しく、ネットワークが安定しており、他の誰もそのファイルに触れていない」——そんな、夢のような環境を前提に設計されたコードは、実務の荒波、特にNASや共有サーバーが絡む環境では、無残にもクラッシュする。

AutoCADの自動化において、もっとも致命的なエラーの一つが、`SaveAs` メソッドの失敗だ。
ユーザーが図面を開いている最中に、ウイルス対策ソフトがスキャンを開始した。あるいは、NASの同期プロセスがファイルハンドルを一時的に握った。その瞬間、君の作成したツールは「実行時エラー」を吐き出し、ユーザーの作業を中断させ、最悪の場合、図面データを破損させる。

プロフェッショナルとは、エラーが起きないことを祈る者ではない。エラーが起きることを前提に、いかにエレガントに、かつ確実にリカバリーするかを設計できる者を指す。

今回は、ネットワーク遅延によるファイルロックを回避するための、実戦的な「リトライ・アルゴリズム」の極意を伝授する。

1. なぜ、単純な `SaveAs` では足りないのか

多くの開発者が陥る罠は、単に `On Error Resume Next` を使ってエラーを無視しようとすることだ。これは設計思想として最悪である。エラーを無視することは、問題の先送りに過ぎず、データの整合性を破壊するリスクを孕んでいる。

ファイルロックが発生する主な要因は以下の3点だ:
1. OSのインデックス作成・ウイルス対策ソフト: ファイルの作成・変更を検知した瞬間にスキャンが走る。
2. ネットワークプロトコル(SMB/CIFS)の遅延: NASへの書き込みリクエストと、ファイルシステムのメタデータ更新の間にタイムラグが生じる。
3. 他ユーザー・他プロセスによるアクセス: 共有サーバー特有の、意図しないファイルハンドルの競合。

これらは「一時的なエラー(Transient Error)」である。つまり、「今すぐ」は無理でも、「数秒後」には成功する可能性が極めて高い。この特性を見極め、適切なインターバルを持って再試行(Retry)を行うことこそが、堅牢なツールの条件だ。

2. 堅牢なリトライ・ロジックの設計指針

設計において、以下の4つのパラメータを厳格に管理せよ。

1. MaxRetries (最大試行回数): 無限ループは厳禁だ。システムをフリーズさせれば、それはツールではなく「バグ」である。
2. RetryInterval (待機時間): 0秒でのリトライは意味がない。OSやネットワークが状態を遷移させるための「猶予」を与える必要がある。
3. Exponential Backoff (指数バックオフ) の検討: 単純な固定秒数ではなく、失敗するごとに待ち時間を増やしていく手法だ。今回は実務的な「固定秒数+DoEvents」を採用する。
4. Error Differentiation (エラーの選別): 「パスが見つからない」といった致命的なエラーと、「ファイルがロックされている」という一時的なエラーを区別できれば理想だが、VBAの `Err.Number` だけでは限界がある。そのため、「SaveAsが成功したかどうか」を判定の軸に据える。

3. プロダクション・クオリティのコード実装

以下に、そのまま実務のモジュールとして組み込めるレベルのコードを示す。
単なるリトライループではない。ユーザーの操作性を損なわないよう `DoEvents` を挟み、プロセスの応答性を維持している点がポイントだ。

Option Explicit

''' <summary>
''' ネットワーク遅延やファイルロックを考慮した、堅牢な「名前を付けて保存」の実装
''' </summary>
''' <param name="targetPath">保存先のフルパス</param>
''' <param name="fileFormat">AutoCADのファイル形式(例: acDbMetricDrawing)</param>
''' <returns>成功した場合はTrue、最大試行回数を超えて失敗した場合はFalse</returns>
Public Function RobustSaveAs(ByVal targetPath As String, ByVal fileFormat As Long) As Boolean
    
    Const MAX_RETRIES As Integer = 5          ' 最大試行回数
    Const RETRY_INTERVAL_SEC As Double = 2.0  ' 再試行までの待機秒数
    
    Dim retryCount As Integer
    Dim isSuccess As Boolean
    Dim startTime As Double
    
    isSuccess = False
    retryCount = 0
    
    ' 実行開始時間の記録(デバッグ・ログ用)
    startTime = Timer
    
    Do While retryCount < MAX_RETRIES
        On Error Resume Next
        
        ' AutoCADのSaveAsを実行
        ' ※ThisDrawingは実行コンテキストに応じて適切に指定すること
        ThisDrawing.SaveAs targetPath, fileFormat
        
        ' エラーチェック
        If Err.Number = 0 Then
            ' 成功した場合
            isSuccess = True
            On Error GoTo 0
            Exit Do
        Else
            ' エラー発生時の処理
            Dim errDesc As String
            errDesc = Err.Description
            Err.Clear
            On Error GoTo 0
            
            retryCount = retryCount + 1
            
            ' ログ出力(実務ではログファイルやイミディエイトウィンドウへ)
            Debug.Print "[RETRY] Attempt " & retryCount & "/" & MAX_RETRIES & _
                        " failed. Error: " & errDesc & " | Path: " & targetPath
            
            ' 失敗した場合、ユーザーに状況を知らせる(ステータスバーへの表示を推奨)
            ' 画面が固まったように見えないよう、DoEventsで制御をOSに戻す
            If retryCount < MAX_RETRIES Then
                ' 指定秒数待機(DoEventsを挟みながらループで待機)
                Dim waitEnd As Double
                waitEnd = Timer + RETRY_INTERVAL_SEC
                Do While Timer < waitEnd
                    DoEvents ' Windowsのメッセージループを処理し、フリーズを防ぐ
                Loop
            End If
        End If
    Loop
    
    ' 最終結果の判定
    If isSuccess Then
        Debug.Print "[SUCCESS] Saved successfully after " & retryCount & " retries."
    Else
        Debug.Print "[FATAL] Failed to save after " & MAX_RETRIES & " attempts."
    End If
    
    RobustSaveAs = isSuccess
    
End Function

''' <summary>
''' 実務での呼び出し例
''' </summary>
Public Sub ExecuteSaveWorkflow()
    Dim savePath As String
    savePath = "Z:\Projects\Shared_Drawing_2023.dwg" ' ネットワークパスの例
    
    ' 業務ロジックの実行
    Debug.Print "Starting save process..."
    
    ' 堅牢な保存メソッドの呼び出し
    If RobustSaveAs(savePath, acDbMetricDrawing) Then
        MsgBox "保存が完了しました。", vbInformation, "完了"
    Else
        ' 最大試行回数を超えた場合の「敗北」への対処
        MsgBox "【警告】保存に失敗しました。" & vbCrLf & _
               "ネットワーク環境を確認するか、ローカルに保存してください。", _
               vbCritical, "致命的エラー"
        
        ' ここでバックアップ保存(C:\Temp等)を試みるなどの高度なリカバリを検討せよ
    End If
End Sub

4. アーキテクトからのアドバイス:さらに高みを目指すために

このコードは、実務レベルの「守り」としては十分だが、さらに大規模なシステムやミッションクリティカルな業務ツールへと昇華させるなら、以下の視点を追加せよ。

① フォールバック戦略(Fallback Strategy)

ネットワークへの保存が完全に失敗した場合、ただエラーを表示して終わるのは二流だ。
「ネットワークがダメなら、一旦ローカルの `C:\Temp` に退避保存し、ユーザーにその旨を通知する」というフォールバック(代替策)を実装せよ。これが、ユーザーのデータを守る真のエンジニアの仕事だ。

② ロギングの重要性

`Debug.Print` は開発用だ。プロダクション環境では、エラー内容、試行回数、発生時刻、ファイルパスをテキストファイルやデータベースに記録せよ。後から「なぜあの時、あの図面が保存されなかったのか」を解析できる環境を作っておくことが、保守性の正体だ。

③ データベース連携時のトランザクション

もし、図面の保存と同時に、外部の管理データベース(SQL Server等)のステータスを更新しているなら、保存の成否とDB更新はアトミック(不可分)でなければならない。
`SaveAs` が失敗したのに、DBだけ「保存済み」になるような設計は、データの不整合を招く。必ず「保存成功を確認してからDB更新」という順序、あるいは失敗時のロールバックを徹底せよ。

まとめ

「動くコード」を書くのは誰にでもできる。しかし、「壊れないコード」を書くには、環境の不確実性に対する深い洞察が必要だ。
ネットワークの遅延、ファイルロック、OSの振る舞い。これらを敵と見なすのではなく、設計の一部として組み込むこと。それが、君のツールを「ただのスクリプト」から「信頼されるシステム」へと変える唯一の道だ。

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