【テクニカル・上級編】【エラーハンドリング】「タスクがロックされています」例外を回避するリトライ処理の実装 – Project VBA解析バイブル

スポンサーリンク

MS Project VBAにおける排他制御の深淵:「タスクはロックされています」例外を全自動で回避するリトライ・アーキテクチャ

大規模プロジェクト管理や複数ユーザー・システム間連携の現場において、Microsoft Project(以下、MS Project)のVBA自動化処理は今なお基幹のポジションを占めています。しかし、ネットワーク共有フォルダ上の`.mpp`ファイルや、Project Online / Project Serverとの同期環境下において、シニアエンジニアを最も苦しめるのが「タスクがロックされている」ことによるランタイムエラー(エラーコード 1101 等)や COM オブジェクトの呼び出し拒否(`RPC_E_CALL_REJECTED` / 0x80010001)です。

安直な `On Error Resume Next` でこれを握りつぶす設計は、データ非整合やサイレントクラッシュを引き起こす最悪のアンチパターンです。MS Projectは単なるリレーショナルデータベースではなく、高度な依存関係計算エンジン(CPM: Critical Path Method)を内部に保持する状態管理マシンだからです。

本記事では、Project VBAのオブジェクトモデルの深層に踏み込み、Windows APIを駆動させた「エクスポネジカル・バックオフ(指数関数的待機)+ジッター(ゆらぎ)」アルゴリズムによる堅牢なリトライ機構の実装パターンを解説します。

1. なぜ MS Project のタスクは「ロック」されるのか?

MS Project VBAでタスクプロパティ(`Task.Start`, `Task.Duration`, `Task.ResourceNames` など)を更新する際、内部では以下の複雑な処理が同期・非同期で発生しています。

1. 先行・後続タスクの再計算処理(計算エンジンの稼働)
2. リソースの平準化(Resource Leveling)に伴う計算ロック
3. Project Server / SharePoint ワークスペースとのバックグラウンド同期
4. 他ユーザーまたは別プロセス(外部VBA/C#等)による同一オブジェクトへの参照保持

この計算または同期処理の最中にVBAスクリプトがプロパティ変更を試みると、MS ProjectのCOMインターフェースはスレッドの安全性を保つために例外を送出し、操作を拒否します。

[VBA Automation App] —(1) Property Update—> [ MS Project Engine ] (LOCKED!)
| |
|<---(2) Runtime Error 1101 / RPC_E_CALL_REJECTED <---| したがって、堅牢なシステムを構築するためには、「ロックはいずれ解除される」という前提に立ち、安全に待機し、COM参照を再構築して再試行する機構が不可欠となります。

2. 堅牢なリトライ処理に不可欠な3大要素

ただループを回して再試行するだけでは、プロセスのデッドロックやCPU枯渇を招きます。エンタープライズ領域で求められるのは以下の3要素です。

① Win32 API `Sleep` による高精度なマイクロウェイト

VBA標準の `Application.Wait` や `Wait` メソッドは秒単位の精度しか持たず、UIスレッドを不必要に長時間ブロックします。Win32 APIの `Sleep` 関数を利用し、ミリ秒単位で精密にスレッドを休止させます。

② エクスポネンシャル・バックオフ + ジッター(Exponential Backoff with Jitter)

試行回数が増えるごとに待機時間を倍々で増やし(100ms → 200ms → 400ms…)、さらにミリ秒単位のランダムな「ゆらぎ(ジッター)」を加えます。これにより、複数クライアントが同時に再試行を叩き込む「衝突の連鎖(Thundering Herd Problem)」を回避します。

③ COMオブジェクト参照の確定的な破棄と再取得(重要)

ロックエラーが発生した際、保持していた `Task` オブジェクト参照は既に汚染(Stale)されている可能性があります。エラー発生時にはCOMオブジェクトの参照を明示的に `Nothing` で解放し、`Project.Tasks(ID)` から再取得しなければなりません。これを行わないと、ロックが解除されてもVBA側が永久に古いCOMポインタを参照し続け、エラーを吐き続けます。

3. 完全実装:生産グレードのリトライ・モジュール

以下に、現場の基幹システムへそのまま組み込める生産グレードのVBAコードを示します。32bit/64bit両対応のWin32 API宣言を含んでいます。

Option Explicit

‘ ==============================================================================
‘ Win32 API 宣言(64bit / 32bit 互換)
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If

‘ リトライ設定定数
Private Const MAX_RETRIES As Long = 5 ‘ 最大リトライ回数
Private Const BASE_DELAY_MS As Long = 200 ‘ 初回待機時間(ミリ秒)
Private Const MAX_JITTER_MS As Long = 100 ‘ ジッターの最大ゆらぎ幅(ミリ秒)

‘ ==============================================================================
‘ [メイン関数] タスクプロパティの安全な更新(リトライ機構付き)
‘ ==============================================================================
‘ @param proj : 対象 Project オブジェクト
‘ @param taskId : 変更対象のタスクID
‘ @param fieldId : 変更対象の PjField 列挙型(例: pjTaskName, pjTaskDuration)
‘ @param newValue : 設定する値
‘ @return : 成功時 True
‘ ==============================================================================
Public Function SafeSetTaskProperty( _
ByVal proj As MSProject.Project, _
ByVal taskId As Long, _
ByVal fieldId As PjField, _
ByVal newValue As Variant _
) As Boolean

Dim attempt As Long
Dim delayMs As Long
Dim success As Boolean
Dim targetTask As MSProject.Task

‘ 乱数シードの初期化(ジッター計算用)
Randomize

success = False

For attempt = 1 To MAX_RETRIES
On Error GoTo ErrorHandler

‘ — 【重要】COM参照の完全再取得 —
‘ エラー後の再試行時は、古いポインタを破棄してからID引きで再取得する
Set targetTask = proj.Tasks(taskId)

If targetTask Is Nothing Then
Err.Raise vbObjectError + 5001, “SafeSetTaskProperty”, “指定されたIDのタスクが存在しません。ID: ” & taskId
End If

‘ プロパティの書き込み試行(ここでロック例外が発生する可能性がある)
targetTask.SetField fieldId, CStr(newValue)

‘ 書き込み成功
success = True
Exit For

ContinueRetry:
‘ — エラー発生時の後処理とリトライ制御 —
On Error GoTo 0

‘ COMオブジェクト参照の明示的解放(ロック保持を解除させる)
Set targetTask = Nothing

If attempt < MAX_RETRIES Then ' バックオフ時間 = BASE_DELAY_MS (2 ^ (attempt - 1)) + ジッター delayMs = (BASE_DELAY_MS (2 ^ (attempt - 1))) + Int(Rnd() MAX_JITTER_MS) ' UIのフリーズを防ぎつつ、スレッドを指定時間休止 DoEvents Sleep delayMs DoEvents End If Next attempt ' 最終COM参照解放 Set targetTask = Nothing SafeSetTaskProperty = success Exit Function ErrorHandler: ' 発生したエラーログの記録(必要に応じてログファイルやイミディエイトウィンドウに出力) Debug.Print "[LOCK WARN] Attempt " & attempt & "/" & MAX_RETRIES & _ " Failed for Task ID " & taskId & ". Error: " & Err.Number & " - " & Err.Description ' エラーハンドラからループ制御へ戻る Resume ContinueRetry End Function ---

4. この実装が保証する極限の安定性

上記のコードには、Project VBAを実運用でトラブルフリーに動かすための高度なノウハウが凝縮されています。

① `Set targetTask = Nothing` による決定的なライフサイクル制御

VBAのガベージコレクションは参照カウント方式です。プロパティ更新に失敗した後、`targetTask` の参照を残したままループを回すと、MS Project内部のC++層で該当タスクのメモリブロックがロックされたままになり、「自らのVBAコードが原因で次のリトライも失敗する」というデッドロックに陥ります。明示的に `Nothing` を代入することで、内部COMカウントを即座に減算します。

② `DoEvents` と `Sleep` の挟み込み

`Sleep` を単体で実行すると、Windowsのメッセージループが停止し、MS Projectからのバックグラウンド同期シグナル(RPCレスポンスなど)処理が遅延する場合があります。`DoEvents` を前後に配置することで、OSおよびMS Projectのメッセージキューを正常に消化させながら安全にウェイトを消化します。

③ `SetField` メソッドの採用

`targetTask.Duration = newValue` のようなダイレクトなプロパティ代入ではなく、`SetField` メソッドを介すことで、MS Project内部の型変換・検証ロジックを安全に通過させ、不要な内部例外を削減します。

5. 超大量タスク更新時のパフォーマンス・チューニング

数百から数千のタスクを一括更新する場合、1タスクごとに再計算が発生するとロック発生率が爆発的に跳ね上がります。以下のアーキテクチャパターンを組み合わせることで、ロックの発生自体を極限まで抑えることが可能です。

Public Sub BatchUpdateTasksEngine(ByVal proj As MSProject.Project)
Dim app As MSProject.Application
Set app = proj.Application

On Error GoTo Finalize

‘ 1. 描画と自動計算を一時停止(ロックの発生源を断つ)
app.ScreenUpdating = False
app.Calculation = pjManual

‘ ———————————————————
‘ 大量タスクの更新処理(ここでSafeSetTaskPropertyを呼び出す)
‘ ———————————————————
Dim i As Long
For i = 1 To proj.Tasks.Count
If Not proj.Tasks(i) Is Nothing Then
‘ 例: Text1 フィールドの一括更新
Call SafeSetTaskProperty(proj, proj.Tasks(i).ID, pjTaskText1, “Updated_Via_Engine”)
End If
Next i

‘ 2. 一括再計算の実行
app.CalculateProject

Finalize:
‘ 3. 必ず元の状態に復元する(例外発生時も確実に通過させる)
app.Calculation = pjAutomatic
app.ScreenUpdating = True

Set app = Nothing
End Sub

チューニングの肝:

  • `app.Calculation = pjManual`: タスク更新ごとの依存関係・クリティカルパスの再計算をオフにします。これにより、内部計算エンジンによるロックの大部分を無効化できます。
  • `app.CalculateProject`: 最後に1度だけ明示的に再計算を行います。計算完了後に発生するロックも、前述のリトライ関数を通していれば安全に処理されます。

6. チーフアーキテクトからの提言

Project VBAにおけるエラーハンドリングは、単なる「例外の回避」ではありません。「COM環境におけるメモリのライフサイクル管理」および「MS Project独自の計算エンジンとの同期」そのものです。

1. `On Error Resume Next` で誤魔化さない:データの整合性が崩壊し、後から追跡不可能な障害となる。
2. COMポインタはエラー時に即座に破棄・再取得する:古い参照を使い回さない。
3. 適切なバックオフとジッターでシステム全体の競合を逃がす

レガシーと揶揄されがちなVBAですが、OSやCOMの低レイヤー構造を正しく理解して実装されたコードは、現代の最高峰のシステムにも劣らない圧倒的な堅牢性とパフォーマンスを発揮します。本稿のパターンを標準ライブラリとして組み込み、ミッションクリティカルなプロジェクト管理の自動化を完成させてください。

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