【極限の設計論】FileSystemWatcherで構築する、堅牢なリアルタイムログ監視システムの正解
業務自動化の現場において、`FileSystemWatcher`は諸刃の剣だ。正しく扱えば強力な武器になるが、多くのエンジニアが「イベントが二重発生する」「UIがフリーズする」「ファイルロックで例外が飛ぶ」という初歩的な罠に沈んでいる。
今日は、場当たり的なコードではなく、プロダクションレベルで通用する「堅牢な非同期ファイル監視システム」の設計思想を叩き込む。
—
1. なぜ「そのまま」書いてはいけないのか?
`FileSystemWatcher`には、設計者が必ず理解しておくべき「3つの落とし穴」がある。
1. イベントのバースト(多重発火): ファイル保存時、OSは短時間に複数の通知を投げることがある。1度の変更で解析処理が10回走っては、CPUが悲鳴を上げる。
2. スレッドの分離: このイベントは「別スレッド」で動く。UI要素(DataGridView等)を直接操作すれば、即座に`InvalidOperationException`が発生する。
3. IO競合: ファイルが書き込み中の瞬間にアクセスすれば、当然エラーになる。
これらを解決するための「疎結合かつ非同期な設計」を実装しよう。
—
2. 堅牢な実装:FileSystemWatcher 構築の実践
以下のコードは、単なるサンプルではない。「スレッドセーフ」「排他制御」「UI更新の安全化」を担保した、実務ベースのアーキテクチャだ。
Imports System.IO
Imports System.Threading.Tasks
Public Class LogMonitorEngine
‘ 監視対象のパス
Private WithEvents watcher As FileSystemWatcher
‘ UI更新用のデリゲート
Public Event DataReceived As Action(Of String)
Public Sub StartMonitoring(path As String)
watcher = New FileSystemWatcher(path, “.csv”)
watcher.NotifyFilter = NotifyFilters.LastWrite ‘ 書き込み終了のみを検知
watcher.EnableRaisingEvents = True
End Sub
‘ 非同期でイベントを処理する
Private Sub watcher_Changed(sender As Object, e As FileSystemEventArgs) Handles watcher.Changed
‘ ファイルロックを考慮したリトライロジック
Dim content As String = SafeReadFile(e.FullPath)
If Not String.IsNullOrEmpty(content) Then
‘ UIスレッドへ通知を投げる(イベント経由)
RaiseEvent DataReceived(content)
End If
End Sub
‘ ロックを回避しつつ読み込む堅牢なメソッド
Private Function SafeReadFile(filePath As String) As String
Dim retryCount = 0
While retryCount < 5
Try
Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
Using sr As New StreamReader(fs)
Return sr.ReadToEnd()
End Using
End Using
Catch ex As IOException
' ファイルがロックされている場合は待機
System.Threading.Thread.Sleep(500)
retryCount += 1
End Try
End While
Return Nothing
End Function
End Class
---
3. UIスレッドの安全な更新:Invokeの作法
UI側でこのデータを受け取るとき、`Invoke`を忘れてはならない。これを怠ると、アプリケーションはいつか必ずクラッシュする。
‘ Form側での実装例
Private Sub LogMonitorEngine_DataReceived(content As String) Handles _engine.DataReceived
‘ UIスレッド以外からの呼び出しならInvoke
If Me.InvokeRequired Then
Me.Invoke(Sub() UpdateGrid(content))
Else
UpdateGrid(content)
End If
End Sub
Private Sub UpdateGrid(data As String)
‘ ここでDataGridView等のUIを更新
‘ データのパース処理等はここで行う
Dim rows = data.Split(Environment.NewLine)
‘ … グリッドへの反映ロジック …
End Sub
—
4. プロフェッショナルへのアドバイス:実務上の注意点
ファイルロックの概念を掌握せよ
`FileShare.ReadWrite`を指定することで、書き込み中のファイルに対しても読み取り権限を要求できる。しかし、それでもデータが不完全な状態で読み取られる可能性がある。本番環境では、「最終行の整合性チェック」や「一時ファイル経由の書き込み」を運用のルールとして徹底させるのが最善手だ。
パフォーマンスの最適化
もし監視対象が数千ファイルを超えるなら、`FileSystemWatcher`を乱立させてはならない。監視対象フォルダは絞り、解析処理は「UI更新」と「データ解析」を分ける。今回のように、イベント内で重い処理(重い計算やDB書き込み)を直接行うのは厳禁だ。必要であれば、`TPL (Task Parallel Library)`を用いてバックグラウンドで処理をキューイングさせる設計に昇華させること。
最後に
コードが「動く」ことは最低条件だ。エンジニアが目指すべきは、「誰が使っても壊れない、運用に耐えうるコード」である。この実装をベースに、あなたの業務環境に合わせてリトライ回数や待機時間を調整してほしい。
自動化の本質は、「機械に任せて人間が楽をすること」ではない。「機械に正確な仕事をさせ、人間はより高度な判断に集中すること」だ。そのための堅牢な基盤として、このコードを活用してほしい。
