【テクニカル・上級編】【排他制御】ロックファイルを利用したVBScriptの二重起動防止(多重実行ガード)パターンの実装 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【排他制御】ロックファイルを利用したVBScriptの二重起動防止(多重実行ガード)パターンの実装

レガシーシステムの基盤、あるいは現行Windows環境におけるタスクスケジューラの常連として、VBScriptは今なおインフラの裏側で静かに稼働し続けている。
しかし、ここに一つの致命的なアンチパターンがある。処理が長大化・複雑化するバッチスクリプトにおいて、「前回の処理が完了していないにもかかわらず、次のトリガーによって二重起動(多重実行)される」というインシデントだ。

データベースの行ロック競合、中間ファイルの破損、あるいはリソースの枯渇。
これを防ぐための排他制御機構を、外部ツールに頼らず、VBScript単体(およびWSH環境)で完璧に実装する知見をここに共有する。

1. なぜVBScriptの排他制御は難しいのか?

VBScriptの本質は、非同期実行を前提としたシングルスレッドのインタプリタである。
C#やJavaのような堅牢なスレッド同期プリミティブ(`Mutex` や `Monitor`)は存在しない。そのため、OSレベルでアトミック(不可分)にリソースを確保する仕組みを自前で構築する必要がある。

実務において、二重起動防止のアプローチとしては主に以下の2つが挙げられる。

1. プロセス名(WMI)による検索: 自身のプロセス名やコマンドライン引数を探索し、一致するプロセスが他に存在すれば終了する。
2. ファイルシステム(ロックファイル)を利用した排他制御: 排他用の空ファイル(またはPID書き込みファイル)を排他モードで生成・維持し、その占有権を競合させる。

結論から言えば、「堅牢性」の観点から選択すべきは後者のロックファイル方式である。
WMIによるプロセス検索は、プロセス情報の伝播遅延や、ユーザー権限の差異による視認性漏れ、さらにはWMIサービス自体の不安定さに起因する誤判定のリスクを孕んでいる。一方、NTFSのファイル排他制御はOSカーネルレベルで保証されており、極めて信頼性が高い。

2. 極限まで洗練されたロックファイル実装パターン

以下に、実業務の現場でそのままデプロイ可能な、堅牢かつモダンなVBScriptの排他制御テンプレートを提示する。

このコードは、単にファイルを置くだけではなく、「異常終了時のゾンビロック残存対策(タイムアウト機能)」「オブジェクトの厳格なメモリ解放」を完全に網羅している。

Option Explicit

‘ ==============================================================================
‘ スクリプト名: BatchProcess_With_Mutex.vbs
‘ 概要: ロックファイルを用いた多重実行ガード付きバッチ処理テンプレート
‘ アーキテクト特記事項:
‘ – ゾンビロック(異常終了時の放置)を防ぐためのタイムアウト機構を実装

  • COMオブジェクトのスコープ管理と明示的破棄によるメモリリーク完全防止

‘ ==============================================================================

‘ 定数定義
Const ScriptName = “DataMigrationBatch”
Const LockFileName = “process.lock”
Const TimeoutSeconds = 300 ‘ ロックの有効期限(5分間。この時間を過ぎた古いロックは強制破棄)

Sub Main()
Dim fso, lockFilePath, wshShell
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set wshShell = CreateObject(“WScript.Shell”)

‘ スクリプト自身の物理パスを取得し、同階層にロックファイルを生成
lockFilePath = fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), ScriptName & “_” & LockFileName)

‘ 1. 排他ロックの獲得試行
If Not AcquireLock(fso, lockFilePath) Then
WScript.Echo “[” & Now & “] 警告: ” & ScriptName & ” は既に実行中です。処理を中断します。”
‘ クリーンアップ
Set fso = Nothing
Set wshShell = Nothing
WScript.Quit 1
End If

‘ — 【ここからメインの業務処理】 —
On Error Resume Next

WScript.Echo “[” & Now & “] 情報: ” & ScriptName & ” の処理を開始します。”

‘ [シミュレーション] 重い処理の実行
WScript.Sleep 5000

If Err.Number <> 0 Then
WScript.Echo “[” & Now & “] エラー発生: ” & Err.Description
Err.Clear
End If

On Error GoTo 0
‘ — 【ここまでメインの業務処理】 —

‘ 2. ロックの解放
Call ReleaseLock(fso, lockFilePath)

WScript.Echo “[” & Now & “] 情報: ” & ScriptName & ” の処理が正常に終了しました。”

‘ オブジェクトの明示的解放(メモリ最適化)
Set fso = Nothing
Set wshShell = Nothing

WScript.Quit 0
End Sub

‘ ——————————————————————————
‘ ロック獲得関数
‘ 戻り値: Boolean (True = 獲得成功, False = 獲得失敗)
‘ ——————————————————————————
Function AcquireLock(fso, lockPath)
Dim file, ts, fileAge, nowTime
nowTime = Now

If fso.FileExists(lockPath) Then
Set file = fso.GetFile(lockPath)
‘ ゾンビロック検知: 最終更新時刻から一定時間経過している場合は強制削除
‘ DateDiff(“s”, 過去, 未来)
fileAge = DateDiff(“s”, file.DateLastModified, nowTime)

If fileAge > TimeoutSeconds Then
‘ タイムアウト超過:前回プロセスが異常終了したとみなしてロックを強制破棄
On Error Resume Next
file.Delete True
On Error GoTo 0
Else
‘ まだ有効なロックが存在する
Set file = Nothing
AcquireLock = False
Exit Function
End If
Set file = Nothing
End If

‘ 排他ファイルを生成してロックを確立する
‘ FileSystemObject.CreateTextFile(path, overwrite, unicode)
‘ overwriteをFalseにすることで、ファイルが万が一瞬時に他者によって作成された場合の競合を防ぐ
On Error Resume Next
Set ts = fso.CreateTextFile(lockPath, False, False)
If Err.Number <> 0 Then
‘ 他プロセスに先を越された場合等
AcquireLock = False
Exit Function
End If

‘ プロセスIDやタイムスタンプを書き込んでおくとデバッグ時に極めて有用
ts.WriteLine “Locked at: ” & nowTime & ” / PID/Env: ” & WScript.ScriptName
ts.Close

If Err.Number <> 0 Then
AcquireLock = False
Else
AcquireLock = True
End If
On Error GoTo 0

Set ts = Nothing
End Function

‘ ——————————————————————————
‘ ロック解放サブプロシージャ
‘ ——————————————————————————
Sub ReleaseLock(fso, lockPath)
On Error Resume Next
If fso.FileExists(lockPath) Then
fso.DeleteFile lockPath, True
End If
On Error GoTo 0
End Sub

‘ 実行のエントリポイント呼び出し
Call Main()

3. チーフアーキテクトが解説する実装の急所

上記のコードが一般的なネット上のサンプルと一線を画す、シニアエンジニア向けの深層設計ポイントを解説する。

A. `CreateTextFile` の第2引数(overwrite)の活用

`fso.CreateTextFile(lockPath, False, False)` の第2引数に `False` を指定している点が極めて重要である。
もしこの引数を `True` にしてしまうと、ファイルが存在する状態でオープンを試みた際、既存のファイルが上書きされてしまい、「二重起動を検知できずに新たなロックを上書き生成してしまう」という致命的な競合(Race Condition)が発生する。
`False` を指定することで、ファイルが既に存在する場合はVBScriptのランタイムエラー(`Err.Number`)が発生するため、これを確実にキャッチして「ロック獲得失敗」と判定できるのである。

B. ゾンビロック(Stale Lock)の自動無効化

バッチ処理が電源断、OSの強制再起動、あるいは無限ループや想定外の致命的エラー(`On Error` のすり抜け)によって突然死した場合、ロックファイルが半永久的にディスク上に残り続ける「ゾンビ化」が発生する。
これを放置すると、次回以降のすべてのバッチ実行が永久にブロックされるという致命的なインシデントに繋がる。
本実装では、ファイルの最終更新日時(`DateLastModified`)を監視し、あらかじめ定めた閾値(例: 5分)を超過している場合は、前回のプロセスは既に死亡していると見なして強制的にロックファイルを削除する自己治癒(Self-Healing)メカニズムを組み込んでいる。

C. オブジェクトの明示的解放(メモリリーク対策)

VBScriptの背後にあるCOMランタイムのガベージコレクションは、スコープを抜ければ最終的には回収されるものの、長期間稼働するタスクや、他の外部COMコンポーネント(Excel.ApplicationやADODB.Connectionなど)と密連携するバッチにおいては、参照カウントの解放遅延がメモリリークを引き起こす。
スクリプトの終端、あるいは処理の節目で `Set fso = Nothing` のように明示的に `Nothing` を代入し、ポインタを即座に解放する規律を徹底すべきだ。

4. 運用・保守フェーズにおけるベストプラクティス

この排他制御パターンを実際の社内システムやクライアント環境に導入する際は、以下の運用設計を合わせて行うことで、システムの堅牢性がさらに高まる。

  • ログ出力の統合: ロック獲得に失敗して終了した際、標準出力(`WScript.Echo`)だけでなく、Windowsのイベントログ(アプリケーションログ)へイベントIDと共に書き込むラッパー関数を組み合わせると、統合監視ツール(ZabbixやSCOMなど)からの検知が容易になる。
  • ネットワーク共有フォルダ(UNCパス)上の注意点:

ロックファイルをローカルディスクではなく、ファイルサーバー等のUNCパス(`\\server\share\lock\`)上に配置して複数サーバーからの二重起動を防ぎたい場合がある。
しかし、ネットワーク経由のファイルロックは、SMBプロトコルのキャッシュ機構やレイテンシの影響により、厳密なアトミック性が担保しにくい。極力、バッチが実行されるローカルノード上の専用テンポラリディレクトリ(例: `%TEMP%` や固定の作業用ローカルパス)をロックファイルの置き場として指定することを強く推奨する。

レガシーであっても、設計思想に妥協がなければ、VBScriptは依然として極めて信頼性の高い自動化の武器となり得る。泥臭い環境であっても、OSのプリミティブを正しく理解し、堅牢なガードを纏わせること。それこそがプロフェッショナルなエンジニアリングである。

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