【Project VBA】「タスクがロックされています」を完全撲滅する。排他制御と堅牢なリトライ設計の極意
Microsoft Project(以下、MS Project)の自動化において、単体端末でのローカルテストでは完璧に動いていたVBAマクロが、Project Online / Project Server環境や共有フォルダ上のファイル運用に投入された途端、突如として「タスクがロックされています」「オブジェクトは現在編集できません」といった例外を吐いて停止する――。
開発現場で一度は遭遇するこの悲劇の原因は、MS Project特有の「非同期なバックグラウンド再計算エンジン」と「複数ユーザー/プロセスの競合による排他制御(ロック)」にあります。
「`On Error Resume Next` で無視すればいい」という甘い考えは即刻捨ててください。不完全な書き込みはスケジュール データの破損を招き、企業のプロジェクト管理を崩壊させます。
本稿では、最高峰の業務自動化エンジニアの視点から、MS Project VBAにおける排他制御のメカニズムを解剖し、プロダクション環境で絶対に落ちない「指数バックオフ(Exponential Backoff)を用いたリトライ処理」の本格実装を伝授します。
—
1. なぜ MS Project で「タスクロック例外」が多発するのか
Excel VBAの感覚でMS Project VBAを組むと、確実に足元をすくわれます。まずはMS Project内部で何が起きているのか、その構造的要因を理解してください。
[VBAプロセスの書き込み] ──── (競合) ────► [MS Project 内部処理]
├─ バックグラウンド自動計算エンジン
├─ Project Server / Online 同期通信
└─ 外部連携 (他ユーザーの参照/編集)
1. バックグラウンド計算エンジン(pjAutomatic)の介在
先行・後行タスクの依存関係(FS, SS等)やリソースの割り当て率を変更した瞬間、MS Projectはバックグラウンドで関連するすべてのタスクの日程を再計算します。この計算中にVBAが次のタスクプロパティを書き込もうとすると、COMオブジェクト自体がロックされます。
2. Project Server / Online のエンタープライズ領域ロック
チェックアウト処理のタイムラグ、あるいはドラフト(下書き)からパブリッシュ状態への同期中にタスクオブジェクトへアクセスすると、サーバー側が保持するロックと競合します。
3. UI描画と非同期イベントの割り込み
ガントチャートの描画更新や、ユーザーの画面操作がマクロ実行と同時に走ると、一時的にタスクプロパティへのアクセス権限が失われます。
これらを力づくで無視するのではなく、「ロックは発生し得るもの」という前提に立ち、安全に待機して再試行する設計こそがプロフェッショナルなアプローチです。
—
2. アンチパターン:現場で見かける「落ちる」「狂う」コード
まずは、実務で絶対に書いてはいけない悪手を紹介します。なぜこれがダメなのか、理由をロジカルに説明できますか?
‘ ❌ 絶対にやってはいけないアンチパターン
Sub BadPattern_UpdateTask()
On Error Resume Next ‘ エラーを完全に無視(危険)
Dim t As Task
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
t.Duration = “5d” ‘ ロックされているとスキップされ、データが不整合を起こす
End If
Next t
On Error GoTo 0
End Sub
なぜ非効率かつ危険なのか?
- データのサイレント破損: エラーを無視すると、更新に失敗したタスクだけが過去の古い期間のまま残り、全体のクリティカルパスが歪みます。
- CPUの無駄遣い(Busy Wait): `DoEvents` だけを回すビジーループで待機しようとすると、CPU使用率が100%に張り付き、MS Project本体のレスポンスを奪ってロック解除をかえって遅らせます。
—
3. 堅牢なリトライ設計:3つの鉄則
本番環境で耐えうるVBAコードを構築するための設計原則は以下の3点です。
1. 自動計算と画面描画の一時停止
処理開始時に `Application.Calculation = pjManual` および `Application.ScreenUpdating = False` を適用し、MS Project内部エンジンによる割り込みロックを根絶する。
2. Win32 APIを用いた `Sleep` によるノンブロッキング待機
CPUに負荷をかけずに、指定ミリ秒だけ正確にスレッドを休止させる。
3. 指数バックオフ+ジッター(揺らぎ)アルゴリズム
リトライごとに「100ms → 200ms → 400ms…」と待機時間を倍増させ、一時的な競合状態を効率的に回避する。
—
4. プロダクション環境用 完全実装コード
以下は、実務の自動化ツールにそのまま組み込んで使用できる汎用リトライモジュールです。32bit/64bit双方のOffice環境(VBA7)に対応しています。
構成
1. `M_TaskSafeUpdater` (メインロジック&汎用リトライラッパー)
Option Explicit
‘ ==============================================================================
‘ 【Win32 API 定義】
‘ CPUに負荷をかけずに指定ミリ秒スレッドを停止させる
‘ ==============================================================================
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
‘ カスタムエラー定義(リトライ上限超過時)
Public Const ERR_TASK_LOCK_TIMEOUT As Long = vbObjectError + 5100
Private Const MAX_RETRIES As Long = 5 ‘ 最大リトライ回数
Private Const INITIAL_DELAY_MS As Long = 100 ‘ 初期待機時間 (ミリ秒)
‘ ==============================================================================
‘ 【メイン処理】安全なタスク一括更新エントリーポイント
‘ ==============================================================================
Public Sub ExecuteSafeTaskUpdate()
Dim origCalcMode As PjCalculation
Dim isScreenUpdatingState As Boolean
‘ 1. MS Projectの内部計算・描画状態を退避および最適化
On Error GoTo CleanUp
origCalcMode = Application.Calculation
isScreenUpdatingState = Application.ScreenUpdating
Application.Calculation = pjManual ‘ 自動計算をOFFにしてバックグラウンドロックを予防
Application.ScreenUpdating = False ‘ 画面描画をOFFにしてUI描画ロックを予防
‘ 2. 本処理実行
Dim t As Task
Dim successCount As Long
Dim failCount As Long
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
‘ 期間(Duration)変更処理をリトライラッパー経由で実行
If SetTaskDurationSafe(t, “5d”) Then
successCount = successCount + 1
Else
failCount = failCount + 1
‘ ログ記録等の後処理
Debug.Print “[WARNING] タスクID: ” & t.ID & ” (” & t.Name & “) の更新に失敗しました。”
End If
End If
Next t
‘ 3. 明示的に手動再計算を実行
Application.CalculateProject
MsgBox “更新完了: 成功 ” & successCount & ” 件 / 失敗 ” & failCount & ” 件”, _
IIf(failCount = 0, vbInformation, vbExclamation), “処理結果”
CleanUp:
‘ 4. 必ず元の状態に復元する(安全設計)
Application.Calculation = origCalcMode
Application.ScreenUpdating = isScreenUpdatingState
If Err.Number <> 0 And Err.Number <> ERR_TASK_LOCK_TIMEOUT Then
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End If
End Sub
‘ ==============================================================================
‘ 【リトライラッパー関数の例】タスクのDuration安全設定
‘ ==============================================================================
Private Function SetTaskDurationSafe(ByRef targetTask As Task, ByVal newDuration As Variant) As Boolean
Dim retryCount As Long
Dim currentDelay As Long
Dim isSuccess As Boolean
retryCount = 0
currentDelay = INITIAL_DELAY_MS
isSuccess = False
Do While retryCount <= MAX_RETRIES
On Error Resume Next
' --- 批判的変更領域 (Critical Section) ---
targetTask.Duration = newDuration
' ---------------------------------------
' エラー判定
If Err.Number = 0 Then
isSuccess = True
Exit Do ' 成功したら即時脱出
Else
' ロックやアクセスタイミング問題によるエラーと判定された場合
' (MS Project特有のCOMエラーや汎用実行時エラーを捕捉)
retryCount = retryCount + 1
If retryCount > MAX_RETRIES Then
Err.Clear
On Error GoTo 0
Debug.Print “[ERROR] 最大リトライ回数を超過しました。Task ID: ” & targetTask.ID
Exit Do
End If
Err.Clear
On Error GoTo 0
‘ メッセージ処理を挟みつつSleep(デッドロック防止)
DoEvents
Sleep currentDelay
‘ 指数バックオフ: 次回の待機時間を倍増
currentDelay = currentDelay 2
End If
Loop
SetTaskDurationSafe = isSuccess
End Function
—
5. チーフアーキテクトによるコード解説と運用の注意点
このコードには、大規模プロジェクト運用に耐えうる以下のテクニックが凝縮されています。
① `Application.Calculation = pjManual` の絶対的重要性
ループの中で1つのタスクのプロパティを変更するたびに、デフォルト(`pjAutomatic`)ではMS Projectは依存する何百ものタスクの計算を開始します。`pjManual` に変更することで、この計算エンジンの介入を完全に遮断できます。処理が終わった後に `Application.CalculateProject` を1回だけ呼び出すのが、パフォーマンスと安定性を両立させるプロの定石です。
② `Sleep` と `DoEvents` の組み合わせ
単純な `Sleep` のみだと、MS Projectのメインスレッド自体が完全にフリーズし、OS側から「応答なし」と判定されるリスクがあります。`DoEvents` を直前に一瞬だけ挟むことで、蓄積されたOSの描画イベントキューを消化させつつ、`Sleep` でCPU使用率を抑えた理想的なバックオフを実現しています。
③ クリーンアップ処理の徹底(`CleanUp:` ラベル)
どのような予期せぬエラーでマクロが中断しても、`Application.Calculation` や `Application.ScreenUpdating` を元の状態に復元する構造にしています。これを怠ると、「マクロ実行後にMS Projectが自動計算されなくなった」というユーザーからのクレームの嵐に見舞われます。
—
6. まとめ
MS Project VBAの自動化において、排他制御エラーは「避けられない現象」です。避けられないものを無理に無視するのではなく、発生を想定した決定論的なリトライ機構を組み込むことこそが、開発者の腕の見せ所です。
- 自動計算(`pjManual`)を制御し、競合の発生源を断つ
- Win32 APIの `Sleep` と指数バックオフによるエレガントな待機ルーチンを標準化する
- どんな例外が発生しても環境設定を必ず元に戻すクリーンアップを実装する
このアーキテクチャを導入し、現場の「止まるツール」を「止まらない強固なシステム」へと昇華させてください。
