【テクニカル・上級編】【マルチ端末実行ログ安全一元化】共有フォルダへのログ書き込み衝突を回避する追記ロック・転送アルゴリズム – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【マルチ端末実行ログ安全一元化】共有フォルダへのログ書き込み衝突を回避する追記ロック・転送アルゴリズム

社内ニッチシステム、あるいは未だに工場やバックオフィスで現役稼働し続けるレガシー端末群。数台から数十台のクライアント端末が、毎朝の始業時や特定バッチの実行時に、一斉にVBScriptを走らせる。

この時、エンジニアの頭を悩ませるのが「ログの集約」だ。
各端末が勝手気ままにネットワーク共有フォルダの単一ファイルへ追記を行えば、そこには必ず「書き込み衝突(ファイル共有違反:Permission Denied)」という名の修羅場が訪れる。

「運良く書き込めるまでループさせればいい」――そう考えて `On Error Resume Next` で力技の再試行(リトライ)を実装したところで、ネットワークの遅延やSMBのロック競合の前には無力だ。結果、重要であるはずの実行ログは欠損し、障害発生時のトレーサビリティは完全に崩壊する。

本稿では、VBScriptとWSH(Windows Script Host)の限界領域を知り尽くしたアーキテクトへ向けて、「ローカル一時保存 + 非同期トランザクション転送 + 排他制御アルゴリズム」を用いた、絶対にログを落とさない堅牢なマルチ端末ログ一元化モジュールを提示する。

1. ネットワーク共有ログ書き込みのメカニズムと限界

VBScriptの `Scripting.FileSystemObject (FSO)` を用いてネットワーク上のファイル(UNCパス)にアクセスする場合、背後ではSMB(Server Message Block)プロトコルが動作している。

[端末A] –(SMB Write)–+
[端末B] –(SMB Write)–+—> [共有フォルダ \Server\Logs\app.log] (即死: 共有違反エラー)
[端末C] –(SMB Write)–+

複数のプロセスが同時にファイルハンドルを取得しようとすると、OSレベルでアクセスが拒否される。VBScript自体には、C#の `Mutex` やJavaの `synchronized` のような洗練されたスレッド間・プロセス間排他制御機構は存在しない。

この物理的制約を突破するためのアプローチは一つしかない。
「リモートへの直接書き込みを諦め、ローカルに安全にバッファリングした後、排他制御を考慮した安全なタイミングでアトミックに転送する」ことだ。

2. 設計思想:ローカルバッファリングとアトミック・リトライ

今回構築するモジュールのライフサイクルとデータフローは以下の通りである。

1. ローカル隔離領域への書き込み: ログ生成時は、ネットワークの遅延や競合の影響を受けないローカル端末内の領域(例: `%TEMP%`)へプレーンテキストとして追記する。
2. ファイル名による一意性担保: 端末名やプロセスID、タイムスタンプを付与した一時ファイルとすることで、同一端末内での競合も排除する。
3. 安全な一括転送(Flush): 処理の終端で共有フォルダへアクセスし、ファイル単位での排他ロック(VBScriptの `Open` ステートメントの排他モード、またはファイル名変異による擬似アトミック操作)を模して追記を試みる。
4. 指数バックオフ(Exponential Backoff)によるリトライ: 共有側がビジーな場合、待機時間を等比級数的に増やしながらリトライを行い、ネットワーク負荷を抑制する。

`SafeLogger.vbs` – 実装コード

以下のコードは、オブジェクトの明示的解放、エラーハンドリング、そしてレガシー環境での安定性を極限まで高めた実用モジュールである。インクルードファイル、またはCOMコンポーネント的なサブルーチンとして利用できる。

‘ ==============================================================================
‘ Module Name: SafeLogger.vbs
‘ Description: 共有フォルダへのログ衝突を完全回避するローカルバッファ&安全転送モジュール
‘ Author: Chief Architect
‘ ==============================================================================

Option Explicit

‘ — テスト実行用メインルーチン —
Call Main()

Sub Main()
Dim logger
Set logger = New SafeLogger

‘ 初期化(ローカル一時パスと共有保存先の設定)
‘ ※環境に合わせて共有パスを変更してください
logger.Initialize “AppExecutionLog”, “\\server01\shared_logs$”, 3

‘ ログ出力のシミュレーション
logger.Log “INFO”, “処理を開始します。”
logger.Log “DEBUG”, “パラメータ読込完了: Mode=Batch”

‘ 処理の途中でエラーが発生した想定
On Error Resume Next
Dim x: x = 1 / 0 ‘ 意図的なゼロ除算エラー
If Err.Number <> 0 Then
logger.Log “ERROR”, “致命的なエラーが発生しました: ” & Err.Description
End If
On Error GoTo 0

logger.Log “INFO”, “処理を正常に終了しました。”

‘ 終了時に共有フォルダへフラッシュ(転送)
logger.FlushAndSync()

Set logger = Nothing
End Sub

‘ ==============================================================================
‘ Class: SafeLogger
‘ ==============================================================================
Class SafeLogger
Private m_FSO
Private m_WshShell
Private m_LogName
Private m_SharedDir
Private m_LocalFilePath
Private m_MaxRetry

‘ コンストラクタ
Private Sub Class_Initialize()
Set m_FSO = CreateObject(“Scripting.FileSystemObject”)
Set m_WshShell = CreateObject(“WScript.Shell”)
m_MaxRetry = 5
End Sub

‘ デストラクタ(メモリの確実に解放)
Private Sub Class_Terminate()
Set m_WshShell = Nothing
Set m_FSO = Nothing
End Sub

‘ 初期設定メソッド
Public Sub Initialize(ByVal logIdentifier, ByVal sharedDirectory, ByVal maxRetryCount)
m_LogName = logIdentifier
m_SharedDir = sharedDirectory
If maxRetryCount > 0 Then m_MaxRetry = maxRetryCount

‘ ローカルの %TEMP% 領域にプロセス固有の一時ファイルを生成
‘ 端末名 + 乱数 + タイムスタンプにより、同一端末内の多重起動でも衝突しない
Dim tempFolder, uniqueFileName
Set tempFolder = m_FSO.GetSpecialFolder(2) ‘ 2 = TemporaryFolder

uniqueFileName = m_LogName & “_” & GetComputerName() & “_” & GetTimestampStr() & “_” & Int(Rnd 10000) & “.log”
m_LocalFilePath = m_FSO.BuildPath(tempFolder, uniqueFileName)

Set tempFolder = Nothing
End Sub

‘ ローカルバッファへのログ追記
Public Sub Log(ByVal logLevel, ByVal message)
Dim logLine
logLine = FormatDateTime(Now, 0) & ” [” & UCase(logLevel) & “] [” & GetComputerName() & “] ” & message

On Error Resume Next
Dim ts
‘ ForAppending (8), Create (True), Format (TristateFalse = ASCII/ANSI)
Set ts = m_FSO.OpenTextFile(m_LocalFilePath, 8, True, 0)
If Err.Number = 0 Then
ts.WriteLine logLine
ts.Close
End If
Set ts = Nothing
On Error GoTo 0
End Sub

‘ 共有フォルダへの安全なフラッシュと同期
Public Sub FlushAndSync()
‘ ローカルファイルが存在しない、またはサイズが0なら何もしない
If Not m_FSO.FileExists(m_LocalFilePath) Then Exit Sub

Dim f
Set f = m_FSO.GetFile(m_LocalFilePath)
If f.Size = 0 Then
Set f = Nothing
m_FSO.DeleteFile m_LocalFilePath, True
Exit Sub
End If
Set f = Nothing

‘ 共有ディレクトリの存在確認
If Not m_FSO.FolderExists(m_SharedDir) Then
‘ 共有が見つからない場合はローカルに保持したまま抜ける(ログロスト防止)
Exit Sub
End If

‘ 共有側の統合ファイルパス(全端末共通のマスターログ)
Dim masterLogPath
masterLogPath = m_FSO.BuildPath(m_SharedDir, m_LogName & “_” & Left(GetTimestampStr(), 8) & “.log”)

‘ 指数バックオフによるリトライ機構付き書き込み
Dim attempt, success, sleepTime
attempt = 0
success = False
sleepTime = 1000 ‘ 初期待機 1秒 (ミリ秒)

Do While attempt < m_MaxRetry And Not success attempt = attempt + 1 On Error Resume Next ' 共有ファイルへの安全な追記処理(排他ロックの模倣) Dim sharedTs ' 共有ファイルを開いて追記 Set sharedTs = m_FSO.OpenTextFile(masterLogPath, 8, True, 0) If Err.Number = 0 Then ' ローカルファイルを読み込んで共有ファイルへ流し込む Dim localTs Set localTs = m_FSO.OpenTextFile(m_LocalFilePath, 1, False, 0) If Err.Number = 0 Then Do While Not localTs.AtEndOfStream sharedTs.WriteLine localTs.ReadLine Loop localTs.Close success = True End If Set localTs = Nothing sharedTs.Close End If Set sharedTs = Nothing If Not success Then ' 失敗時はウェイトを入れてリトライ(指数バックオフ + 乱数でスラッシング回避) WScript.Sleep sleepTime + (Int(Rnd 500)) sleepTime = sleepTime 2 ' 待ち時間を倍増 End If On Error GoTo 0 Loop ' 転送に成功した場合のみローカル一時ファイルを削除 If success Then On Error Resume Next m_FSO.DeleteFile m_LocalFilePath, True On Error GoTo 0 End If End Sub ' --- ヘルパー関数群 --- Private Function GetComputerName() GetComputerName = m_WshShell.ExpandEnvironmentStrings("%COMPUTERNAME%") End Function Private Function GetTimestampStr() Dim d d = Now GetTimestampStr = Right("0000" & Year(d), 4) & _ Right("00" & Month(d), 2) & _ Right("00" & Day(d), 2) & "_" & _ Right("00" & Hour(d), 2) & _ Right("00" & Minute(d), 2) & _ Right("00" & Second(d), 2) End Function End Class ---

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

このモジュールを実装するにあたり、現場の過酷な環境(瞬断するVPN、過負荷なファイルサーバー)を生き抜くための3つの「極限の知見」が組み込まれている。

① スラッシング(Thundering Herd Problem)の回避

複数端末が同時に共有フォルダへの書き込みに失敗した際、一斉に同じタイミング(例: 1秒後)でリトライをかけると、再びサーバー上で衝突が起きる。
これを防ぐため、コード内では `sleepTime + (Int(Rnd 500))` とし、ランダムな揺らぎ(Jitter)を待機時間に付加している。これにより、競合の確率を劇的に低下させている。

② メモリリークとオブジェクトライフサイクルの完全制御

VBScriptのCOMコンポーネント(特に `FileSystemObject` や `WScript.Shell`)は、スコープを抜けても即座にメモリが解放されないケースがある。
本クラスでは、明示的に `Set xxx = Nothing` を `Class_Terminate` 内で行うのはもちろん、メソッド内でも用済みになったテキストストリームオブジェクト(`ts`, `localTs`, `sharedTs`)をその都度破棄している。長時間の連続稼働やメモリが枯渇しがちな仮想デスクトップ(VDI)環境でもリークを起こさない。

③ ゼロ・データロスト設計(Fail-Safe)

万が一、ネットワークの切断等で最終的な `FlushAndSync()` が失敗した場合でも、ローカルの一時ファイル(`m_LocalFilePath`)はあえて削除されずに残る。
これにより、ネットワーク復旧後に再度スクリプトを実行するか、別タスクで残存一時ファイルを回収するフェールセーフ機構を後から構築することが可能となる。ログが「消える」というシステム開発における最大の罪悪をここで断絶している。

4. 導入運用におけるベストプラクティス

1. アクセス権限の最小化:
共有フォルダ(`\\server01\shared_logs$`)に対する各クライアント端末の権限は、「作成 (Create Files / Append Data)」のみを付与し、既存ファイルの削除や上書き権限は与えないこと。これにより、悪意あるスクリプトやバグによる全ログの消失リスクを防ぐ。
2. 日別ログのローテーション:
サンプルコード内では `masterLogPath` に `YYYYMMDD` を付与して日別ファイルに集約している。ファイル肥大化によるI/Oのボトルネックを防ぐため、この粒度での分割設計がインフラ運用上必須である。

VBScriptはレガシーな言語と揶揄されがちだが、OSの根底に深く刺さるWSHの機動力と適切なアーキテクチャ設計を組み合わせれば、現代の大規模分散システムに対抗しうる堅牢な末端処理基盤となり得る。現場の安定稼働のために、ぜひこのパターンを武器として取り入れてほしい。

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